user-edit.vue 40 KB

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