feat: 个人信息修改
This commit is contained in:
24
components.d.ts
vendored
24
components.d.ts
vendored
@@ -12,25 +12,27 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AvatarCropper: typeof import('./src/components/baseLayouts/AvatarCropper.vue')['default']
|
||||
CreateRoomDialog: typeof import('./src/components/baseLayouts/CreateRoomDialog.vue')['default']
|
||||
EditProfileDialog: typeof import('./src/components/baseLayouts/EditProfileDialog.vue')['default']
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
ElInputOtp: typeof import('element-plus/es')['ElInputOtp']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSubmenu: typeof import('element-plus/es')['ElSubmenu']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
GameList: typeof import('./src/components/baseLayouts/GameList.vue')['default']
|
||||
Lobby: typeof import('./src/components/baseLayouts/Lobby.vue')['default']
|
||||
@@ -43,25 +45,27 @@ declare module 'vue' {
|
||||
|
||||
// For TSX support
|
||||
declare global {
|
||||
const AvatarCropper: typeof import('./src/components/baseLayouts/AvatarCropper.vue')['default']
|
||||
const CreateRoomDialog: typeof import('./src/components/baseLayouts/CreateRoomDialog.vue')['default']
|
||||
const EditProfileDialog: typeof import('./src/components/baseLayouts/EditProfileDialog.vue')['default']
|
||||
const ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
const ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||
const ElButton: typeof import('element-plus/es')['ElButton']
|
||||
const ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
const ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
const ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
const ElForm: typeof import('element-plus/es')['ElForm']
|
||||
const ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
const ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
const ElInput: typeof import('element-plus/es')['ElInput']
|
||||
const ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
const ElInputOtp: typeof import('element-plus/es')['ElInputOtp']
|
||||
const ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
const ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
const ElOption: typeof import('element-plus/es')['ElOption']
|
||||
const ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||
const ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
const ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
const ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
const ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
const ElSubmenu: typeof import('element-plus/es')['ElSubmenu']
|
||||
const ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
const ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
const ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
const GameList: typeof import('./src/components/baseLayouts/GameList.vue')['default']
|
||||
const Lobby: typeof import('./src/components/baseLayouts/Lobby.vue')['default']
|
||||
|
||||
142
package-lock.json
generated
142
package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"axios": "^1.17.0",
|
||||
"cropperjs": "^2.1.1",
|
||||
"element-plus": "^2.14.2",
|
||||
"pinia": "^3.0.4",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
@@ -20,6 +21,7 @@
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@tsconfig/node24": "^24.0.4",
|
||||
"@types/node": "^24.12.2",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"@vitejs/plugin-vue": "^6.0.6",
|
||||
"@vitejs/plugin-vue-jsx": "^5.1.5",
|
||||
"@vue/tsconfig": "^0.9.1",
|
||||
@@ -499,6 +501,126 @@
|
||||
"devOptional": true,
|
||||
"license": "(Apache-2.0 AND BSD-3-Clause)"
|
||||
},
|
||||
"node_modules/@cropper/element": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@cropper/element/-/element-2.1.1.tgz",
|
||||
"integrity": "sha512-pthgIQq3PFAFRGUts96yrMgmMY/4rS/zKEq/Vvw7L0Ur09MgzrUg15z4k96K53bJ8XvNeXdQ0qIDw90gh4Xcug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cropper/utils": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@cropper/element-canvas": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@cropper/element-canvas/-/element-canvas-2.1.1.tgz",
|
||||
"integrity": "sha512-QHiHnPHsykkc4salOiCLOIKWPK3sLQ1SzpJYFE/yQPvqTPu6ERZNcxVwB1x7WhNN+SdP0S0CEHPYIkgIfUo7VA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cropper/element": "^2.1.1",
|
||||
"@cropper/utils": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@cropper/element-crosshair": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@cropper/element-crosshair/-/element-crosshair-2.1.1.tgz",
|
||||
"integrity": "sha512-DtZnOiY2RZSizDZzMUvya3wZ55YnWclf3hmIrLW6jdcFlQkYEbX6bkOyOJzQmEX0yIbmwMkXfFcMkEZ9dE1rJQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cropper/element": "^2.1.1",
|
||||
"@cropper/utils": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@cropper/element-grid": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@cropper/element-grid/-/element-grid-2.1.1.tgz",
|
||||
"integrity": "sha512-MZdPBh5QMU10T78nqG+5cK+gq4DHoWZvdOo+/wZ9/z3VH0u21sROeHOCnf0QAdxguPHSSFsPjzARK+ffS4Loog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cropper/element": "^2.1.1",
|
||||
"@cropper/utils": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@cropper/element-handle": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@cropper/element-handle/-/element-handle-2.1.1.tgz",
|
||||
"integrity": "sha512-TmSSphyMr2NEA0DqUTf9PCN8kcpzM0BxreOMyzbfuq1NUUePV1LxYgSOUT7dftMjZnTcu8cHQeFYa69U2BZnBA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cropper/element": "^2.1.1",
|
||||
"@cropper/utils": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@cropper/element-image": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@cropper/element-image/-/element-image-2.1.1.tgz",
|
||||
"integrity": "sha512-cldzN3hVJo0Luui+FDSIkmbQtwIjxWiTcTMLCMSi3hqIq6lc7YOBz+rC5R8AYnYV7YvsWonE6PrJZS+iZpBkdQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cropper/element": "^2.1.1",
|
||||
"@cropper/element-canvas": "^2.1.1",
|
||||
"@cropper/utils": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@cropper/element-selection": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@cropper/element-selection/-/element-selection-2.1.1.tgz",
|
||||
"integrity": "sha512-QtMYhcuR+8JG3QEv8iSRCfQEVYljor61vLPrnGCFIkDJhvcyEWDx4H/Kfn6Jrdo90PyXf2VbpTG7TgH8i/EZFg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cropper/element": "^2.1.1",
|
||||
"@cropper/element-canvas": "^2.1.1",
|
||||
"@cropper/element-image": "^2.1.1",
|
||||
"@cropper/utils": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@cropper/element-shade": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@cropper/element-shade/-/element-shade-2.1.1.tgz",
|
||||
"integrity": "sha512-MhJVc3jC87TjQu4EfBnNFddL3LxvA+Wzjy1Qhyi0wNdeTAzs97uYJ3KwGVtvzPYP/4JaSU4JDpw+BfIdUqR0bg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cropper/element": "^2.1.1",
|
||||
"@cropper/element-canvas": "^2.1.1",
|
||||
"@cropper/element-selection": "^2.1.1",
|
||||
"@cropper/utils": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@cropper/element-viewer": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@cropper/element-viewer/-/element-viewer-2.1.1.tgz",
|
||||
"integrity": "sha512-RpXTGW/rTtJVNd3/R3imdCBJzyZRhA/OeWNo7RID4qW7UYEiL6Lwh7Six3QhFIci/IaUAyL9NAAN10av2YWqog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cropper/element": "^2.1.1",
|
||||
"@cropper/element-canvas": "^2.1.1",
|
||||
"@cropper/element-image": "^2.1.1",
|
||||
"@cropper/element-selection": "^2.1.1",
|
||||
"@cropper/utils": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@cropper/elements": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@cropper/elements/-/elements-2.1.1.tgz",
|
||||
"integrity": "sha512-cHShk4l6iExM4S6B1xx7+09gtDs+kdxWqe7Yv6DEbAyWhxVIVJyJAPpKlzLfA5MYnlSnkvCvY66hLLQPwooRMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cropper/element": "^2.1.1",
|
||||
"@cropper/element-canvas": "^2.1.1",
|
||||
"@cropper/element-crosshair": "^2.1.1",
|
||||
"@cropper/element-grid": "^2.1.1",
|
||||
"@cropper/element-handle": "^2.1.1",
|
||||
"@cropper/element-image": "^2.1.1",
|
||||
"@cropper/element-selection": "^2.1.1",
|
||||
"@cropper/element-shade": "^2.1.1",
|
||||
"@cropper/element-viewer": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@cropper/utils": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@cropper/utils/-/utils-2.1.1.tgz",
|
||||
"integrity": "sha512-0eQs08WyQUNFMfU3ieE091dEQkrW0YdgZ3n6Thnk1EUQv45ypfiL+MevHwr7UzNnUlqUYR2FNSCgE6qBxK0sFw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@ctrl/tinycolor": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz",
|
||||
@@ -1647,6 +1769,16 @@
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qrcode": {
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmmirror.com/@types/qrcode/-/qrcode-1.5.6.tgz",
|
||||
"integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.21",
|
||||
"resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
||||
@@ -2373,6 +2505,16 @@
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/cropperjs": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/cropperjs/-/cropperjs-2.1.1.tgz",
|
||||
"integrity": "sha512-FDJMarkY+/SepYarPZsvkG2LmI2PElecciMFnvBiBIoKnFYua/scprC5qejCLLyuX2jEqJRS2njbAsHxfjtIXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cropper/elements": "^2.1.1",
|
||||
"@cropper/utils": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.17.0",
|
||||
"cropperjs": "^2.1.1",
|
||||
"element-plus": "^2.14.2",
|
||||
"pinia": "^3.0.4",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
|
||||
110
pnpm-lock.yaml
generated
110
pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
||||
axios:
|
||||
specifier: ^1.17.0
|
||||
version: 1.17.0
|
||||
cropperjs:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
element-plus:
|
||||
specifier: ^2.14.2
|
||||
version: 2.14.2(vue@3.5.35(typescript@6.0.3))
|
||||
@@ -245,6 +248,39 @@ packages:
|
||||
'@bufbuild/protobuf@2.12.0':
|
||||
resolution: {integrity: sha512-B/XlCaFIP8LOwzo+bz5uFzATYokcwCKQcghqnlfwSmM5eX/qTkvDBnDPs+gXtX/RyjxJ4DRikECcPJbyALA8FA==}
|
||||
|
||||
'@cropper/element-canvas@2.1.1':
|
||||
resolution: {integrity: sha512-QHiHnPHsykkc4salOiCLOIKWPK3sLQ1SzpJYFE/yQPvqTPu6ERZNcxVwB1x7WhNN+SdP0S0CEHPYIkgIfUo7VA==}
|
||||
|
||||
'@cropper/element-crosshair@2.1.1':
|
||||
resolution: {integrity: sha512-DtZnOiY2RZSizDZzMUvya3wZ55YnWclf3hmIrLW6jdcFlQkYEbX6bkOyOJzQmEX0yIbmwMkXfFcMkEZ9dE1rJQ==}
|
||||
|
||||
'@cropper/element-grid@2.1.1':
|
||||
resolution: {integrity: sha512-MZdPBh5QMU10T78nqG+5cK+gq4DHoWZvdOo+/wZ9/z3VH0u21sROeHOCnf0QAdxguPHSSFsPjzARK+ffS4Loog==}
|
||||
|
||||
'@cropper/element-handle@2.1.1':
|
||||
resolution: {integrity: sha512-TmSSphyMr2NEA0DqUTf9PCN8kcpzM0BxreOMyzbfuq1NUUePV1LxYgSOUT7dftMjZnTcu8cHQeFYa69U2BZnBA==}
|
||||
|
||||
'@cropper/element-image@2.1.1':
|
||||
resolution: {integrity: sha512-cldzN3hVJo0Luui+FDSIkmbQtwIjxWiTcTMLCMSi3hqIq6lc7YOBz+rC5R8AYnYV7YvsWonE6PrJZS+iZpBkdQ==}
|
||||
|
||||
'@cropper/element-selection@2.1.1':
|
||||
resolution: {integrity: sha512-QtMYhcuR+8JG3QEv8iSRCfQEVYljor61vLPrnGCFIkDJhvcyEWDx4H/Kfn6Jrdo90PyXf2VbpTG7TgH8i/EZFg==}
|
||||
|
||||
'@cropper/element-shade@2.1.1':
|
||||
resolution: {integrity: sha512-MhJVc3jC87TjQu4EfBnNFddL3LxvA+Wzjy1Qhyi0wNdeTAzs97uYJ3KwGVtvzPYP/4JaSU4JDpw+BfIdUqR0bg==}
|
||||
|
||||
'@cropper/element-viewer@2.1.1':
|
||||
resolution: {integrity: sha512-RpXTGW/rTtJVNd3/R3imdCBJzyZRhA/OeWNo7RID4qW7UYEiL6Lwh7Six3QhFIci/IaUAyL9NAAN10av2YWqog==}
|
||||
|
||||
'@cropper/element@2.1.1':
|
||||
resolution: {integrity: sha512-pthgIQq3PFAFRGUts96yrMgmMY/4rS/zKEq/Vvw7L0Ur09MgzrUg15z4k96K53bJ8XvNeXdQ0qIDw90gh4Xcug==}
|
||||
|
||||
'@cropper/elements@2.1.1':
|
||||
resolution: {integrity: sha512-cHShk4l6iExM4S6B1xx7+09gtDs+kdxWqe7Yv6DEbAyWhxVIVJyJAPpKlzLfA5MYnlSnkvCvY66hLLQPwooRMQ==}
|
||||
|
||||
'@cropper/utils@2.1.1':
|
||||
resolution: {integrity: sha512-0eQs08WyQUNFMfU3ieE091dEQkrW0YdgZ3n6Thnk1EUQv45ypfiL+MevHwr7UzNnUlqUYR2FNSCgE6qBxK0sFw==}
|
||||
|
||||
'@ctrl/tinycolor@4.2.0':
|
||||
resolution: {integrity: sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -891,6 +927,9 @@ packages:
|
||||
resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
cropperjs@2.1.1:
|
||||
resolution: {integrity: sha512-FDJMarkY+/SepYarPZsvkG2LmI2PElecciMFnvBiBIoKnFYua/scprC5qejCLLyuX2jEqJRS2njbAsHxfjtIXA==}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -2077,6 +2116,72 @@ snapshots:
|
||||
|
||||
'@bufbuild/protobuf@2.12.0': {}
|
||||
|
||||
'@cropper/element-canvas@2.1.1':
|
||||
dependencies:
|
||||
'@cropper/element': 2.1.1
|
||||
'@cropper/utils': 2.1.1
|
||||
|
||||
'@cropper/element-crosshair@2.1.1':
|
||||
dependencies:
|
||||
'@cropper/element': 2.1.1
|
||||
'@cropper/utils': 2.1.1
|
||||
|
||||
'@cropper/element-grid@2.1.1':
|
||||
dependencies:
|
||||
'@cropper/element': 2.1.1
|
||||
'@cropper/utils': 2.1.1
|
||||
|
||||
'@cropper/element-handle@2.1.1':
|
||||
dependencies:
|
||||
'@cropper/element': 2.1.1
|
||||
'@cropper/utils': 2.1.1
|
||||
|
||||
'@cropper/element-image@2.1.1':
|
||||
dependencies:
|
||||
'@cropper/element': 2.1.1
|
||||
'@cropper/element-canvas': 2.1.1
|
||||
'@cropper/utils': 2.1.1
|
||||
|
||||
'@cropper/element-selection@2.1.1':
|
||||
dependencies:
|
||||
'@cropper/element': 2.1.1
|
||||
'@cropper/element-canvas': 2.1.1
|
||||
'@cropper/element-image': 2.1.1
|
||||
'@cropper/utils': 2.1.1
|
||||
|
||||
'@cropper/element-shade@2.1.1':
|
||||
dependencies:
|
||||
'@cropper/element': 2.1.1
|
||||
'@cropper/element-canvas': 2.1.1
|
||||
'@cropper/element-selection': 2.1.1
|
||||
'@cropper/utils': 2.1.1
|
||||
|
||||
'@cropper/element-viewer@2.1.1':
|
||||
dependencies:
|
||||
'@cropper/element': 2.1.1
|
||||
'@cropper/element-canvas': 2.1.1
|
||||
'@cropper/element-image': 2.1.1
|
||||
'@cropper/element-selection': 2.1.1
|
||||
'@cropper/utils': 2.1.1
|
||||
|
||||
'@cropper/element@2.1.1':
|
||||
dependencies:
|
||||
'@cropper/utils': 2.1.1
|
||||
|
||||
'@cropper/elements@2.1.1':
|
||||
dependencies:
|
||||
'@cropper/element': 2.1.1
|
||||
'@cropper/element-canvas': 2.1.1
|
||||
'@cropper/element-crosshair': 2.1.1
|
||||
'@cropper/element-grid': 2.1.1
|
||||
'@cropper/element-handle': 2.1.1
|
||||
'@cropper/element-image': 2.1.1
|
||||
'@cropper/element-selection': 2.1.1
|
||||
'@cropper/element-shade': 2.1.1
|
||||
'@cropper/element-viewer': 2.1.1
|
||||
|
||||
'@cropper/utils@2.1.1': {}
|
||||
|
||||
'@ctrl/tinycolor@4.2.0': {}
|
||||
|
||||
'@element-plus/icons-vue@2.3.2(vue@3.5.35(typescript@6.0.3))':
|
||||
@@ -2659,6 +2764,11 @@ snapshots:
|
||||
dependencies:
|
||||
is-what: 5.5.0
|
||||
|
||||
cropperjs@2.1.1:
|
||||
dependencies:
|
||||
'@cropper/elements': 2.1.1
|
||||
'@cropper/utils': 2.1.1
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
|
||||
@@ -70,6 +70,61 @@ export function getUserInfo() {
|
||||
return requestClient.get<UserInfoResponse>('/user/info')
|
||||
}
|
||||
|
||||
/** 更新用户信息请求(所有字段可选,传什么更新什么) */
|
||||
export interface UpdateUserInfoRequest {
|
||||
/** 用户昵称,最长 16 位 */
|
||||
nickName?: string
|
||||
/** 头像图片 URL 地址 */
|
||||
avatar?: string
|
||||
/** 个性签名,最长 64 位 */
|
||||
signature?: string
|
||||
/** 年龄 */
|
||||
age?: number
|
||||
/** 性别:1=男,2=女 */
|
||||
sex?: number
|
||||
/** 生日(ISO 8601 日期格式,如 "1998-06-15") */
|
||||
birthday?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新当前登录用户信息
|
||||
*
|
||||
* 所有字段可选,传什么更新什么。
|
||||
*
|
||||
* @param data - 更新请求
|
||||
* @returns 更新后的用户信息
|
||||
*/
|
||||
export function updateUserInfo(data: UpdateUserInfoRequest) {
|
||||
return requestClient.put<UserInfoResponse>('/user/info', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传用户头像
|
||||
*
|
||||
* 接收裁剪后的图片文件,保存后返回更新后的用户信息。
|
||||
*
|
||||
* @param file - 头像文件(JPEG/PNG/WebP/GIF,≤2MB)
|
||||
* @returns 更新后的用户信息
|
||||
*/
|
||||
export function uploadAvatar(file: File) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return requestClient.post<UserInfoResponse>('/user/avatar', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
} as any)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户头像
|
||||
*
|
||||
* 删除磁盘文件并将 DB 中 avatar 置空。
|
||||
*
|
||||
* @returns 更新后的用户信息(avatar 为 null)
|
||||
*/
|
||||
export function deleteAvatar() {
|
||||
return requestClient.delete<UserInfoResponse>('/user/avatar')
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新当前登录用户在线状态
|
||||
*
|
||||
|
||||
243
src/components/baseLayouts/AvatarCropper.vue
Normal file
243
src/components/baseLayouts/AvatarCropper.vue
Normal file
@@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div class="avatar-cropper-overlay" @click.self="handleCancel">
|
||||
<div class="avatar-cropper">
|
||||
<!-- 标题栏 -->
|
||||
<div class="avatar-cropper__header">
|
||||
<span class="avatar-cropper__title">裁剪头像</span>
|
||||
<el-icon :size="20" class="avatar-cropper__close" @click="handleCancel">
|
||||
<Close />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<!-- 裁剪画布容器 -->
|
||||
<div ref="containerRef" class="avatar-cropper__canvas-wrapper">
|
||||
<img
|
||||
ref="imageRef"
|
||||
:src="imageSrc"
|
||||
alt="裁剪预览"
|
||||
class="avatar-cropper__image"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作 -->
|
||||
<div class="avatar-cropper__footer">
|
||||
<span class="avatar-cropper__hint">拖拽或缩放调整裁剪区域</span>
|
||||
<div class="avatar-cropper__actions">
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" :loading="confirming" @click="handleConfirm">确认</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { Close } from '@element-plus/icons-vue'
|
||||
import Cropper, { type CropperSelection } from 'cropperjs'
|
||||
|
||||
/** 裁剪输出配置 */
|
||||
const CROP_SIZE = 200
|
||||
/** JPEG 导出质量 */
|
||||
const CROP_QUALITY = 0.85
|
||||
/** 允许的图片 MIME 类型 */
|
||||
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'] as const
|
||||
/** 文件大小上限 */
|
||||
const MAX_FILE_SIZE = 2 * 1024 * 1024
|
||||
|
||||
/** cropperjs v2 模板:预配置 1:1 裁剪框 */
|
||||
const CROPPER_TEMPLATE = `
|
||||
<cropper-canvas background>
|
||||
<cropper-image></cropper-image>
|
||||
<cropper-shade hidden></cropper-shade>
|
||||
<cropper-handle action="select" plain></cropper-handle>
|
||||
<cropper-selection
|
||||
aspect-ratio="1"
|
||||
initial-coverage="0.9"
|
||||
movable
|
||||
resizable
|
||||
zoomable
|
||||
outlined
|
||||
></cropper-selection>
|
||||
<cropper-grid role="grid" covered></cropper-grid>
|
||||
<cropper-crosshair centered></cropper-crosshair>
|
||||
<cropper-handle action="move" theme-color="rgba(255, 255, 255, 0.35)"></cropper-handle>
|
||||
</cropper-canvas>
|
||||
`
|
||||
|
||||
const props = defineProps<{
|
||||
/** 要裁剪的图片文件 */
|
||||
file: File
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'confirm', blob: Blob): void
|
||||
(e: 'cancel'): void
|
||||
}>()
|
||||
|
||||
const containerRef = ref<HTMLDivElement>()
|
||||
const imageRef = ref<HTMLImageElement>()
|
||||
const imageSrc = ref('')
|
||||
const confirming = ref(false)
|
||||
|
||||
let cropper: Cropper | null = null
|
||||
|
||||
/** 校验文件 */
|
||||
function validateFile(file: File): string | null {
|
||||
if (!(ALLOWED_TYPES as readonly string[]).includes(file.type)) {
|
||||
return '仅支持 JPG、PNG、WebP、GIF 格式'
|
||||
}
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
return '文件大小不能超过 2MB'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const error = validateFile(props.file)
|
||||
if (error) {
|
||||
emit('cancel')
|
||||
return
|
||||
}
|
||||
|
||||
imageSrc.value = URL.createObjectURL(props.file)
|
||||
await nextTick()
|
||||
|
||||
if (!imageRef.value || !containerRef.value) return
|
||||
|
||||
cropper = new Cropper(imageRef.value, {
|
||||
container: containerRef.value,
|
||||
template: CROPPER_TEMPLATE
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (cropper) {
|
||||
cropper.destroy()
|
||||
cropper = null
|
||||
}
|
||||
if (imageSrc.value) {
|
||||
URL.revokeObjectURL(imageSrc.value)
|
||||
}
|
||||
})
|
||||
|
||||
/** 确认裁剪:异步导出 Canvas → Blob */
|
||||
async function handleConfirm() {
|
||||
if (!cropper) return
|
||||
|
||||
confirming.value = true
|
||||
try {
|
||||
const selection: CropperSelection | null = cropper.getCropperSelection()
|
||||
if (!selection) {
|
||||
emit('cancel')
|
||||
return
|
||||
}
|
||||
|
||||
const canvas = await selection.$toCanvas({
|
||||
width: CROP_SIZE,
|
||||
height: CROP_SIZE
|
||||
})
|
||||
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (!blob) {
|
||||
emit('cancel')
|
||||
return
|
||||
}
|
||||
emit('confirm', blob)
|
||||
},
|
||||
'image/jpeg',
|
||||
CROP_QUALITY
|
||||
)
|
||||
} catch {
|
||||
emit('cancel')
|
||||
} finally {
|
||||
confirming.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
emit('cancel')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.avatar-cropper-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.avatar-cropper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 440px;
|
||||
max-width: 90vw;
|
||||
background: var(--bg-surface);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar-cropper__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-md) var(--space-lg);
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
.avatar-cropper__title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.avatar-cropper__close {
|
||||
cursor: pointer;
|
||||
color: var(--text-tertiary);
|
||||
transition: color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-cropper__canvas-wrapper {
|
||||
width: 100%;
|
||||
min-height: 300px;
|
||||
max-height: 400px;
|
||||
background: var(--bg-body);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar-cropper__image {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.avatar-cropper__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-sm) var(--space-lg) var(--space-md);
|
||||
}
|
||||
|
||||
.avatar-cropper__hint {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.avatar-cropper__actions {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
</style>
|
||||
977
src/components/baseLayouts/EditProfileDialog.vue
Normal file
977
src/components/baseLayouts/EditProfileDialog.vue
Normal file
@@ -0,0 +1,977 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
width="640px"
|
||||
:close-on-click-modal="false"
|
||||
:show-close="false"
|
||||
class="edit-profile-dialog"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<template #header>
|
||||
<div class="ep-dialog__header">
|
||||
<span class="ep-dialog__title">用户设置</span>
|
||||
<span class="ep-dialog__subtitle">管理你的个人资料与偏好</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="ep-layout">
|
||||
<!-- 左侧导航 -->
|
||||
<nav class="ep-nav">
|
||||
<div
|
||||
class="ep-nav__item"
|
||||
:class="{ 'is-active': activeTab === 'profile' }"
|
||||
@click="activeTab = 'profile'"
|
||||
>
|
||||
<span class="ep-nav__icon"
|
||||
><el-icon :size="18"><User /></el-icon
|
||||
></span>
|
||||
<span class="ep-nav__text">个人信息</span>
|
||||
<span v-if="activeTab === 'profile'" class="ep-nav__indicator" />
|
||||
</div>
|
||||
<div
|
||||
class="ep-nav__item"
|
||||
:class="{ 'is-active': activeTab === 'settings' }"
|
||||
@click="activeTab = 'settings'"
|
||||
>
|
||||
<span class="ep-nav__icon"
|
||||
><el-icon :size="18"><Setting /></el-icon
|
||||
></span>
|
||||
<span class="ep-nav__text">全局设置</span>
|
||||
<span v-if="activeTab === 'settings'" class="ep-nav__indicator" />
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 右侧内容 -->
|
||||
<div class="ep-content">
|
||||
<Transition name="tab-fade" mode="out-in">
|
||||
<!-- ========== 个人信息 ========== -->
|
||||
<div v-if="activeTab === 'profile'" key="profile" class="ep-tab">
|
||||
<!-- 头像 —— 签名元素:渐变光环 -->
|
||||
<div class="ep-avatar-block">
|
||||
<div
|
||||
class="ep-avatar__ring"
|
||||
:class="{ 'has-avatar': hasAvatar }"
|
||||
@click="triggerFilePicker"
|
||||
title="点击更换头像"
|
||||
>
|
||||
<el-avatar
|
||||
:size="88"
|
||||
shape="circle"
|
||||
:src="avatarDisplaySrc"
|
||||
class="ep-avatar__img"
|
||||
/>
|
||||
<div class="ep-avatar__overlay">
|
||||
<el-icon :size="22"><Camera /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ep-avatar__meta">
|
||||
<span class="ep-avatar__name">{{ form.nickName || '你的昵称' }}</span>
|
||||
<span class="ep-avatar__hint">点击头像更换,支持 JPG / PNG / WebP</span>
|
||||
<div class="ep-avatar__btns">
|
||||
<el-button size="small" round @click="triggerFilePicker">
|
||||
<el-icon :size="14"><Upload /></el-icon>
|
||||
更换
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasAvatar"
|
||||
size="small"
|
||||
round
|
||||
class="ep-avatar__remove-btn"
|
||||
@click="handleRemoveAvatar"
|
||||
>
|
||||
<el-icon :size="14"><Delete /></el-icon>
|
||||
移除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
accept="image/jpeg,image/png,image/webp,image/gif"
|
||||
class="ep-file-input"
|
||||
@change="handleFileChange"
|
||||
/>
|
||||
|
||||
<AvatarCropper
|
||||
v-if="cropperFile"
|
||||
:file="cropperFile"
|
||||
@confirm="handleCropConfirm"
|
||||
@cancel="handleCropCancel"
|
||||
/>
|
||||
|
||||
<!-- 表单 -->
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="56px"
|
||||
label-position="left"
|
||||
size="large"
|
||||
class="ep-form"
|
||||
>
|
||||
<el-form-item label="昵称" prop="nickName">
|
||||
<el-input
|
||||
v-model="form.nickName"
|
||||
placeholder="给自己起个响亮的名字"
|
||||
maxlength="16"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="签名" prop="signature">
|
||||
<el-input
|
||||
v-model="form.signature"
|
||||
type="textarea"
|
||||
placeholder="用一句话介绍自己…"
|
||||
maxlength="64"
|
||||
show-word-limit
|
||||
:autosize="{ minRows: 2, maxRows: 4 }"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="性别">
|
||||
<el-radio-group v-model="form.sex" class="ep-radio-group">
|
||||
<el-radio-button :value="0">保密</el-radio-button>
|
||||
<el-radio-button :value="1">
|
||||
<el-icon :size="14"><Male /></el-icon>
|
||||
男
|
||||
</el-radio-button>
|
||||
<el-radio-button :value="2">
|
||||
<el-icon :size="14"><Female /></el-icon>
|
||||
女
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="生日">
|
||||
<el-date-picker
|
||||
v-model="form.birthday"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
:disabled-date="disabledDate"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="年龄">
|
||||
<div class="ep-age">
|
||||
<span v-if="form.birthday" class="ep-age__num">{{ calculatedAge }}</span>
|
||||
<span v-if="form.birthday" class="ep-age__unit">岁</span>
|
||||
<span v-else class="ep-age__hint">选择生日后自动计算</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- ========== 全局设置 ========== -->
|
||||
<div v-else key="settings" class="ep-tab">
|
||||
<div class="ep-placeholder">
|
||||
<div class="ep-placeholder__icon">
|
||||
<el-icon :size="36"><Setting /></el-icon>
|
||||
</div>
|
||||
<h3 class="ep-placeholder__title">全局设置</h3>
|
||||
<p class="ep-placeholder__desc">更多个性化选项即将上线,敬请期待</p>
|
||||
<div class="ep-feature-list">
|
||||
<div class="ep-feature-card">
|
||||
<span class="ep-feature-card__icon"
|
||||
><el-icon :size="20"><Bell /></el-icon
|
||||
></span>
|
||||
<div class="ep-feature-card__body">
|
||||
<span class="ep-feature-card__title">消息通知</span>
|
||||
<span class="ep-feature-card__desc">管理游戏邀请与系统消息</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ep-feature-card">
|
||||
<span class="ep-feature-card__icon"
|
||||
><el-icon :size="20"><Brush /></el-icon
|
||||
></span>
|
||||
<div class="ep-feature-card__body">
|
||||
<span class="ep-feature-card__title">主题切换</span>
|
||||
<span class="ep-feature-card__desc">选择你喜欢的颜色方案</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ep-feature-card">
|
||||
<span class="ep-feature-card__icon"
|
||||
><el-icon :size="20"><Lock /></el-icon
|
||||
></span>
|
||||
<div class="ep-feature-card__body">
|
||||
<span class="ep-feature-card__title">隐私设置</span>
|
||||
<span class="ep-feature-card__desc">控制谁可以查看你的信息</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="activeTab === 'profile'" #footer>
|
||||
<div class="ep-dialog__footer">
|
||||
<span class="ep-dialog__footer-hint">修改将在保存后生效</span>
|
||||
<div class="ep-dialog__footer-actions">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="handleSave">保存</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch, nextTick } from 'vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
Setting,
|
||||
User,
|
||||
Camera,
|
||||
Upload,
|
||||
Delete,
|
||||
Male,
|
||||
Female,
|
||||
Bell,
|
||||
Brush,
|
||||
Lock
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useAccountStore } from '@/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import AvatarCropper from './AvatarCropper.vue'
|
||||
|
||||
/** 编辑资料表单数据 */
|
||||
interface EditProfileForm {
|
||||
nickName: string
|
||||
signature: string
|
||||
sex: number | null
|
||||
age: number | null
|
||||
birthday: string | null
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}>()
|
||||
|
||||
/** 对话框可见性(v-model 双向绑定) */
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val: boolean) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const accountStore = useAccountStore()
|
||||
const { accountInfo } = storeToRefs(accountStore)
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const fileInputRef = ref<HTMLInputElement>()
|
||||
const saving = ref(false)
|
||||
|
||||
/** 当前激活的导航 tab */
|
||||
const activeTab = ref<'profile' | 'settings'>('profile')
|
||||
|
||||
/** 表单数据 */
|
||||
const form = reactive<EditProfileForm>({
|
||||
nickName: '',
|
||||
signature: '',
|
||||
sex: null,
|
||||
age: null,
|
||||
birthday: null
|
||||
})
|
||||
|
||||
/** 表单校验规则 */
|
||||
const rules: FormRules = {
|
||||
nickName: [{ max: 16, message: '昵称不超过 16 个字符', trigger: 'blur' }],
|
||||
signature: [{ max: 64, message: '签名不超过 64 个字符', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// ========== 头像裁剪相关 ==========
|
||||
|
||||
/** 待裁剪的文件(非 null 时显示 AvatarCropper) */
|
||||
const cropperFile = ref<File | null>(null)
|
||||
/** 裁剪后的 Blob(待上传) */
|
||||
const croppedBlob = ref<Blob | null>(null)
|
||||
/** 裁剪结果的预览 Object URL */
|
||||
const croppedPreviewUrl = ref('')
|
||||
/** 是否已标记移除头像 */
|
||||
const avatarRemoved = ref(false)
|
||||
|
||||
/** 当前是否有头像(含自定义头像和默认 SVG) */
|
||||
const hasAvatar = computed(() => {
|
||||
const av = accountInfo.value?.avatar
|
||||
return !!(av && av.length > 0)
|
||||
})
|
||||
|
||||
/** 头像显示源:裁剪预览 > 移除状态 > store 数据 */
|
||||
const avatarDisplaySrc = computed(() => {
|
||||
if (avatarRemoved.value) return ''
|
||||
if (croppedPreviewUrl.value) return croppedPreviewUrl.value
|
||||
return accountInfo.value?.avatar || ''
|
||||
})
|
||||
|
||||
/** 根据生日计算年龄(周岁) */
|
||||
function calcAge(birthday: string | null): number | null {
|
||||
if (!birthday) return null
|
||||
const birth = new Date(birthday)
|
||||
const now = new Date()
|
||||
let age = now.getFullYear() - birth.getFullYear()
|
||||
const monthDiff = now.getMonth() - birth.getMonth()
|
||||
if (monthDiff < 0 || (monthDiff === 0 && now.getDate() < birth.getDate())) {
|
||||
age--
|
||||
}
|
||||
return age
|
||||
}
|
||||
|
||||
/** 由生日自动计算的年龄(只读) */
|
||||
const calculatedAge = computed(() => calcAge(form.birthday))
|
||||
|
||||
/** 生日变化时同步计算年龄到表单数据(用于提交) */
|
||||
watch(
|
||||
() => form.birthday,
|
||||
(val) => {
|
||||
form.age = calcAge(val)
|
||||
}
|
||||
)
|
||||
|
||||
/** 未来日期不可选(生日不能是未来) */
|
||||
function disabledDate(date: Date): boolean {
|
||||
return date.getTime() > Date.now()
|
||||
}
|
||||
|
||||
// ========== 初始化表单 ==========
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (val) initForm()
|
||||
}
|
||||
)
|
||||
|
||||
/** 从 store 填充表单 */
|
||||
function initForm() {
|
||||
const info = accountInfo.value
|
||||
form.nickName = info?.nickName || ''
|
||||
form.signature = info?.signature || ''
|
||||
form.sex = info?.sex != null && [1, 2].includes(info.sex) ? info.sex : 0
|
||||
form.age = info?.age ?? null
|
||||
form.birthday = info?.birthday || null
|
||||
form.age = calcAge(form.birthday)
|
||||
|
||||
// 重置头像状态
|
||||
releaseCroppedPreview()
|
||||
croppedBlob.value = null
|
||||
avatarRemoved.value = false
|
||||
activeTab.value = 'profile'
|
||||
}
|
||||
|
||||
/** 清理裁剪预览 Object URL */
|
||||
function releaseCroppedPreview() {
|
||||
if (croppedPreviewUrl.value) {
|
||||
URL.revokeObjectURL(croppedPreviewUrl.value)
|
||||
croppedPreviewUrl.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 头像操作 ==========
|
||||
|
||||
/** 触发文件选择器 */
|
||||
function triggerFilePicker() {
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
|
||||
/** 文件选择后打开裁剪弹窗 */
|
||||
function handleFileChange(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
// 基础校验(AvatarCropper 内部还会再校验)
|
||||
const MAX_SIZE = 2 * 1024 * 1024
|
||||
const ALLOWED = ['image/jpeg', 'image/png', 'image/webp', 'image/gif']
|
||||
if (!ALLOWED.includes(file.type)) {
|
||||
ElMessage.warning('仅支持 JPG、PNG、WebP、GIF 格式')
|
||||
clearFileInput()
|
||||
return
|
||||
}
|
||||
if (file.size > MAX_SIZE) {
|
||||
ElMessage.warning('文件大小不能超过 2MB')
|
||||
clearFileInput()
|
||||
return
|
||||
}
|
||||
|
||||
cropperFile.value = file
|
||||
clearFileInput()
|
||||
}
|
||||
|
||||
/** 清空 file input(允许重复选择同一文件) */
|
||||
function clearFileInput() {
|
||||
if (fileInputRef.value) {
|
||||
fileInputRef.value.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
/** 裁剪确认:接收 Blob,生成预览 */
|
||||
function handleCropConfirm(blob: Blob) {
|
||||
releaseCroppedPreview()
|
||||
croppedBlob.value = blob
|
||||
croppedPreviewUrl.value = URL.createObjectURL(blob)
|
||||
avatarRemoved.value = false
|
||||
cropperFile.value = null
|
||||
}
|
||||
|
||||
/** 取消裁剪 */
|
||||
function handleCropCancel() {
|
||||
cropperFile.value = null
|
||||
clearFileInput()
|
||||
}
|
||||
|
||||
/** 移除头像 */
|
||||
function handleRemoveAvatar() {
|
||||
releaseCroppedPreview()
|
||||
croppedBlob.value = null
|
||||
avatarRemoved.value = true
|
||||
}
|
||||
|
||||
// ========== 保存 ==========
|
||||
|
||||
async function handleSave() {
|
||||
const valid = await formRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
// 1. 头像变更优先处理
|
||||
if (avatarRemoved.value) {
|
||||
await accountStore.removeAvatar()
|
||||
} else if (croppedBlob.value) {
|
||||
// Blob → File
|
||||
const file = new File([croppedBlob.value], 'avatar.jpg', {
|
||||
type: croppedBlob.value.type || 'image/jpeg'
|
||||
})
|
||||
await accountStore.uploadAvatar(file)
|
||||
}
|
||||
|
||||
// 2. 更新文本字段(只传变更的字段)
|
||||
const info = accountInfo.value
|
||||
const payload: Record<string, unknown> = {}
|
||||
|
||||
if (form.nickName !== (info?.nickName || '')) {
|
||||
payload.nickName = form.nickName
|
||||
}
|
||||
if (form.signature !== (info?.signature || '')) {
|
||||
payload.signature = form.signature
|
||||
}
|
||||
if (form.sex !== (info?.sex ?? 0)) {
|
||||
payload.sex = form.sex === 0 ? null : form.sex
|
||||
}
|
||||
if (form.age !== (info?.age ?? null)) {
|
||||
payload.age = form.age
|
||||
}
|
||||
if (form.birthday !== (info?.birthday || null)) {
|
||||
payload.birthday = form.birthday
|
||||
}
|
||||
|
||||
if (Object.keys(payload).length > 0) {
|
||||
await accountStore.updateProfile(payload)
|
||||
}
|
||||
|
||||
visible.value = false
|
||||
ElMessage.success('个人资料已更新')
|
||||
} catch {
|
||||
ElMessage.error('保存失败,请重试')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 弹窗关闭动画结束后重置表单 */
|
||||
function handleClosed() {
|
||||
releaseCroppedPreview()
|
||||
croppedBlob.value = null
|
||||
avatarRemoved.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* ============================================================
|
||||
* 用户设置弹窗 — 样式
|
||||
* 设计方向:"精奢游戏大厅",高端棋牌平台质感
|
||||
* 签名元素:头像渐变光环,呼应 Auth 页面径向渐变品牌语言
|
||||
* ============================================================ */
|
||||
|
||||
.ep-layout {
|
||||
display: flex;
|
||||
height: 460px;
|
||||
}
|
||||
|
||||
/* ========== Tab 过渡 ========== */
|
||||
.tab-fade-enter-active,
|
||||
.tab-fade-leave-active {
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
.tab-fade-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
.tab-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
|
||||
/* ========== 左侧导航 ========== */
|
||||
.ep-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 148px;
|
||||
flex-shrink: 0;
|
||||
padding: var(--space-md) var(--space-sm);
|
||||
background-color: var(--bg-sidebar);
|
||||
border-right: 1px solid var(--border-light);
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.ep-nav__item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-surface-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: var(--bg-surface);
|
||||
color: var(--color-primary);
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
.ep-nav__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: var(--radius-sm);
|
||||
flex-shrink: 0;
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
.ep-nav__item.is-active & {
|
||||
background-color: var(--color-primary-light);
|
||||
}
|
||||
}
|
||||
|
||||
.ep-nav__text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 激活态左侧指示条 */
|
||||
.ep-nav__indicator {
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 20px;
|
||||
border-radius: 0 2px 2px 0;
|
||||
background: linear-gradient(180deg, var(--color-primary), #818cf8);
|
||||
}
|
||||
|
||||
/* ========== 右侧内容区 ========== */
|
||||
.ep-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-thumb);
|
||||
border-radius: 10px;
|
||||
&:hover {
|
||||
background: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ep-tab {
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
|
||||
/* ========== 头像区块(签名元素) ========== */
|
||||
.ep-avatar-block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-lg);
|
||||
padding: var(--space-xs) 0 var(--space-lg);
|
||||
margin-bottom: var(--space-md);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
/* 渐变光环 —— 与 Auth 页面径向渐变呼应 */
|
||||
.ep-avatar__ring {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
padding: 3px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(79, 70, 229, 0.3),
|
||||
rgba(59, 130, 246, 0.3),
|
||||
rgba(16, 185, 129, 0.25)
|
||||
);
|
||||
transition:
|
||||
transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1),
|
||||
box-shadow 0.25s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 20px rgba(79, 70, 229, 0.15);
|
||||
}
|
||||
|
||||
/* 无头像时不显示光环 = 纯装饰 */
|
||||
&.has-avatar {
|
||||
background: linear-gradient(135deg, var(--color-primary), #6366f1, var(--color-info), #34d399);
|
||||
}
|
||||
}
|
||||
|
||||
.ep-avatar__img {
|
||||
display: block;
|
||||
border: 3px solid var(--bg-surface);
|
||||
border-radius: 50%;
|
||||
transition: border-color 0.2s ease;
|
||||
|
||||
.ep-avatar__ring:hover & {
|
||||
border-color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.ep-avatar__overlay {
|
||||
position: absolute;
|
||||
inset: 3px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(15, 23, 42, 0.4);
|
||||
color: #fff;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
pointer-events: none;
|
||||
|
||||
.ep-avatar__ring:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ep-avatar__meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ep-avatar__name {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ep-avatar__hint {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.ep-avatar__btns {
|
||||
display: flex;
|
||||
gap: var(--space-xs);
|
||||
margin-top: var(--space-sm);
|
||||
}
|
||||
|
||||
.ep-avatar__remove-btn {
|
||||
color: var(--text-tertiary);
|
||||
border-color: var(--border-default);
|
||||
&:hover {
|
||||
color: var(--color-danger);
|
||||
border-color: var(--color-danger);
|
||||
background-color: var(--color-danger-light);
|
||||
}
|
||||
}
|
||||
|
||||
.ep-file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ========== 表单 ========== */
|
||||
.ep-form {
|
||||
--el-form-label-font-size: 13px;
|
||||
--el-form-label-color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.ep-form :deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.ep-form :deep(.el-form-item__label) {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 输入框 & 文本域:自定义焦点环 */
|
||||
.ep-form :deep(.el-input__wrapper),
|
||||
.ep-form :deep(.el-textarea__inner) {
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: 0 0 0 1px var(--border-default) inset;
|
||||
transition: box-shadow 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1px var(--text-tertiary) inset;
|
||||
}
|
||||
}
|
||||
|
||||
.ep-form :deep(.el-input__wrapper.is-focus),
|
||||
.ep-form :deep(.el-textarea__inner:focus) {
|
||||
box-shadow:
|
||||
0 0 0 3px rgba(79, 70, 229, 0.1) inset,
|
||||
0 0 0 1px var(--color-primary) inset;
|
||||
}
|
||||
|
||||
/* 单选按钮组 */
|
||||
.ep-radio-group :deep(.el-radio-button__inner) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px 14px;
|
||||
border-radius: 0;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.ep-radio-group :deep(.el-radio-button:first-child .el-radio-button__inner) {
|
||||
border-radius: var(--radius-sm) 0 0 var(--radius-sm);
|
||||
}
|
||||
.ep-radio-group :deep(.el-radio-button:last-child .el-radio-button__inner) {
|
||||
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
||||
}
|
||||
|
||||
/* 年龄展示 */
|
||||
.ep-age {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.ep-age__num {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
line-height: 1;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
.ep-age__unit {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.ep-age__hint {
|
||||
font-size: 13px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
/* ========== 全局设置占位页 ========== */
|
||||
.ep-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 380px;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.ep-placeholder__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 50%;
|
||||
background:
|
||||
radial-gradient(circle at 30% 30%, rgba(79, 70, 229, 0.08), transparent 70%), var(--bg-sidebar);
|
||||
color: var(--text-tertiary);
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.ep-placeholder__title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ep-placeholder__desc {
|
||||
font-size: 13px;
|
||||
color: var(--text-tertiary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ep-feature-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: var(--space-lg);
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.ep-feature-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 14px 16px;
|
||||
border-radius: var(--radius-md);
|
||||
background-color: var(--bg-sidebar);
|
||||
border: 1px solid var(--border-light);
|
||||
cursor: default;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
border-color 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-surface);
|
||||
border-color: var(--border-default);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
}
|
||||
|
||||
.ep-feature-card__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--radius-sm);
|
||||
background-color: var(--color-primary-light);
|
||||
color: var(--color-primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ep-feature-card__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.ep-feature-card__title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.ep-feature-card__desc {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
/* ============================================================
|
||||
* 弹窗全局样式(非 scoped,覆盖 el-dialog 默认样式)
|
||||
* ============================================================ */
|
||||
|
||||
.edit-profile-dialog {
|
||||
.el-dialog {
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-dialog__header {
|
||||
position: relative;
|
||||
padding: var(--space-lg) var(--space-lg) var(--space-md);
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
|
||||
/* 顶部渐变装饰线 —— 呼应 Auth 页面背景渐变 */
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, var(--color-primary), #818cf8, var(--color-info), #34d399);
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
padding: var(--space-md) var(--space-lg);
|
||||
border-top: 1px solid var(--border-default);
|
||||
background-color: var(--bg-sidebar);
|
||||
}
|
||||
}
|
||||
|
||||
/* 弹窗标题区 */
|
||||
.ep-dialog__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.ep-dialog__title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.ep-dialog__subtitle {
|
||||
font-size: 13px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
/* 弹窗底部 */
|
||||
.ep-dialog__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ep-dialog__footer-hint {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.ep-dialog__footer-actions {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
</style>
|
||||
@@ -2,6 +2,7 @@
|
||||
<div class="user-bar">
|
||||
<div class="container">
|
||||
<el-popover
|
||||
v-model:visible="showUserPanel"
|
||||
trigger="click"
|
||||
:width="280"
|
||||
:show-arrow="false"
|
||||
@@ -124,16 +125,20 @@
|
||||
<el-icon :size="26"><Setting /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑个人资料弹窗 -->
|
||||
<EditProfileDialog v-model="showEditProfile" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Setting, EditPen, ArrowRight, Check, DocumentCopy, SwitchButton } from '@element-plus/icons-vue'
|
||||
import { useAccountStore } from '@/stores'
|
||||
import { UserStatus, type UserStatusType } from '@/api/modules/user'
|
||||
import EditProfileDialog from './EditProfileDialog.vue'
|
||||
|
||||
/** 在线状态选项 */
|
||||
interface StatusOption {
|
||||
@@ -155,6 +160,9 @@ const statusMap = new Map<UserStatusType, StatusOption>(STATUS_OPTIONS.map((s) =
|
||||
const accountStore = useAccountStore()
|
||||
const { accountInfo } = storeToRefs(accountStore)
|
||||
|
||||
const showEditProfile = ref(false)
|
||||
const showUserPanel = ref(false)
|
||||
|
||||
const statusOptions = STATUS_OPTIONS
|
||||
|
||||
const currentStatus = computed<StatusOption>(() => {
|
||||
@@ -167,7 +175,9 @@ const displayName = computed(() => accountInfo.value?.nickName || '未登录')
|
||||
const signatureText = computed(() => accountInfo.value?.signature || '这个人很懒,什么都没写...')
|
||||
|
||||
const handleEditProfile = () => {
|
||||
ElMessage.info('编辑个人资料功能开发中')
|
||||
// 先关闭 popover,再打开弹窗,避免浮层残留
|
||||
showUserPanel.value = false
|
||||
showEditProfile.value = true
|
||||
}
|
||||
|
||||
const selectStatus = async (option: StatusOption) => {
|
||||
|
||||
@@ -7,7 +7,15 @@ import {
|
||||
type LoginRequest,
|
||||
type RegisterRequest
|
||||
} from '@/api/modules/account'
|
||||
import { getUserInfo, updateStatus as updateStatusApi, type UserStatusType } from '@/api/modules/user'
|
||||
import {
|
||||
getUserInfo,
|
||||
updateUserInfo as updateUserInfoApi,
|
||||
uploadAvatar as uploadAvatarApi,
|
||||
deleteAvatar as deleteAvatarApi,
|
||||
updateStatus as updateStatusApi,
|
||||
type UserStatusType,
|
||||
type UpdateUserInfoRequest
|
||||
} from '@/api/modules/user'
|
||||
|
||||
/** 账户信息 */
|
||||
export interface AccountState {
|
||||
@@ -109,6 +117,35 @@ export const useAccountStore = defineStore(
|
||||
return accessToken.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新当前用户的个人资料
|
||||
* 调用后端接口持久化,并同步更新本地 accountInfo
|
||||
* @param data - 要更新的字段(所有字段可选)
|
||||
*/
|
||||
const updateProfile = async (data: UpdateUserInfoRequest) => {
|
||||
const res = await updateUserInfoApi(data)
|
||||
accountInfo.value = res.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传用户头像
|
||||
* 调用后端接口上传文件并更新 DB,同步更新本地 accountInfo
|
||||
* @param file - 头像文件
|
||||
*/
|
||||
const uploadAvatar = async (file: File) => {
|
||||
const res = await uploadAvatarApi(file)
|
||||
accountInfo.value = res.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户头像
|
||||
* 调用后端接口删除文件并清空 DB,同步更新本地 accountInfo
|
||||
*/
|
||||
const removeAvatar = async () => {
|
||||
const res = await deleteAvatarApi()
|
||||
accountInfo.value = res.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新当前用户的在线状态
|
||||
* 调用后端接口持久化,并同步更新本地 accountInfo
|
||||
@@ -141,6 +178,9 @@ export const useAccountStore = defineStore(
|
||||
logout,
|
||||
refreshAccessToken,
|
||||
fetchUserInfo,
|
||||
updateProfile,
|
||||
uploadAvatar,
|
||||
removeAvatar,
|
||||
updateUserStatus
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user