user-edit.vue 31 KB

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