profile.vue 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. <template>
  2. <CommonTopBanner title="个人信息">
  3. <FlexCol :padding="30">
  4. <Form
  5. ref="formRef"
  6. :model="formModel"
  7. :rules="rules"
  8. validateTrigger="submit"
  9. labelAlign="right"
  10. :labelWidth="140"
  11. >
  12. <!-- 头像 -->
  13. <view class="avatar-section">
  14. <view class="avatar-container" @click="handleAvatarClick">
  15. <image
  16. :src="formModel.avatar || DefaultAvatar"
  17. class="avatar-image"
  18. mode="aspectFill"
  19. ></image>
  20. <text class="avatar-hint">点击可修改头像</text>
  21. </view>
  22. </view>
  23. <Field name="nickname" label="昵称" placeholder="请输入昵称" required />
  24. <Field name="bio" multiline label="个人简介" placeholder="输入个人简介" :inputStyle="{width: '240px'}" />
  25. </Form>
  26. <Height :height="40" />
  27. <Button type="primary" :loading="loading" @click="submitForm" >
  28. 保存修改
  29. </Button>
  30. <Height :height="20" />
  31. <Button :plain="true" @click="navTo('/pages/user/update/password')">
  32. 修改密码
  33. </Button>
  34. </FlexCol>
  35. </CommonTopBanner>
  36. </template>
  37. <script setup lang="ts">
  38. import { ref, onMounted } from 'vue';
  39. import { useAuthStore } from '@/store/auth';
  40. import UserApi from '@/api/auth/UserApi';
  41. import CommonContent from '@/api/CommonContent';
  42. import Form from '@/components/form/Form.vue';
  43. import Field from '@/components/form/Field.vue';
  44. import FlexCol from '@/components/layout/FlexCol.vue';
  45. import Button from '@/components/basic/Button.vue';
  46. import Height from '@/components/layout/space/Height.vue';
  47. import type { Rules } from 'async-validator';
  48. import { navTo } from '@/components/utils/PageAction';
  49. import { showError } from '@/common/composeabe/ErrorDisplay';
  50. import CommonTopBanner from '@/common/components/CommonTopBanner.vue';
  51. const DefaultAvatar = 'https://mncdn.wenlvti.net/app_static/xiangyuan/images/home/UserHead.png';
  52. const authStore = useAuthStore();
  53. const formRef = ref<any>(null);
  54. const loading = ref(false);
  55. const uploading = ref(false);
  56. const formModel = ref({
  57. avatar: '',
  58. nickname: '',
  59. bio: '',
  60. });
  61. const rules : Rules = {
  62. nickname: [
  63. { required: true, message: '请输入昵称' },
  64. { min: 2, message: '昵称长度至少2个字符' },
  65. { max: 20, message: '昵称长度最多20个字符' }
  66. ],
  67. bio: [
  68. { max: 100, message: '个人简介最多100个字符' }
  69. ],
  70. };
  71. // 处理头像点击事件
  72. const handleAvatarClick = async () => {
  73. try {
  74. // 选择图片
  75. const chooseResult = await uni.chooseImage({
  76. count: 1,
  77. sizeType: ['compressed'],
  78. sourceType: ['album', 'camera'],
  79. });
  80. const tempFilePath = chooseResult.tempFilePaths[0];
  81. // 上传图片
  82. uploading.value = true;
  83. const uploadResult = await CommonContent.uploadFile(tempFilePath, 'image');
  84. // 更新头像并保存到服务器
  85. await updateAvatar(uploadResult.fullurl);
  86. } catch (error: any) {
  87. if (error.errMsg !== 'chooseImage:fail cancel') {
  88. uni.showToast({
  89. title: '头像更换失败',
  90. icon: 'none',
  91. duration: 2000
  92. });
  93. }
  94. } finally {
  95. uploading.value = false;
  96. }
  97. };
  98. // 更新头像到服务器
  99. const updateAvatar = async (avatarUrl: string) => {
  100. try {
  101. await UserApi.updateSystemUserInfo({
  102. avatar: avatarUrl,
  103. nickname: formModel.value.nickname,
  104. bio: formModel.value.bio
  105. });
  106. formModel.value.avatar = avatarUrl;
  107. if (authStore.userInfo) {
  108. authStore.userInfo.avatar = avatarUrl;
  109. authStore.saveLoginState();
  110. }
  111. uni.showToast({
  112. title: '头像更新成功',
  113. icon: 'success',
  114. duration: 2000
  115. });
  116. authStore.refreshUserInfo();
  117. } catch (error: any) {
  118. throw new Error(error?.message || '头像更新失败');
  119. }
  120. };
  121. onMounted(() => {
  122. if (authStore.userInfo) {
  123. formModel.value.avatar = authStore.userInfo.avatar || '';
  124. formModel.value.nickname = authStore.userInfo.nickname || '';
  125. formModel.value.bio = (authStore.userInfo.intro || authStore.userInfo.bio || '') as string;
  126. }
  127. });
  128. // 提交表单
  129. const submitForm = async () => {
  130. try {
  131. await formRef.value?.validate();
  132. } catch {
  133. return;
  134. }
  135. loading.value = true;
  136. try {
  137. await UserApi.updateSystemUserInfo({
  138. avatar: formModel.value.avatar,
  139. nickname: formModel.value.nickname,
  140. bio: formModel.value.bio
  141. });
  142. uni.showToast({
  143. title: '个人信息更新成功',
  144. icon: 'success',
  145. duration: 2000
  146. });
  147. await authStore.refreshUserInfo();
  148. setTimeout(() => {
  149. uni.navigateBack();
  150. }, 2000);
  151. } catch (error: any) {
  152. showError(error, '个人信息更新失败');
  153. } finally {
  154. loading.value = false;
  155. }
  156. };
  157. </script>
  158. <style scoped>
  159. .avatar-section {
  160. text-align: center;
  161. }
  162. .avatar-label {
  163. font-size: 14px;
  164. color: #333333;
  165. margin-bottom: 12px;
  166. font-weight: 500;
  167. text-align: left;
  168. }
  169. .avatar-container {
  170. position: relative;
  171. display: flex;
  172. flex-direction: column;
  173. align-items: center;
  174. margin-bottom: 18px;
  175. }
  176. .avatar-image {
  177. width: 100px;
  178. height: 100px;
  179. border-radius: 50%;
  180. border: 2px solid #e0e0e0;
  181. background-color: #f5f5f5;
  182. }
  183. .avatar-hint {
  184. font-size: 12px;
  185. color: #007aff;
  186. margin-top: 4px;
  187. }
  188. /* 上传中遮罩 */
  189. .uploading-mask {
  190. position: fixed;
  191. top: 0;
  192. left: 0;
  193. width: 100%;
  194. height: 100%;
  195. background-color: rgba(0, 0, 0, 0.5);
  196. display: flex;
  197. justify-content: center;
  198. align-items: center;
  199. z-index: 9999;
  200. }
  201. .uploading-content {
  202. background-color: #ffffff;
  203. padding: 20px;
  204. border-radius: 8px;
  205. text-align: center;
  206. }
  207. .uploading-content uni-loading {
  208. margin-bottom: 10px;
  209. }
  210. </style>