diff --git a/components.d.ts b/components.d.ts index 532ad4d..5fa8439 100644 --- a/components.d.ts +++ b/components.d.ts @@ -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'] diff --git a/package-lock.json b/package-lock.json index 4c0a7be..c826ce3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index f71e836..4faa283 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 905f559..ce3db55 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/api/modules/user/info.ts b/src/api/modules/user/info.ts index c985df8..7348143 100644 --- a/src/api/modules/user/info.ts +++ b/src/api/modules/user/info.ts @@ -70,6 +70,61 @@ export function getUserInfo() { return requestClient.get('/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('/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('/user/avatar', formData, { + headers: { 'Content-Type': 'multipart/form-data' } + } as any) +} + +/** + * 删除用户头像 + * + * 删除磁盘文件并将 DB 中 avatar 置空。 + * + * @returns 更新后的用户信息(avatar 为 null) + */ +export function deleteAvatar() { + return requestClient.delete('/user/avatar') +} + /** * 更新当前登录用户在线状态 * diff --git a/src/components/baseLayouts/AvatarCropper.vue b/src/components/baseLayouts/AvatarCropper.vue new file mode 100644 index 0000000..b430392 --- /dev/null +++ b/src/components/baseLayouts/AvatarCropper.vue @@ -0,0 +1,243 @@ + + + + + diff --git a/src/components/baseLayouts/EditProfileDialog.vue b/src/components/baseLayouts/EditProfileDialog.vue new file mode 100644 index 0000000..b07543d --- /dev/null +++ b/src/components/baseLayouts/EditProfileDialog.vue @@ -0,0 +1,977 @@ + + + + + + + diff --git a/src/components/baseLayouts/UserBar.vue b/src/components/baseLayouts/UserBar.vue index 4b9e762..ee635e3 100644 --- a/src/components/baseLayouts/UserBar.vue +++ b/src/components/baseLayouts/UserBar.vue @@ -2,6 +2,7 @@
+ + +