Profile.vue 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. <template>
  2. <div class="profile-container">
  3. <el-card class="profile-card">
  4. <template #header>
  5. <div class="card-header">
  6. <h2>个人资料</h2>
  7. </div>
  8. </template>
  9. <el-tabs v-model="activeTab" type="border-card">
  10. <!-- 基本信息 -->
  11. <el-tab-pane label="基本信息" name="basic">
  12. <el-form
  13. ref="profileFormRef"
  14. :model="profileForm"
  15. :rules="profileRules"
  16. label-width="120px"
  17. style="max-width: 600px"
  18. >
  19. <el-form-item label="头像">
  20. <div class="avatar-upload">
  21. <el-avatar
  22. :size="100"
  23. :src="profileForm.avatar || defaultAvatar"
  24. class="avatar-preview"
  25. >
  26. <el-icon><User /></el-icon>
  27. </el-avatar>
  28. <el-upload
  29. class="avatar-uploader"
  30. action="#"
  31. :show-file-list="false"
  32. :before-upload="beforeAvatarUpload"
  33. :http-request="handleAvatarUpload"
  34. >
  35. <el-button type="primary" size="small">上传头像</el-button>
  36. </el-upload>
  37. <div class="avatar-tip">支持 JPG、PNG 格式,大小不超过 2MB</div>
  38. </div>
  39. </el-form-item>
  40. <el-form-item label="用户名">
  41. <el-input v-model="profileForm.username" disabled />
  42. </el-form-item>
  43. <el-form-item label="真实姓名" prop="realName">
  44. <el-input v-model="profileForm.realName" placeholder="请输入真实姓名" />
  45. </el-form-item>
  46. <el-form-item label="手机号" prop="phone">
  47. <el-input v-model="profileForm.phone" placeholder="请输入手机号" />
  48. </el-form-item>
  49. <el-form-item label="邮箱" prop="email">
  50. <el-input v-model="profileForm.email" placeholder="请输入邮箱" />
  51. </el-form-item>
  52. <el-form-item>
  53. <el-button type="primary" @click="handleSaveProfile" :loading="saving">
  54. 保存
  55. </el-button>
  56. <el-button @click="handleReset">重置</el-button>
  57. </el-form-item>
  58. </el-form>
  59. </el-tab-pane>
  60. <!-- 修改密码 -->
  61. <el-tab-pane label="修改密码" name="password">
  62. <el-form
  63. ref="passwordFormRef"
  64. :model="passwordForm"
  65. :rules="passwordRules"
  66. label-width="120px"
  67. style="max-width: 600px"
  68. >
  69. <el-form-item label="原密码" prop="oldPassword">
  70. <el-input
  71. v-model="passwordForm.oldPassword"
  72. type="password"
  73. placeholder="请输入原密码"
  74. show-password
  75. />
  76. </el-form-item>
  77. <el-form-item label="新密码" prop="newPassword">
  78. <el-input
  79. v-model="passwordForm.newPassword"
  80. type="password"
  81. placeholder="请输入新密码(至少6位)"
  82. show-password
  83. />
  84. </el-form-item>
  85. <el-form-item label="确认新密码" prop="confirmPassword">
  86. <el-input
  87. v-model="passwordForm.confirmPassword"
  88. type="password"
  89. placeholder="请再次输入新密码"
  90. show-password
  91. />
  92. </el-form-item>
  93. <el-form-item>
  94. <el-button type="primary" @click="handleChangePassword" :loading="changingPassword">
  95. 修改密码
  96. </el-button>
  97. <el-button @click="handleResetPassword">重置</el-button>
  98. </el-form-item>
  99. </el-form>
  100. </el-tab-pane>
  101. </el-tabs>
  102. </el-card>
  103. </div>
  104. </template>
  105. <script setup>
  106. import { ref, reactive, onMounted } from 'vue'
  107. import { ElMessage } from 'element-plus'
  108. import { User } from '@element-plus/icons-vue'
  109. import request from '@/utils/request'
  110. import { API_ENDPOINTS } from '@/config/api'
  111. import { useUserStore } from '@/stores/user'
  112. const userStore = useUserStore()
  113. const activeTab = ref('basic')
  114. const saving = ref(false)
  115. const changingPassword = ref(false)
  116. const profileFormRef = ref(null)
  117. const passwordFormRef = ref(null)
  118. const defaultAvatar = 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
  119. const profileForm = reactive({
  120. id: null,
  121. username: '',
  122. realName: '',
  123. phone: '',
  124. email: '',
  125. avatar: ''
  126. })
  127. const passwordForm = reactive({
  128. oldPassword: '',
  129. newPassword: '',
  130. confirmPassword: ''
  131. })
  132. // 表单验证规则
  133. const profileRules = {
  134. realName: [
  135. { max: 50, message: '真实姓名长度不能超过50个字符', trigger: 'blur' }
  136. ],
  137. phone: [
  138. { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
  139. ],
  140. email: [
  141. { type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
  142. ]
  143. }
  144. const passwordRules = {
  145. oldPassword: [
  146. { required: true, message: '请输入原密码', trigger: 'blur' }
  147. ],
  148. newPassword: [
  149. { required: true, message: '请输入新密码', trigger: 'blur' },
  150. { min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
  151. ],
  152. confirmPassword: [
  153. { required: true, message: '请确认新密码', trigger: 'blur' },
  154. {
  155. validator: (rule, value, callback) => {
  156. if (value !== passwordForm.newPassword) {
  157. callback(new Error('两次输入的密码不一致'))
  158. } else {
  159. callback()
  160. }
  161. },
  162. trigger: 'blur'
  163. }
  164. ]
  165. }
  166. // 加载个人资料
  167. const loadProfile = async () => {
  168. try {
  169. const response = await request.get(API_ENDPOINTS.ADMIN_USER_CURRENT_PROFILE)
  170. if (response.code === 200) {
  171. const data = response.data
  172. profileForm.id = data.id
  173. profileForm.username = data.username || ''
  174. profileForm.realName = data.realName || ''
  175. profileForm.phone = data.phone || ''
  176. profileForm.email = data.email || ''
  177. profileForm.avatar = data.avatar || ''
  178. }
  179. } catch (error) {
  180. console.error('加载个人资料失败:', error)
  181. ElMessage.error('加载个人资料失败')
  182. }
  183. }
  184. // 保存个人资料
  185. const handleSaveProfile = async () => {
  186. try {
  187. await profileFormRef.value.validate()
  188. saving.value = true
  189. const response = await request.put(API_ENDPOINTS.ADMIN_USER_UPDATE_CURRENT_PROFILE, {
  190. realName: profileForm.realName,
  191. phone: profileForm.phone,
  192. email: profileForm.email,
  193. avatar: profileForm.avatar
  194. })
  195. if (response.code === 200) {
  196. ElMessage.success('保存成功')
  197. // 更新用户信息
  198. await userStore.getUserInfo()
  199. } else {
  200. ElMessage.error(response.message || '保存失败')
  201. }
  202. } catch (error) {
  203. if (error !== false) { // validate 返回 false 时不显示错误
  204. console.error('保存个人资料失败:', error)
  205. ElMessage.error(error.message || '保存失败')
  206. }
  207. } finally {
  208. saving.value = false
  209. }
  210. }
  211. // 重置表单
  212. const handleReset = () => {
  213. loadProfile()
  214. }
  215. // 修改密码
  216. const handleChangePassword = async () => {
  217. try {
  218. await passwordFormRef.value.validate()
  219. changingPassword.value = true
  220. const response = await request.put(API_ENDPOINTS.ADMIN_USER_UPDATE_CURRENT_PASSWORD, {
  221. oldPassword: passwordForm.oldPassword,
  222. newPassword: passwordForm.newPassword
  223. })
  224. if (response.code === 200) {
  225. ElMessage.success('密码修改成功')
  226. handleResetPassword()
  227. } else {
  228. ElMessage.error(response.message || '密码修改失败')
  229. }
  230. } catch (error) {
  231. if (error !== false) { // validate 返回 false 时不显示错误
  232. console.error('修改密码失败:', error)
  233. ElMessage.error(error.message || '修改密码失败')
  234. }
  235. } finally {
  236. changingPassword.value = false
  237. }
  238. }
  239. // 重置密码表单
  240. const handleResetPassword = () => {
  241. passwordForm.oldPassword = ''
  242. passwordForm.newPassword = ''
  243. passwordForm.confirmPassword = ''
  244. passwordFormRef.value?.clearValidate()
  245. }
  246. // 头像上传前验证
  247. const beforeAvatarUpload = (file) => {
  248. const isImage = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/jpg'
  249. const isLt2M = file.size / 1024 / 1024 < 2
  250. if (!isImage) {
  251. ElMessage.error('头像图片只能是 JPG/PNG 格式!')
  252. return false
  253. }
  254. if (!isLt2M) {
  255. ElMessage.error('头像图片大小不能超过 2MB!')
  256. return false
  257. }
  258. return true
  259. }
  260. // 头像上传
  261. const handleAvatarUpload = async (options) => {
  262. try {
  263. const formData = new FormData()
  264. formData.append('file', options.file)
  265. const response = await request.post(API_ENDPOINTS.UPLOAD_IMAGE, formData, {
  266. headers: {
  267. 'Content-Type': 'multipart/form-data'
  268. }
  269. })
  270. if (response.code === 200 && response.data) {
  271. profileForm.avatar = response.data.url || response.data
  272. ElMessage.success('头像上传成功')
  273. } else {
  274. ElMessage.error(response.message || '头像上传失败')
  275. }
  276. } catch (error) {
  277. console.error('头像上传失败:', error)
  278. ElMessage.error('头像上传失败')
  279. }
  280. }
  281. onMounted(() => {
  282. loadProfile()
  283. })
  284. </script>
  285. <style scoped>
  286. .profile-container {
  287. padding: 20px;
  288. }
  289. .profile-card {
  290. max-width: 900px;
  291. margin: 0 auto;
  292. }
  293. .card-header h2 {
  294. margin: 0;
  295. font-size: 20px;
  296. font-weight: 600;
  297. color: #303133;
  298. }
  299. .avatar-upload {
  300. display: flex;
  301. flex-direction: column;
  302. align-items: flex-start;
  303. gap: 12px;
  304. }
  305. .avatar-preview {
  306. border: 2px solid #dcdfe6;
  307. cursor: pointer;
  308. }
  309. .avatar-uploader {
  310. margin-top: 8px;
  311. }
  312. .avatar-tip {
  313. font-size: 12px;
  314. color: #909399;
  315. margin-top: -4px;
  316. }
  317. :deep(.el-tabs__content) {
  318. padding: 24px;
  319. }
  320. :deep(.el-form-item__label) {
  321. font-weight: 500;
  322. }
  323. :deep(.el-input__inner) {
  324. max-width: 400px;
  325. }
  326. </style>