user-edit.vue 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078
  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. <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 class="form-item">
  60. <text class="label">真实姓名</text>
  61. <input type="text" v-model="form.realName" placeholder="选填,用于家庭管理" class="input" maxlength="20" />
  62. </view>
  63. <!-- 性别(成长规划师必填) -->
  64. <view class="form-item">
  65. <text class="label">性别 <text class="required" v-if="form.role === 'teacher'">*</text></text>
  66. <radio-group class="role-select" @change="onGenderChange">
  67. <label class="role-option">
  68. <radio value="male" :checked="form.gender === 'male'" />
  69. <text>男</text>
  70. </label>
  71. <label class="role-option">
  72. <radio value="female" :checked="form.gender === 'female'" />
  73. <text>女</text>
  74. </label>
  75. </radio-group>
  76. </view>
  77. <!-- 身份证号(成长规划师必填) -->
  78. <view class="form-item">
  79. <text class="label">身份证号 <text class="required" v-if="form.role === 'teacher'">*</text></text>
  80. <input type="idcard" v-model="form.idCard" placeholder="选填,填写后自动生成生日" class="input" maxlength="18" @input="onIdCardInput" />
  81. </view>
  82. <!-- 生日 -->
  83. <view class="form-item">
  84. <text class="label">生日</text>
  85. <picker mode="date" :value="form.birthday" @change="onBirthdayChange">
  86. <view class="picker-input">{{ form.birthday || '请选择生日' }}</view>
  87. </picker>
  88. </view>
  89. <!-- 出生时辰 -->
  90. <view class="form-item">
  91. <text class="label">出生时辰</text>
  92. <picker mode="selector" :range="birthHourOptions" :value="birthHourIndex" @change="onBirthHourChange">
  93. <view class="picker-input">{{ form.birthHour || '请选择出生时辰' }}</view>
  94. </picker>
  95. </view>
  96. <!-- 民族 -->
  97. <view class="form-item">
  98. <text class="label">民族</text>
  99. <picker mode="selector" :range="ethnicityOptions" :value="ethnicityIndex" @change="onEthnicityChange">
  100. <view class="picker-input">{{ form.ethnicity || '请选择民族' }}</view>
  101. </picker>
  102. </view>
  103. <!-- 血型 -->
  104. <view class="form-item">
  105. <text class="label">血型</text>
  106. <picker mode="selector" :range="bloodTypeOptions" :value="bloodTypeIndex" @change="onBloodTypeChange">
  107. <view class="picker-input">{{ form.bloodType || '请选择血型' }}</view>
  108. </picker>
  109. </view>
  110. <!-- 最高学历 -->
  111. <view class="form-item">
  112. <text class="label">最高学历</text>
  113. <picker mode="selector" :range="educationOptions" :value="educationIndex" @change="onEducationChange">
  114. <view class="picker-input">{{ form.highestEducation || '请选择最高学历' }}</view>
  115. </picker>
  116. </view>
  117. <!-- 婚姻状态 -->
  118. <view class="form-item">
  119. <text class="label">婚姻状态</text>
  120. <picker mode="selector" :range="maritalOptions" :value="maritalIndex" @change="onMaritalChange">
  121. <view class="picker-input">{{ form.maritalStatus || '请选择婚姻状态' }}</view>
  122. </picker>
  123. </view>
  124. <!-- 所在地区(省市区三级联动,与订单收货人地址一致) -->
  125. <view class="form-item address-row" @click="openRegionPicker">
  126. <text class="label">所在地区</text>
  127. <view class="address-value-wrap">
  128. <text v-if="form.address" class="address-value">{{ form.address }}</text>
  129. <text v-else class="address-placeholder">请选择省市区</text>
  130. <text class="address-arrow">›</text>
  131. </view>
  132. </view>
  133. <!-- 详细地址 -->
  134. <view class="form-item">
  135. <text class="label">详细地址</text>
  136. <input type="text" v-model="form.street" placeholder="请输入详细地址(选填)" class="input" maxlength="100" @input="updateFullAddress" />
  137. </view>
  138. <!-- 兴趣爱好 -->
  139. <view class="form-item">
  140. <text class="label">兴趣爱好</text>
  141. <input type="text" v-model="form.hobbies" placeholder="如: 阅读、运动、音乐" class="input" maxlength="200" />
  142. </view>
  143. <!-- 饮食偏好 -->
  144. <view class="form-item">
  145. <text class="label">饮食偏好</text>
  146. <input type="text" v-model="form.dietPreferences" placeholder="如: 清淡、无辣、低糖" class="input" maxlength="200" />
  147. </view>
  148. <!-- 成长规划师额外字段 -->
  149. <view class="form-section-title" v-if="form.role === 'teacher'">成长规划师信息</view>
  150. <!-- 成长规划师证书号(成长规划师必填) -->
  151. <view class="form-item" v-if="form.role === 'teacher'">
  152. <text class="label">成长规划师证书号 <text class="required">*</text></text>
  153. <input type="text" v-model="form.teacherNo" placeholder="请输入成长规划师证书号" class="input" maxlength="50" />
  154. </view>
  155. <!-- 成长规划师照片(成长规划师必填) -->
  156. <view class="form-item" v-if="form.role === 'teacher'">
  157. <text class="label">成长规划师照片 <text class="required">*</text></text>
  158. <view class="photo-upload" @click="chooseTeacherPhoto">
  159. <image v-if="form.teacherPhoto" :src="form.teacherPhoto" class="photo-preview" mode="aspectFill"></image>
  160. <view v-else class="photo-placeholder">
  161. <text class="plus-icon">+</text>
  162. <text class="placeholder-text">上传照片</text>
  163. </view>
  164. </view>
  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="chooseCertificate">
  170. <image v-if="form.certificateImage" :src="form.certificateImage" 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. </view>
  178. <!-- 助手选择(会员专属;新用户流程不展示,避免注册时被助手选择打扰) -->
  179. <view class="family-section" v-if="isEdit && !isNewUserFlow && memberLevel !== 'FREE'">
  180. <view class="section-title">🤖 选择助手</view>
  181. <view class="mascot-cards">
  182. <view class="mascot-card" :class="{ active: form.mascot === 'xibao' }" @click="selectMascot('xibao')">
  183. <view class="mascot-card-icon xibao-bg">🌟</view>
  184. <view class="mascot-card-info">
  185. <text class="mascot-card-name">浠宝</text>
  186. <text class="mascot-card-desc">温柔活泼的阳光女孩</text>
  187. </view>
  188. </view>
  189. <view class="mascot-card" :class="{ active: form.mascot === 'fubao' }" @click="selectMascot('fubao')">
  190. <view class="mascot-card-icon fubao-bg">💪</view>
  191. <view class="mascot-card-info">
  192. <text class="mascot-card-name">福宝</text>
  193. <text class="mascot-card-desc">开朗自信的阳光男孩</text>
  194. </view>
  195. </view>
  196. </view>
  197. </view>
  198. <!-- 免费用户提示(新用户首次注册信息完善时不展示,注册流程不推送会员) -->
  199. <view class="family-section" v-if="isEdit && !isNewUserFlow && memberLevel === 'FREE'">
  200. <view class="mascot-locked-hint" @click="goUpgrade">
  201. <text class="locked-icon">🔒</text>
  202. <text class="locked-text">开通会员解锁更多能力</text>
  203. <text class="locked-arrow">›</text>
  204. </view>
  205. </view>
  206. <!-- 修改个人资料保存按钮(非新用户流程时显示) -->
  207. <view class="family-section" v-if="isEdit && !isNewUserFlow && form.role !== 'teacher'">
  208. <view class="family-actions">
  209. <button class="btn btn-primary" @click="saveUserInfo" :loading="loading" :disabled="loading">
  210. 保存修改
  211. </button>
  212. </view>
  213. </view>
  214. <!-- 成长规划师提交按钮 -->
  215. <view class="family-section" v-if="isEdit && form.role === 'teacher' && isNewUserFlow">
  216. <view class="family-actions">
  217. <button class="btn btn-primary" @click="submitTeacherInfo" :loading="loading" :disabled="loading">
  218. 提交申请
  219. </button>
  220. </view>
  221. </view>
  222. </view>
  223. <view v-else-if="isEdit">
  224. <FamilyEmptyState type="card" />
  225. </view>
  226. <!-- 区域选择弹窗:统一使用 components/address-picker.vue(3 级省市区) -->
  227. <address-picker
  228. ref="addressPicker"
  229. :levels="3"
  230. :show-trigger="false"
  231. :initial="regionInitial"
  232. @change="onRegionChange"
  233. />
  234. </view>
  235. </template>
  236. <script>
  237. import { updateUserInfo, createFamily, directRegister, getUserInfo, requestJoinByCode, getMyMembership, bindReferralIfNotSelf } from '../../utils/api.js'
  238. import AddressPicker from '@/components/address-picker.vue'
  239. import { pickImage } from '@/utils/image-picker.js'
  240. export default {
  241. components: { AddressPicker },
  242. data() {
  243. return {
  244. isEdit: false,
  245. showRoleSelect: true,
  246. isNewUserFlow: false, // 是否是新用户注册流程
  247. memberLevel: 'FREE', // 会员等级,用于判断是否解锁助手
  248. form: {
  249. role: '',
  250. avatar: '',
  251. nickname: '',
  252. realName: '',
  253. gender: '',
  254. idCard: '',
  255. birthday: '',
  256. birthHour: '',
  257. inviteCode: '',
  258. familyRole: '', // 家庭角色:爸爸/妈妈/爷爷/奶奶/姥爷/姥姥/其他(可选)
  259. mascot: '',
  260. phone: '',
  261. teacherNo: '',
  262. teacherPhoto: '',
  263. certificateImage: '',
  264. ethnicity: '',
  265. bloodType: '',
  266. highestEducation: '',
  267. maritalStatus: '',
  268. hobbies: '',
  269. dietPreferences: '',
  270. address: '',
  271. province: '',
  272. city: '',
  273. district: '',
  274. street: ''
  275. },
  276. ethnicityOptions: ['汉族','蒙古族','回族','藏族','维吾尔族','苗族','彝族','壮族','布依族','朝鲜族','满族','侗族','瑶族','白族','土家族','哈尼族','哈萨克族','傣族','黎族','其他'],
  277. bloodTypeOptions: ['A','B','AB','O','未知'],
  278. 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)'],
  279. birthHourValues: ['子','丑','寅','卯','辰','巳','午','未','申','酉','戌','亥'],
  280. educationOptions: ['小学','初中','高中','中专','大专','本科','硕士','博士'],
  281. maritalOptions: ['未婚','已婚','离异','丧偶'],
  282. familyRoleOptions: ['爸爸', '妈妈', '爷爷', '奶奶', '姥爷', '姥姥', '其他'],
  283. loading: false,
  284. userId: '',
  285. token: ''
  286. }
  287. },
  288. computed: {
  289. ethnicityIndex() {
  290. return this.ethnicityOptions.indexOf(this.form.ethnicity)
  291. },
  292. bloodTypeIndex() {
  293. return this.bloodTypeOptions.indexOf(this.form.bloodType)
  294. },
  295. birthHourIndex() {
  296. return this.birthHourValues.indexOf(this.form.birthHour)
  297. },
  298. educationIndex() {
  299. return this.educationOptions.indexOf(this.form.highestEducation)
  300. },
  301. maritalIndex() {
  302. return this.maritalOptions.indexOf(this.form.maritalStatus)
  303. },
  304. familyRoleIndex: {
  305. get() {
  306. return this.familyRoleOptions.indexOf(this.form.familyRole)
  307. },
  308. set(val) {
  309. this.form.familyRole = this.familyRoleOptions[val] || ''
  310. }
  311. },
  312. hasFamily: function() {
  313. if (this.token) return true
  314. return this.$store.getters.hasFamily
  315. },
  316. // 编辑回显:把已保存的省/市/区县名称传给统一地址组件,打开时按名称回选
  317. regionInitial() {
  318. return {
  319. province: this.form.province || '',
  320. city: this.form.city || '',
  321. district: this.form.district || ''
  322. }
  323. }
  324. },
  325. onLoad(options) {
  326. // 注册零填写改造:本页仅保留「个人中心编辑资料」用途
  327. // 新用户注册不再跳转本页(autoParent / needsRoleSelect 参数已废弃,不再处理)
  328. this.showRoleSelect = false
  329. this.isEdit = true
  330. this.isNewUserFlow = false
  331. // 加载已有用户信息
  332. this.loadUserInfo()
  333. // 加载会员等级(用于助手解锁)
  334. var self = this
  335. getMyMembership().then(function(res) {
  336. if (res.data && res.data.memberLevel) {
  337. self.memberLevel = res.data.memberLevel
  338. }
  339. }).catch(function() {})
  340. },
  341. methods: {
  342. selectRole(role) {
  343. this.form.role = role
  344. },
  345. confirmRole() {
  346. if (!this.form.role) {
  347. uni.showToast({ title: '请选择角色', icon: 'none' })
  348. return
  349. }
  350. this.showRoleSelect = false
  351. this.isEdit = true
  352. // 测试环境:直接注册用户(不需要验证码)
  353. if (!this.token && this.form.phone) {
  354. // 自动进入家庭绑定流程
  355. }
  356. },
  357. chooseAvatar() {
  358. pickImage({
  359. count: 1,
  360. success: (res) => {
  361. this.form.avatar = res.tempFilePaths[0]
  362. }
  363. })
  364. },
  365. chooseTeacherPhoto() {
  366. pickImage({
  367. count: 1,
  368. success: (res) => {
  369. this.form.teacherPhoto = res.tempFilePaths[0]
  370. }
  371. })
  372. },
  373. chooseCertificate() {
  374. pickImage({
  375. count: 1,
  376. success: (res) => {
  377. this.form.certificateImage = res.tempFilePaths[0]
  378. }
  379. })
  380. },
  381. onGenderChange(e) {
  382. this.form.gender = e.detail.value
  383. },
  384. onIdCardInput(e) {
  385. const idCard = e.detail.value
  386. if (idCard.length === 18) {
  387. // 根据身份证号自动提取生日
  388. const year = idCard.substring(6, 10)
  389. const month = idCard.substring(10, 12)
  390. const day = idCard.substring(12, 14)
  391. this.form.birthday = `${year}-${month}-${day}`
  392. }
  393. },
  394. onBirthdayChange(e) {
  395. this.form.birthday = e.detail.value
  396. },
  397. onEthnicityChange(e) {
  398. this.form.ethnicity = this.ethnicityOptions[e.detail.value]
  399. },
  400. onBloodTypeChange(e) {
  401. this.form.bloodType = this.bloodTypeOptions[e.detail.value]
  402. },
  403. onBirthHourChange(e) {
  404. this.form.birthHour = this.birthHourValues[e.detail.value]
  405. },
  406. onEducationChange(e) {
  407. this.form.highestEducation = this.educationOptions[e.detail.value]
  408. },
  409. onMaritalChange(e) {
  410. this.form.maritalStatus = this.maritalOptions[e.detail.value]
  411. },
  412. onFamilyRoleChange(e) {
  413. this.form.familyRole = this.familyRoleOptions[e.detail.value] || ''
  414. },
  415. selectMascot(code) {
  416. this.form.mascot = code
  417. },
  418. goUpgrade() {
  419. uni.navigateTo({ url: '/pages/membership/upgrade' })
  420. },
  421. // ===== 区域选择(统一组件 components/address-picker.vue) =====
  422. openRegionPicker() {
  423. if (this.$refs.addressPicker) {
  424. this.$refs.addressPicker.open()
  425. }
  426. },
  427. onRegionChange(data) {
  428. this.form.province = data.province || ''
  429. this.form.city = data.city || ''
  430. this.form.district = data.district || ''
  431. this.updateFullAddress()
  432. },
  433. updateFullAddress() {
  434. this.form.address = (this.form.province || '') + (this.form.city || '') + (this.form.district || '') + (this.form.street || '')
  435. },
  436. // 加载已有用户信息
  437. async loadUserInfo() {
  438. try {
  439. const res = await getUserInfo()
  440. if (res.code === 200 && res.data) {
  441. const userData = res.data
  442. this.form.nickname = userData.nickname || ''
  443. this.form.realName = userData.realName || ''
  444. this.form.gender = userData.gender || ''
  445. this.form.idCard = userData.idCard || ''
  446. // 生日只取日期部分,去掉时间和 ISO 格式的 T 分隔符
  447. this.form.birthday = userData.birthday ? String(userData.birthday).split(/[T ]/)[0] : ''
  448. this.form.avatar = userData.avatar || ''
  449. this.form.role = userData.role || ''
  450. this.form.phone = userData.phone || ''
  451. this.form.ethnicity = userData.ethnicity || ''
  452. this.form.bloodType = userData.bloodType || ''
  453. this.form.highestEducation = userData.highestEducation || ''
  454. this.form.maritalStatus = userData.maritalStatus || ''
  455. this.form.hobbies = userData.hobbies || ''
  456. this.form.dietPreferences = userData.dietPreferences || ''
  457. this.form.mascot = userData.mascot || ''
  458. this.form.address = userData.address || ''
  459. this.form.province = userData.province || ''
  460. this.form.city = userData.city || ''
  461. this.form.district = userData.district || ''
  462. this.form.street = userData.street || ''
  463. // 若无完整地址文本但有省市区,则拼接
  464. if (!this.form.address && (this.form.province || this.form.city || this.form.district)) {
  465. this.updateFullAddress()
  466. }
  467. }
  468. } catch (e) {
  469. console.error('加载用户信息失败', e)
  470. }
  471. },
  472. // 保存修改
  473. async saveUserInfo() {
  474. if (this.loading) return
  475. // 新用户流程:昵称可跳过(跳过时用默认昵称);老用户流程:昵称必填
  476. if (!this.isNewUserFlow && !this.form.nickname) {
  477. uni.showToast({ title: '请输入昵称', icon: 'none' })
  478. return
  479. }
  480. if (this.isNewUserFlow && !this.form.nickname) {
  481. var phone = this.form.phone || ''
  482. this.form.nickname = phone ? '用户' + phone.substring(phone.length - 4) : '微信用户'
  483. }
  484. // 从省市区街道字段同步完整地址文本
  485. this.updateFullAddress()
  486. this.loading = true
  487. try {
  488. await updateUserInfo({
  489. nickname: this.form.nickname,
  490. realName: this.form.realName,
  491. gender: this.form.gender,
  492. idCard: this.form.idCard,
  493. birthday: this.form.birthday,
  494. avatar: this.form.avatar,
  495. mascot: this.form.mascot,
  496. ethnicity: this.form.ethnicity,
  497. bloodType: this.form.bloodType,
  498. highestEducation: this.form.highestEducation,
  499. maritalStatus: this.form.maritalStatus,
  500. hobbies: this.form.hobbies,
  501. dietPreferences: this.form.dietPreferences,
  502. address: this.form.address,
  503. province: this.form.province,
  504. city: this.form.city,
  505. district: this.form.district,
  506. street: this.form.street
  507. })
  508. uni.showToast({ title: '保存成功', icon: 'success' })
  509. setTimeout(() => {
  510. if (this.isNewUserFlow) {
  511. this.navigateToHome()
  512. } else {
  513. uni.navigateBack()
  514. }
  515. }, 1500)
  516. } catch (e) {
  517. uni.showToast({ title: '保存失败', icon: 'none' })
  518. } finally {
  519. this.loading = false
  520. }
  521. },
  522. async submitTeacherInfo() {
  523. if (this.loading) return
  524. // 验证必填字段
  525. if (!this.form.nickname) {
  526. uni.showToast({ title: '请输入昵称', icon: 'none' })
  527. return
  528. }
  529. if (!this.form.gender) {
  530. uni.showToast({ title: '请选择性别', icon: 'none' })
  531. return
  532. }
  533. if (!this.form.idCard) {
  534. uni.showToast({ title: '请输入身份证号', icon: 'none' })
  535. return
  536. }
  537. if (!this.form.teacherNo) {
  538. uni.showToast({ title: '请输入成长规划师证书号', icon: 'none' })
  539. return
  540. }
  541. if (!this.form.teacherPhoto) {
  542. uni.showToast({ title: '请上传成长规划师照片', icon: 'none' })
  543. return
  544. }
  545. if (!this.form.certificateImage) {
  546. uni.showToast({ title: '请上传成长规划师证书', icon: 'none' })
  547. return
  548. }
  549. this.loading = true
  550. try {
  551. await updateUserInfo({
  552. ...this.form,
  553. roles: this.form.role
  554. })
  555. uni.showToast({ title: '提交成功,请等待审核' })
  556. setTimeout(() => {
  557. uni.switchTab({ url: '/pages/index-home/index' })
  558. }, 1500)
  559. } catch (e) {
  560. uni.showToast({ title: '提交失败', icon: 'none' })
  561. } finally {
  562. this.loading = false
  563. }
  564. },
  565. async joinFamilyAction() {
  566. if (this.loading) return
  567. if (!this.form.inviteCode) {
  568. uni.showToast({ title: '请输入邀请码', icon: 'none' })
  569. return
  570. }
  571. if (!this.form.nickname) {
  572. uni.showToast({ title: '请输入昵称', icon: 'none' })
  573. return
  574. }
  575. this.loading = true
  576. try {
  577. let userId = this.userId
  578. // 测试环境:没有token,需要先注册用户
  579. if (!this.token && this.form.phone) {
  580. const regRes = await directRegister({
  581. phone: this.form.phone,
  582. nickname: this.form.nickname,
  583. avatar: this.form.avatar,
  584. role: this.form.role,
  585. mascot: this.form.mascot,
  586. birthday: this.form.birthday,
  587. gender: this.form.gender,
  588. inviteCode: this.form.inviteCode
  589. })
  590. userId = regRes.data.userId
  591. // 保存登录信息
  592. uni.setStorageSync('token', regRes.data.token)
  593. uni.setStorageSync('userId', userId)
  594. uni.setStorageSync('role', regRes.data.role)
  595. // 邀请码注册不写入 familyId(加入需管理员审批)
  596. } else {
  597. await updateUserInfo(this.form)
  598. await requestJoinByCode(this.form.inviteCode)
  599. }
  600. uni.showToast({ title: '申请已提交,等待管理员审批' })
  601. setTimeout(() => {
  602. this.navigateToHome()
  603. }, 1500)
  604. } catch (e) {
  605. uni.showToast({ title: e.message || '加入失败', icon: 'none' })
  606. } finally {
  607. this.loading = false
  608. }
  609. },
  610. async createFamilyAction() {
  611. if (!this.form.nickname) {
  612. uni.showToast({ title: '请输入昵称', icon: 'none' })
  613. return
  614. }
  615. this.loading = true
  616. try {
  617. let userId = this.userId
  618. // 测试环境:没有token,需要先注册用户
  619. if (!this.token && this.form.phone) {
  620. const regRes = await directRegister({
  621. phone: this.form.phone,
  622. nickname: this.form.nickname,
  623. avatar: this.form.avatar,
  624. role: this.form.role,
  625. mascot: this.form.mascot,
  626. birthday: this.form.birthday,
  627. gender: this.form.gender
  628. })
  629. userId = regRes.data.userId
  630. // 保存登录信息
  631. uni.setStorageSync('token', regRes.data.token)
  632. uni.setStorageSync('userId', userId)
  633. uni.setStorageSync('role', regRes.data.role)
  634. // 注册不写入 familyId(后端根据 userId 获取)
  635. } else {
  636. await updateUserInfo(this.form)
  637. const famRes = await createFamily(this.form.nickname + '的家庭')
  638. // 同步 familyId 到 store(hasFamily getter 依赖)
  639. if (famRes && famRes.data) {
  640. this.$store.commit('setFamilyId', famRes.data)
  641. }
  642. }
  643. uni.showToast({ title: '创建成功' })
  644. setTimeout(() => {
  645. this.navigateToHome()
  646. }, 1500)
  647. } catch (e) {
  648. uni.showToast({ title: e.message || '创建失败', icon: 'none' })
  649. } finally {
  650. this.loading = false
  651. }
  652. },
  653. skip() {
  654. // 跳过时不清理邀请码,用户可后续重新进入编辑页提交申请
  655. this.navigateToHome()
  656. },
  657. navigateToHome() {
  658. // 保存 mascot 到 userInfo 存储,供聊天页读取
  659. if (this.form.mascot) {
  660. try {
  661. const userInfo = uni.getStorageSync('userInfo') || {}
  662. userInfo.mascot = this.form.mascot
  663. uni.setStorageSync('userInfo', userInfo)
  664. } catch (e) {
  665. console.error('保存 mascot 失败', e)
  666. }
  667. }
  668. // 新用户流程:消费分享者推荐码,绑定推荐人(推荐码统一为 refCode storage)
  669. // 登录页 completeLogin 在 needsRoleSelect=true 时直接跳本页,跳过了 handleReferralBind,
  670. // 因此在此处统一消费 storage 中的 refCode/inviteCode,避免推荐人绑定丢失
  671. var refCode = uni.getStorageSync('refCode')
  672. var inviteCode = uni.getStorageSync('inviteCode')
  673. var inviteType = uni.getStorageSync('inviteType')
  674. var code = refCode || (inviteCode && inviteType !== 'team' ? inviteCode : '')
  675. if (code) {
  676. var self = this
  677. bindReferralIfNotSelf(code).then(function(res) {
  678. if (res && res.code === 200) {
  679. console.log('新用户绑定推荐人成功', code)
  680. }
  681. }).catch(function(e) {
  682. // 后端 Result.error(code=500) → request() 已 toast 业务消息
  683. if (e && e.code !== undefined && e.code !== 401) {
  684. console.log('新用户绑定推荐人业务拒绝', code, e.message || '')
  685. } else {
  686. console.log('新用户绑定推荐人失败', e)
  687. }
  688. })
  689. }
  690. uni.removeStorageSync('refCode')
  691. uni.removeStorageSync('inviteCode')
  692. uni.removeStorageSync('inviteType')
  693. // 用 switchTab 跳转带底部 TabBar 的首页(行/身/智/心/富)
  694. // parent-index/child-index 是分包页面(非 TabBar 页),用 navigateTo 会丢失底部 TabBar
  695. uni.switchTab({ url: '/pages/index-home/index' })
  696. }
  697. }
  698. }
  699. </script>
  700. <style scoped>
  701. .user-edit-container {
  702. min-height: 100vh;
  703. background: #f5f5f5;
  704. padding: 30rpx;
  705. }
  706. .header {
  707. margin-bottom: 40rpx;
  708. text-align: center;
  709. }
  710. .title {
  711. font-size: 40rpx;
  712. font-weight: bold;
  713. color: #333;
  714. display: block;
  715. margin-bottom: 10rpx;
  716. }
  717. .subtitle {
  718. font-size: 28rpx;
  719. color: #999;
  720. }
  721. /* 角色选择 */
  722. .role-select-section {
  723. margin-bottom: 40rpx;
  724. }
  725. .role-card {
  726. background: #fff;
  727. border-radius: 16rpx;
  728. padding: 40rpx;
  729. margin-bottom: 20rpx;
  730. display: flex;
  731. flex-direction: column;
  732. align-items: center;
  733. border: 2rpx solid transparent;
  734. transition: border-color 0.3s, background-color 0.3s;
  735. }
  736. .role-card.active {
  737. border-color: #4CAF50;
  738. background: #f8fdf8;
  739. }
  740. .role-icon {
  741. font-size: 80rpx;
  742. margin-bottom: 16rpx;
  743. }
  744. .role-name {
  745. font-size: 32rpx;
  746. font-weight: bold;
  747. color: #333;
  748. margin-bottom: 8rpx;
  749. }
  750. .role-desc {
  751. font-size: 24rpx;
  752. color: #999;
  753. }
  754. .btn-next {
  755. background: #4CAF50;
  756. color: #fff;
  757. border-radius: 40rpx;
  758. margin-top: 20rpx;
  759. }
  760. /* 表单 */
  761. .form-section {
  762. background: #fff;
  763. border-radius: 16rpx;
  764. padding: 30rpx;
  765. margin-bottom: 30rpx;
  766. }
  767. .form-section-title {
  768. font-size: 30rpx;
  769. font-weight: bold;
  770. color: #333;
  771. margin-bottom: 20rpx;
  772. padding-bottom: 16rpx;
  773. border-bottom: 1rpx solid #f0f0f0;
  774. }
  775. .form-item {
  776. margin-bottom: 30rpx;
  777. }
  778. .label {
  779. display: block;
  780. font-size: 28rpx;
  781. color: #333;
  782. margin-bottom: 16rpx;
  783. }
  784. .required {
  785. color: #f44336;
  786. }
  787. .input, .picker-input {
  788. padding: 20rpx;
  789. background: #f5f5f5;
  790. border-radius: 12rpx;
  791. font-size: 28rpx;
  792. }
  793. .role-select {
  794. display: flex;
  795. gap: 40rpx;
  796. }
  797. .role-option {
  798. display: flex;
  799. align-items: center;
  800. gap: 8rpx;
  801. }
  802. /* 头像上传 */
  803. .avatar-upload {
  804. width: 150rpx;
  805. height: 150rpx;
  806. }
  807. .avatar-preview {
  808. width: 150rpx;
  809. height: 150rpx;
  810. border-radius: 50%;
  811. }
  812. .avatar-placeholder {
  813. width: 150rpx;
  814. height: 150rpx;
  815. background: #f5f5f5;
  816. border-radius: 50%;
  817. display: flex;
  818. align-items: center;
  819. justify-content: center;
  820. font-size: 60rpx;
  821. color: #ccc;
  822. }
  823. /* 照片上传 */
  824. .photo-upload {
  825. width: 300rpx;
  826. height: 200rpx;
  827. }
  828. .photo-preview {
  829. width: 300rpx;
  830. height: 200rpx;
  831. border-radius: 12rpx;
  832. }
  833. .photo-placeholder {
  834. width: 300rpx;
  835. height: 200rpx;
  836. background: #f5f5f5;
  837. border-radius: 12rpx;
  838. display: flex;
  839. flex-direction: column;
  840. align-items: center;
  841. justify-content: center;
  842. }
  843. .plus-icon {
  844. font-size: 60rpx;
  845. color: #ccc;
  846. }
  847. .placeholder-text {
  848. font-size: 24rpx;
  849. color: #999;
  850. margin-top: 8rpx;
  851. }
  852. /* 助手选择 */
  853. .mascot-cards {
  854. display: flex;
  855. gap: 20rpx;
  856. margin-top: 16rpx;
  857. }
  858. .mascot-card {
  859. flex: 1;
  860. background: #f8f8f8;
  861. border-radius: 16rpx;
  862. padding: 24rpx;
  863. display: flex;
  864. align-items: center;
  865. gap: 16rpx;
  866. border: 2rpx solid transparent;
  867. transition: border-color 0.3s, background-color 0.3s;
  868. }
  869. .mascot-card.active {
  870. border-color: #4CAF50;
  871. background: #f0fdf4;
  872. }
  873. .mascot-card-icon {
  874. width: 72rpx;
  875. height: 72rpx;
  876. border-radius: 50%;
  877. display: flex;
  878. align-items: center;
  879. justify-content: center;
  880. font-size: 36rpx;
  881. flex-shrink: 0;
  882. }
  883. .xibao-bg {
  884. background: linear-gradient(135deg, #FFF0DB, #FFD6A5);
  885. }
  886. .fubao-bg {
  887. background: linear-gradient(135deg, #FFF8E1, #FCD34D);
  888. }
  889. .mascot-card-info {
  890. display: flex;
  891. flex-direction: column;
  892. gap: 4rpx;
  893. }
  894. .mascot-card-name {
  895. font-size: 30rpx;
  896. font-weight: bold;
  897. color: #333;
  898. }
  899. .mascot-card-desc {
  900. font-size: 22rpx;
  901. color: #999;
  902. }
  903. /* 家庭绑定 */
  904. .family-section {
  905. background: #fff;
  906. border-radius: 16rpx;
  907. padding: 30rpx;
  908. }
  909. .section-title {
  910. font-size: 30rpx;
  911. font-weight: bold;
  912. color: #333;
  913. margin-bottom: 20rpx;
  914. }
  915. .family-actions {
  916. display: flex;
  917. flex-direction: column;
  918. gap: 20rpx;
  919. margin-top: 30rpx;
  920. }
  921. /* 新用户流程(接受家庭邀请):提交申请/跳过固定悬浮页面底部,不随滚动消失 */
  922. .family-actions-fixed {
  923. position: fixed;
  924. left: 0;
  925. right: 0;
  926. bottom: 0;
  927. z-index: 100;
  928. margin-top: 0;
  929. padding: 20rpx 32rpx;
  930. padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
  931. background: #ffffff;
  932. box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.06);
  933. }
  934. /* 新用户流程表单底部留白,避免内容被固定悬浮按钮遮挡 */
  935. .user-form-wrap {
  936. padding-bottom: 340rpx;
  937. }
  938. .btn {
  939. border-radius: 40rpx;
  940. font-size: 30rpx;
  941. }
  942. .btn-primary {
  943. background: #4CAF50;
  944. color: #fff;
  945. }
  946. .btn-skip {
  947. background: #f5f5f5;
  948. color: #999;
  949. }
  950. /* ===== 地址行(所在地区) ===== */
  951. .address-row {
  952. display: flex;
  953. justify-content: space-between;
  954. align-items: center;
  955. }
  956. .address-value-wrap {
  957. display: flex;
  958. align-items: center;
  959. flex: 1;
  960. justify-content: flex-end;
  961. }
  962. .address-value {
  963. font-size: 28rpx;
  964. color: #333;
  965. }
  966. .address-placeholder {
  967. font-size: 28rpx;
  968. color: #999;
  969. }
  970. .address-arrow {
  971. font-size: 28rpx;
  972. color: #ccc;
  973. margin-left: 8rpx;
  974. }
  975. /* 助手锁定提示 */
  976. .mascot-locked-hint {
  977. display: flex;
  978. align-items: center;
  979. padding: 28rpx 32rpx;
  980. background: linear-gradient(135deg, #FEF3C7, #FDE68A);
  981. border-radius: 20rpx;
  982. margin-top: 16rpx;
  983. }
  984. .locked-icon {
  985. font-size: 36rpx;
  986. margin-right: 16rpx;
  987. }
  988. .locked-text {
  989. flex: 1;
  990. font-size: 28rpx;
  991. font-weight: 600;
  992. color: #92400e;
  993. }
  994. .locked-arrow {
  995. font-size: 32rpx;
  996. color: #92400e;
  997. margin-left: 8rpx;
  998. }
  999. </style>