user-edit.vue 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381
  1. <template>
  2. <view class="user-edit-container">
  3. <view class="header">
  4. <text class="title">{{ isEdit ? '完善个人信息' : '选择角色' }}</text>
  5. <text class="subtitle">{{ isEdit ? '以下信息可帮助您更好地使用服务' : '请选择您的身份类型' }}</text>
  6. </view>
  7. <!-- 角色选择(新用户首次登录时显示) -->
  8. <view class="role-select-section" v-if="!isEdit && showRoleSelect">
  9. <view
  10. class="role-card"
  11. :class="{ active: form.role === 'parent' }"
  12. @click="selectRole('parent')"
  13. >
  14. <text class="role-icon">👨‍👩‍👧</text>
  15. <text class="role-name">家长</text>
  16. <text class="role-desc">管理孩子任务、审核奖励</text>
  17. </view>
  18. <view
  19. class="role-card"
  20. :class="{ active: form.role === 'child' }"
  21. @click="selectRole('child')"
  22. >
  23. <text class="role-icon">👶</text>
  24. <text class="role-name">孩子</text>
  25. <text class="role-desc">执行任务、赚取积分</text>
  26. </view>
  27. <view
  28. class="role-card"
  29. :class="{ active: form.role === 'teacher' }"
  30. @click="selectRole('teacher')"
  31. >
  32. <text class="role-icon">👨‍🏫</text>
  33. <text class="role-name">成长规划师</text>
  34. <text class="role-desc">DAN测评、制定成长方案</text>
  35. </view>
  36. <button class="btn-next" @click="confirmRole" :disabled="!form.role">
  37. 下一步
  38. </button>
  39. </view>
  40. <!-- 详细信息填写(选择角色后显示) -->
  41. <view v-if="isEdit && (!isNewUserFlow || hasFamily)">
  42. <view class="form-section">
  43. <!-- 头像 -->
  44. <view class="form-item avatar-item">
  45. <text class="label">头像</text>
  46. <view class="avatar-upload" @click="chooseAvatar">
  47. <image v-if="form.avatar" :src="form.avatar" class="avatar-preview"></image>
  48. <view v-else class="avatar-placeholder">
  49. <text class="iconfont icon-plus">+</text>
  50. </view>
  51. </view>
  52. </view>
  53. <!-- 昵称 -->
  54. <view class="form-item">
  55. <text class="label">昵称 <text class="required">*</text></text>
  56. <input type="text" v-model="form.nickname" placeholder="请输入昵称" class="input" maxlength="20" />
  57. </view>
  58. <!-- 新用户流程:仅展示头像+昵称,其余字段按需在功能页补充 -->
  59. <view v-if="!isNewUserFlow">
  60. <!-- 真实姓名 -->
  61. <view class="form-item">
  62. <text class="label">真实姓名</text>
  63. <input type="text" v-model="form.realName" placeholder="选填,用于家庭管理" class="input" maxlength="20" />
  64. </view>
  65. <!-- 性别(成长规划师必填) -->
  66. <view class="form-item">
  67. <text class="label">性别 <text class="required" v-if="form.role === 'teacher'">*</text></text>
  68. <radio-group class="role-select" @change="onGenderChange">
  69. <label class="role-option">
  70. <radio value="male" :checked="form.gender === 'male'" />
  71. <text>男</text>
  72. </label>
  73. <label class="role-option">
  74. <radio value="female" :checked="form.gender === 'female'" />
  75. <text>女</text>
  76. </label>
  77. </radio-group>
  78. </view>
  79. <!-- 身份证号(成长规划师必填) -->
  80. <view class="form-item">
  81. <text class="label">身份证号 <text class="required" v-if="form.role === 'teacher'">*</text></text>
  82. <input type="idcard" v-model="form.idCard" placeholder="选填,填写后自动生成生日" class="input" maxlength="18" @input="onIdCardInput" />
  83. </view>
  84. <!-- 生日 -->
  85. <view class="form-item">
  86. <text class="label">生日</text>
  87. <picker mode="date" :value="form.birthday" @change="onBirthdayChange">
  88. <view class="picker-input">{{ form.birthday || '请选择生日' }}</view>
  89. </picker>
  90. </view>
  91. <!-- 出生时辰 -->
  92. <view class="form-item">
  93. <text class="label">出生时辰</text>
  94. <picker mode="selector" :range="birthHourOptions" :value="birthHourIndex" @change="onBirthHourChange">
  95. <view class="picker-input">{{ form.birthHour || '请选择出生时辰' }}</view>
  96. </picker>
  97. </view>
  98. <!-- 民族 -->
  99. <view class="form-item">
  100. <text class="label">民族</text>
  101. <picker mode="selector" :range="ethnicityOptions" :value="ethnicityIndex" @change="onEthnicityChange">
  102. <view class="picker-input">{{ form.ethnicity || '请选择民族' }}</view>
  103. </picker>
  104. </view>
  105. <!-- 血型 -->
  106. <view class="form-item">
  107. <text class="label">血型</text>
  108. <picker mode="selector" :range="bloodTypeOptions" :value="bloodTypeIndex" @change="onBloodTypeChange">
  109. <view class="picker-input">{{ form.bloodType || '请选择血型' }}</view>
  110. </picker>
  111. </view>
  112. <!-- 最高学历 -->
  113. <view class="form-item">
  114. <text class="label">最高学历</text>
  115. <picker mode="selector" :range="educationOptions" :value="educationIndex" @change="onEducationChange">
  116. <view class="picker-input">{{ form.highestEducation || '请选择最高学历' }}</view>
  117. </picker>
  118. </view>
  119. <!-- 婚姻状态 -->
  120. <view class="form-item">
  121. <text class="label">婚姻状态</text>
  122. <picker mode="selector" :range="maritalOptions" :value="maritalIndex" @change="onMaritalChange">
  123. <view class="picker-input">{{ form.maritalStatus || '请选择婚姻状态' }}</view>
  124. </picker>
  125. </view>
  126. <!-- 所在地区(省市区三级联动,与订单收货人地址一致) -->
  127. <view class="form-item address-row" @click="openRegionPicker">
  128. <text class="label">所在地区 <text class="required" v-if="isNewUserFlow">*</text></text>
  129. <view class="address-value-wrap">
  130. <text v-if="form.address" class="address-value">{{ form.address }}</text>
  131. <text v-else class="address-placeholder">请选择省市区</text>
  132. <text class="address-arrow">›</text>
  133. </view>
  134. </view>
  135. <!-- 详细地址 -->
  136. <view class="form-item">
  137. <text class="label">详细地址</text>
  138. <input type="text" v-model="form.street" placeholder="请输入详细地址(选填)" class="input" maxlength="100" @input="updateFullAddress" />
  139. </view>
  140. <!-- 兴趣爱好 -->
  141. <view class="form-item">
  142. <text class="label">兴趣爱好</text>
  143. <input type="text" v-model="form.hobbies" placeholder="如: 阅读、运动、音乐" class="input" maxlength="200" />
  144. </view>
  145. <!-- 饮食偏好 -->
  146. <view class="form-item">
  147. <text class="label">饮食偏好</text>
  148. <input type="text" v-model="form.dietPreferences" placeholder="如: 清淡、无辣、低糖" class="input" maxlength="200" />
  149. </view>
  150. </view>
  151. <!-- 成长规划师额外字段 -->
  152. <view class="form-section-title" v-if="form.role === 'teacher'">成长规划师信息</view>
  153. <!-- 成长规划师证书号(成长规划师必填) -->
  154. <view class="form-item" v-if="form.role === 'teacher'">
  155. <text class="label">成长规划师证书号 <text class="required">*</text></text>
  156. <input type="text" v-model="form.teacherNo" placeholder="请输入成长规划师证书号" class="input" maxlength="50" />
  157. </view>
  158. <!-- 成长规划师照片(成长规划师必填) -->
  159. <view class="form-item" v-if="form.role === 'teacher'">
  160. <text class="label">成长规划师照片 <text class="required">*</text></text>
  161. <view class="photo-upload" @click="chooseTeacherPhoto">
  162. <image v-if="form.teacherPhoto" :src="form.teacherPhoto" class="photo-preview" mode="aspectFill"></image>
  163. <view v-else class="photo-placeholder">
  164. <text class="plus-icon">+</text>
  165. <text class="placeholder-text">上传照片</text>
  166. </view>
  167. </view>
  168. </view>
  169. <!-- 成长规划师证书照片(成长规划师必填) -->
  170. <view class="form-item" v-if="form.role === 'teacher'">
  171. <text class="label">成长规划师证书 <text class="required">*</text></text>
  172. <view class="photo-upload" @click="chooseCertificate">
  173. <image v-if="form.certificateImage" :src="form.certificateImage" class="photo-preview" mode="aspectFill"></image>
  174. <view v-else class="photo-placeholder">
  175. <text class="plus-icon">+</text>
  176. <text class="placeholder-text">上传证书</text>
  177. </view>
  178. </view>
  179. </view>
  180. </view>
  181. <!-- 助手选择(会员专属) -->
  182. <view class="family-section" v-if="isEdit && memberLevel !== 'FREE'">
  183. <view class="section-title">🤖 选择助手</view>
  184. <view class="mascot-cards">
  185. <view class="mascot-card" :class="{ active: form.mascot === 'xibao' }" @click="selectMascot('xibao')">
  186. <view class="mascot-card-icon xibao-bg">🌟</view>
  187. <view class="mascot-card-info">
  188. <text class="mascot-card-name">浠宝</text>
  189. <text class="mascot-card-desc">温柔活泼的阳光女孩</text>
  190. </view>
  191. </view>
  192. <view class="mascot-card" :class="{ active: form.mascot === 'fubao' }" @click="selectMascot('fubao')">
  193. <view class="mascot-card-icon fubao-bg">💪</view>
  194. <view class="mascot-card-info">
  195. <text class="mascot-card-name">福宝</text>
  196. <text class="mascot-card-desc">开朗自信的阳光男孩</text>
  197. </view>
  198. </view>
  199. </view>
  200. </view>
  201. <!-- 家庭角色选择(新用户流程、角色为家长、有家庭邀请码时显示,可选) -->
  202. <view class="family-section" v-if="isEdit && form.role === 'parent' && isNewUserFlow && hasPendingFamilyInvite">
  203. <view class="section-title">👨‍👩‍👧‍👦 家庭身份(可选)</view>
  204. <view class="form-item">
  205. <text class="label">您在家庭中的身份</text>
  206. <picker mode="selector" :range="familyRoleOptions" :value="familyRoleIndex" @change="onFamilyRoleChange">
  207. <view class="picker-input">{{ form.familyRole || '请选择(可跳过)' }}</view>
  208. </picker>
  209. </view>
  210. <text class="hint-text">不选也可先保存,后续在家庭页面发起申请</text>
  211. </view>
  212. <!-- 免费用户提示 -->
  213. <view class="family-section" v-if="isEdit && memberLevel === 'FREE'">
  214. <view class="mascot-locked-hint" @click="goUpgrade">
  215. <text class="locked-icon">🔒</text>
  216. <text class="locked-text">开通会员解锁更多能力</text>
  217. <text class="locked-arrow">›</text>
  218. </view>
  219. </view>
  220. <!-- 新用户流程:保存资料 / 跳过 -->
  221. <view class="family-section" v-if="isEdit && form.role !== 'teacher' && isNewUserFlow">
  222. <view class="family-actions">
  223. <button class="btn btn-primary" @click="saveUserInfo" :loading="loading">
  224. 保存资料
  225. </button>
  226. <button class="btn btn-skip" @click="skip">
  227. 跳过
  228. </button>
  229. </view>
  230. </view>
  231. <!-- 修改个人资料保存按钮(非新用户流程时显示) -->
  232. <view class="family-section" v-if="isEdit && !isNewUserFlow && form.role !== 'teacher'">
  233. <view class="family-actions">
  234. <button class="btn btn-primary" @click="saveUserInfo" :loading="loading">
  235. 保存修改
  236. </button>
  237. </view>
  238. </view>
  239. <!-- 成长规划师提交按钮 -->
  240. <view class="family-section" v-if="isEdit && form.role === 'teacher' && isNewUserFlow">
  241. <view class="family-actions">
  242. <button class="btn btn-primary" @click="submitTeacherInfo" :loading="loading">
  243. 提交申请
  244. </button>
  245. </view>
  246. </view>
  247. </view>
  248. <view v-else-if="isEdit">
  249. <FamilyEmptyState type="card" />
  250. </view>
  251. <!-- 区域选择弹窗(省市区三级联动) -->
  252. <view class="region-mask" v-if="showRegionPicker" @click="cancelRegion">
  253. <view class="region-picker" @click.stop>
  254. <view class="region-header">
  255. <text class="region-header-btn" @click="cancelRegion">取消</text>
  256. <text class="region-header-title">选择地区</text>
  257. <text class="region-header-btn region-confirm" @click="confirmRegion">确定</text>
  258. </view>
  259. <view class="region-breadcrumb">
  260. <text
  261. v-for="(name, i) in regionBreadcrumb"
  262. :key="i"
  263. :class="['crumb-item', i === currentRegionLevel ? 'crumb-active' : '']"
  264. @click="jumpRegionLevel(i)"
  265. >{{ name || '请选择' }}</text>
  266. <text class="crumb-arrow" v-if="currentRegionLevel < 2">›</text>
  267. </view>
  268. <scroll-view class="region-list" scroll-y>
  269. <view
  270. v-for="(item, idx) in currentRegionItems"
  271. :key="idx"
  272. :class="['region-item', isRegionSelected(item) ? 'region-item-selected' : '']"
  273. @click="selectRegionItem(item)"
  274. >
  275. <text class="region-item-text">{{ getRegionName(item) }}</text>
  276. <text class="region-item-check" v-if="isRegionSelected(item)">✓</text>
  277. </view>
  278. <view v-if="regionLoading" class="region-loading">
  279. <text>加载中...</text>
  280. </view>
  281. </scroll-view>
  282. </view>
  283. </view>
  284. </view>
  285. </template>
  286. <script>
  287. import { updateUserInfo, createFamily, directRegister, getUserInfo, requestJoinByCode, getMyMembership, bindReferral } from '../../utils/api.js'
  288. import config from '@/config.js'
  289. export default {
  290. data() {
  291. return {
  292. isEdit: false,
  293. showRoleSelect: true,
  294. isNewUserFlow: false, // 是否是新用户注册流程
  295. memberLevel: 'FREE', // 会员等级,用于判断是否解锁助手
  296. form: {
  297. role: '',
  298. avatar: '',
  299. nickname: '',
  300. realName: '',
  301. gender: '',
  302. idCard: '',
  303. birthday: '',
  304. birthHour: '',
  305. inviteCode: '',
  306. familyRole: '', // 家庭角色:爸爸/妈妈/爷爷/奶奶/姥爷/姥姥/其他(可选)
  307. mascot: '',
  308. phone: '',
  309. teacherNo: '',
  310. teacherPhoto: '',
  311. certificateImage: '',
  312. ethnicity: '',
  313. bloodType: '',
  314. highestEducation: '',
  315. maritalStatus: '',
  316. hobbies: '',
  317. dietPreferences: '',
  318. address: '',
  319. province: '',
  320. city: '',
  321. district: '',
  322. street: ''
  323. },
  324. // 区域选择弹窗(省市区三级联动,与订单收货人地址一致)
  325. showRegionPicker: false,
  326. currentRegionLevel: 0, // 0=省份, 1=城市, 2=区县
  327. regionLoading: false,
  328. provinceList: [],
  329. cityList: [],
  330. districtList: [],
  331. tempProvince: null,
  332. tempCity: null,
  333. tempDistrict: null,
  334. ethnicityOptions: ['汉族','蒙古族','回族','藏族','维吾尔族','苗族','彝族','壮族','布依族','朝鲜族','满族','侗族','瑶族','白族','土家族','哈尼族','哈萨克族','傣族','黎族','其他'],
  335. bloodTypeOptions: ['A','B','AB','O','未知'],
  336. birthHourOptions: ['子时(23-01)','丑时(01-03)','寅时(03-05)','卯时(05-07)','辰时(07-09)','巳时(09-11)','午时(11-13)','未时(13-15)','申时(15-17)','酉时(17-19)','戌时(19-21)','亥时(21-23)'],
  337. birthHourValues: ['子','丑','寅','卯','辰','巳','午','未','申','酉','戌','亥'],
  338. educationOptions: ['小学','初中','高中','中专','大专','本科','硕士','博士'],
  339. maritalOptions: ['未婚','已婚','离异','丧偶'],
  340. familyRoleOptions: ['爸爸', '妈妈', '爷爷', '奶奶', '姥爷', '姥姥', '其他'],
  341. loading: false,
  342. userId: '',
  343. token: ''
  344. }
  345. },
  346. computed: {
  347. ethnicityIndex() {
  348. return this.ethnicityOptions.indexOf(this.form.ethnicity)
  349. },
  350. bloodTypeIndex() {
  351. return this.bloodTypeOptions.indexOf(this.form.bloodType)
  352. },
  353. birthHourIndex() {
  354. return this.birthHourValues.indexOf(this.form.birthHour)
  355. },
  356. educationIndex() {
  357. return this.educationOptions.indexOf(this.form.highestEducation)
  358. },
  359. maritalIndex() {
  360. return this.maritalOptions.indexOf(this.form.maritalStatus)
  361. },
  362. familyRoleIndex: {
  363. get() {
  364. return this.familyRoleOptions.indexOf(this.form.familyRole)
  365. },
  366. set(val) {
  367. this.form.familyRole = this.familyRoleOptions[val] || ''
  368. }
  369. },
  370. hasFamily: function() {
  371. if (this.token) return true
  372. return this.$store.getters.hasFamily
  373. },
  374. // 是否有待处理的家庭邀请码
  375. hasPendingFamilyInvite: function() {
  376. return !!uni.getStorageSync('pendingFamilyInviteCode')
  377. },
  378. regionBreadcrumb: function() {
  379. var names = []
  380. names.push(this.tempProvince ? this.getRegionName(this.tempProvince) : '省份')
  381. names.push(this.tempCity ? this.getRegionName(this.tempCity) : '城市')
  382. names.push(this.tempDistrict ? this.getRegionName(this.tempDistrict) : '区县')
  383. return names
  384. },
  385. currentRegionItems: function() {
  386. if (this.currentRegionLevel === 0) return this.provinceList
  387. if (this.currentRegionLevel === 1) return this.cityList
  388. if (this.currentRegionLevel === 2) return this.districtList
  389. return []
  390. },
  391. },
  392. onLoad(options) {
  393. if (options.token && options.userId) {
  394. this.token = options.token
  395. this.userId = options.userId
  396. if (options.autoParent === 'true') {
  397. // 首次登录:跳过角色选择,默认 parent,直接进入信息补充页
  398. this.showRoleSelect = false
  399. this.isEdit = true
  400. this.isNewUserFlow = true
  401. this.form.role = 'parent'
  402. } else {
  403. // 检查是否需要选择角色(新用户)- 只有明确传递 needsRoleSelect=true 才显示选择
  404. this.showRoleSelect = options.needsRoleSelect === 'true'
  405. this.isEdit = !this.showRoleSelect
  406. this.isNewUserFlow = options.needsRoleSelect === 'true'
  407. }
  408. } else if (options.phone && (options.autoParent === 'true' || options.needsRoleSelect === 'true')) {
  409. // 测试环境:没有token,只有手机号,是新用户注册流程
  410. this.showRoleSelect = options.needsRoleSelect === 'true'
  411. this.isEdit = !this.showRoleSelect
  412. this.isNewUserFlow = true
  413. if (options.autoParent === 'true') {
  414. this.form.role = 'parent'
  415. }
  416. // 将手机号传递给表单
  417. this.form.phone = options.phone
  418. } else {
  419. // 其他情况:默认进入编辑模式(如从"我的"页面进入)
  420. this.showRoleSelect = false
  421. this.isEdit = true
  422. this.isNewUserFlow = false
  423. }
  424. // 非新用户流程时,加载已有用户信息
  425. if (this.isEdit && !this.isNewUserFlow) {
  426. this.loadUserInfo()
  427. }
  428. // 加载会员等级(用于助手解锁)
  429. var self = this
  430. getMyMembership().then(function(res) {
  431. if (res.data && res.data.memberLevel) {
  432. self.memberLevel = res.data.memberLevel
  433. }
  434. }).catch(function() {})
  435. },
  436. methods: {
  437. selectRole(role) {
  438. this.form.role = role
  439. },
  440. confirmRole() {
  441. if (!this.form.role) {
  442. uni.showToast({ title: '请选择角色', icon: 'none' })
  443. return
  444. }
  445. this.showRoleSelect = false
  446. this.isEdit = true
  447. // 测试环境:直接注册用户(不需要验证码)
  448. if (!this.token && this.form.phone) {
  449. // 自动进入家庭绑定流程
  450. }
  451. },
  452. chooseAvatar() {
  453. uni.chooseImage({
  454. count: 1,
  455. success: (res) => {
  456. this.form.avatar = res.tempFilePaths[0]
  457. }
  458. })
  459. },
  460. chooseTeacherPhoto() {
  461. uni.chooseImage({
  462. count: 1,
  463. success: (res) => {
  464. this.form.teacherPhoto = res.tempFilePaths[0]
  465. }
  466. })
  467. },
  468. chooseCertificate() {
  469. uni.chooseImage({
  470. count: 1,
  471. success: (res) => {
  472. this.form.certificateImage = res.tempFilePaths[0]
  473. }
  474. })
  475. },
  476. onGenderChange(e) {
  477. this.form.gender = e.detail.value
  478. },
  479. onIdCardInput(e) {
  480. const idCard = e.detail.value
  481. if (idCard.length === 18) {
  482. // 根据身份证号自动提取生日
  483. const year = idCard.substring(6, 10)
  484. const month = idCard.substring(10, 12)
  485. const day = idCard.substring(12, 14)
  486. this.form.birthday = `${year}-${month}-${day}`
  487. }
  488. },
  489. onBirthdayChange(e) {
  490. this.form.birthday = e.detail.value
  491. },
  492. onEthnicityChange(e) {
  493. this.form.ethnicity = this.ethnicityOptions[e.detail.value]
  494. },
  495. onBloodTypeChange(e) {
  496. this.form.bloodType = this.bloodTypeOptions[e.detail.value]
  497. },
  498. onBirthHourChange(e) {
  499. this.form.birthHour = this.birthHourValues[e.detail.value]
  500. },
  501. onEducationChange(e) {
  502. this.form.highestEducation = this.educationOptions[e.detail.value]
  503. },
  504. onMaritalChange(e) {
  505. this.form.maritalStatus = this.maritalOptions[e.detail.value]
  506. },
  507. selectMascot(code) {
  508. this.form.mascot = code
  509. },
  510. goUpgrade() {
  511. uni.navigateTo({ url: '/pages/membership/upgrade' })
  512. },
  513. // ===== 省市区三级联动弹窗(与订单收货人地址一致) =====
  514. openRegionPicker() {
  515. this.tempProvince = this.form.province ? { name: this.form.province, id: null } : null
  516. this.tempCity = this.form.city ? { name: this.form.city, id: null } : null
  517. this.tempDistrict = this.form.district ? { name: this.form.district, id: null } : null
  518. this.currentRegionLevel = 0
  519. this.showRegionPicker = true
  520. this.loadRegionProvinces()
  521. },
  522. loadRegionProvinces() {
  523. var self = this
  524. self.regionLoading = true
  525. uni.request({
  526. url: config.api('/api/region/provinces'),
  527. method: 'POST',
  528. header: {
  529. 'Content-Type': 'application/json',
  530. 'Authorization': 'Bearer ' + uni.getStorageSync('token')
  531. },
  532. success: function(res) {
  533. self.regionLoading = false
  534. if (res.data && res.data.code === 200) {
  535. self.provinceList = res.data.data || []
  536. }
  537. },
  538. fail: function() {
  539. self.regionLoading = false
  540. }
  541. })
  542. },
  543. loadRegionCities(parentId) {
  544. if (!parentId) return
  545. var self = this
  546. self.regionLoading = true
  547. uni.request({
  548. url: config.api('/api/region/cities'),
  549. method: 'POST',
  550. data: { parentId: parentId },
  551. header: {
  552. 'Content-Type': 'application/json',
  553. 'Authorization': 'Bearer ' + uni.getStorageSync('token')
  554. },
  555. success: function(res) {
  556. self.regionLoading = false
  557. if (res.data && res.data.code === 200) {
  558. self.cityList = res.data.data || []
  559. }
  560. },
  561. fail: function() {
  562. self.regionLoading = false
  563. }
  564. })
  565. },
  566. loadRegionDistricts(parentId) {
  567. if (!parentId) return
  568. var self = this
  569. self.regionLoading = true
  570. uni.request({
  571. url: config.api('/api/region/districts'),
  572. method: 'POST',
  573. data: { parentId: parentId },
  574. header: {
  575. 'Content-Type': 'application/json',
  576. 'Authorization': 'Bearer ' + uni.getStorageSync('token')
  577. },
  578. success: function(res) {
  579. self.regionLoading = false
  580. if (res.data && res.data.code === 200) {
  581. self.districtList = res.data.data || []
  582. }
  583. },
  584. fail: function() {
  585. self.regionLoading = false
  586. }
  587. })
  588. },
  589. selectRegionItem(item) {
  590. if (this.currentRegionLevel === 0) {
  591. this.tempProvince = item
  592. this.tempCity = null
  593. this.tempDistrict = null
  594. this.cityList = []
  595. this.districtList = []
  596. this.currentRegionLevel = 1
  597. this.loadRegionCities(this.getRegionId(item))
  598. } else if (this.currentRegionLevel === 1) {
  599. this.tempCity = item
  600. this.tempDistrict = null
  601. this.districtList = []
  602. this.currentRegionLevel = 2
  603. this.loadRegionDistricts(this.getRegionId(item))
  604. } else if (this.currentRegionLevel === 2) {
  605. this.tempDistrict = item
  606. this.confirmRegion()
  607. }
  608. },
  609. confirmRegion() {
  610. if (this.tempProvince) {
  611. this.form.province = this.getRegionName(this.tempProvince)
  612. }
  613. if (this.tempCity) {
  614. this.form.city = this.getRegionName(this.tempCity)
  615. }
  616. if (this.tempDistrict) {
  617. this.form.district = this.getRegionName(this.tempDistrict)
  618. }
  619. this.updateFullAddress()
  620. this.showRegionPicker = false
  621. },
  622. cancelRegion() {
  623. this.showRegionPicker = false
  624. },
  625. jumpRegionLevel(level) {
  626. if (level < this.currentRegionLevel) {
  627. this.currentRegionLevel = level
  628. }
  629. },
  630. getRegionName(item) {
  631. return item && (item.name || item.province || item.city || item.district || '')
  632. },
  633. getRegionId(item) {
  634. return item && (item.id || item.code)
  635. },
  636. isRegionSelected(item) {
  637. var name = this.getRegionName(item)
  638. if (this.currentRegionLevel === 0) return this.tempProvince && this.getRegionName(this.tempProvince) === name
  639. if (this.currentRegionLevel === 1) return this.tempCity && this.getRegionName(this.tempCity) === name
  640. if (this.currentRegionLevel === 2) return this.tempDistrict && this.getRegionName(this.tempDistrict) === name
  641. return false
  642. },
  643. updateFullAddress() {
  644. this.form.address = (this.form.province || '') + (this.form.city || '') + (this.form.district || '') + (this.form.street || '')
  645. },
  646. // 加载已有用户信息
  647. async loadUserInfo() {
  648. try {
  649. const res = await getUserInfo()
  650. if (res.code === 200 && res.data) {
  651. const userData = res.data
  652. this.form.nickname = userData.nickname || ''
  653. this.form.realName = userData.realName || ''
  654. this.form.gender = userData.gender || ''
  655. this.form.idCard = userData.idCard || ''
  656. // 生日只取日期部分,去掉时间和 ISO 格式的 T 分隔符
  657. this.form.birthday = userData.birthday ? String(userData.birthday).split(/[T ]/)[0] : ''
  658. this.form.avatar = userData.avatar || ''
  659. this.form.role = userData.role || ''
  660. this.form.phone = userData.phone || ''
  661. this.form.ethnicity = userData.ethnicity || ''
  662. this.form.bloodType = userData.bloodType || ''
  663. this.form.highestEducation = userData.highestEducation || ''
  664. this.form.maritalStatus = userData.maritalStatus || ''
  665. this.form.hobbies = userData.hobbies || ''
  666. this.form.dietPreferences = userData.dietPreferences || ''
  667. this.form.mascot = userData.mascot || ''
  668. this.form.address = userData.address || ''
  669. this.form.province = userData.province || ''
  670. this.form.city = userData.city || ''
  671. this.form.district = userData.district || ''
  672. this.form.street = userData.street || ''
  673. // 若无完整地址文本但有省市区,则拼接
  674. if (!this.form.address && (this.form.province || this.form.city || this.form.district)) {
  675. this.updateFullAddress()
  676. }
  677. }
  678. } catch (e) {
  679. console.error('加载用户信息失败', e)
  680. }
  681. },
  682. // 保存修改
  683. async saveUserInfo() {
  684. // 新用户流程:昵称可跳过(跳过时用默认昵称);老用户流程:昵称必填
  685. if (!this.isNewUserFlow && !this.form.nickname) {
  686. uni.showToast({ title: '请输入昵称', icon: 'none' })
  687. return
  688. }
  689. if (this.isNewUserFlow && !this.form.nickname) {
  690. var phone = this.form.phone || ''
  691. this.form.nickname = phone ? '用户' + phone.substring(phone.length - 4) : '微信用户'
  692. }
  693. // 从省市区街道字段同步完整地址文本
  694. this.updateFullAddress()
  695. this.loading = true
  696. try {
  697. await updateUserInfo({
  698. nickname: this.form.nickname,
  699. realName: this.form.realName,
  700. gender: this.form.gender,
  701. idCard: this.form.idCard,
  702. birthday: this.form.birthday,
  703. avatar: this.form.avatar,
  704. mascot: this.form.mascot,
  705. ethnicity: this.form.ethnicity,
  706. bloodType: this.form.bloodType,
  707. highestEducation: this.form.highestEducation,
  708. maritalStatus: this.form.maritalStatus,
  709. hobbies: this.form.hobbies,
  710. dietPreferences: this.form.dietPreferences,
  711. address: this.form.address,
  712. province: this.form.province,
  713. city: this.form.city,
  714. district: this.form.district,
  715. street: this.form.street
  716. })
  717. // 新用户流程且有家庭邀请码:提交加入家庭申请
  718. if (this.isNewUserFlow && this.hasPendingFamilyInvite) {
  719. var inviteCode = uni.getStorageSync('pendingFamilyInviteCode')
  720. var familyRole = this.form.familyRole || ''
  721. if (inviteCode) {
  722. try {
  723. await requestJoinByCode(inviteCode, familyRole)
  724. uni.showToast({ title: '加入家庭申请已提交,等待管理员审批', icon: 'success' })
  725. } catch (e) {
  726. uni.showToast({ title: e.message || '申请提交失败,可稍后在家庭页重试', icon: 'none' })
  727. }
  728. uni.removeStorageSync('pendingFamilyInviteCode')
  729. }
  730. setTimeout(() => {
  731. this.navigateToHome()
  732. }, 1500)
  733. } else {
  734. uni.showToast({ title: '保存成功', icon: 'success' })
  735. setTimeout(() => {
  736. if (this.isNewUserFlow) {
  737. this.navigateToHome()
  738. } else {
  739. uni.navigateBack()
  740. }
  741. }, 1500)
  742. }
  743. } catch (e) {
  744. uni.showToast({ title: '保存失败', icon: 'none' })
  745. } finally {
  746. this.loading = false
  747. }
  748. },
  749. async submitTeacherInfo() {
  750. // 验证必填字段
  751. if (!this.form.nickname) {
  752. uni.showToast({ title: '请输入昵称', icon: 'none' })
  753. return
  754. }
  755. if (!this.form.gender) {
  756. uni.showToast({ title: '请选择性别', icon: 'none' })
  757. return
  758. }
  759. if (!this.form.idCard) {
  760. uni.showToast({ title: '请输入身份证号', icon: 'none' })
  761. return
  762. }
  763. if (!this.form.teacherNo) {
  764. uni.showToast({ title: '请输入成长规划师证书号', icon: 'none' })
  765. return
  766. }
  767. if (!this.form.teacherPhoto) {
  768. uni.showToast({ title: '请上传成长规划师照片', icon: 'none' })
  769. return
  770. }
  771. if (!this.form.certificateImage) {
  772. uni.showToast({ title: '请上传成长规划师证书', icon: 'none' })
  773. return
  774. }
  775. this.loading = true
  776. try {
  777. await updateUserInfo({
  778. ...this.form,
  779. roles: this.form.role
  780. })
  781. uni.showToast({ title: '提交成功,请等待审核' })
  782. setTimeout(() => {
  783. uni.switchTab({ url: '/pages/index-home/index' })
  784. }, 1500)
  785. } catch (e) {
  786. uni.showToast({ title: '提交失败', icon: 'none' })
  787. } finally {
  788. this.loading = false
  789. }
  790. },
  791. async joinFamilyAction() {
  792. if (!this.form.inviteCode) {
  793. uni.showToast({ title: '请输入邀请码', icon: 'none' })
  794. return
  795. }
  796. if (!this.form.nickname) {
  797. uni.showToast({ title: '请输入昵称', icon: 'none' })
  798. return
  799. }
  800. this.loading = true
  801. try {
  802. let userId = this.userId
  803. // 测试环境:没有token,需要先注册用户
  804. if (!this.token && this.form.phone) {
  805. const regRes = await directRegister({
  806. phone: this.form.phone,
  807. nickname: this.form.nickname,
  808. avatar: this.form.avatar,
  809. role: this.form.role,
  810. mascot: this.form.mascot,
  811. birthday: this.form.birthday,
  812. gender: this.form.gender,
  813. inviteCode: this.form.inviteCode
  814. })
  815. userId = regRes.data.userId
  816. // 保存登录信息
  817. uni.setStorageSync('token', regRes.data.token)
  818. uni.setStorageSync('userId', userId)
  819. uni.setStorageSync('role', regRes.data.role)
  820. // 邀请码注册不写入 familyId(加入需管理员审批)
  821. } else {
  822. await updateUserInfo(this.form)
  823. await requestJoinByCode(this.form.inviteCode)
  824. }
  825. uni.showToast({ title: '申请已提交,等待管理员审批' })
  826. setTimeout(() => {
  827. this.navigateToHome()
  828. }, 1500)
  829. } catch (e) {
  830. uni.showToast({ title: e.message || '加入失败', icon: 'none' })
  831. } finally {
  832. this.loading = false
  833. }
  834. },
  835. async createFamilyAction() {
  836. if (!this.form.nickname) {
  837. uni.showToast({ title: '请输入昵称', icon: 'none' })
  838. return
  839. }
  840. this.loading = true
  841. try {
  842. let userId = this.userId
  843. // 测试环境:没有token,需要先注册用户
  844. if (!this.token && this.form.phone) {
  845. const regRes = await directRegister({
  846. phone: this.form.phone,
  847. nickname: this.form.nickname,
  848. avatar: this.form.avatar,
  849. role: this.form.role,
  850. mascot: this.form.mascot,
  851. birthday: this.form.birthday,
  852. gender: this.form.gender
  853. })
  854. userId = regRes.data.userId
  855. // 保存登录信息
  856. uni.setStorageSync('token', regRes.data.token)
  857. uni.setStorageSync('userId', userId)
  858. uni.setStorageSync('role', regRes.data.role)
  859. // 注册不写入 familyId(后端根据 userId 获取)
  860. } else {
  861. await updateUserInfo(this.form)
  862. const famRes = await createFamily(this.form.nickname + '的家庭')
  863. // 同步 familyId 到 store(hasFamily getter 依赖)
  864. if (famRes && famRes.data) {
  865. this.$store.commit('setFamilyId', famRes.data)
  866. }
  867. }
  868. uni.showToast({ title: '创建成功' })
  869. setTimeout(() => {
  870. this.navigateToHome()
  871. }, 1500)
  872. } catch (e) {
  873. uni.showToast({ title: e.message || '创建失败', icon: 'none' })
  874. } finally {
  875. this.loading = false
  876. }
  877. },
  878. skip() {
  879. // 跳过时不清理邀请码,用户可后续重新进入编辑页提交申请
  880. this.navigateToHome()
  881. },
  882. navigateToHome() {
  883. // 保存 mascot 到 userInfo 存储,供聊天页读取
  884. if (this.form.mascot) {
  885. try {
  886. const userInfo = uni.getStorageSync('userInfo') || {}
  887. userInfo.mascot = this.form.mascot
  888. uni.setStorageSync('userInfo', userInfo)
  889. } catch (e) {
  890. console.error('保存 mascot 失败', e)
  891. }
  892. }
  893. // 新用户流程:消费分享者推荐码,绑定推荐人(非 team 类型)
  894. // 登录页 completeLogin 在 needsRoleSelect=true 时直接跳本页,跳过了 handleReferralBind,
  895. // 因此在此处统一消费 storage 中的 inviteCode,避免推荐人绑定丢失
  896. var inviteCode = uni.getStorageSync('inviteCode')
  897. var inviteType = uni.getStorageSync('inviteType')
  898. if (inviteCode && inviteType !== 'team') {
  899. var self = this
  900. bindReferral(inviteCode).then(function(res) {
  901. if (res && res.code === 200) {
  902. console.log('新用户绑定推荐人成功', inviteCode)
  903. } else if (res && res.code === 400) {
  904. console.log('用户已有推荐人,跳过绑定')
  905. }
  906. }).catch(function(e) {
  907. console.log('新用户绑定推荐人失败', e)
  908. })
  909. }
  910. uni.removeStorageSync('inviteCode')
  911. uni.removeStorageSync('inviteType')
  912. // 用 switchTab 跳转带底部 TabBar 的首页(行/身/智/心/富)
  913. // parent-index/child-index 是分包页面(非 TabBar 页),用 navigateTo 会丢失底部 TabBar
  914. uni.switchTab({ url: '/pages/index-home/index' })
  915. }
  916. }
  917. }
  918. </script>
  919. <style scoped>
  920. .user-edit-container {
  921. min-height: 100vh;
  922. background: #f5f5f5;
  923. padding: 30rpx;
  924. }
  925. .header {
  926. margin-bottom: 40rpx;
  927. text-align: center;
  928. }
  929. .title {
  930. font-size: 40rpx;
  931. font-weight: bold;
  932. color: #333;
  933. display: block;
  934. margin-bottom: 10rpx;
  935. }
  936. .subtitle {
  937. font-size: 28rpx;
  938. color: #999;
  939. }
  940. /* 角色选择 */
  941. .role-select-section {
  942. margin-bottom: 40rpx;
  943. }
  944. .role-card {
  945. background: #fff;
  946. border-radius: 16rpx;
  947. padding: 40rpx;
  948. margin-bottom: 20rpx;
  949. display: flex;
  950. flex-direction: column;
  951. align-items: center;
  952. border: 2rpx solid transparent;
  953. transition: all 0.3s;
  954. }
  955. .role-card.active {
  956. border-color: #4CAF50;
  957. background: #f8fdf8;
  958. }
  959. .role-icon {
  960. font-size: 80rpx;
  961. margin-bottom: 16rpx;
  962. }
  963. .role-name {
  964. font-size: 32rpx;
  965. font-weight: bold;
  966. color: #333;
  967. margin-bottom: 8rpx;
  968. }
  969. .role-desc {
  970. font-size: 24rpx;
  971. color: #999;
  972. }
  973. .btn-next {
  974. background: #4CAF50;
  975. color: #fff;
  976. border-radius: 40rpx;
  977. margin-top: 20rpx;
  978. }
  979. /* 表单 */
  980. .form-section {
  981. background: #fff;
  982. border-radius: 16rpx;
  983. padding: 30rpx;
  984. margin-bottom: 30rpx;
  985. }
  986. .form-section-title {
  987. font-size: 30rpx;
  988. font-weight: bold;
  989. color: #333;
  990. margin-bottom: 20rpx;
  991. padding-bottom: 16rpx;
  992. border-bottom: 1rpx solid #f0f0f0;
  993. }
  994. .form-item {
  995. margin-bottom: 30rpx;
  996. }
  997. .label {
  998. display: block;
  999. font-size: 28rpx;
  1000. color: #333;
  1001. margin-bottom: 16rpx;
  1002. }
  1003. .required {
  1004. color: #f44336;
  1005. }
  1006. .input, .picker-input {
  1007. padding: 20rpx;
  1008. background: #f5f5f5;
  1009. border-radius: 12rpx;
  1010. font-size: 28rpx;
  1011. }
  1012. .role-select {
  1013. display: flex;
  1014. gap: 40rpx;
  1015. }
  1016. .role-option {
  1017. display: flex;
  1018. align-items: center;
  1019. gap: 8rpx;
  1020. }
  1021. /* 头像上传 */
  1022. .avatar-upload {
  1023. width: 150rpx;
  1024. height: 150rpx;
  1025. }
  1026. .avatar-preview {
  1027. width: 150rpx;
  1028. height: 150rpx;
  1029. border-radius: 50%;
  1030. }
  1031. .avatar-placeholder {
  1032. width: 150rpx;
  1033. height: 150rpx;
  1034. background: #f5f5f5;
  1035. border-radius: 50%;
  1036. display: flex;
  1037. align-items: center;
  1038. justify-content: center;
  1039. font-size: 60rpx;
  1040. color: #ccc;
  1041. }
  1042. /* 照片上传 */
  1043. .photo-upload {
  1044. width: 300rpx;
  1045. height: 200rpx;
  1046. }
  1047. .photo-preview {
  1048. width: 300rpx;
  1049. height: 200rpx;
  1050. border-radius: 12rpx;
  1051. }
  1052. .photo-placeholder {
  1053. width: 300rpx;
  1054. height: 200rpx;
  1055. background: #f5f5f5;
  1056. border-radius: 12rpx;
  1057. display: flex;
  1058. flex-direction: column;
  1059. align-items: center;
  1060. justify-content: center;
  1061. }
  1062. .plus-icon {
  1063. font-size: 60rpx;
  1064. color: #ccc;
  1065. }
  1066. .placeholder-text {
  1067. font-size: 24rpx;
  1068. color: #999;
  1069. margin-top: 8rpx;
  1070. }
  1071. /* 助手选择 */
  1072. .mascot-cards {
  1073. display: flex;
  1074. gap: 20rpx;
  1075. margin-top: 16rpx;
  1076. }
  1077. .mascot-card {
  1078. flex: 1;
  1079. background: #f8f8f8;
  1080. border-radius: 16rpx;
  1081. padding: 24rpx;
  1082. display: flex;
  1083. align-items: center;
  1084. gap: 16rpx;
  1085. border: 2rpx solid transparent;
  1086. transition: all 0.3s;
  1087. }
  1088. .mascot-card.active {
  1089. border-color: #4CAF50;
  1090. background: #f0fdf4;
  1091. }
  1092. .mascot-card-icon {
  1093. width: 72rpx;
  1094. height: 72rpx;
  1095. border-radius: 50%;
  1096. display: flex;
  1097. align-items: center;
  1098. justify-content: center;
  1099. font-size: 36rpx;
  1100. flex-shrink: 0;
  1101. }
  1102. .xibao-bg {
  1103. background: linear-gradient(135deg, #FFF0DB, #FFD6A5);
  1104. }
  1105. .fubao-bg {
  1106. background: linear-gradient(135deg, #FFF8E1, #FCD34D);
  1107. }
  1108. .mascot-card-info {
  1109. display: flex;
  1110. flex-direction: column;
  1111. gap: 4rpx;
  1112. }
  1113. .mascot-card-name {
  1114. font-size: 30rpx;
  1115. font-weight: bold;
  1116. color: #333;
  1117. }
  1118. .mascot-card-desc {
  1119. font-size: 22rpx;
  1120. color: #999;
  1121. }
  1122. /* 家庭绑定 */
  1123. .family-section {
  1124. background: #fff;
  1125. border-radius: 16rpx;
  1126. padding: 30rpx;
  1127. }
  1128. .section-title {
  1129. font-size: 30rpx;
  1130. font-weight: bold;
  1131. color: #333;
  1132. margin-bottom: 20rpx;
  1133. }
  1134. .family-actions {
  1135. display: flex;
  1136. flex-direction: column;
  1137. gap: 20rpx;
  1138. margin-top: 30rpx;
  1139. }
  1140. .btn {
  1141. border-radius: 40rpx;
  1142. font-size: 30rpx;
  1143. }
  1144. .btn-primary {
  1145. background: #4CAF50;
  1146. color: #fff;
  1147. }
  1148. .btn-skip {
  1149. background: #f5f5f5;
  1150. color: #999;
  1151. }
  1152. /* ===== 区域选择弹窗 ===== */
  1153. .region-mask {
  1154. position: fixed;
  1155. top: 0;
  1156. left: 0;
  1157. right: 0;
  1158. bottom: 0;
  1159. background: rgba(0, 0, 0, 0.5);
  1160. z-index: 999;
  1161. display: flex;
  1162. align-items: flex-end;
  1163. }
  1164. .region-picker {
  1165. background: #fff;
  1166. border-radius: 20rpx 20rpx 0 0;
  1167. width: 100%;
  1168. max-height: 70vh;
  1169. display: flex;
  1170. flex-direction: column;
  1171. }
  1172. .region-header {
  1173. display: flex;
  1174. justify-content: space-between;
  1175. align-items: center;
  1176. padding: 24rpx 30rpx;
  1177. border-bottom: 1rpx solid #f0f0f0;
  1178. }
  1179. .region-header-btn {
  1180. font-size: 28rpx;
  1181. color: #999;
  1182. }
  1183. .region-confirm {
  1184. color: #F97316;
  1185. font-weight: bold;
  1186. }
  1187. .region-header-title {
  1188. font-size: 30rpx;
  1189. font-weight: bold;
  1190. color: #333;
  1191. }
  1192. .region-breadcrumb {
  1193. display: flex;
  1194. align-items: center;
  1195. padding: 20rpx 30rpx;
  1196. border-bottom: 1rpx solid #f5f5f5;
  1197. }
  1198. .crumb-item {
  1199. font-size: 26rpx;
  1200. color: #999;
  1201. padding: 8rpx 12rpx;
  1202. border-radius: 8rpx;
  1203. }
  1204. .crumb-item.crumb-active {
  1205. color: #F97316;
  1206. font-weight: bold;
  1207. }
  1208. .crumb-arrow {
  1209. font-size: 24rpx;
  1210. color: #ccc;
  1211. margin: 0 8rpx;
  1212. }
  1213. .region-list {
  1214. max-height: 50vh;
  1215. overflow-y: auto;
  1216. }
  1217. .region-item {
  1218. display: flex;
  1219. justify-content: space-between;
  1220. align-items: center;
  1221. padding: 28rpx 30rpx;
  1222. border-bottom: 1rpx solid #f5f5f5;
  1223. }
  1224. .region-item:active {
  1225. background: #FFF7ED;
  1226. }
  1227. .region-item-text {
  1228. font-size: 28rpx;
  1229. color: #333;
  1230. }
  1231. .region-item-check {
  1232. font-size: 28rpx;
  1233. color: #F97316;
  1234. font-weight: bold;
  1235. }
  1236. .region-item-selected {
  1237. background: #FFF7ED;
  1238. }
  1239. .region-item-selected .region-item-text {
  1240. color: #F97316;
  1241. font-weight: bold;
  1242. }
  1243. .region-loading {
  1244. padding: 40rpx;
  1245. text-align: center;
  1246. font-size: 26rpx;
  1247. color: #999;
  1248. }
  1249. /* ===== 地址行(所在地区) ===== */
  1250. .address-row {
  1251. display: flex;
  1252. justify-content: space-between;
  1253. align-items: center;
  1254. }
  1255. .address-value-wrap {
  1256. display: flex;
  1257. align-items: center;
  1258. flex: 1;
  1259. justify-content: flex-end;
  1260. }
  1261. .address-value {
  1262. font-size: 28rpx;
  1263. color: #333;
  1264. }
  1265. .address-placeholder {
  1266. font-size: 28rpx;
  1267. color: #999;
  1268. }
  1269. .address-arrow {
  1270. font-size: 28rpx;
  1271. color: #ccc;
  1272. margin-left: 8rpx;
  1273. }
  1274. /* 助手锁定提示 */
  1275. .mascot-locked-hint {
  1276. display: flex;
  1277. align-items: center;
  1278. padding: 28rpx 32rpx;
  1279. background: linear-gradient(135deg, #FEF3C7, #FDE68A);
  1280. border-radius: 20rpx;
  1281. margin-top: 16rpx;
  1282. }
  1283. .locked-icon {
  1284. font-size: 36rpx;
  1285. margin-right: 16rpx;
  1286. }
  1287. .locked-text {
  1288. flex: 1;
  1289. font-size: 28rpx;
  1290. font-weight: 600;
  1291. color: #92400e;
  1292. }
  1293. .locked-arrow {
  1294. font-size: 32rpx;
  1295. color: #92400e;
  1296. margin-left: 8rpx;
  1297. }
  1298. </style>