Dashboard.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. <template>
  2. <el-container class="dashboard-container">
  3. <el-main class="dashboard-main">
  4. <div class="top-blocks-row">
  5. <div class="section-block flex-item">
  6. <h3 class="section-title">我的角色</h3>
  7. <div class="btn-group">
  8. <el-button
  9. v-for="item in roles"
  10. :key="item"
  11. icon="el-icon-user-solid"
  12. size="small"
  13. class="custom-btn"
  14. @click="goToRole(item)"
  15. >
  16. {{ item }}
  17. </el-button>
  18. </div>
  19. </div>
  20. <div class="section-block flex-item">
  21. <h3 class="section-title">快捷工作台</h3>
  22. <div class="btn-group">
  23. <el-button icon="el-icon-files" size="small" class="custom-btn" @click="navTo('/disk')">Disk</el-button>
  24. <el-button icon="el-icon-chat-dot-square" size="small" class="custom-btn" @click="navTo('/chat')">Chat</el-button>
  25. <el-button icon="el-icon-film" size="small" class="custom-btn" @click="navTo('/')">VOD</el-button>
  26. <el-button icon="el-icon-document" size="small" class="custom-btn" @click="navTo('/blog')">Blog</el-button>
  27. </div>
  28. </div>
  29. </div>
  30. <el-card class="box-card profile-card" shadow="hover">
  31. <div slot="header" class="card-header-custom">
  32. <i class="el-icon-postcard" />
  33. <span>个人档案资料</span>
  34. </div>
  35. <div class="profile-card-content">
  36. <div class="avatar-section">
  37. <div class="avatar-wrapper">
  38. <el-image :src="loginUser.avatarUrl" class="user-avatar" fit="cover">
  39. <div slot="error" class="avatar-error-slot">
  40. <i class="el-icon-user" />
  41. </div>
  42. </el-image>
  43. </div>
  44. <el-button
  45. type="primary"
  46. size="mini"
  47. icon="el-icon-camera"
  48. class="update-avatar-btn"
  49. @click="openAvatarDialog"
  50. >
  51. 更换头像
  52. </el-button>
  53. </div>
  54. <el-form ref="profileForm" :model="loginUser" label-width="80px" size="small" class="profile-form">
  55. <div class="form-grid">
  56. <el-form-item label="用户 ID">
  57. <el-input v-model="loginUser.userId" readonly class="is-readonly" />
  58. </el-form-item>
  59. <el-form-item label="用户名">
  60. <el-input v-model="loginUser.username" readonly class="is-readonly" />
  61. </el-form-item>
  62. <el-form-item label="显示名">
  63. <div class="input-with-action">
  64. <el-input v-model="loginUser.screenName" readonly />
  65. <el-button type="text" icon="el-icon-edit" @click="openEditDialog('screenName', '显示名', loginUser.screenName)">修改</el-button>
  66. </div>
  67. </el-form-item>
  68. <el-form-item label="电子邮箱">
  69. <div class="input-with-action">
  70. <el-input v-model="loginUser.email" readonly />
  71. <el-button type="text" icon="el-icon-edit" @click="openEditDialog('email', '电子邮箱', loginUser.email)">修改</el-button>
  72. </div>
  73. </el-form-item>
  74. <el-form-item label="手机号码">
  75. <div class="input-with-action">
  76. <el-input v-model="loginUser.mobile" readonly />
  77. <el-button type="text" icon="el-icon-edit" @click="openEditDialog('mobile', '手机号码', loginUser.mobile)">修改</el-button>
  78. </div>
  79. </el-form-item>
  80. <el-form-item label="安全密码">
  81. <div class="input-with-action">
  82. <el-input value="******" type="password" readonly show-password />
  83. <el-button type="text" icon="el-icon-key" @click="passwordDialogVisible = true">重置</el-button>
  84. </div>
  85. </el-form-item>
  86. </div>
  87. <el-form-item label="个性签名" class="full-width-item signature-item">
  88. <el-input
  89. ref="signatureInput"
  90. v-model="loginUser.signature"
  91. type="textarea"
  92. :rows="3"
  93. :readonly="!isEditingSignature"
  94. :placeholder="isEditingSignature ? '请输入您的个性签名...' : '这个人很懒,什么都没有留下~'"
  95. resize="none"
  96. />
  97. <div class="signature-actions">
  98. <el-button v-if="!isEditingSignature" type="text" icon="el-icon-edit" @click="startEditSignature">修改签名</el-button>
  99. <template v-else>
  100. <el-button size="mini" type="primary" round @click="saveSignature">保存</el-button>
  101. <el-button size="mini" round @click="cancelEditSignature">取消</el-button>
  102. </template>
  103. </div>
  104. </el-form-item>
  105. </el-form>
  106. </div>
  107. </el-card>
  108. </el-main>
  109. <el-dialog title="更新个人头像" :visible.sync="avatarDialogVisible" width="400px" append-to-body center custom-class="custom-dialog">
  110. <div class="avatar-upload-box">
  111. <el-upload
  112. class="avatar-uploader"
  113. :action="imgOssUrl"
  114. :headers="imgHeaders"
  115. :data="imgData"
  116. :with-credentials="false"
  117. :show-file-list="false"
  118. :before-upload="beforeAvatarUpload"
  119. :on-success="handleAvatarSuccess"
  120. >
  121. <div v-if="previewUrl" class="upload-preview-wrapper">
  122. <img :src="previewUrl" class="upload-preview" alt="preview">
  123. <div class="upload-mask"><i class="el-icon-edit" /><span>更换图片</span></div>
  124. </div>
  125. <div v-else-if="loginUser.avatarUrl" class="upload-preview-wrapper">
  126. <img :src="loginUser.avatarUrl" class="upload-preview" alt="current">
  127. <div class="upload-mask"><i class="el-icon-plus" /><span>上传新头像</span></div>
  128. </div>
  129. <div v-else class="upload-placeholder">
  130. <i class="el-icon-plus" /><p>点击选择新头像</p>
  131. </div>
  132. </el-upload>
  133. </div>
  134. <div class="upload-tip-text">仅支持 JPG 格式,大小不超过 2MB</div>
  135. <span slot="footer" class="dialog-footer">
  136. <el-button size="small" round @click="cancelAvatarUpdate">取 消</el-button>
  137. <el-button size="small" type="primary" round :loading="submitLoading" @click="confirmAvatarUpdate">确 认</el-button>
  138. </span>
  139. </el-dialog>
  140. <el-dialog :title="'修改' + editModel.title" :visible.sync="editDialogVisible" width="420px" append-to-body custom-class="custom-dialog">
  141. <el-form label-position="top" size="small" style="padding: 0 10px;">
  142. <el-form-item :label="'新的' + editModel.title">
  143. <el-input v-model="editModel.value" autocomplete="off" clearable :placeholder="'请输入新的' + editModel.title" />
  144. </el-form-item>
  145. </el-form>
  146. <span slot="footer" class="dialog-footer">
  147. <el-button size="small" round @click="editDialogVisible = false">取 消</el-button>
  148. <el-button size="small" type="primary" round :loading="editSubmitLoading" @click="submitSingleField">确 认</el-button>
  149. </span>
  150. </el-dialog>
  151. <el-dialog title="重置安全密码" :visible.sync="passwordDialogVisible" width="420px" append-to-body custom-class="custom-dialog">
  152. <el-form ref="passwordForm" :model="passwordForm" label-position="top" size="small" style="padding: 0 10px;">
  153. <el-form-item label="当前旧密码">
  154. <el-input v-model="passwordForm.oldPassword" type="password" show-password placeholder="请输入当前密码验证身份" />
  155. </el-form-item>
  156. <el-form-item label="输入新密码">
  157. <el-input v-model="passwordForm.newPassword" type="password" show-password placeholder="新密码长度建议大于6位" />
  158. </el-form-item>
  159. </el-form>
  160. <span slot="footer" class="dialog-footer">
  161. <el-button size="small" round @click="closePasswordDialog">取 消</el-button>
  162. <el-button size="small" type="primary" round :loading="passwordSubmitLoading" @click="submitPassword">确 认</el-button>
  163. </span>
  164. </el-dialog>
  165. </el-container>
  166. </template>
  167. <script>
  168. import { userMixin } from 'assets/js/mixin'
  169. import { getAuthedUser, updateAuthedUser } from '@/utils/auth'
  170. import { updateAvatar } from '@/api/account' // 假设你的其它更新 API 也在 account 或对应模块里
  171. import { getAvatarChannelInfo } from '@/api/file'
  172. import { addApprovalRequest } from '@/api/user'
  173. export default {
  174. name: 'Dashboard',
  175. mixins: [userMixin],
  176. data() {
  177. return {
  178. loginUser: {
  179. avatarUrl: '',
  180. gender: '',
  181. signature: '',
  182. userId: '',
  183. username: '',
  184. screenName: '',
  185. mobile: '',
  186. email: '',
  187. roles: []
  188. },
  189. roles: [],
  190. // 头像上传状态
  191. avatarDialogVisible: false,
  192. submitLoading: false,
  193. imgOssUrl: '',
  194. imgHeaders: { Authorization: '' },
  195. imgData: { channelCode: 0 },
  196. previewUrl: '',
  197. uploadSuccessData: null,
  198. // 常规单字段修改弹窗状态
  199. editDialogVisible: false,
  200. editSubmitLoading: false,
  201. editModel: {
  202. field: '',
  203. title: '',
  204. value: ''
  205. },
  206. // 签名行内修改状态
  207. isEditingSignature: false,
  208. cachedSignature: '',
  209. // 密码修改弹窗状态
  210. passwordDialogVisible: false,
  211. passwordSubmitLoading: false,
  212. passwordForm: {
  213. oldPassword: '',
  214. newPassword: ''
  215. }
  216. }
  217. },
  218. created() {
  219. this.initUserData()
  220. },
  221. methods: {
  222. initUserData() {
  223. document.title = 'Dashboard'
  224. const user = getAuthedUser()
  225. if (user) {
  226. this.loginUser = user
  227. this.roles = user.roles || []
  228. }
  229. },
  230. navTo(path) {
  231. if (this.$route.path === path) {
  232. this.$router.go(0)
  233. return
  234. }
  235. this.$router.push(path)
  236. },
  237. requireRole() {
  238. const requestData = {
  239. bizType: 'applyRole',
  240. bizPayload: JSON.stringify({ amount: 5 })
  241. }
  242. addApprovalRequest(requestData).then(resp => {
  243. if (resp.code === 0) {
  244. this.$message.info('角色申请请求已提交')
  245. } else {
  246. this.$message.warning(resp.msg)
  247. }
  248. }).catch(error => {
  249. this.$message.error('角色申请失败: ' + error.message)
  250. })
  251. },
  252. // ==================== 1. 常规单字段(显示名、邮箱、手机)修改业务 ====================
  253. openEditDialog(field, title, currentVal) {
  254. this.editModel.field = field
  255. this.editModel.title = title
  256. this.editModel.value = currentVal
  257. this.editDialogVisible = true
  258. },
  259. submitSingleField() {
  260. if (!this.editModel.value.trim()) {
  261. return this.$message.warning(`请输入有效的${this.editModel.title}`)
  262. }
  263. this.editSubmitLoading = true
  264. // 模拟或者组装你的 API 请求参数,例如:
  265. // const updateData = { [this.editModel.field]: this.editModel.value }
  266. // updateUserInfo(updateData).then(res => { ... })
  267. setTimeout(() => {
  268. // 1. 动态同步到本地模型
  269. this.loginUser[this.editModel.field] = this.editModel.value
  270. // 2. 持久化到 localStorage/cookie 缓存
  271. updateAuthedUser(this.loginUser)
  272. this.$message.success(`${this.editModel.title}已成功修改`)
  273. this.editDialogVisible = false
  274. this.editSubmitLoading = false
  275. }, 600)
  276. },
  277. // ==================== 2. 签名行内即时修改业务 ====================
  278. startEditSignature() {
  279. this.cachedSignature = this.loginUser.signature // 缓存旧签名以便取消
  280. this.isEditingSignature = true
  281. this.$nextTick(() => {
  282. this.$refs.signatureInput.focus() // 自动聚焦到输入框
  283. })
  284. },
  285. saveSignature() {
  286. // 触发后端 API 保存签名逻辑
  287. this.$message.success('个性签名已同步更新')
  288. updateAuthedUser(this.loginUser)
  289. this.isEditingSignature = false
  290. },
  291. cancelEditSignature() {
  292. this.loginUser.signature = this.cachedSignature // 恢复原状
  293. this.isEditingSignature = false
  294. },
  295. // ==================== 3. 独立密码修改业务 ====================
  296. submitPassword() {
  297. if (!this.passwordForm.oldPassword || !this.passwordForm.newPassword) {
  298. return this.$message.warning('请完整填写当前密码与新密码')
  299. }
  300. if (this.passwordForm.newPassword.length < 6) {
  301. return this.$message.warning('为了您的账户安全,新密码不得少于6位')
  302. }
  303. this.passwordSubmitLoading = true
  304. // 调用你的密码更新 API 接口
  305. // changePassword(this.passwordForm).then(res => { ... })
  306. setTimeout(() => {
  307. this.$message.success('安全密码修改成功,请妥善保管')
  308. this.closePasswordDialog()
  309. }, 800)
  310. },
  311. closePasswordDialog() {
  312. this.passwordDialogVisible = false
  313. this.passwordForm.oldPassword = ''
  314. this.passwordForm.newPassword = ''
  315. this.passwordSubmitLoading = false
  316. },
  317. // ==================== 4. 头像模块云存储控制(保持不变) ====================
  318. openAvatarDialog() {
  319. const pageLoading = this.$loading({
  320. lock: true,
  321. text: '正在开辟安全云传输通道...',
  322. spinner: 'el-icon-loading',
  323. background: 'rgba(255, 255, 255, 0.7)'
  324. })
  325. getAvatarChannelInfo().then(res => {
  326. if (res.code === 0) {
  327. this.imgData.channelCode = res.data.channelCode
  328. this.imgOssUrl = res.data.ossUrl
  329. this.imgHeaders.Authorization = 'Bearer ' + res.data.token
  330. this.avatarDialogVisible = true
  331. } else {
  332. this.$message.error(res.msg)
  333. }
  334. }).catch(error => {
  335. this.$message.error(error.message)
  336. }).finally(() => {
  337. pageLoading.close()
  338. })
  339. },
  340. beforeAvatarUpload(file) {
  341. if (!this.imgOssUrl) {
  342. this.$message.error('未检测到可用的云端存储节点!')
  343. return false
  344. }
  345. const isJPG = file.type === 'image/jpeg'
  346. const isLt2M = file.size / 1024 / 1024 < 2
  347. if (!isJPG) this.$message.error('仅允许上传 JPG 格式的图片!')
  348. if (!isLt2M) this.$message.error('图片体积不能超过 2MB!')
  349. return isJPG && isLt2M
  350. },
  351. handleAvatarSuccess(res, file) {
  352. if (res.code === 0) {
  353. this.previewUrl = URL.createObjectURL(file.raw)
  354. this.uploadSuccessData = res.data
  355. this.$message.success('新头像本地预载完成,请点击确认进行同步')
  356. } else {
  357. this.$message.error('文件服务器解析异常: ' + res.msg)
  358. }
  359. },
  360. confirmAvatarUpdate() {
  361. if (!this.uploadSuccessData) {
  362. return this.$message.warning('尚未检测到新上传的图像资源')
  363. }
  364. this.submitLoading = true
  365. const postParam = {
  366. channelCode: this.imgData.channelCode,
  367. uploadId: this.uploadSuccessData.uploadId
  368. }
  369. updateAvatar(postParam).then(resp => {
  370. if (resp.code === 0) {
  371. this.loginUser.avatarUrl = resp.data.avatarUrl
  372. updateAuthedUser(this.loginUser)
  373. this.$message.success('个人头像已成功同步至云端')
  374. this.avatarDialogVisible = false
  375. this.clearUploadStates()
  376. } else {
  377. this.$message.warning('配置档案同步失败,请稍后重试')
  378. }
  379. }).finally(() => {
  380. this.submitLoading = false
  381. })
  382. },
  383. cancelAvatarUpdate() {
  384. this.avatarDialogVisible = false
  385. this.clearUploadStates()
  386. },
  387. clearUploadStates() {
  388. if (this.previewUrl && this.previewUrl.startsWith('blob:')) {
  389. URL.revokeObjectURL(this.previewUrl)
  390. }
  391. this.previewUrl = ''
  392. this.uploadSuccessData = null
  393. },
  394. goToRole(role) { this.$message.info(`当前操作角色: ${role}`) }
  395. }
  396. }
  397. </script>
  398. <style scoped>
  399. /* 基础布局样式保持不变 */
  400. .dashboard-container { background-color: #f6f8f9; min-height: 100vh; }
  401. .dashboard-main { padding: 24px; }
  402. .top-blocks-row { display: flex; gap: 20px; margin-bottom: 20px; flex-wrap: wrap; }
  403. .flex-item { flex: 1; min-width: 320px; margin-bottom: 0 !important; }
  404. .section-block { background: #ffffff; padding: 20px; border-radius: 8px; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.02); border: 1px solid #eef1f5; }
  405. .section-title { margin-top: 0; margin-bottom: 18px; color: #1f2d3d; font-size: 15px; font-weight: 600; border-left: 4px solid #1890ff; padding-left: 12px; }
  406. .btn-group { display: flex; flex-wrap: wrap; gap: 10px; }
  407. .custom-btn { border-radius: 6px; transition: all 0.2s ease; background: #f8fafc; border-color: #e2e8f0; color: #475569; }
  408. .custom-btn:hover { background: #fff; border-color: #1890ff; color: #1890ff; transform: translateY(-1px); box-shadow: 0 2px 6px rgba(24, 144, 255, 0.15); }
  409. /* 资料档案卡片 */
  410. .profile-card { border-radius: 8px; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.02) !important; border: 1px solid #eef1f5; }
  411. .card-header-custom { display: flex; align-items: center; gap: 8px; font-weight: 600; color: #334155; font-size: 15px; }
  412. .card-header-custom i { color: #1890ff; font-size: 17px; }
  413. .profile-card-content { display: flex; gap: 40px; padding: 10px 0; flex-wrap: wrap; }
  414. /* 左侧头像展示区 */
  415. .avatar-section { display: flex; flex-direction: column; align-items: center; flex-shrink: 0; width: 140px; }
  416. .avatar-wrapper { padding: 4px; border: 1px solid #e2e8f0; border-radius: 50%; background: #fff; box-shadow: 0 4px 10px rgba(0,0,0,0.03); }
  417. .user-avatar { width: 110px; height: 110px; border-radius: 50%; background-color: #f1f5f9; }
  418. .avatar-error-slot { display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; background: #f8fafc; color: #94a3b8; font-size: 36px; }
  419. .update-avatar-btn { margin-top: 16px !important; width: 100%; border-radius: 20px; box-shadow: 0 4px 10px rgba(24, 144, 255, 0.15); }
  420. /* 右侧多列网格表单及动作按钮整合 */
  421. .profile-form { flex: 1; min-width: 280px; }
  422. .form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 4px 32px; }
  423. /* 核心:带操作按钮的复合型 Input */
  424. .input-with-action {
  425. display: flex;
  426. align-items: center;
  427. gap: 10px;
  428. }
  429. .input-with-action .el-input {
  430. flex: 1;
  431. }
  432. .input-with-action .el-button--text {
  433. padding: 0;
  434. font-weight: 500;
  435. color: #1890ff;
  436. }
  437. /* 独立修饰绝对不可编辑的项 */
  438. .is-readonly ::v-deep .el-input__inner {
  439. background-color: #f1f5f9 !important;
  440. color: #94a3b8 !important;
  441. }
  442. /* 签名大文本定制 */
  443. .full-width-item { grid-column: 1 / -1; margin-top: 12px; }
  444. .signature-item { position: relative; }
  445. .signature-actions {
  446. display: flex;
  447. justify-content: flex-end;
  448. gap: 10px;
  449. margin-top: 6px;
  450. }
  451. /* 全局统一下降级 Input 的只读底色质感 */
  452. ::v-deep .el-input__inner,
  453. ::v-deep .el-textarea__inner {
  454. border-color: #e2e8f0 !important;
  455. color: #334155 !important;
  456. background-color: #ffffff; /* 允许编辑的框默认亮白 */
  457. }
  458. ::v-deep .el-input__inner[readonly],
  459. ::v-deep .el-textarea__inner[readonly] {
  460. background-color: #f8fafc !important; /* 处于只读状态时柔和变灰 */
  461. }
  462. ::v-deep .el-form-item__label { color: #64748b; font-weight: 500; }
  463. /* 弹窗及云上传通用 */
  464. .avatar-upload-box { display: flex; justify-content: center; padding: 10px 0 5px 0; }
  465. .avatar-uploader ::v-deep .el-upload { border: 2px dashed #cbd5e1; border-radius: 50%; cursor: pointer; position: relative; overflow: hidden; width: 140px; height: 140px; background-color: #f8fafc; transition: all 0.25s ease; }
  466. .avatar-uploader ::v-deep .el-upload:hover { border-color: #1890ff; background-color: #f0f7ff; }
  467. .upload-placeholder { display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100%; color: #64748b; }
  468. .upload-placeholder i { font-size: 28px; margin-bottom: 6px; color: #94a3b8;}
  469. .upload-placeholder p { margin: 0; font-size: 13px; }
  470. .upload-preview-wrapper { position: relative; width: 140px; height: 140px; }
  471. .upload-preview { width: 140px; height: 140px; object-fit: cover; border-radius: 50%; }
  472. .upload-mask { position: absolute; inset: 0; background: rgba(15, 23, 42, 0.6); color: #fff; display: flex; flex-direction: column; gap: 6px; justify-content: center; align-items: center; font-size: 12px; border-radius: 50%; opacity: 0; transition: opacity 0.25s ease; }
  473. .upload-preview-wrapper:hover .upload-mask { opacity: 1; }
  474. .upload-tip-text { text-align: center; font-size: 12px; color: #64748b; margin-top: 12px; }
  475. /* 统一高级圆角弹窗 */
  476. ::v-deep .custom-dialog { border-radius: 12px; overflow: hidden; }
  477. </style>