user-edit.vue 36 KB

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