Profile.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. <template>
  2. <div class="profile-content">
  3. <div class="page-header">
  4. <h2>个人资料</h2>
  5. <p>管理您的个人信息和账户设置</p>
  6. </div>
  7. <div class="profile-container">
  8. <el-row :gutter="24">
  9. <!-- 左侧:基本信息 -->
  10. <el-col :span="16">
  11. <el-card class="profile-card">
  12. <template #header>
  13. <span>基本信息</span>
  14. </template>
  15. <el-form
  16. ref="profileFormRef"
  17. :model="profileForm"
  18. :rules="profileRules"
  19. label-width="100px"
  20. @submit.prevent="updateProfile"
  21. >
  22. <el-form-item label="用户名" prop="username">
  23. <el-input
  24. v-model="profileForm.username"
  25. placeholder="请输入用户名"
  26. :disabled="true"
  27. />
  28. <div class="form-tip">用户名不可修改</div>
  29. </el-form-item>
  30. <el-form-item label="邮箱" prop="email">
  31. <el-input
  32. v-model="profileForm.email"
  33. placeholder="请输入邮箱"
  34. type="email"
  35. />
  36. </el-form-item>
  37. <el-form-item label="手机号" prop="phone">
  38. <el-input
  39. v-model="profileForm.phone"
  40. placeholder="请输入手机号"
  41. />
  42. </el-form-item>
  43. <el-form-item label="真实姓名" prop="real_name">
  44. <el-input
  45. v-model="profileForm.real_name"
  46. placeholder="请输入真实姓名"
  47. />
  48. </el-form-item>
  49. <el-form-item label="公司" prop="company">
  50. <el-input
  51. v-model="profileForm.company"
  52. placeholder="请输入公司名称"
  53. />
  54. </el-form-item>
  55. <el-form-item label="部门" prop="department">
  56. <el-input
  57. v-model="profileForm.department"
  58. placeholder="请输入部门"
  59. />
  60. </el-form-item>
  61. <el-form-item label="职位" prop="position">
  62. <el-input
  63. v-model="profileForm.position"
  64. placeholder="请输入职位"
  65. />
  66. </el-form-item>
  67. <el-form-item>
  68. <el-button
  69. type="primary"
  70. :loading="loading"
  71. @click="updateProfile"
  72. >
  73. 保存修改
  74. </el-button>
  75. <el-button @click="resetForm">
  76. 重置
  77. </el-button>
  78. </el-form-item>
  79. </el-form>
  80. </el-card>
  81. </el-col>
  82. <!-- 右侧:头像和安全设置 -->
  83. <el-col :span="8">
  84. <!-- 头像设置 -->
  85. <el-card class="avatar-card">
  86. <template #header>
  87. <span>头像设置</span>
  88. </template>
  89. <div class="avatar-section">
  90. <el-avatar
  91. :src="profileForm.avatar_url"
  92. :size="120"
  93. class="user-avatar"
  94. >
  95. {{ profileForm.username?.charAt(0).toUpperCase() }}
  96. </el-avatar>
  97. <el-upload
  98. class="avatar-uploader"
  99. action="/api/v1/upload/avatar"
  100. :headers="uploadHeaders"
  101. :show-file-list="false"
  102. :on-success="handleAvatarSuccess"
  103. :before-upload="beforeAvatarUpload"
  104. >
  105. <el-button type="primary" size="small">
  106. <el-icon><Upload /></el-icon>
  107. 更换头像
  108. </el-button>
  109. </el-upload>
  110. </div>
  111. </el-card>
  112. <!-- 账户信息 -->
  113. <el-card class="account-info">
  114. <template #header>
  115. <span>账户信息</span>
  116. </template>
  117. <div class="info-item">
  118. <span class="label">用户ID:</span>
  119. <span class="value">{{ userStore.user?.id }}</span>
  120. </div>
  121. <div class="info-item">
  122. <span class="label">注册时间:</span>
  123. <span class="value">{{ formatDate(userStore.user?.created_at) }}</span>
  124. </div>
  125. <div class="info-item">
  126. <span class="label">最后登录:</span>
  127. <span class="value">{{ formatDate(userStore.user?.last_login_at) }}</span>
  128. </div>
  129. <div class="info-item">
  130. <span class="label">账户状态:</span>
  131. <el-tag :type="userStore.user?.is_active ? 'success' : 'danger'">
  132. {{ userStore.user?.is_active ? '正常' : '已禁用' }}
  133. </el-tag>
  134. </div>
  135. </el-card>
  136. <!-- 安全设置 -->
  137. <el-card class="security-card">
  138. <template #header>
  139. <span>安全设置</span>
  140. </template>
  141. <div class="security-actions">
  142. <el-button
  143. type="warning"
  144. size="small"
  145. @click="showChangePassword = true"
  146. >
  147. <el-icon><Lock /></el-icon>
  148. 修改密码
  149. </el-button>
  150. <el-button
  151. type="info"
  152. size="small"
  153. @click="$router.push('/user/tokens')"
  154. >
  155. <el-icon><Key /></el-icon>
  156. 令牌管理
  157. </el-button>
  158. <el-button
  159. type="success"
  160. size="small"
  161. @click="$router.push('/user/activity')"
  162. >
  163. <el-icon><Clock /></el-icon>
  164. 登录日志
  165. </el-button>
  166. </div>
  167. </el-card>
  168. </el-col>
  169. </el-row>
  170. </div>
  171. <!-- 修改密码对话框 -->
  172. <el-dialog
  173. v-model="showChangePassword"
  174. title="修改密码"
  175. width="400px"
  176. >
  177. <el-form
  178. ref="passwordFormRef"
  179. :model="passwordForm"
  180. :rules="passwordRules"
  181. label-width="100px"
  182. >
  183. <el-form-item label="当前密码" prop="old_password">
  184. <el-input
  185. v-model="passwordForm.old_password"
  186. type="password"
  187. placeholder="请输入当前密码"
  188. show-password
  189. />
  190. </el-form-item>
  191. <el-form-item label="新密码" prop="new_password">
  192. <el-input
  193. v-model="passwordForm.new_password"
  194. type="password"
  195. placeholder="请输入新密码"
  196. show-password
  197. />
  198. </el-form-item>
  199. <el-form-item label="确认密码" prop="confirm_password">
  200. <el-input
  201. v-model="passwordForm.confirm_password"
  202. type="password"
  203. placeholder="请再次输入新密码"
  204. show-password
  205. />
  206. </el-form-item>
  207. </el-form>
  208. <template #footer>
  209. <el-button @click="showChangePassword = false">取消</el-button>
  210. <el-button
  211. type="primary"
  212. :loading="passwordLoading"
  213. @click="changePassword"
  214. >
  215. 确定
  216. </el-button>
  217. </template>
  218. </el-dialog>
  219. </div>
  220. </template>
  221. <script setup lang="ts">
  222. import { ref, reactive, onMounted } from 'vue'
  223. import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
  224. import { useAuthStore } from '@/stores/auth'
  225. import { getToken } from '@/utils/auth'
  226. const userStore = useAuthStore()
  227. const loading = ref(false)
  228. const passwordLoading = ref(false)
  229. const showChangePassword = ref(false)
  230. const profileFormRef = ref<FormInstance>()
  231. const passwordFormRef = ref<FormInstance>()
  232. // 个人资料表单
  233. const profileForm = reactive({
  234. username: '',
  235. email: '',
  236. phone: '',
  237. real_name: '',
  238. company: '',
  239. department: '',
  240. position: '',
  241. avatar_url: ''
  242. })
  243. // 密码修改表单
  244. const passwordForm = reactive({
  245. old_password: '',
  246. new_password: '',
  247. confirm_password: ''
  248. })
  249. // 表单验证规则
  250. const profileRules: FormRules = {
  251. email: [
  252. { required: true, message: '请输入邮箱', trigger: 'blur' },
  253. { type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
  254. ],
  255. phone: [
  256. { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
  257. ]
  258. }
  259. const passwordRules: FormRules = {
  260. old_password: [
  261. { required: true, message: '请输入当前密码', trigger: 'blur' }
  262. ],
  263. new_password: [
  264. { required: true, message: '请输入新密码', trigger: 'blur' },
  265. { min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
  266. ],
  267. confirm_password: [
  268. { required: true, message: '请确认新密码', trigger: 'blur' },
  269. {
  270. validator: (rule, value, callback) => {
  271. if (value !== passwordForm.new_password) {
  272. callback(new Error('两次输入的密码不一致'))
  273. } else {
  274. callback()
  275. }
  276. },
  277. trigger: 'blur'
  278. }
  279. ]
  280. }
  281. // 上传头像的请求头
  282. const uploadHeaders = {
  283. Authorization: `Bearer ${getToken()}`
  284. }
  285. // 初始化表单数据
  286. const initForm = () => {
  287. if (userStore.user) {
  288. Object.assign(profileForm, {
  289. username: userStore.user.username,
  290. email: userStore.user.email,
  291. phone: userStore.user.phone || '',
  292. real_name: userStore.user.real_name || '',
  293. company: userStore.user.company || '',
  294. department: userStore.user.department || '',
  295. position: userStore.user.position || '',
  296. avatar_url: userStore.user.avatar_url || ''
  297. })
  298. }
  299. }
  300. // 更新个人资料
  301. const updateProfile = async () => {
  302. if (!profileFormRef.value) return
  303. try {
  304. await profileFormRef.value.validate()
  305. loading.value = true
  306. // TODO: 调用API更新用户信息
  307. await new Promise(resolve => setTimeout(resolve, 1000)) // 模拟API调用
  308. ElMessage.success('个人资料更新成功')
  309. // 更新store中的用户信息
  310. await userStore.fetchUserInfo()
  311. } catch (error) {
  312. console.error('更新个人资料失败:', error)
  313. ElMessage.error('更新失败,请重试')
  314. } finally {
  315. loading.value = false
  316. }
  317. }
  318. // 重置表单
  319. const resetForm = () => {
  320. initForm()
  321. ElMessage.info('表单已重置')
  322. }
  323. // 修改密码
  324. const changePassword = async () => {
  325. if (!passwordFormRef.value) return
  326. try {
  327. await passwordFormRef.value.validate()
  328. passwordLoading.value = true
  329. // TODO: 调用API修改密码
  330. await new Promise(resolve => setTimeout(resolve, 1000)) // 模拟API调用
  331. ElMessage.success('密码修改成功')
  332. showChangePassword.value = false
  333. // 重置密码表单
  334. Object.assign(passwordForm, {
  335. old_password: '',
  336. new_password: '',
  337. confirm_password: ''
  338. })
  339. } catch (error) {
  340. console.error('修改密码失败:', error)
  341. ElMessage.error('修改密码失败,请重试')
  342. } finally {
  343. passwordLoading.value = false
  344. }
  345. }
  346. // 头像上传成功回调
  347. const handleAvatarSuccess = (response: any) => {
  348. if (response.code === '000000' || response.code === 0) {
  349. profileForm.avatar_url = response.data.url
  350. ElMessage.success('头像上传成功')
  351. } else {
  352. ElMessage.error('头像上传失败')
  353. }
  354. }
  355. // 头像上传前验证
  356. const beforeAvatarUpload = (file: File) => {
  357. const isJPG = file.type === 'image/jpeg' || file.type === 'image/png'
  358. const isLt2M = file.size / 1024 / 1024 < 2
  359. if (!isJPG) {
  360. ElMessage.error('头像只能是 JPG/PNG 格式!')
  361. return false
  362. }
  363. if (!isLt2M) {
  364. ElMessage.error('头像大小不能超过 2MB!')
  365. return false
  366. }
  367. return true
  368. }
  369. // 格式化日期
  370. const formatDate = (date: string | undefined) => {
  371. if (!date) return '-'
  372. return new Date(date).toLocaleString('zh-CN')
  373. }
  374. onMounted(() => {
  375. initForm()
  376. })
  377. </script>
  378. <style scoped>
  379. .profile-content {
  380. padding: 20px;
  381. }
  382. .page-header {
  383. margin-bottom: 24px;
  384. }
  385. .page-header h2 {
  386. margin: 0 0 8px 0;
  387. color: #333;
  388. font-size: 24px;
  389. font-weight: 600;
  390. }
  391. .page-header p {
  392. margin: 0;
  393. color: #666;
  394. font-size: 14px;
  395. }
  396. .profile-container {
  397. max-width: 1200px;
  398. margin: 0 auto;
  399. }
  400. .profile-card {
  401. margin-bottom: 20px;
  402. }
  403. .form-tip {
  404. font-size: 12px;
  405. color: #999;
  406. margin-top: 4px;
  407. }
  408. .avatar-card {
  409. margin-bottom: 20px;
  410. }
  411. .avatar-section {
  412. text-align: center;
  413. }
  414. .user-avatar {
  415. margin-bottom: 16px;
  416. }
  417. .avatar-uploader {
  418. display: block;
  419. }
  420. .account-info {
  421. margin-bottom: 20px;
  422. }
  423. .info-item {
  424. display: flex;
  425. justify-content: space-between;
  426. align-items: center;
  427. padding: 8px 0;
  428. border-bottom: 1px solid #f0f0f0;
  429. }
  430. .info-item:last-child {
  431. border-bottom: none;
  432. }
  433. .label {
  434. font-size: 14px;
  435. color: #666;
  436. }
  437. .value {
  438. font-size: 14px;
  439. color: #333;
  440. word-break: break-all;
  441. }
  442. .security-card {
  443. margin-bottom: 20px;
  444. }
  445. .security-actions {
  446. display: flex;
  447. flex-direction: column;
  448. gap: 8px;
  449. }
  450. .security-actions .el-button {
  451. justify-content: flex-start;
  452. }
  453. </style>