edit-profile.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. <template>
  2. <view class="container">
  3. <!-- 顶部导航栏 -->
  4. <view class="header">
  5. <view class="back-btn" @click="goBack">
  6. <text class="back-icon">←</text>
  7. </view>
  8. <text class="header-title">编辑资料</text>
  9. <view class="save-btn" @click="handleSave">
  10. <text class="save-text">保存</text>
  11. </view>
  12. </view>
  13. <scroll-view class="scroll-content" scroll-y>
  14. <!-- 头像 -->
  15. <view class="profile-item" @click="handleAvatarClick">
  16. <text class="item-label">头像</text>
  17. <view class="item-value">
  18. <image class="avatar" :src="userInfo.avatar" mode="aspectFill"></image>
  19. <text class="arrow">></text>
  20. </view>
  21. </view>
  22. <!-- 昵称 -->
  23. <view class="profile-item" @click="handleNicknameClick">
  24. <text class="item-label">昵称</text>
  25. <view class="item-value">
  26. <text class="nickname-text">{{ userInfo.nickname }}</text>
  27. <text class="arrow">></text>
  28. </view>
  29. </view>
  30. <!-- 性别 -->
  31. <view class="profile-item" @click="handleGenderClick">
  32. <text class="item-label">性别</text>
  33. <view class="item-value">
  34. <text class="gender-text">{{ userInfo.gender || '未设置' }}</text>
  35. <text class="arrow">></text>
  36. </view>
  37. </view>
  38. <!-- 生日 -->
  39. <view class="profile-item" @click="handleBirthdayClick">
  40. <text class="item-label">生日</text>
  41. <view class="item-value">
  42. <text class="birthday-text">{{ userInfo.birthday || '未设置' }}</text>
  43. <text class="arrow">></text>
  44. </view>
  45. </view>
  46. <!-- 个人简介 -->
  47. <view class="profile-item" @click="handleBioClick">
  48. <text class="item-label">个人简介</text>
  49. <view class="item-value">
  50. <text class="bio-text">{{ userInfo.bio || '未设置' }}</text>
  51. <text class="arrow">></text>
  52. </view>
  53. </view>
  54. </scroll-view>
  55. <view class="login-mask" v-if="!isLogin">
  56. <button class="login-btn" @click="goToLogin">请先登录</button>
  57. </view>
  58. </view>
  59. </template>
  60. <script>
  61. import { getUserProfile, updateUserProfile } from '../../utils/api.js'
  62. const defaultAvatar = 'https://picsum.photos/seed/avatar/200/200'
  63. export default {
  64. data() {
  65. return {
  66. userInfo: {
  67. id: null,
  68. username: '',
  69. nickname: '',
  70. avatar: defaultAvatar,
  71. gender: '',
  72. birthday: '',
  73. bio: '',
  74. phone: '',
  75. email: ''
  76. },
  77. isLogin: false,
  78. isSaving: false
  79. }
  80. },
  81. onLoad() {
  82. this.loadUserInfo()
  83. },
  84. onShow() {
  85. // 先读取可能从文本编辑页返回的临时内容
  86. const tempBio = uni.getStorageSync('temp_text_bio')
  87. if (typeof tempBio === 'string') {
  88. // 如果有临时内容,直接设置到 userInfo,不重新加载服务器数据
  89. this.userInfo.bio = tempBio
  90. uni.removeStorageSync('temp_text_bio')
  91. // 同时更新本地存储
  92. const storedUser = uni.getStorageSync('userInfo')
  93. if (storedUser) {
  94. storedUser.bio = tempBio
  95. uni.setStorageSync('userInfo', storedUser)
  96. }
  97. return
  98. }
  99. // 如果没有临时内容,才从服务器加载
  100. this.loadUserInfo(true)
  101. },
  102. methods: {
  103. async loadUserInfo(forceRemote = false) {
  104. try {
  105. const storedUser = uni.getStorageSync('userInfo') || null
  106. const isLogin = uni.getStorageSync('isLogin')
  107. if (!storedUser || !storedUser.id || !isLogin) {
  108. this.isLogin = false
  109. this.userInfo = {
  110. ...this.userInfo,
  111. id: null,
  112. username: '',
  113. nickname: '',
  114. avatar: defaultAvatar,
  115. gender: '',
  116. birthday: '',
  117. bio: '',
  118. phone: '',
  119. email: ''
  120. }
  121. return
  122. }
  123. this.isLogin = true
  124. this.userInfo = {
  125. ...this.userInfo,
  126. ...storedUser,
  127. avatar: storedUser.avatar || defaultAvatar
  128. }
  129. if (!forceRemote && this.userInfo.nickname) {
  130. return
  131. }
  132. const res = await getUserProfile(storedUser.id)
  133. if (res && res.code === 200 && res.data) {
  134. this.userInfo = {
  135. ...this.userInfo,
  136. ...res.data,
  137. avatar: res.data.avatar || defaultAvatar
  138. }
  139. uni.setStorageSync('userInfo', {
  140. ...storedUser,
  141. ...res.data
  142. })
  143. }
  144. } catch (e) {
  145. console.error('加载用户资料失败:', e)
  146. uni.showToast({
  147. title: '加载失败,请重试',
  148. icon: 'none'
  149. })
  150. }
  151. },
  152. goBack() {
  153. uni.navigateBack()
  154. },
  155. async handleSave() {
  156. try {
  157. if (!this.isLogin || !this.userInfo.id) {
  158. uni.showToast({ title: '请先登录', icon: 'none' })
  159. return
  160. }
  161. if (this.isSaving) return
  162. this.isSaving = true
  163. uni.showLoading({ title: '保存中...' })
  164. const payload = {
  165. userId: this.userInfo.id,
  166. nickname: this.userInfo.nickname || '',
  167. avatar: this.userInfo.avatar === defaultAvatar ? '' : (this.userInfo.avatar || ''),
  168. gender: this.userInfo.gender || '',
  169. birthday: this.userInfo.birthday || '',
  170. bio: this.userInfo.bio || '',
  171. phone: this.userInfo.phone || '',
  172. email: this.userInfo.email || ''
  173. }
  174. const res = await updateUserProfile(payload)
  175. uni.hideLoading()
  176. this.isSaving = false
  177. if (res && res.code === 200) {
  178. const merged = {
  179. ...this.userInfo,
  180. ...res.data,
  181. avatar: res.data.avatar || this.userInfo.avatar || defaultAvatar
  182. }
  183. this.userInfo = merged
  184. uni.setStorageSync('userInfo', merged)
  185. uni.showToast({ title: '保存成功', icon: 'success' })
  186. setTimeout(() => uni.navigateBack(), 800)
  187. } else {
  188. uni.showToast({ title: (res && res.message) || '保存失败', icon: 'none' })
  189. }
  190. } catch (e) {
  191. uni.hideLoading()
  192. this.isSaving = false
  193. console.error('保存用户资料失败:', e)
  194. uni.showToast({ title: '网络错误,请稍后再试', icon: 'none' })
  195. }
  196. },
  197. handleAvatarClick() {
  198. uni.chooseImage({
  199. count: 1,
  200. sizeType: ['compressed'],
  201. sourceType: ['album', 'camera'],
  202. success: (res) => {
  203. const tempFilePath = res.tempFilePaths[0]
  204. // 这里可以上传图片到服务器
  205. // 暂时直接使用本地路径
  206. this.userInfo.avatar = tempFilePath
  207. uni.showToast({
  208. title: '头像已更新',
  209. icon: 'success'
  210. })
  211. }
  212. })
  213. },
  214. handleNicknameClick() {
  215. uni.showModal({
  216. title: '修改昵称',
  217. editable: true,
  218. placeholderText: '请输入昵称',
  219. content: this.userInfo.nickname,
  220. success: (res) => {
  221. if (res.confirm && res.content) {
  222. this.userInfo.nickname = res.content.trim()
  223. }
  224. }
  225. })
  226. },
  227. handleGenderClick() {
  228. if (!this.isLogin) {
  229. this.goToLogin()
  230. return
  231. }
  232. uni.showActionSheet({
  233. itemList: ['男', '女', '保密'],
  234. success: (res) => {
  235. const genders = ['男', '女', '保密']
  236. this.userInfo.gender = genders[res.tapIndex]
  237. }
  238. })
  239. },
  240. handleBirthdayClick() {
  241. if (!this.isLogin) {
  242. this.goToLogin()
  243. return
  244. }
  245. // 兼容各端:使用可编辑输入框让用户填写 YYYY-MM-DD
  246. uni.showModal({
  247. title: '设置生日',
  248. editable: true,
  249. placeholderText: '格式:YYYY-MM-DD',
  250. content: this.userInfo.birthday || '',
  251. success: (modalRes) => {
  252. if (modalRes.confirm) {
  253. const value = (modalRes.content || '').trim()
  254. // 简单校验 YYYY-MM-DD
  255. const ok = /^\d{4}-\d{2}-\d{2}$/.test(value)
  256. if (!value || ok) {
  257. this.userInfo.birthday = value
  258. } else {
  259. uni.showToast({ title: '日期格式应为YYYY-MM-DD', icon: 'none' })
  260. }
  261. }
  262. }
  263. })
  264. },
  265. handleBioClick() {
  266. if (!this.isLogin) {
  267. this.goToLogin()
  268. return
  269. }
  270. // 跳转到文本编辑页面
  271. const bioValue = this.userInfo.bio || ''
  272. uni.navigateTo({
  273. url: `/pages/text-edit/text-edit?key=bio&value=${encodeURIComponent(bioValue)}&placeholder=${encodeURIComponent('请输入个人简介')}&title=${encodeURIComponent('个人简介')}`
  274. })
  275. },
  276. goToLogin() {
  277. uni.navigateTo({
  278. url: '/pages/login/login'
  279. })
  280. }
  281. }
  282. }
  283. </script>
  284. <style scoped>
  285. .container {
  286. width: 100%;
  287. height: 100vh;
  288. background-color: #FFFFFF;
  289. display: flex;
  290. flex-direction: column;
  291. padding-top: 30px;
  292. box-sizing: border-box;
  293. position: relative;
  294. }
  295. .header {
  296. display: flex;
  297. align-items: center;
  298. justify-content: space-between;
  299. padding: 20rpx 30rpx;
  300. background-color: #FFFFFF;
  301. border-bottom: 1rpx solid #E0E0E0;
  302. position: relative;
  303. }
  304. .back-btn {
  305. width: 60rpx;
  306. height: 60rpx;
  307. display: flex;
  308. align-items: center;
  309. justify-content: center;
  310. }
  311. .back-icon {
  312. font-size: 40rpx;
  313. color: #333333;
  314. font-weight: bold;
  315. }
  316. .header-title {
  317. position: absolute;
  318. left: 50%;
  319. transform: translateX(-50%);
  320. font-size: 36rpx;
  321. font-weight: bold;
  322. color: #333333;
  323. }
  324. .save-btn {
  325. width: 80rpx;
  326. height: 60rpx;
  327. display: flex;
  328. align-items: center;
  329. justify-content: center;
  330. }
  331. .save-text {
  332. font-size: 28rpx;
  333. color: #4FC3F7;
  334. }
  335. .scroll-content {
  336. flex: 1;
  337. width: 100%;
  338. }
  339. .profile-item {
  340. display: flex;
  341. align-items: center;
  342. justify-content: space-between;
  343. padding: 30rpx;
  344. border-bottom: 1rpx solid #F0F0F0;
  345. background-color: #FFFFFF;
  346. }
  347. .item-label {
  348. font-size: 30rpx;
  349. color: #666666;
  350. }
  351. .item-value {
  352. display: flex;
  353. align-items: center;
  354. flex: 1;
  355. justify-content: flex-end;
  356. margin-left: 30rpx;
  357. }
  358. .avatar {
  359. width: 100rpx;
  360. height: 100rpx;
  361. border-radius: 50%;
  362. background-color: #F5F5F5;
  363. margin-right: 20rpx;
  364. }
  365. .nickname-text,
  366. .gender-text,
  367. .birthday-text,
  368. .bio-text {
  369. font-size: 30rpx;
  370. color: #333333;
  371. flex: 1;
  372. text-align: right;
  373. margin-right: 20rpx;
  374. }
  375. .bio-text {
  376. max-width: 400rpx;
  377. overflow: hidden;
  378. text-overflow: ellipsis;
  379. white-space: nowrap;
  380. }
  381. .arrow {
  382. font-size: 32rpx;
  383. color: #CCCCCC;
  384. }
  385. .login-mask {
  386. position: absolute;
  387. left: 0;
  388. right: 0;
  389. top: 0;
  390. bottom: 0;
  391. display: flex;
  392. align-items: center;
  393. justify-content: center;
  394. background-color: rgba(255, 255, 255, 0.92);
  395. }
  396. .login-btn {
  397. width: 220rpx;
  398. height: 80rpx;
  399. background-color: #4FC3F7;
  400. color: #FFFFFF;
  401. border: none;
  402. border-radius: 40rpx;
  403. font-size: 30rpx;
  404. display: flex;
  405. align-items: center;
  406. justify-content: center;
  407. }
  408. </style>