💄 restructure and beautify some page
This commit is contained in:
26
components.d.ts
vendored
26
components.d.ts
vendored
@@ -14,14 +14,15 @@ declare module 'vue' {
|
|||||||
ABadge: typeof import('ant-design-vue/es')['Badge']
|
ABadge: typeof import('ant-design-vue/es')['Badge']
|
||||||
AButton: typeof import('ant-design-vue/es')['Button']
|
AButton: typeof import('ant-design-vue/es')['Button']
|
||||||
ACard: typeof import('ant-design-vue/es')['Card']
|
ACard: typeof import('ant-design-vue/es')['Card']
|
||||||
ACardMeta: typeof import('ant-design-vue/es')['CardMeta']
|
|
||||||
ACascader: typeof import('ant-design-vue/es')['Cascader']
|
ACascader: typeof import('ant-design-vue/es')['Cascader']
|
||||||
AccountSetting: typeof import('./src/views/User/AccountSetting/AccountSetting.vue')['default']
|
AccountSetting: typeof import('./src/views/User/AccountSetting/AccountSetting.vue')['default']
|
||||||
AccountSettingBackup: typeof import('./src/views/User/AccountSetting/components/AccountSettingBackup/AccountSettingBackup.vue')['default']
|
AccountSettingBackup: typeof import('./src/views/User/AccountSetting/components/AccountSettingBackup/AccountSettingBackup.vue')['default']
|
||||||
AccountSettingHome: typeof import('./src/views/User/AccountSetting/components/AccountSettingHome/AccountSettingHome.vue')['default']
|
AccountSettingHome: typeof import('./src/views/User/AccountSetting/components/AccountSettingHome/AccountSettingHome.vue')['default']
|
||||||
AccountSettingInfo: typeof import('./src/views/User/AccountSetting/components/AccountSettingInfo/AccountSettingInfo.vue')['default']
|
AccountSettingInfo: typeof import('./src/views/User/AccountSetting/components/AccountSettingInfo/AccountSettingInfo.vue')['default']
|
||||||
|
AccountSettingLog: typeof import('./src/views/User/AccountSetting/components/AccountSettingLog/AccountSettingLog.vue')['default']
|
||||||
AccountSettingSidebar: typeof import('./src/views/User/AccountSetting/components/AccountSettingSidebar/AccountSettingSidebar.vue')['default']
|
AccountSettingSidebar: typeof import('./src/views/User/AccountSetting/components/AccountSettingSidebar/AccountSettingSidebar.vue')['default']
|
||||||
AccountSettingStorage: typeof import('./src/views/User/AccountSetting/components/AccountSettingStorage/AccountSettingStorage.vue')['default']
|
AccountSettingStorage: typeof import('./src/views/User/AccountSetting/components/AccountSettingStorage/AccountSettingStorage.vue')['default']
|
||||||
|
AccountSettingTask: typeof import('./src/views/User/AccountSetting/components/AccountSettingTask/AccountSettingTask.vue')['default']
|
||||||
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
|
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
|
||||||
ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
|
ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
|
||||||
ACol: typeof import('ant-design-vue/es')['Col']
|
ACol: typeof import('ant-design-vue/es')['Col']
|
||||||
@@ -67,7 +68,6 @@ declare module 'vue' {
|
|||||||
ASelect: typeof import('ant-design-vue/es')['Select']
|
ASelect: typeof import('ant-design-vue/es')['Select']
|
||||||
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
||||||
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
|
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
|
||||||
ASlider: typeof import('ant-design-vue/es')['Slider']
|
|
||||||
ASpace: typeof import('ant-design-vue/es')['Space']
|
ASpace: typeof import('ant-design-vue/es')['Space']
|
||||||
ASpin: typeof import('ant-design-vue/es')['Spin']
|
ASpin: typeof import('ant-design-vue/es')['Spin']
|
||||||
AStatistic: typeof import('ant-design-vue/es')['Statistic']
|
AStatistic: typeof import('ant-design-vue/es')['Statistic']
|
||||||
@@ -78,9 +78,10 @@ declare module 'vue' {
|
|||||||
ATabs: typeof import('ant-design-vue/es')['Tabs']
|
ATabs: typeof import('ant-design-vue/es')['Tabs']
|
||||||
ATag: typeof import('ant-design-vue/es')['Tag']
|
ATag: typeof import('ant-design-vue/es')['Tag']
|
||||||
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
||||||
|
ATimeline: typeof import('ant-design-vue/es')['Timeline']
|
||||||
|
ATimelineItem: typeof import('ant-design-vue/es')['TimelineItem']
|
||||||
|
ATimePicker: typeof import('ant-design-vue/es')['TimePicker']
|
||||||
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
||||||
ATree: typeof import('ant-design-vue/es')['Tree']
|
|
||||||
ATreeSelect: typeof import('ant-design-vue/es')['TreeSelect']
|
|
||||||
ATypography: typeof import('ant-design-vue/es')['Typography']
|
ATypography: typeof import('ant-design-vue/es')['Typography']
|
||||||
ATypographyParagraph: typeof import('ant-design-vue/es')['TypographyParagraph']
|
ATypographyParagraph: typeof import('ant-design-vue/es')['TypographyParagraph']
|
||||||
AUpload: typeof import('ant-design-vue/es')['Upload']
|
AUpload: typeof import('ant-design-vue/es')['Upload']
|
||||||
@@ -94,6 +95,7 @@ declare module 'vue' {
|
|||||||
CheckCard: typeof import('./src/components/CheckCard/CheckCard.vue')['default']
|
CheckCard: typeof import('./src/components/CheckCard/CheckCard.vue')['default']
|
||||||
CheckCircleOutlined: typeof import('@ant-design/icons-vue')['CheckCircleOutlined']
|
CheckCircleOutlined: typeof import('@ant-design/icons-vue')['CheckCircleOutlined']
|
||||||
CheckOutlined: typeof import('@ant-design/icons-vue')['CheckOutlined']
|
CheckOutlined: typeof import('@ant-design/icons-vue')['CheckOutlined']
|
||||||
|
ClockCircleOutlined: typeof import('@ant-design/icons-vue')['ClockCircleOutlined']
|
||||||
CloseCircleOutlined: typeof import('@ant-design/icons-vue')['CloseCircleOutlined']
|
CloseCircleOutlined: typeof import('@ant-design/icons-vue')['CloseCircleOutlined']
|
||||||
CloseOutlined: typeof import('@ant-design/icons-vue')['CloseOutlined']
|
CloseOutlined: typeof import('@ant-design/icons-vue')['CloseOutlined']
|
||||||
Clouds: typeof import('./src/components/Clouds/Clouds.vue')['default']
|
Clouds: typeof import('./src/components/Clouds/Clouds.vue')['default']
|
||||||
@@ -106,31 +108,30 @@ declare module 'vue' {
|
|||||||
CommonPhoneUpload: typeof import('./src/views/Phone/CommonPhoneUpload/CommonPhoneUpload.vue')['default']
|
CommonPhoneUpload: typeof import('./src/views/Phone/CommonPhoneUpload/CommonPhoneUpload.vue')['default']
|
||||||
CompareImage: typeof import('./src/views/Upscale/CompareImage.vue')['default']
|
CompareImage: typeof import('./src/views/Upscale/CompareImage.vue')['default']
|
||||||
Dashboard: typeof import('./src/views/Admin/System/Pages/Dashboard.vue')['default']
|
Dashboard: typeof import('./src/views/Admin/System/Pages/Dashboard.vue')['default']
|
||||||
|
DeleteConfirmModal: typeof import('./src/views/User/AccountSetting/components/AccountSettingTask/components/DeleteConfirmModal.vue')['default']
|
||||||
DeleteOutlined: typeof import('@ant-design/icons-vue')['DeleteOutlined']
|
DeleteOutlined: typeof import('@ant-design/icons-vue')['DeleteOutlined']
|
||||||
DownloadOutlined: typeof import('@ant-design/icons-vue')['DownloadOutlined']
|
DownloadOutlined: typeof import('@ant-design/icons-vue')['DownloadOutlined']
|
||||||
DownOutlined: typeof import('@ant-design/icons-vue')['DownOutlined']
|
DownOutlined: typeof import('@ant-design/icons-vue')['DownOutlined']
|
||||||
DynamicTitle: typeof import('./src/components/DynamicTitle/DynamicTitle.vue')['default']
|
DynamicTitle: typeof import('./src/components/DynamicTitle/DynamicTitle.vue')['default']
|
||||||
EditOutlined: typeof import('@ant-design/icons-vue')['EditOutlined']
|
EditOutlined: typeof import('@ant-design/icons-vue')['EditOutlined']
|
||||||
EmailModal: typeof import('./src/views/User/AccountSetting/components/AccountSettingHome/EmailModal.vue')['default']
|
EmailModal: typeof import('./src/views/User/AccountSetting/components/AccountSettingHome/EmailModal.vue')['default']
|
||||||
|
ExclamationCircleOutlined: typeof import('@ant-design/icons-vue')['ExclamationCircleOutlined']
|
||||||
EyeInvisibleOutlined: typeof import('@ant-design/icons-vue')['EyeInvisibleOutlined']
|
EyeInvisibleOutlined: typeof import('@ant-design/icons-vue')['EyeInvisibleOutlined']
|
||||||
EyeOutlined: typeof import('@ant-design/icons-vue')['EyeOutlined']
|
EyeOutlined: typeof import('@ant-design/icons-vue')['EyeOutlined']
|
||||||
FileImageOutlined: typeof import('@ant-design/icons-vue')['FileImageOutlined']
|
FileImageOutlined: typeof import('@ant-design/icons-vue')['FileImageOutlined']
|
||||||
FilerobotImageEditor: typeof import('./src/components/FilerobotImageEditor/FilerobotImageEditor.vue')['default']
|
FilerobotImageEditor: typeof import('./src/components/FilerobotImageEditor/FilerobotImageEditor.vue')['default']
|
||||||
Folder: typeof import('./src/components/Folder/Folder.vue')['default']
|
Folder: typeof import('./src/components/Folder/Folder.vue')['default']
|
||||||
FolderOutlined: typeof import('@ant-design/icons-vue')['FolderOutlined']
|
|
||||||
ForgetPage: typeof import('./src/views/Forget/ForgetPage.vue')['default']
|
ForgetPage: typeof import('./src/views/Forget/ForgetPage.vue')['default']
|
||||||
GradientText: typeof import('./src/components/MyUI/GradientText/GradientText.vue')['default']
|
GradientText: typeof import('./src/components/MyUI/GradientText/GradientText.vue')['default']
|
||||||
|
HeatmapMax: typeof import('./src/components/HeatmapMax/HeatmapMax.vue')['default']
|
||||||
HeatmapPro: typeof import('./src/components/HeatmapPro/HeatmapPro.vue')['default']
|
HeatmapPro: typeof import('./src/components/HeatmapPro/HeatmapPro.vue')['default']
|
||||||
ImageBed: typeof import('./src/views/ImageBed/index.vue')['default']
|
ImageBed: typeof import('./src/views/ImageBed/index.vue')['default']
|
||||||
ImageEnhancer: typeof import('./src/components/ImageEnhancer/ImageEnhancer.vue')['default']
|
ImageEnhancer: typeof import('./src/components/ImageEnhancer/ImageEnhancer.vue')['default']
|
||||||
ImageList: typeof import('./src/views/Photograph/PrivacySpace/ImageList.vue')['default']
|
|
||||||
ImageShare: typeof import('./src/views/Share/ImageShare/ImageShare.vue')['default']
|
ImageShare: typeof import('./src/views/Share/ImageShare/ImageShare.vue')['default']
|
||||||
ImageToolbar: typeof import('./src/components/ImageToolbar/ImageToolbar.vue')['default']
|
ImageToolbar: typeof import('./src/components/ImageToolbar/ImageToolbar.vue')['default']
|
||||||
ImageUpload: typeof import('./src/components/ImageUpload/ImageUpload.vue')['default']
|
ImageUpload: typeof import('./src/components/ImageUpload/ImageUpload.vue')['default']
|
||||||
ImageWaterfallList: typeof import('./src/components/ImageWaterfallList/ImageWaterfallList.vue')['default']
|
ImageWaterfallList: typeof import('./src/components/ImageWaterfallList/ImageWaterfallList.vue')['default']
|
||||||
InboxOutlined: typeof import('@ant-design/icons-vue')['InboxOutlined']
|
|
||||||
Index: typeof import('./src/views/Admin/System/Index.vue')['default']
|
Index: typeof import('./src/views/Admin/System/Index.vue')['default']
|
||||||
InfoCircleOutlined: typeof import('@ant-design/icons-vue')['InfoCircleOutlined']
|
|
||||||
LandingPage: typeof import('./src/views/Landing/LandingPage.vue')['default']
|
LandingPage: typeof import('./src/views/Landing/LandingPage.vue')['default']
|
||||||
LeftOutlined: typeof import('@ant-design/icons-vue')['LeftOutlined']
|
LeftOutlined: typeof import('@ant-design/icons-vue')['LeftOutlined']
|
||||||
LinkOutlined: typeof import('@ant-design/icons-vue')['LinkOutlined']
|
LinkOutlined: typeof import('@ant-design/icons-vue')['LinkOutlined']
|
||||||
@@ -147,7 +148,6 @@ declare module 'vue' {
|
|||||||
LogoutOutlined: typeof import('@ant-design/icons-vue')['LogoutOutlined']
|
LogoutOutlined: typeof import('@ant-design/icons-vue')['LogoutOutlined']
|
||||||
MainPage: typeof import('./src/views/Main/MainPage.vue')['default']
|
MainPage: typeof import('./src/views/Main/MainPage.vue')['default']
|
||||||
MessageReport: typeof import('./src/components/CommentReply/src/MessageReport/MessageReport.vue')['default']
|
MessageReport: typeof import('./src/components/CommentReply/src/MessageReport/MessageReport.vue')['default']
|
||||||
MinusOutlined: typeof import('@ant-design/icons-vue')['MinusOutlined']
|
|
||||||
NotFound: typeof import('./src/views/404/NotFound.vue')['default']
|
NotFound: typeof import('./src/views/404/NotFound.vue')['default']
|
||||||
OrderedListOutlined: typeof import('@ant-design/icons-vue')['OrderedListOutlined']
|
OrderedListOutlined: typeof import('@ant-design/icons-vue')['OrderedListOutlined']
|
||||||
PageError403: typeof import('./src/views/Admin/Error/PageError403.vue')['default']
|
PageError403: typeof import('./src/views/Admin/Error/PageError403.vue')['default']
|
||||||
@@ -171,6 +171,7 @@ declare module 'vue' {
|
|||||||
Popover: typeof import('./src/components/MyUI/Popover/Popover.vue')['default']
|
Popover: typeof import('./src/components/MyUI/Popover/Popover.vue')['default']
|
||||||
PreviewBlurDetect: typeof import('./src/views/Preview/PreviewBlurDetect/PreviewBlurDetect.vue')['default']
|
PreviewBlurDetect: typeof import('./src/views/Preview/PreviewBlurDetect/PreviewBlurDetect.vue')['default']
|
||||||
PreviewOCR: typeof import('./src/views/Preview/PreviewOCR/PreviewOCR.vue')['default']
|
PreviewOCR: typeof import('./src/views/Preview/PreviewOCR/PreviewOCR.vue')['default']
|
||||||
|
PreviewQRCode: typeof import('./src/views/Preview/PreviewQRCode/PreviewQRCode.vue')['default']
|
||||||
PrivacyImageList: typeof import('./src/views/Photograph/PrivacySpace/PrivacyImageList.vue')['default']
|
PrivacyImageList: typeof import('./src/views/Photograph/PrivacySpace/PrivacyImageList.vue')['default']
|
||||||
PrivacySpace: typeof import('./src/views/Photograph/PrivacySpace/PrivacySpace.vue')['default']
|
PrivacySpace: typeof import('./src/views/Photograph/PrivacySpace/PrivacySpace.vue')['default']
|
||||||
QrcodeOutlined: typeof import('@ant-design/icons-vue')['QrcodeOutlined']
|
QrcodeOutlined: typeof import('@ant-design/icons-vue')['QrcodeOutlined']
|
||||||
@@ -200,12 +201,14 @@ declare module 'vue' {
|
|||||||
StarButton: typeof import('./src/components/StarButton/StarButton.vue')['default']
|
StarButton: typeof import('./src/components/StarButton/StarButton.vue')['default']
|
||||||
StorageCard: typeof import('./src/views/User/AccountSetting/components/AccountSettingStorage/StorageCard.vue')['default']
|
StorageCard: typeof import('./src/views/User/AccountSetting/components/AccountSettingStorage/StorageCard.vue')['default']
|
||||||
StorageManagement: typeof import('./src/views/Admin/System/Pages/StorageManagement.vue')['default']
|
StorageManagement: typeof import('./src/views/Admin/System/Pages/StorageManagement.vue')['default']
|
||||||
System: typeof import('./src/views/Admin/System/index.vue')['default']
|
|
||||||
SystemHeader: typeof import('./src/views/Admin/System/Components/SystemHeader.vue')['default']
|
SystemHeader: typeof import('./src/views/Admin/System/Components/SystemHeader.vue')['default']
|
||||||
SystemLogs: typeof import('./src/views/Admin/System/Pages/SystemLogs.vue')['default']
|
SystemLogs: typeof import('./src/views/Admin/System/Pages/SystemLogs.vue')['default']
|
||||||
SystemSidebar: typeof import('./src/views/Admin/System/Components/SystemSidebar.vue')['default']
|
SystemSidebar: typeof import('./src/views/Admin/System/Components/SystemSidebar.vue')['default']
|
||||||
TabletOutlined: typeof import('@ant-design/icons-vue')['TabletOutlined']
|
TabletOutlined: typeof import('@ant-design/icons-vue')['TabletOutlined']
|
||||||
TestView: typeof import('./src/views/Preview/TestView.vue')['default']
|
TaskCard: typeof import('./src/views/User/AccountSetting/components/AccountSettingTask/components/TaskCard.vue')['default']
|
||||||
|
TaskForm: typeof import('./src/views/User/AccountSetting/components/AccountSettingTask/components/TaskForm.vue')['default']
|
||||||
|
TaskSchedule: typeof import('./src/components/TaskSchedule/TaskSchedule.vue')['default']
|
||||||
|
TaskTypeSelector: typeof import('./src/views/User/AccountSetting/components/AccountSettingTask/components/TaskTypeSelector.vue')['default']
|
||||||
ThingAlbumDetail: typeof import('./src/views/Album/ThingAlbum/ThingAlbumDetail.vue')['default']
|
ThingAlbumDetail: typeof import('./src/views/Album/ThingAlbum/ThingAlbumDetail.vue')['default']
|
||||||
ThingAlbumIndex: typeof import('./src/views/Album/ThingAlbum/ThingAlbumIndex.vue')['default']
|
ThingAlbumIndex: typeof import('./src/views/Album/ThingAlbum/ThingAlbumIndex.vue')['default']
|
||||||
ThingAlbumList: typeof import('./src/views/Album/ThingAlbum/ThingAlbumList.vue')['default']
|
ThingAlbumList: typeof import('./src/views/Album/ThingAlbum/ThingAlbumList.vue')['default']
|
||||||
@@ -223,7 +226,6 @@ declare module 'vue' {
|
|||||||
UserList: typeof import('./src/views/Admin/System/Pages/UserList.vue')['default']
|
UserList: typeof import('./src/views/Admin/System/Pages/UserList.vue')['default']
|
||||||
UserOutlined: typeof import('@ant-design/icons-vue')['UserOutlined']
|
UserOutlined: typeof import('@ant-design/icons-vue')['UserOutlined']
|
||||||
VisitStatistics: typeof import('./src/views/Admin/System/Pages/VisitStatistics.vue')['default']
|
VisitStatistics: typeof import('./src/views/Admin/System/Pages/VisitStatistics.vue')['default']
|
||||||
VueCompareImage: typeof import('./src/components/VueCompareImage/VueCompareImage.vue')['default']
|
|
||||||
WarningOutlined: typeof import('@ant-design/icons-vue')['WarningOutlined']
|
WarningOutlined: typeof import('@ant-design/icons-vue')['WarningOutlined']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -12,6 +12,8 @@ server {
|
|||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
access_log /var/log/nginx/host.access.log main;
|
access_log /var/log/nginx/host.access.log main;
|
||||||
error_log /var/log/nginx/error.log error;
|
error_log /var/log/nginx/error.log error;
|
||||||
|
client_max_body_size 120M;
|
||||||
|
client_body_buffer_size 10m;
|
||||||
|
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
@@ -39,6 +41,8 @@ server {
|
|||||||
proxy_send_timeout 3600s; # 设置为1小时
|
proxy_send_timeout 3600s; # 设置为1小时
|
||||||
send_timeout 3600s; # 设置为1小时
|
send_timeout 3600s; # 设置为1小时
|
||||||
keepalive_timeout 3600s; # 设置为1小时
|
keepalive_timeout 3600s; # 设置为1小时
|
||||||
|
client_max_body_size 120M;
|
||||||
|
client_body_buffer_size 10m;
|
||||||
}
|
}
|
||||||
|
|
||||||
error_page 500 502 503 504 /50x.html;
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
14
package.json
14
package.json
@@ -33,8 +33,8 @@
|
|||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/json-stringify-safe": "^5.0.3",
|
"@types/json-stringify-safe": "^5.0.3",
|
||||||
"@types/leaflet": "^1.9.16",
|
"@types/leaflet": "^1.9.17",
|
||||||
"@types/node": "^22.13.11",
|
"@types/node": "^22.13.14",
|
||||||
"@types/nprogress": "^0.2.3",
|
"@types/nprogress": "^0.2.3",
|
||||||
"@vladmandic/face-api": "^1.7.15",
|
"@vladmandic/face-api": "^1.7.15",
|
||||||
"@vuepic/vue-datepicker": "^11.0.2",
|
"@vuepic/vue-datepicker": "^11.0.2",
|
||||||
@@ -66,12 +66,12 @@
|
|||||||
"nsfwjs": "^4.2.1",
|
"nsfwjs": "^4.2.1",
|
||||||
"opencv-qr": "^0.7.0",
|
"opencv-qr": "^0.7.0",
|
||||||
"pinia": "^3.0.1",
|
"pinia": "^3.0.1",
|
||||||
"pinia-plugin-persistedstate-2": "^2.0.29",
|
"pinia-plugin-persistedstate-2": "^2.0.30",
|
||||||
"qr-scanner-wechat": "^0.1.3",
|
"qr-scanner-wechat": "^0.1.3",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"seedrandom": "^3.0.5",
|
"seedrandom": "^3.0.5",
|
||||||
"swiper": "^11.2.6",
|
"swiper": "^11.2.6",
|
||||||
"unplugin-auto-import": "^19.1.1",
|
"unplugin-auto-import": "^19.1.2",
|
||||||
"upscaler": "^1.0.0-beta.19",
|
"upscaler": "^1.0.0-beta.19",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vite-plugin-html": "^3.2.2",
|
"vite-plugin-html": "^3.2.2",
|
||||||
@@ -91,16 +91,16 @@
|
|||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"sass": "^1.86.0",
|
"sass": "^1.86.0",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.2",
|
||||||
"typescript-eslint": "^8.27.0",
|
"typescript-eslint": "^8.28.0",
|
||||||
"unplugin-vue-components": "^28.4.1",
|
"unplugin-vue-components": "^28.4.1",
|
||||||
"vite": "^6.2.2",
|
"vite": "^6.2.3",
|
||||||
"vite-plugin-bundle-obfuscator": "1.4.2",
|
"vite-plugin-bundle-obfuscator": "1.4.2",
|
||||||
"vite-plugin-chunk-split": "^0.5.0",
|
"vite-plugin-chunk-split": "^0.5.0",
|
||||||
"vue-tsc": "2.2.8"
|
"vue-tsc": "2.2.8"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"vite-plugin-chunk-split": {
|
"vite-plugin-chunk-split": {
|
||||||
"vite": "^6.2.0"
|
"vite": "^6.2.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,17 +2,14 @@ import {service} from "@/utils/alova/service.ts";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 从一个存储桶备份到另一个存储桶
|
* 从一个存储桶备份到另一个存储桶
|
||||||
* @param sourceProvider 源存储商
|
* @param sourceStorage 源存储商
|
||||||
* @param sourceBucket 源存储桶
|
* @param targetStorage 目标存储商
|
||||||
* @param targetProvider 目标存储商
|
|
||||||
* @param targetBucket 目标存储桶
|
|
||||||
*/
|
*/
|
||||||
export const backupStorageApi = (sourceProvider: string, sourceBucket: string, targetProvider: string, targetBucket: string) => {
|
export const backupStorageApi = (sourceStorage: string, targetStorage: string) => {
|
||||||
return service.Post('/api/auth/storage/backup', {
|
return service.Post('/api/auth/storage/backup', {
|
||||||
source_provider: sourceProvider,
|
source_storage: sourceStorage,
|
||||||
source_bucket: sourceBucket,
|
target_storage: targetStorage,
|
||||||
target_provider: targetProvider,
|
|
||||||
target_bucket: targetBucket,
|
|
||||||
}, {
|
}, {
|
||||||
meta: {
|
meta: {
|
||||||
ignoreToken: false,
|
ignoreToken: false,
|
||||||
|
6
src/assets/svgs/cleanup.svg
Normal file
6
src/assets/svgs/cleanup.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 6h18"/>
|
||||||
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||||
|
<path d="M10 11v6"/>
|
||||||
|
<path d="M14 11v6"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 326 B |
1
src/assets/svgs/endpoint.svg
Normal file
1
src/assets/svgs/endpoint.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1743043408733" class="icon" viewBox="0 0 1037 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5882" width="200" height="200"><path d="M562.201852 861.528553h248.388751a54.824544 54.824544 0 0 0 0-109.649089H562.201852a54.824544 54.824544 0 0 0 0 109.649089zM303.62551 454.142734a147.808385 147.808385 0 0 0 147.572834-147.572833c0.058888-39.39594-15.310829-76.495256-43.16476-104.349187s-64.953246-43.341423-104.408074-43.341422A147.867272 147.867272 0 0 0 156.052677 308.572086a147.808385 147.808385 0 0 0 147.572833 145.570648zM265.58399 308.042096l0.058888-1.648859a38.159296 38.159296 0 0 1 37.982632-37.923745 38.100408 38.100408 0 0 1 0 76.083041 38.159296 38.159296 0 0 1-38.04152-36.510437z" fill="#4DA1FF" p-id="5883"></path><path d="M912.230951 10.010926H115.066766A102.170338 102.170338 0 0 0 13.249756 111.827937v797.340848a101.934786 101.934786 0 0 0 101.81701 101.758123h797.340848a100.698143 100.698143 0 0 0 71.902007-29.915004c19.197424-19.256311 29.73834-44.872506 29.679453-72.019783V111.827937A101.993674 101.993674 0 0 0 912.230951 10.010926z m-7.832078 109.590201v781.73558H122.839956V119.601127h781.558917z" fill="#4DA1FF" p-id="5884"></path></svg>
|
After Width: | Height: | Size: 1.2 KiB |
8
src/assets/svgs/recycle.svg
Normal file
8
src/assets/svgs/recycle.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
||||||
|
<path d="M7.5 4.21l4.5 2.6 4.5-2.6"/>
|
||||||
|
<path d="M7.5 19.79V14.6L3 12"/>
|
||||||
|
<path d="M21 12l-4.5 2.6v5.19"/>
|
||||||
|
<path d="M3.27 6.96L12 12.01l8.73-5.05"/>
|
||||||
|
<path d="M12 22.08V12"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 482 B |
539
src/components/HeatmapMax/HeatmapMax.vue
Normal file
539
src/components/HeatmapMax/HeatmapMax.vue
Normal file
@@ -0,0 +1,539 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="containerRef" class="heatmap-container">
|
||||||
|
<!-- 标题和颜色图例 -->
|
||||||
|
<div class="header">
|
||||||
|
<h3 class="month-title">{{ currentMonthTitle }}</h3>
|
||||||
|
<div class="legend">
|
||||||
|
<span class="legend-text">低</span>
|
||||||
|
<div v-for="(color, index) in colorLevels" :key="index" class="legend-item">
|
||||||
|
<a-tooltip :title="color.description">
|
||||||
|
<div class="legend-block" :style="{ backgroundColor: color.color }" />
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
<span class="legend-text">高</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主图表区域 -->
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
|
||||||
|
<!-- 贡献图主体 -->
|
||||||
|
<div class="chart-body">
|
||||||
|
<!-- 星期坐标轴 -->
|
||||||
|
<div class="weekday-axis">
|
||||||
|
<div v-for="(weekday, index) in weekdays" :key="index" class="weekday-label">{{ weekday }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 贡献格子 -->
|
||||||
|
<div class="weeks-container" :style="{ gap: `${cellGap}px` }">
|
||||||
|
<div
|
||||||
|
v-for="(week, wi) in visibleWeeks"
|
||||||
|
:key="wi"
|
||||||
|
class="week-column"
|
||||||
|
:style="{ gap: `${cellGap}px` }"
|
||||||
|
>
|
||||||
|
<a-tooltip
|
||||||
|
v-for="(day, di) in week"
|
||||||
|
:key="di"
|
||||||
|
:mouseEnterDelay="0.3"
|
||||||
|
:title="`${formatTooltip(day.date)}: ${day.count} 次上传`"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="day-cell"
|
||||||
|
:style="{
|
||||||
|
width: `${cellSize}px`,
|
||||||
|
height: `${cellSize}px`,
|
||||||
|
backgroundColor: getColor(day.count)
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
||||||
|
import {
|
||||||
|
format,
|
||||||
|
startOfWeek,
|
||||||
|
addDays,
|
||||||
|
eachDayOfInterval,
|
||||||
|
getMonth,
|
||||||
|
parseISO,
|
||||||
|
getYear,
|
||||||
|
subDays
|
||||||
|
} from 'date-fns';
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
import { zhCN } from 'date-fns/locale';
|
||||||
|
|
||||||
|
interface Contribution {
|
||||||
|
date: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ColorLevel {
|
||||||
|
color: string;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MonthLabel {
|
||||||
|
name: string;
|
||||||
|
position: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
contributions: {
|
||||||
|
type: Array as PropType<Contribution[]>,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
colorLevels: {
|
||||||
|
type: Array as PropType<ColorLevel[]>,
|
||||||
|
default: () => [
|
||||||
|
{
|
||||||
|
color: '#ebedf0',
|
||||||
|
min: 0,
|
||||||
|
max: 0,
|
||||||
|
description: '没有上传'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#9be9a8',
|
||||||
|
min: 1,
|
||||||
|
max: 3,
|
||||||
|
description: '1-3 上传'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#40c463',
|
||||||
|
min: 4,
|
||||||
|
max: 5,
|
||||||
|
description: '4-5 上传'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#30a14e',
|
||||||
|
min: 6,
|
||||||
|
max: 7,
|
||||||
|
description: '6-7 上传'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#216e39',
|
||||||
|
min: 8,
|
||||||
|
max: Infinity,
|
||||||
|
description: '8+ 上传'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// 新增:可配置显示的周数
|
||||||
|
weeksToShow: {
|
||||||
|
type: Number,
|
||||||
|
default: 52 // 默认显示52周,一年
|
||||||
|
},
|
||||||
|
// 新增:可配置星期标签
|
||||||
|
weekdayLabels: {
|
||||||
|
type: Array as PropType<string[]>,
|
||||||
|
default: () => ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const containerRef = ref<HTMLElement | null>(null);
|
||||||
|
const cellSize = ref(12); // 初始单元格大小
|
||||||
|
const cellGap = ref(3); // 初始单元格间距
|
||||||
|
// 使用可配置的星期标签,并确保正确显示7天
|
||||||
|
const weekdays = computed(() => {
|
||||||
|
// 如果用户提供了自定义标签,使用用户提供的标签
|
||||||
|
// 否则使用默认的7天标签
|
||||||
|
return props.weekdayLabels;
|
||||||
|
});
|
||||||
|
const visibleWeeks = ref<Array<Array<{ date: Date; count: number }>>>([]);
|
||||||
|
|
||||||
|
// 响应式变量,用于控制图表尺寸
|
||||||
|
const chartWidth = ref(0);
|
||||||
|
const chartHeight = ref(0);
|
||||||
|
|
||||||
|
// 当前月份标题
|
||||||
|
const currentMonthTitle = computed(() => {
|
||||||
|
const today = new Date();
|
||||||
|
return format(today, 'yyyy年MM月', { locale: zhCN });
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新尺寸函数
|
||||||
|
const updateSize = () => {
|
||||||
|
if (!containerRef.value) return;
|
||||||
|
|
||||||
|
const container = containerRef.value;
|
||||||
|
const containerWidth = container.offsetWidth;
|
||||||
|
const containerHeight = container.offsetHeight;
|
||||||
|
|
||||||
|
// 保留边距空间,考虑到星期标签的空间
|
||||||
|
const availableWidth = containerWidth - 70; // 左右边距,增加左侧星期标签的空间
|
||||||
|
const availableHeight = containerHeight - 110; // 上下边距,考虑标题和底部空间
|
||||||
|
|
||||||
|
// 更新图表尺寸
|
||||||
|
chartWidth.value = availableWidth;
|
||||||
|
chartHeight.value = availableHeight;
|
||||||
|
|
||||||
|
// 计算单元格尺寸,确保合理的显示效果
|
||||||
|
// 对于单月视图,周数通常为4-6周
|
||||||
|
const weeksCount = visibleWeeks.value.length || 6; // 默认按6周计算
|
||||||
|
const daysCount = 7; // 一周7天
|
||||||
|
|
||||||
|
// 计算单元格的最大可能尺寸
|
||||||
|
const maxCellWidth = Math.max(1, (availableWidth - 50) / weeksCount); // 水平方向,确保至少为1
|
||||||
|
const maxCellHeight = Math.max(1, availableHeight / daysCount); // 垂直方向,确保至少为1
|
||||||
|
|
||||||
|
// 取较小值确保单元格是正方形,但为单月视图增加单元格大小
|
||||||
|
const maxCellSize = Math.min(maxCellWidth, maxCellHeight);
|
||||||
|
|
||||||
|
// 设置单元格大小和间距,单月视图可以使用更大的单元格
|
||||||
|
cellSize.value = Math.max(8, Math.min(18, maxCellSize - 2)); // 减去2给间距留空间,最小值改为8,最大值增加到18
|
||||||
|
cellGap.value = Math.max(2, Math.min(4, cellSize.value * 0.15)); // 最小间距改为2
|
||||||
|
|
||||||
|
// 强制下一帧更新DOM
|
||||||
|
nextTick(() => {
|
||||||
|
// 确保星期标签正确分布
|
||||||
|
const weekdayAxis = container.querySelector('.weekday-axis');
|
||||||
|
if (weekdayAxis) {
|
||||||
|
const weekdayLabels = weekdayAxis.querySelectorAll('.weekday-label');
|
||||||
|
const totalLabels = weekdayLabels.length;
|
||||||
|
|
||||||
|
if (totalLabels > 0) {
|
||||||
|
// 计算每个标签应该的位置
|
||||||
|
weekdayLabels.forEach((label, index) => {
|
||||||
|
const element = label as HTMLElement;
|
||||||
|
// 平均分布标签
|
||||||
|
element.style.height = `${100 / totalLabels}%`;
|
||||||
|
element.style.display = 'flex';
|
||||||
|
element.style.alignItems = 'center';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 日期有效性检查
|
||||||
|
const isValidDate = (date: Date) => {
|
||||||
|
return date instanceof Date && !isNaN(date.getTime());
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成当前月份的日期网格
|
||||||
|
const generateDateGrid = () => {
|
||||||
|
const today = new Date();
|
||||||
|
|
||||||
|
// 获取当前月份的第一天
|
||||||
|
const currentMonth = today.getMonth();
|
||||||
|
const currentYear = today.getFullYear();
|
||||||
|
const firstDayOfMonth = new Date(currentYear, currentMonth, 1);
|
||||||
|
|
||||||
|
// 获取当前月份的最后一天
|
||||||
|
const lastDayOfMonth = new Date(currentYear, currentMonth + 1, 0);
|
||||||
|
|
||||||
|
// 获取第一天所在周的周一
|
||||||
|
const startDate = startOfWeek(firstDayOfMonth, { weekStartsOn: 1 }); // 从周一开始
|
||||||
|
|
||||||
|
// 获取最后一天(或今天,取较早的日期)
|
||||||
|
const endDate = today > lastDayOfMonth ? lastDayOfMonth : today;
|
||||||
|
|
||||||
|
const weeksArray: Date[][] = [];
|
||||||
|
let currentWeekStart = startDate;
|
||||||
|
|
||||||
|
while (currentWeekStart <= endDate) {
|
||||||
|
const weekEnd = addDays(currentWeekStart, 6);
|
||||||
|
const weekDays = eachDayOfInterval({
|
||||||
|
start: currentWeekStart,
|
||||||
|
end: weekEnd > endDate ? endDate : weekEnd
|
||||||
|
}).filter(date => isValidDate(date));
|
||||||
|
|
||||||
|
if (weekDays.length > 0) {
|
||||||
|
weeksArray.push(weekDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentWeekStart = addDays(currentWeekStart, 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
return weeksArray;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理贡献数据,只显示当前月份的数据
|
||||||
|
const processContributions = () => {
|
||||||
|
// 获取当前月份
|
||||||
|
const today = new Date();
|
||||||
|
const currentMonth = today.getMonth();
|
||||||
|
const currentYear = today.getFullYear();
|
||||||
|
|
||||||
|
const contributionMap = new Map(
|
||||||
|
props.contributions
|
||||||
|
.filter(c => {
|
||||||
|
try {
|
||||||
|
const date = parseISO(c.date);
|
||||||
|
// 只保留当前月份的数据
|
||||||
|
return isValidDate(date) &&
|
||||||
|
date.getMonth() === currentMonth &&
|
||||||
|
date.getFullYear() === currentYear;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(c => [format(parseISO(c.date), 'yyyy-MM-dd'), c.count])
|
||||||
|
);
|
||||||
|
|
||||||
|
return generateDateGrid()
|
||||||
|
.map(week =>
|
||||||
|
week.map(date => ({
|
||||||
|
date,
|
||||||
|
count: contributionMap.get(format(date, 'yyyy-MM-dd')) || 0
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
.filter(week => week.length > 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 不再需要月份标签,因为只显示当前月份
|
||||||
|
const monthLabels = computed(() => []);
|
||||||
|
|
||||||
|
// 颜色匹配逻辑
|
||||||
|
const getColor = (count: number) => {
|
||||||
|
return (
|
||||||
|
props.colorLevels.find(l => count >= l.min && count <= l.max)?.color ||
|
||||||
|
'#ebedf0'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化工具提示
|
||||||
|
const formatTooltip = (date: Date) => {
|
||||||
|
return format(date, 'yyyy年MM月dd日 EEEE', { locale: zhCN });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听贡献数据变化
|
||||||
|
watch(
|
||||||
|
() => props.contributions,
|
||||||
|
() => {
|
||||||
|
visibleWeeks.value = processContributions();
|
||||||
|
nextTick(() => {
|
||||||
|
updateSize();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
// 先处理数据
|
||||||
|
visibleWeeks.value = processContributions();
|
||||||
|
|
||||||
|
// 立即执行一次更新尺寸,确保初始渲染正确
|
||||||
|
nextTick(() => {
|
||||||
|
updateSize();
|
||||||
|
|
||||||
|
// 添加窗口大小变化监听
|
||||||
|
window.addEventListener('resize', updateSize);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 使用ResizeObserver监听容器大小变化
|
||||||
|
const observer = new ResizeObserver(() => {
|
||||||
|
updateSize();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (containerRef.value) {
|
||||||
|
observer.observe(containerRef.value);
|
||||||
|
|
||||||
|
// 同时监听父容器的大小变化
|
||||||
|
const parentElement = containerRef.value.parentElement;
|
||||||
|
if (parentElement) {
|
||||||
|
observer.observe(parentElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理函数
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (observer) observer.disconnect();
|
||||||
|
window.removeEventListener('resize', updateSize);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.heatmap-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 300px;
|
||||||
|
min-height: 150px;
|
||||||
|
padding: 20px 15px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #24292e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 2;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-text {
|
||||||
|
color: #999999;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item + .legend-item {
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-block {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-wrapper {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 40px;
|
||||||
|
margin-left: 40px;
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
height: calc(100% - 40px);
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-axis {
|
||||||
|
position: absolute;
|
||||||
|
top: -25px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 20px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-label {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #586069;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
pointer-events: none;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-body {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-axis {
|
||||||
|
position: absolute;
|
||||||
|
left: -35px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 30px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-label {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #586069;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||||
|
height: calc(100% / 7); /* 确保7个标签均匀分布 */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weeks-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
padding-right: 20px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weeks-container::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome, Safari, Opera */
|
||||||
|
}
|
||||||
|
|
||||||
|
.week-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-cell {
|
||||||
|
border-radius: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-cell:hover {
|
||||||
|
transform: scale(1.15);
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.heatmap-container {
|
||||||
|
padding: 15px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend {
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-block {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-text {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-wrapper {
|
||||||
|
margin-top: 35px;
|
||||||
|
margin-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-axis {
|
||||||
|
left: -25px;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-label {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@@ -63,7 +63,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, computed, onMounted, onBeforeUnmount} from 'vue';
|
import {ref, computed, onMounted, onBeforeUnmount, nextTick} from 'vue';
|
||||||
import {
|
import {
|
||||||
format,
|
format,
|
||||||
startOfWeek,
|
startOfWeek,
|
||||||
@@ -73,7 +73,6 @@ import {
|
|||||||
parseISO,
|
parseISO,
|
||||||
getYear
|
getYear
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import {debounce} from 'lodash-es';
|
|
||||||
import type {PropType} from 'vue';
|
import type {PropType} from 'vue';
|
||||||
import {zhCN} from 'date-fns/locale';
|
import {zhCN} from 'date-fns/locale';
|
||||||
|
|
||||||
@@ -137,16 +136,17 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const containerRef = ref<HTMLElement>();
|
const containerRef = ref<HTMLElement>();
|
||||||
|
// 设置固定的初始值,避免从小到大的变化
|
||||||
const cellSize = ref(12);
|
const cellSize = ref(12);
|
||||||
const cellGap = ref(3);
|
const cellGap = ref(3);
|
||||||
const weekdays = ['Mon', 'Wed', 'Fri'];
|
const weekdays = ['Mon', 'Wed', 'Fri'];
|
||||||
const visibleWeeks = ref<Array<Array<{ date: Date; count: number }>>>([]);
|
const visibleWeeks = ref<Array<Array<{ date: Date; count: number }>>>([]);
|
||||||
|
|
||||||
// 新增响应式变量
|
// 响应式变量,用于控制图表宽度
|
||||||
const chartMaxWidth = ref(0);
|
const chartMaxWidth = ref(0);
|
||||||
|
|
||||||
// 修改后的updateSize函数
|
// 优化updateSize函数,移除debounce,避免延迟导致的动画效果
|
||||||
const updateSize = debounce(() => {
|
const updateSize = () => {
|
||||||
if (!containerRef.value) return;
|
if (!containerRef.value) return;
|
||||||
|
|
||||||
const container = containerRef.value;
|
const container = containerRef.value;
|
||||||
@@ -156,15 +156,16 @@ const updateSize = debounce(() => {
|
|||||||
// 动态计算最大宽度
|
// 动态计算最大宽度
|
||||||
chartMaxWidth.value = containerWidth - 40;
|
chartMaxWidth.value = containerWidth - 40;
|
||||||
|
|
||||||
// 重新计算单元格尺寸
|
// 计算单元格尺寸,确保合理的显示效果
|
||||||
const maxCellSize = Math.min(
|
const maxCellSize = Math.min(
|
||||||
(containerWidth - 40) / 54, // 更精确的计算
|
(containerWidth - 40) / 54, // 更精确的计算
|
||||||
containerHeight / 7 - cellGap.value
|
containerHeight / 7 - 3 // 使用固定的间距值,避免循环依赖
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 设置单元格大小和间距
|
||||||
cellSize.value = Math.max(8, Math.min(14, maxCellSize));
|
cellSize.value = Math.max(8, Math.min(14, maxCellSize));
|
||||||
cellGap.value = Math.max(2, cellSize.value * 0.15);
|
cellGap.value = Math.max(2, Math.min(4, cellSize.value * 0.15));
|
||||||
}, 150);
|
};
|
||||||
|
|
||||||
// 日期有效性检查
|
// 日期有效性检查
|
||||||
const isValidDate = (date: Date) => {
|
const isValidDate = (date: Date) => {
|
||||||
@@ -260,13 +261,20 @@ const formatTooltip = (date: Date) => {
|
|||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// 先处理数据,再更新尺寸,避免不必要的重新计算
|
||||||
visibleWeeks.value = processContributions();
|
visibleWeeks.value = processContributions();
|
||||||
updateSize();
|
|
||||||
|
|
||||||
const observer = new ResizeObserver(debounce(() => {
|
// 立即执行一次更新尺寸,确保初始渲染正确
|
||||||
|
nextTick(() => {
|
||||||
updateSize();
|
updateSize();
|
||||||
visibleWeeks.value = processContributions();
|
});
|
||||||
}, 200));
|
|
||||||
|
// 使用ResizeObserver监听容器大小变化
|
||||||
|
const observer = new ResizeObserver(() => {
|
||||||
|
// 直接调用updateSize,不使用debounce,避免动画效果
|
||||||
|
updateSize();
|
||||||
|
// 不需要重新处理贡献数据,因为数据不会因为大小变化而改变
|
||||||
|
});
|
||||||
|
|
||||||
if (containerRef.value) observer.observe(containerRef.value);
|
if (containerRef.value) observer.observe(containerRef.value);
|
||||||
onBeforeUnmount(() => observer.disconnect());
|
onBeforeUnmount(() => observer.disconnect());
|
||||||
@@ -279,13 +287,15 @@ onMounted(() => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
padding: 20px 15px; /* 调整左右padding */
|
padding: 20px 15px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow-x: auto; /* 允许横向滚动 */
|
overflow-x: auto; /* 允许横向滚动 */
|
||||||
|
/* 防止内容溢出 */
|
||||||
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend {
|
.legend {
|
||||||
position: absolute; /* 粘性定位 */
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -295,16 +305,15 @@ onMounted(() => {
|
|||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
margin-bottom: 15px; /* 增加底部间距 */
|
|
||||||
z-index: 2; /* 确保在图上层级 */
|
z-index: 2; /* 确保在图上层级 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend-item {
|
.legend-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
& + & {
|
|
||||||
margin-left: 4px; /* 增加色块间距 */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.legend-item + .legend-item {
|
||||||
|
margin-left: 4px; /* 增加色块间距 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend-block {
|
.legend-block {
|
||||||
@@ -317,7 +326,8 @@ onMounted(() => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
margin-top: 45px; /* 增加顶部间距 */
|
margin-top: 45px; /* 增加顶部间距 */
|
||||||
margin-left: 40px;
|
margin-left: 40px;
|
||||||
min-width: 520px; /* 最小宽度保证布局 */
|
/* 移除固定最小宽度,使用更灵活的方式 */
|
||||||
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.month-axis {
|
.month-axis {
|
||||||
@@ -382,7 +392,6 @@ onMounted(() => {
|
|||||||
.chart-wrapper {
|
.chart-wrapper {
|
||||||
margin-top: 35px;
|
margin-top: 35px;
|
||||||
margin-left: 30px;
|
margin-left: 30px;
|
||||||
min-width: 480px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,7 +402,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
.day-cell {
|
.day-cell {
|
||||||
border-radius: 15%;
|
border-radius: 15%;
|
||||||
transition: all 0.2s ease;
|
/* 移除过渡效果,避免加载时的动画 */
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -402,5 +411,7 @@ onMounted(() => {
|
|||||||
transform: scale(1.15);
|
transform: scale(1.15);
|
||||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
/* 只在悬停时添加过渡效果 */
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -13,14 +13,23 @@
|
|||||||
placeholder="选择存储桶">
|
placeholder="选择存储桶">
|
||||||
</ACascader>
|
</ACascader>
|
||||||
</template>
|
</template>
|
||||||
|
<ABadge dot
|
||||||
|
color="green"
|
||||||
|
:offset="[-9, 12]">
|
||||||
<ATooltip title="选择存储桶" color="orange">
|
<ATooltip title="选择存储桶" color="orange">
|
||||||
<AButton type="text" shape="circle" size="large" class="header-menu-item-btn">
|
<AButton type="text" shape="circle" size="large" class="header-menu-item-btn"
|
||||||
|
:class="{'breathing': !uploadStore.storageSelected?.length}">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<AAvatar size="default" shape="circle"
|
<AAvatar size="default" shape="circle"
|
||||||
:src="ProviderIcon[uploadStore.storageSelected?.[0]] ? ProviderIcon[uploadStore.storageSelected?.[0]] : wenhao"/>
|
:src="ProviderIcon[uploadStore.storageSelected?.[0]] ? ProviderIcon[uploadStore.storageSelected?.[0]] : wenhao"/>
|
||||||
</template>
|
</template>
|
||||||
</AButton>
|
</AButton>
|
||||||
</ATooltip>
|
</ATooltip>
|
||||||
|
<template #count>
|
||||||
|
<ExclamationCircleOutlined style="color: red" v-if="!uploadStore.storageSelected?.length"/>
|
||||||
|
</template>
|
||||||
|
</ABadge>
|
||||||
|
|
||||||
</APopover>
|
</APopover>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -53,6 +62,12 @@
|
|||||||
<span class="tool-box-card-title">OCR文字识别</span>
|
<span class="tool-box-card-title">OCR文字识别</span>
|
||||||
</AFlex>
|
</AFlex>
|
||||||
</ACard>
|
</ACard>
|
||||||
|
<ACard hoverable class="tool-box-card" @click="router.push('/preview/qrcode')">
|
||||||
|
<AFlex :vertical="false" align="center" justify="flex-start" gap="small">
|
||||||
|
<AAvatar size="large" shape="square" :src="qr"/>
|
||||||
|
<span class="tool-box-card-title">二维码识别</span>
|
||||||
|
</AFlex>
|
||||||
|
</ACard>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<AButton type="text" shape="circle" size="large" class="header-menu-item-btn">
|
<AButton type="text" shape="circle" size="large" class="header-menu-item-btn">
|
||||||
@@ -211,7 +226,9 @@ import {ProviderIcon} from "@/constant/provider_map.ts";
|
|||||||
import toolBox from "@/assets/svgs/tool-box.svg";
|
import toolBox from "@/assets/svgs/tool-box.svg";
|
||||||
import blur from "@/assets/svgs/blur.svg";
|
import blur from "@/assets/svgs/blur.svg";
|
||||||
import scanIcon from "@/assets/svgs/scan.svg";
|
import scanIcon from "@/assets/svgs/scan.svg";
|
||||||
|
import qr from "@/assets/svgs/qr.svg";
|
||||||
import imgBed from "@/assets/svgs/img_bed.svg";
|
import imgBed from "@/assets/svgs/img_bed.svg";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
|
||||||
@@ -422,6 +439,20 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.breathing {
|
||||||
|
animation: breathing 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes breathing {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@@ -17,4 +17,13 @@ export default [
|
|||||||
title: 'PreviewOcr',
|
title: 'PreviewOcr',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/preview/qrcode',
|
||||||
|
name: 'PreviewQRCode',
|
||||||
|
component: () => import('@/views/Preview/PreviewQRCode/PreviewQRCode.vue'),
|
||||||
|
meta: {
|
||||||
|
requiresAuth: false,
|
||||||
|
title: 'PreviewQRCode',
|
||||||
|
}
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
@@ -95,6 +95,24 @@ export default [
|
|||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
title: '图像备份'
|
title: '图像备份'
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/main/user/setting/task',
|
||||||
|
name: 'AccountSettingTask',
|
||||||
|
component: () => import('@/views/User/AccountSetting/components/AccountSettingTask/AccountSettingTask.vue'),
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
title: '定时任务'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/main/user/setting/log',
|
||||||
|
name: 'AccountSettingLog',
|
||||||
|
component: () => import('@/views/User/AccountSetting/components/AccountSettingLog/AccountSettingLog.vue'),
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
title: '执行记录'
|
||||||
|
},
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
@@ -170,7 +170,7 @@ const removeLogo = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 将文件转换为Base64
|
// 将文件转换为Base64
|
||||||
const getBase64 = (img: File, callback: Function) => {
|
const getBase64 = (img: File, callback: () => void) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.addEventListener('load', () => callback(reader.result));
|
reader.addEventListener('load', () => callback(reader.result));
|
||||||
reader.readAsDataURL(img);
|
reader.readAsDataURL(img);
|
||||||
|
@@ -331,8 +331,8 @@ const handlePermissionModalOk = () => {
|
|||||||
const newId: any = Math.max(...permissionList.value.map(item => item.id)) + 1;
|
const newId: any = Math.max(...permissionList.value.map(item => item.id)) + 1;
|
||||||
const statusText = permissionForm.status === 'active' ? '启用' : '禁用';
|
const statusText = permissionForm.status === 'active' ? '启用' : '禁用';
|
||||||
const now = new Date().toLocaleString();
|
const now = new Date().toLocaleString();
|
||||||
|
permissionForm.id = newId;
|
||||||
permissionList.value.push({
|
permissionList.value.push({
|
||||||
newId,
|
|
||||||
...permissionForm,
|
...permissionForm,
|
||||||
statusText,
|
statusText,
|
||||||
createTime: now
|
createTime: now
|
||||||
|
@@ -275,11 +275,11 @@ const handleRoleModalOk = () => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 新增角色
|
// 新增角色
|
||||||
const id = Math.max(...roleList.value.map(item => item.id)) + 1;
|
const id: any = Math.max(...roleList.value.map(item => item.id)) + 1;
|
||||||
const statusText = roleForm.status === 'active' ? '启用' : '禁用';
|
const statusText = roleForm.status === 'active' ? '启用' : '禁用';
|
||||||
const now = new Date().toLocaleString();
|
const now = new Date().toLocaleString();
|
||||||
|
roleForm.id = id;
|
||||||
roleList.value.push({
|
roleList.value.push({
|
||||||
id,
|
|
||||||
...roleForm,
|
...roleForm,
|
||||||
statusText,
|
statusText,
|
||||||
createTime: now,
|
createTime: now,
|
||||||
|
@@ -239,7 +239,7 @@ import {
|
|||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
|
|
||||||
// 搜索表单
|
// 搜索表单
|
||||||
const searchForm = reactive({
|
const searchForm = reactive<any>({
|
||||||
timeRange: [],
|
timeRange: [],
|
||||||
userType: undefined,
|
userType: undefined,
|
||||||
registerSource: undefined,
|
registerSource: undefined,
|
||||||
|
@@ -92,7 +92,8 @@
|
|||||||
@cancel="handleUserModalCancel"
|
@cancel="handleUserModalCancel"
|
||||||
:confirmLoading="modalLoading"
|
:confirmLoading="modalLoading"
|
||||||
>
|
>
|
||||||
<AForm :model="userForm" :rules="userFormRules" ref="userFormRef" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
|
<AForm :model="userForm" :rules="userFormRules" ref="userFormRef" :label-col="{ span: 6 }"
|
||||||
|
:wrapper-col="{ span: 16 }">
|
||||||
<AFormItem label="用户名" name="username">
|
<AFormItem label="用户名" name="username">
|
||||||
<AInput v-model:value="userForm.username" placeholder="请输入用户名"/>
|
<AInput v-model:value="userForm.username" placeholder="请输入用户名"/>
|
||||||
</AFormItem>
|
</AFormItem>
|
||||||
@@ -383,12 +384,12 @@ const handleUserModalOk = () => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 新增用户
|
// 新增用户
|
||||||
const id = Math.max(...userList.value.map(item => item.id)) + 1;
|
const id: any = Math.max(...userList.value.map(item => item.id)) + 1;
|
||||||
const roleText = userForm.role === 'admin' ? '管理员' : (userForm.role === 'vip' ? 'VIP用户' : '普通用户');
|
const roleText = userForm.role === 'admin' ? '管理员' : (userForm.role === 'vip' ? 'VIP用户' : '普通用户');
|
||||||
const statusText = userForm.status === 'active' ? '正常' : (userForm.status === 'inactive' ? '禁用' : '待审核');
|
const statusText = userForm.status === 'active' ? '正常' : (userForm.status === 'inactive' ? '禁用' : '待审核');
|
||||||
const now = new Date().toLocaleString();
|
const now = new Date().toLocaleString();
|
||||||
|
userForm.id = id;
|
||||||
userList.value.push({
|
userList.value.push({
|
||||||
id,
|
|
||||||
...userForm,
|
...userForm,
|
||||||
roleText,
|
roleText,
|
||||||
statusText,
|
statusText,
|
||||||
|
@@ -437,7 +437,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
.image-bed-upload-container {
|
.image-bed-upload-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
//padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-bed-upload-content {
|
.image-bed-upload-content {
|
||||||
|
@@ -528,7 +528,7 @@ const handleRecognize = async () => {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="scss">
|
||||||
.ocr-detection {
|
.ocr-detection {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
@@ -656,7 +656,6 @@ const handleRecognize = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.result-list {
|
.result-list {
|
||||||
max-height: 350px;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
434
src/views/Preview/PreviewQRCode/PreviewQRCode.vue
Normal file
434
src/views/Preview/PreviewQRCode/PreviewQRCode.vue
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
<template>
|
||||||
|
<div class="qrcode-detection">
|
||||||
|
<a-card class="main-card">
|
||||||
|
<template #title>
|
||||||
|
<div class="card-title">
|
||||||
|
<scan-outlined/>
|
||||||
|
<span>二维码识别</span>
|
||||||
|
<a-tag color="blue" class="version-tag">增强版</a-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<a-row :gutter="[16, 16]">
|
||||||
|
<a-col :span="24">
|
||||||
|
<a-alert type="info" show-icon>
|
||||||
|
<template #message>上传图片进行二维码识别</template>
|
||||||
|
<template #description>支持JPG、PNG等常见图片格式,将自动识别图片中的二维码内容</template>
|
||||||
|
</a-alert>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<div class="upload-container">
|
||||||
|
<a-upload
|
||||||
|
v-model:file-list="fileList"
|
||||||
|
list-type="picture-card"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:multiple="false"
|
||||||
|
@remove="handleRemove"
|
||||||
|
>
|
||||||
|
<div v-if="!fileList.length">
|
||||||
|
<plus-outlined/>
|
||||||
|
<div style="margin-top: 8px">上传图片</div>
|
||||||
|
</div>
|
||||||
|
</a-upload>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="preview-container" v-if="imageUrl">
|
||||||
|
<a-row :gutter="[16, 16]">
|
||||||
|
<a-col :span="12">
|
||||||
|
<div class="canvas-wrapper">
|
||||||
|
<!-- 图片预览区域 -->
|
||||||
|
<div class="image-preview-controls">
|
||||||
|
<div class="zoom-controls">
|
||||||
|
<a-button type="text" @click="zoomOut" :disabled="zoomLevel <= 10">
|
||||||
|
<template #icon><minus-outlined /></template>
|
||||||
|
</a-button>
|
||||||
|
<span class="zoom-level">{{ zoomLevel }}%</span>
|
||||||
|
<a-button type="text" @click="zoomIn" :disabled="zoomLevel >= 200">
|
||||||
|
<template #icon><plus-outlined /></template>
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<a-tooltip title="使用鼠标滚轮缩放图片,按住鼠标拖动移动图片">
|
||||||
|
<info-circle-outlined />
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="canvas-container"
|
||||||
|
ref="canvasContainerRef"
|
||||||
|
@wheel="handleWheel"
|
||||||
|
@mousedown="startDrag"
|
||||||
|
@mousemove="onDrag"
|
||||||
|
@mouseup="stopDrag"
|
||||||
|
@mouseleave="stopDrag"
|
||||||
|
>
|
||||||
|
<canvas ref="canvasRef" :style="`transform: scale(${zoomLevel / 100}) translate(${dragOffset.x}px, ${dragOffset.y}px); transform-origin: top left;`"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-card title="识别结果" :bordered="false" class="result-card">
|
||||||
|
<template #extra>
|
||||||
|
<a-button type="primary" :loading="recognizing" @click="handleRecognize">
|
||||||
|
{{ recognizing ? '识别中...' : '开始识别' }}
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
<div class="result-content">
|
||||||
|
<a-empty v-if="!qrCodeResult" description="暂无识别结果"/>
|
||||||
|
<div v-else class="qrcode-result">
|
||||||
|
<div class="result-header">
|
||||||
|
<div class="result-stats">
|
||||||
|
<a-tag color="blue">二维码类型: {{ qrCodeType }}</a-tag>
|
||||||
|
</div>
|
||||||
|
<div class="result-actions">
|
||||||
|
<a-button type="link" size="small" @click="copyQRCodeResult">
|
||||||
|
<template #icon><copy-outlined /></template>
|
||||||
|
复制内容
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-detail">
|
||||||
|
<a-typography-paragraph>
|
||||||
|
<template v-if="isUrl(qrCodeResult)">
|
||||||
|
<a-alert type="success" show-icon>
|
||||||
|
<template #message>识别到网址</template>
|
||||||
|
<template #description>
|
||||||
|
<a :href="qrCodeResult" target="_blank">{{ qrCodeResult }}</a>
|
||||||
|
</template>
|
||||||
|
</a-alert>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<a-alert type="info" show-icon>
|
||||||
|
<template #message>识别到文本内容</template>
|
||||||
|
<template #description>
|
||||||
|
<div class="qrcode-text">{{ qrCodeResult }}</div>
|
||||||
|
</template>
|
||||||
|
</a-alert>
|
||||||
|
</template>
|
||||||
|
</a-typography-paragraph>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
ScanOutlined,
|
||||||
|
MinusOutlined,
|
||||||
|
CopyOutlined,
|
||||||
|
InfoCircleOutlined
|
||||||
|
} from '@ant-design/icons-vue';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
import type { UploadProps } from 'ant-design-vue';
|
||||||
|
import { ready, scan } from 'qr-scanner-wechat';
|
||||||
|
|
||||||
|
// 状态变量
|
||||||
|
const fileList = ref<any[]>([]);
|
||||||
|
const imageUrl = ref<string>('');
|
||||||
|
const recognizing = ref<boolean>(false);
|
||||||
|
const qrCodeResult = ref<string>('');
|
||||||
|
const qrCodeType = ref<string>('未知');
|
||||||
|
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
||||||
|
const canvasContainerRef = ref<HTMLDivElement | null>(null);
|
||||||
|
const zoomLevel = ref<number>(100); // 默认缩放级别为100%
|
||||||
|
|
||||||
|
// 拖动相关状态
|
||||||
|
const isDragging = ref<boolean>(false);
|
||||||
|
const dragOffset = ref<{x: number, y: number}>({x: 0, y: 0});
|
||||||
|
const dragStart = ref<{x: number, y: number}>({x: 0, y: 0});
|
||||||
|
|
||||||
|
// 初始化二维码扫描模块
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
await ready();
|
||||||
|
message.success('二维码识别模块初始化成功');
|
||||||
|
} catch (error) {
|
||||||
|
message.error('二维码识别模块初始化失败');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 判断是否为URL
|
||||||
|
const isUrl = (str: string): boolean => {
|
||||||
|
try {
|
||||||
|
new URL(str);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 复制二维码结果
|
||||||
|
const copyQRCodeResult = () => {
|
||||||
|
if (qrCodeResult.value) {
|
||||||
|
navigator.clipboard.writeText(qrCodeResult.value)
|
||||||
|
.then(() => {
|
||||||
|
message.success('复制成功');
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
message.error('复制失败');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 图片上传前的处理
|
||||||
|
const beforeUpload: UploadProps['beforeUpload'] = async (file) => {
|
||||||
|
const isImage = file.type.startsWith('image/');
|
||||||
|
if (!isImage) {
|
||||||
|
message.error('只能上传图片文件!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除之前的结果
|
||||||
|
qrCodeResult.value = '';
|
||||||
|
qrCodeType.value = '未知';
|
||||||
|
|
||||||
|
// 创建图片URL
|
||||||
|
imageUrl.value = URL.createObjectURL(file);
|
||||||
|
|
||||||
|
// 在Canvas上绘制图片
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
if (canvasRef.value) {
|
||||||
|
const canvas = canvasRef.value;
|
||||||
|
canvas.width = img.width;
|
||||||
|
canvas.height = img.height;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (ctx) {
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.drawImage(img, 0, 0, img.width, img.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
img.src = imageUrl.value;
|
||||||
|
|
||||||
|
return false; // 阻止默认上传行为
|
||||||
|
};
|
||||||
|
|
||||||
|
// 移除图片
|
||||||
|
const handleRemove = () => {
|
||||||
|
imageUrl.value = '';
|
||||||
|
qrCodeResult.value = '';
|
||||||
|
qrCodeType.value = '未知';
|
||||||
|
if (canvasRef.value) {
|
||||||
|
const ctx = canvasRef.value.getContext('2d');
|
||||||
|
if (ctx) {
|
||||||
|
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 重置拖动和缩放
|
||||||
|
zoomLevel.value = 100;
|
||||||
|
dragOffset.value = { x: 0, y: 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
// 识别二维码
|
||||||
|
const handleRecognize = async () => {
|
||||||
|
if (!imageUrl.value) {
|
||||||
|
message.warning('请先上传图片');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
recognizing.value = true;
|
||||||
|
try {
|
||||||
|
const img = new Image();
|
||||||
|
img.src = imageUrl.value;
|
||||||
|
|
||||||
|
// 等待图片加载完成
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
if (img.complete) {
|
||||||
|
resolve(true);
|
||||||
|
} else {
|
||||||
|
img.onload = () => resolve(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 使用qr-scanner-wechat进行识别
|
||||||
|
const result:any = await scan(img);
|
||||||
|
|
||||||
|
if (result.text) {
|
||||||
|
qrCodeResult.value = result.text;
|
||||||
|
qrCodeType.value = result.type || '标准二维码';
|
||||||
|
message.success('二维码识别成功');
|
||||||
|
} else {
|
||||||
|
message.warning('未检测到二维码');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('二维码识别失败:', error);
|
||||||
|
message.error('二维码识别失败');
|
||||||
|
} finally {
|
||||||
|
recognizing.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 缩放相关函数
|
||||||
|
const zoomIn = () => {
|
||||||
|
zoomLevel.value = Math.min(zoomLevel.value + 10, 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
const zoomOut = () => {
|
||||||
|
zoomLevel.value = Math.max(zoomLevel.value - 10, 10);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWheel = (e: WheelEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.deltaY < 0) {
|
||||||
|
zoomIn();
|
||||||
|
} else {
|
||||||
|
zoomOut();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 拖动相关函数
|
||||||
|
const startDrag = (e: MouseEvent) => {
|
||||||
|
isDragging.value = true;
|
||||||
|
dragStart.value = {
|
||||||
|
x: e.clientX - dragOffset.value.x,
|
||||||
|
y: e.clientY - dragOffset.value.y
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDrag = (e: MouseEvent) => {
|
||||||
|
if (isDragging.value) {
|
||||||
|
dragOffset.value = {
|
||||||
|
x: e.clientX - dragStart.value.x,
|
||||||
|
y: e.clientY - dragStart.value.y
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopDrag = () => {
|
||||||
|
isDragging.value = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.qrcode-detection {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.main-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
.version-tag {
|
||||||
|
margin-left: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-container {
|
||||||
|
margin: 20px 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
.canvas-wrapper {
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 400px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.image-preview-controls {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.zoom-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
.zoom-level {
|
||||||
|
min-width: 50px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
background: #f5f5f5;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card {
|
||||||
|
min-height: 400px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.result-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 10px 0;
|
||||||
|
|
||||||
|
.qrcode-result {
|
||||||
|
.result-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-detail {
|
||||||
|
margin-top: 15px;
|
||||||
|
|
||||||
|
.qrcode-text {
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@@ -19,14 +19,14 @@
|
|||||||
<AForm layout="vertical">
|
<AForm layout="vertical">
|
||||||
<AFormItem label="存储商" name="sourceProvider">
|
<AFormItem label="存储商" name="sourceProvider">
|
||||||
<ASelect
|
<ASelect
|
||||||
v-model:value="sourceStorage.provider"
|
v-model:value="sourceStorage.id"
|
||||||
placeholder="请选择源存储商"
|
placeholder="请选择源存储商"
|
||||||
@change="handleSourceProviderChange"
|
@change="handleSourceProviderChange"
|
||||||
>
|
>
|
||||||
<ASelectOption
|
<ASelectOption
|
||||||
v-for="item in storageList"
|
v-for="item in storageList"
|
||||||
:key="item.provider + '-' + item.bucket"
|
:key="item.id"
|
||||||
:value="item.provider + '-' + item.bucket"
|
:value="item.id"
|
||||||
>
|
>
|
||||||
<AFlex align="center" gap="small">
|
<AFlex align="center" gap="small">
|
||||||
<AAvatar :size="20" shape="circle" :src="ProviderIcon[item.provider]" />
|
<AAvatar :size="20" shape="circle" :src="ProviderIcon[item.provider]" />
|
||||||
@@ -41,17 +41,39 @@
|
|||||||
<AAvatar :size="40" shape="circle" :src="ProviderIcon[selectedSourceStorage.provider]" />
|
<AAvatar :size="40" shape="circle" :src="ProviderIcon[selectedSourceStorage.provider]" />
|
||||||
<div class="storage-info">
|
<div class="storage-info">
|
||||||
<div class="storage-name">{{ selectedSourceStorage.bucket }}</div>
|
<div class="storage-name">{{ selectedSourceStorage.bucket }}</div>
|
||||||
<ATag :color="ProviderColorMap[selectedSourceStorage.provider]">{{ ProviderNameMap[selectedSourceStorage.provider] }}</ATag>
|
<ATag :color="ProviderColorMap[selectedSourceStorage.provider]">
|
||||||
|
{{ ProviderNameMap[selectedSourceStorage.provider] }}
|
||||||
|
</ATag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="storage-card-content">
|
<div class="storage-card-content">
|
||||||
<div class="storage-detail">
|
<div class="storage-detail">
|
||||||
<AAvatar size="small" shape="square" :src="bucket" />
|
<AAvatar size="small" shape="square" :src="bucket" />
|
||||||
<span>{{ selectedSourceStorage.capacity }}GB</span>
|
<div class="detail-text">
|
||||||
|
<div class="detail-label">存储容量</div>
|
||||||
|
<div class="detail-value">{{ selectedSourceStorage.capacity }}GB</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="storage-detail">
|
<div class="storage-detail">
|
||||||
<AAvatar size="small" shape="circle" :src="location" />
|
<AAvatar size="small" shape="circle" :src="location" />
|
||||||
<span>{{ AliRegionMap[selectedSourceStorage.region] || selectedSourceStorage.region }}</span>
|
<div class="detail-text">
|
||||||
|
<div class="detail-label">存储区域</div>
|
||||||
|
<div class="detail-value">{{ AliRegionMap[selectedSourceStorage.region] || selectedSourceStorage.region }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="storage-detail">
|
||||||
|
<AAvatar size="small" shape="square" :src="endpointIcon" />
|
||||||
|
<div class="detail-text">
|
||||||
|
<div class="detail-label">访问端点</div>
|
||||||
|
<div class="detail-value">{{ selectedSourceStorage.endpoint }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="storage-detail">
|
||||||
|
<AAvatar size="small" shape="square" :src="dateIcon" />
|
||||||
|
<div class="detail-text">
|
||||||
|
<div class="detail-label">创建时间</div>
|
||||||
|
<div class="detail-value">{{ formatDate(selectedSourceStorage.created_at) }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -71,15 +93,15 @@
|
|||||||
<AForm layout="vertical">
|
<AForm layout="vertical">
|
||||||
<AFormItem label="存储商" name="targetProvider">
|
<AFormItem label="存储商" name="targetProvider">
|
||||||
<ASelect
|
<ASelect
|
||||||
v-model:value="targetStorage.provider"
|
v-model:value="targetStorage.id"
|
||||||
placeholder="请选择目标存储商"
|
placeholder="请选择目标存储商"
|
||||||
@change="handleTargetProviderChange"
|
@change="handleTargetProviderChange"
|
||||||
:disabled="!sourceStorage.provider"
|
:disabled="!sourceStorage.id"
|
||||||
>
|
>
|
||||||
<ASelectOption
|
<ASelectOption
|
||||||
v-for="item in availableTargetStorages"
|
v-for="item in availableTargetStorages"
|
||||||
:key="item.provider + '-' + item.bucket"
|
:key="item.id"
|
||||||
:value="item.provider + '-' + item.bucket"
|
:value="item.id"
|
||||||
>
|
>
|
||||||
<AFlex align="center" gap="small">
|
<AFlex align="center" gap="small">
|
||||||
<AAvatar :size="20" shape="circle" :src="ProviderIcon[item.provider]" />
|
<AAvatar :size="20" shape="circle" :src="ProviderIcon[item.provider]" />
|
||||||
@@ -94,17 +116,39 @@
|
|||||||
<AAvatar :size="40" shape="circle" :src="ProviderIcon[selectedTargetStorage.provider]" />
|
<AAvatar :size="40" shape="circle" :src="ProviderIcon[selectedTargetStorage.provider]" />
|
||||||
<div class="storage-info">
|
<div class="storage-info">
|
||||||
<div class="storage-name">{{ selectedTargetStorage.bucket }}</div>
|
<div class="storage-name">{{ selectedTargetStorage.bucket }}</div>
|
||||||
<ATag :color="ProviderColorMap[selectedTargetStorage.provider]">{{ ProviderNameMap[selectedTargetStorage.provider] }}</ATag>
|
<ATag :color="ProviderColorMap[selectedTargetStorage.provider]">
|
||||||
|
{{ ProviderNameMap[selectedTargetStorage.provider] }}
|
||||||
|
</ATag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="storage-card-content">
|
<div class="storage-card-content">
|
||||||
<div class="storage-detail">
|
<div class="storage-detail">
|
||||||
<AAvatar size="small" shape="square" :src="bucket" />
|
<AAvatar size="small" shape="square" :src="bucket" />
|
||||||
<span>{{ selectedTargetStorage.capacity }}GB</span>
|
<div class="detail-text">
|
||||||
|
<div class="detail-label">存储容量</div>
|
||||||
|
<div class="detail-value">{{ selectedTargetStorage.capacity }}GB</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="storage-detail">
|
<div class="storage-detail">
|
||||||
<AAvatar size="small" shape="circle" :src="location" />
|
<AAvatar size="small" shape="circle" :src="location" />
|
||||||
<span>{{ AliRegionMap[selectedTargetStorage.region] || selectedTargetStorage.region }}</span>
|
<div class="detail-text">
|
||||||
|
<div class="detail-label">存储区域</div>
|
||||||
|
<div class="detail-value">{{ AliRegionMap[selectedTargetStorage.region] || selectedTargetStorage.region }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="storage-detail">
|
||||||
|
<AAvatar size="small" shape="square" :src="endpointIcon" />
|
||||||
|
<div class="detail-text">
|
||||||
|
<div class="detail-label">访问端点</div>
|
||||||
|
<div class="detail-value">{{ selectedTargetStorage.endpoint }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="storage-detail">
|
||||||
|
<AAvatar size="small" shape="square" :src="dateIcon" />
|
||||||
|
<div class="detail-text">
|
||||||
|
<div class="detail-label">创建时间</div>
|
||||||
|
<div class="detail-value">{{ formatDate(selectedTargetStorage.created_at) }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -154,8 +198,8 @@ import targetIcon from "@/assets/svgs/target-storage.svg";
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const storageList = ref<any[]>([]);
|
const storageList = ref<any[]>([]);
|
||||||
const sourceStorage = ref({ provider: '', bucket: '' });
|
const sourceStorage = ref({ id: null });
|
||||||
const targetStorage = ref({ provider: '', bucket: '' });
|
const targetStorage = ref({ id: null });
|
||||||
const selectedSourceStorage = ref<any>(null);
|
const selectedSourceStorage = ref<any>(null);
|
||||||
const selectedTargetStorage = ref<any>(null);
|
const selectedTargetStorage = ref<any>(null);
|
||||||
const backupModalVisible = ref(false);
|
const backupModalVisible = ref(false);
|
||||||
@@ -164,14 +208,25 @@ const backupStatus = ref('准备开始备份...');
|
|||||||
const backupInProgress = ref(false);
|
const backupInProgress = ref(false);
|
||||||
const backupTaskId = ref('');
|
const backupTaskId = ref('');
|
||||||
|
|
||||||
|
import dateIcon from "@/assets/svgs/time.svg";
|
||||||
|
import endpointIcon from "@/assets/svgs/endpoint.svg";
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
return new Date(dateString).toLocaleDateString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// 获取存储列表
|
// 获取存储列表
|
||||||
async function getStorageList() {
|
async function getStorageList() {
|
||||||
const res: any = await listUserStorageConfigApi();
|
const res: any = await listUserStorageConfigApi();
|
||||||
if (res && res.code === 200) {
|
if (res && res.code === 200) {
|
||||||
storageList.value = res.data.records;
|
storageList.value = res.data.records;
|
||||||
// 重置选择
|
// 重置选择
|
||||||
sourceStorage.value = { provider: '', bucket: '' };
|
sourceStorage.value = { id: null };
|
||||||
targetStorage.value = { provider: '', bucket: '' };
|
targetStorage.value = { id: null };
|
||||||
selectedSourceStorage.value = null;
|
selectedSourceStorage.value = null;
|
||||||
selectedTargetStorage.value = null;
|
selectedTargetStorage.value = null;
|
||||||
}
|
}
|
||||||
@@ -179,12 +234,8 @@ async function getStorageList() {
|
|||||||
|
|
||||||
// 计算可用的目标存储(排除已选择的源存储)
|
// 计算可用的目标存储(排除已选择的源存储)
|
||||||
const availableTargetStorages = computed(() => {
|
const availableTargetStorages = computed(() => {
|
||||||
if (!sourceStorage.value.provider) return [];
|
if (!sourceStorage.value.id) return [];
|
||||||
return storageList.value.filter(item => {
|
return storageList.value.filter(item => item.id !== sourceStorage.value.id);
|
||||||
const sourceKey = sourceStorage.value.provider;
|
|
||||||
const itemKey = item.provider + '-' + item.bucket;
|
|
||||||
return sourceKey !== itemKey;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 判断是否可以开始备份
|
// 判断是否可以开始备份
|
||||||
@@ -199,22 +250,17 @@ function handleSourceProviderChange(value) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [provider, bucket] = value.split('-');
|
const selected = storageList.value.find(item => item.id === value);
|
||||||
const selected = storageList.value.find(item =>
|
|
||||||
item.provider === provider && item.bucket === bucket
|
|
||||||
);
|
|
||||||
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
selectedSourceStorage.value = selected;
|
selectedSourceStorage.value = selected;
|
||||||
|
|
||||||
// 如果目标存储与源存储相同,则清空目标存储
|
// 如果目标存储与源存储相同,则清空目标存储
|
||||||
if (targetStorage.value.provider) {
|
if (targetStorage.value.id === value) {
|
||||||
const [targetProvider, targetBucket] = targetStorage.value.provider.split('-');
|
targetStorage.value.id = null;
|
||||||
if (targetProvider === provider && targetBucket === bucket) {
|
|
||||||
targetStorage.value.provider = '';
|
|
||||||
selectedTargetStorage.value = null;
|
selectedTargetStorage.value = null;
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
selectedSourceStorage.value = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,13 +271,12 @@ function handleTargetProviderChange(value) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [provider, bucket] = value.split('-');
|
const selected = storageList.value.find(item => item.id === value);
|
||||||
const selected = storageList.value.find(item =>
|
|
||||||
item.provider === provider && item.bucket === bucket
|
|
||||||
);
|
|
||||||
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
selectedTargetStorage.value = selected;
|
selectedTargetStorage.value = selected;
|
||||||
|
} else {
|
||||||
|
selectedTargetStorage.value = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,17 +290,9 @@ async function startBackup() {
|
|||||||
backupStatus.value = '正在准备备份...';
|
backupStatus.value = '正在准备备份...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 调用备份API
|
|
||||||
const sourceProviderInfo = selectedSourceStorage.value.provider;
|
|
||||||
const sourceBucketInfo = selectedSourceStorage.value.bucket;
|
|
||||||
const targetProviderInfo = selectedTargetStorage.value.provider;
|
|
||||||
const targetBucketInfo = selectedTargetStorage.value.bucket;
|
|
||||||
|
|
||||||
const res: any = await backupStorageApi(
|
const res: any = await backupStorageApi(
|
||||||
sourceProviderInfo,
|
selectedSourceStorage.value.id,
|
||||||
sourceBucketInfo,
|
selectedTargetStorage.value.id
|
||||||
targetProviderInfo,
|
|
||||||
targetBucketInfo
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res && res.code === 200) {
|
if (res && res.code === 200) {
|
||||||
@@ -343,10 +380,11 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
/* 样式保持不变 */
|
||||||
.account-setting-backup {
|
.account-setting-backup {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
//height: 100%;
|
||||||
overflow: auto;
|
//overflow: auto;
|
||||||
|
|
||||||
.account-setting-backup-header {
|
.account-setting-backup-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -459,13 +497,34 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.storage-card-content {
|
.storage-card-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 12px;
|
||||||
|
|
||||||
.storage-detail {
|
.storage-detail {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-bottom: 8px;
|
padding: 8px;
|
||||||
color: #666;
|
background: rgba(245, 245, 245, 0.5);
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.detail-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -537,7 +537,7 @@ const handleThirdPartySuccess = (_type: string) => {
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
//box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
border: 1px solid #f0f0f0;
|
border: 1px solid #f0f0f0;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
@@ -63,7 +63,7 @@
|
|||||||
@finish="handleUpdatePhone"
|
@finish="handleUpdatePhone"
|
||||||
>
|
>
|
||||||
<AFormItem label="当前手机">
|
<AFormItem label="当前手机">
|
||||||
<AInput :value="currentPhone" disabled />
|
<AInput :value="userStore.user.phone" disabled />
|
||||||
</AFormItem>
|
</AFormItem>
|
||||||
|
|
||||||
<AFormItem
|
<AFormItem
|
||||||
@@ -120,13 +120,6 @@ import { message } from 'ant-design-vue';
|
|||||||
import { sendMessage } from '@/api/user';
|
import { sendMessage } from '@/api/user';
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
|
|
||||||
defineProps({
|
|
||||||
currentPhone: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
// 获取用户store
|
// 获取用户store
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
:width="500"
|
:width="500"
|
||||||
:maskClosable="false"
|
:maskClosable="false"
|
||||||
@cancel="handleCancel"
|
@cancel="handleCancel"
|
||||||
|
:footer="null"
|
||||||
>
|
>
|
||||||
<div class="third-party-container">
|
<div class="third-party-container">
|
||||||
<div class="third-party-item">
|
<div class="third-party-item">
|
||||||
@@ -141,10 +142,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<AButton key="back" @click="handleCancel">关闭</AButton>
|
|
||||||
</template>
|
|
||||||
</AModal>
|
</AModal>
|
||||||
|
|
||||||
<!-- 解绑确认模态窗口 -->
|
<!-- 解绑确认模态窗口 -->
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,126 @@
|
|||||||
|
// 执行记录页面样式
|
||||||
|
.account-setting-log {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.account-setting-log-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-setting-log-body {
|
||||||
|
.log-filter-section {
|
||||||
|
background-color: white;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
.storage-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-info {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #d9d9d9;
|
||||||
|
margin-right: 4px;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-detail-info {
|
||||||
|
.detail-item {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: rgba(0, 0, 0, 0.65);
|
||||||
|
margin-right: 8px;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-detail {
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 24px 0 16px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.files-list, .task-logs {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-item {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.log-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-message {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&.log-info {
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.log-success {
|
||||||
|
background-color: #f6ffed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.log-warning {
|
||||||
|
background-color: #fffbe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.log-error {
|
||||||
|
background-color: #fff2f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -35,6 +35,18 @@
|
|||||||
</template>
|
</template>
|
||||||
<span class="ant-menu-item-title">图像备份</span>
|
<span class="ant-menu-item-title">图像备份</span>
|
||||||
</AMenuItem>
|
</AMenuItem>
|
||||||
|
<AMenuItem title="定时任务" key="task" :style="menuCSSStyle">
|
||||||
|
<template #icon>
|
||||||
|
<AAvatar shape="square" size="small" :src="time"/>
|
||||||
|
</template>
|
||||||
|
<span class="ant-menu-item-title">定时任务</span>
|
||||||
|
</AMenuItem>
|
||||||
|
<AMenuItem title="执行记录" key="log" :style="menuCSSStyle">
|
||||||
|
<template #icon>
|
||||||
|
<AAvatar shape="square" size="small" :src="logIcon"/>
|
||||||
|
</template>
|
||||||
|
<span class="ant-menu-item-title">执行记录</span>
|
||||||
|
</AMenuItem>
|
||||||
|
|
||||||
</AMenu>
|
</AMenu>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,6 +58,8 @@ import home from "@/assets/svgs/home.svg";
|
|||||||
import peopleAlbum from "@/assets/svgs/people-album.svg";
|
import peopleAlbum from "@/assets/svgs/people-album.svg";
|
||||||
import storage from "@/assets/svgs/storage.svg";
|
import storage from "@/assets/svgs/storage.svg";
|
||||||
import backup from "@/assets/svgs/source-storage.svg";
|
import backup from "@/assets/svgs/source-storage.svg";
|
||||||
|
import time from "@/assets/svgs/time.svg";
|
||||||
|
import logIcon from "@/assets/svgs/data_analysis.svg";
|
||||||
|
|
||||||
const menuStore = useStore().menu;
|
const menuStore = useStore().menu;
|
||||||
const menuCSSStyle: any = reactive({
|
const menuCSSStyle: any = reactive({
|
||||||
|
@@ -0,0 +1,846 @@
|
|||||||
|
<template>
|
||||||
|
<div class="account-setting-task">
|
||||||
|
<div class="account-setting-task-header">
|
||||||
|
<span>定时任务</span>
|
||||||
|
<AButton type="primary" @click="showAddTaskModal">
|
||||||
|
<template #icon>
|
||||||
|
<PlusOutlined/>
|
||||||
|
</template>
|
||||||
|
新增任务
|
||||||
|
</AButton>
|
||||||
|
</div>
|
||||||
|
<div class="account-setting-task-body">
|
||||||
|
<!-- 任务列表 -->
|
||||||
|
<AEmpty v-if="taskList.length === 0" description="暂无定时任务"/>
|
||||||
|
<div v-else class="task-list">
|
||||||
|
<div v-for="(task, index) in taskList" :key="index" class="task-card" :class="getTaskTypeClass(task.type)">
|
||||||
|
<div class="task-card-header">
|
||||||
|
<div class="task-card-title">
|
||||||
|
<AAvatar shape="square" size="small" :src="getTaskIcon(task.type)" class="task-icon"/>
|
||||||
|
<span>{{ getTaskTypeName(task.type) }}</span>
|
||||||
|
<ATag :color="getTaskTypeColor(task.type)" class="task-type-tag">{{ getTaskTypeShortName(task.type) }}</ATag>
|
||||||
|
</div>
|
||||||
|
<div class="task-card-actions">
|
||||||
|
<AButton type="text" size="small" @click="editTask(index)" class="action-btn edit-btn">
|
||||||
|
<template #icon>
|
||||||
|
<EditOutlined/>
|
||||||
|
</template>
|
||||||
|
编辑
|
||||||
|
</AButton>
|
||||||
|
<AButton type="text" size="small" @click="deleteTask(index)" class="action-btn delete-btn">
|
||||||
|
<template #icon>
|
||||||
|
<DeleteOutlined/>
|
||||||
|
</template>
|
||||||
|
删除
|
||||||
|
</AButton>
|
||||||
|
<ASwitch v-model:checked="task.enabled" @change="(checked) => toggleTaskStatus(index, checked)" class="task-switch"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="task-card-content" :class="{ 'disabled': !task.enabled }">
|
||||||
|
<div class="task-info">
|
||||||
|
<div class="task-info-group">
|
||||||
|
<div class="task-info-item">
|
||||||
|
<span class="label">执行频率:</span>
|
||||||
|
<span class="value">
|
||||||
|
<ATag :color="getFrequencyColor(task.frequency)" class="info-tag">{{ getFrequencyText(task.frequency) }}</ATag>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="task-info-item">
|
||||||
|
<span class="label">执行时间:</span>
|
||||||
|
<span class="value">
|
||||||
|
<ATag color="blue" class="info-tag time-tag">{{ formatTime(task.time) }}</ATag>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 备份任务特有信息 -->
|
||||||
|
<template v-if="task.type === 'backup'">
|
||||||
|
<div class="task-info-group backup-info">
|
||||||
|
<div class="group-title">备份配置</div>
|
||||||
|
<div class="task-info-item">
|
||||||
|
<span class="label">备份存储:</span>
|
||||||
|
<span class="value">
|
||||||
|
<AFlex align="center" gap="small" class="storage-info">
|
||||||
|
<AAvatar v-if="getStorageById(task.storageId)" :size="20" shape="circle"
|
||||||
|
:src="ProviderIcon[getStorageById(task.storageId)?.provider]"/>
|
||||||
|
<span>{{
|
||||||
|
getStorageById(task.storageId) ? `${ProviderNameMap[getStorageById(task.storageId)?.provider]} - ${getStorageById(task.storageId)?.bucket}` : '未设置'
|
||||||
|
}}</span>
|
||||||
|
</AFlex>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="task-info-item">
|
||||||
|
<span class="label">备份内容:</span>
|
||||||
|
<span class="value content-tags">
|
||||||
|
<template v-if="task.content && task.content.length">
|
||||||
|
<ATag v-for="item in task.content" :key="item" color="cyan" class="content-tag">{{ getContentItemName(item) }}</ATag>
|
||||||
|
</template>
|
||||||
|
<template v-else>无</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 清理任务特有信息 -->
|
||||||
|
<template v-if="task.type === 'cleanup'">
|
||||||
|
<div class="task-info-group cleanup-info">
|
||||||
|
<div class="group-title">清理配置</div>
|
||||||
|
<div class="task-info-item">
|
||||||
|
<span class="label">清理策略:</span>
|
||||||
|
<span class="value">
|
||||||
|
<ATag :color="getCleanupStrategyColor(task.strategy)" class="info-tag">{{ getCleanupStrategyText(task.strategy) }}</ATag>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="task.strategy !== 'duplicates'" class="task-info-item">
|
||||||
|
<span class="label">未使用阈值:</span>
|
||||||
|
<span class="value">
|
||||||
|
<ATag color="orange" class="info-tag">{{ task.unusedThreshold }}天</ATag>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 回收站任务特有信息 -->
|
||||||
|
<template v-if="task.type === 'recycle'">
|
||||||
|
<div class="task-info-group recycle-info">
|
||||||
|
<div class="group-title">回收站配置</div>
|
||||||
|
<div class="task-info-item">
|
||||||
|
<span class="label">保留策略:</span>
|
||||||
|
<span class="value">
|
||||||
|
<ATag :color="getRetentionPolicyColor(task.retentionPolicy)" class="info-tag">{{ getRetentionPolicyText(task.retentionPolicy) }}</ATag>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="task.retentionPolicy === 'time'" class="task-info-item">
|
||||||
|
<span class="label">保留时间:</span>
|
||||||
|
<span class="value">
|
||||||
|
<ATag color="purple" class="info-tag">{{ task.retentionDays }}天</ATag>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="task-info-item">
|
||||||
|
<span class="label">清空前通知:</span>
|
||||||
|
<span class="value">
|
||||||
|
<ATag :color="task.notifyBeforeEmpty ? 'success' : 'default'" class="info-tag">{{ task.notifyBeforeEmpty ? '是' : '否' }}</ATag>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="task-status">
|
||||||
|
<ATag :color="task.enabled ? 'success' : 'default'" class="status-tag">
|
||||||
|
<template #icon>
|
||||||
|
<span class="status-dot" :class="{'active': task.enabled}"></span>
|
||||||
|
</template>
|
||||||
|
{{ task.enabled ? '已启用' : '已禁用' }}
|
||||||
|
</ATag>
|
||||||
|
<div class="task-last-run" v-if="task.lastRun">
|
||||||
|
<ATag color="#87878a" bordered class="last-run-tag">
|
||||||
|
<template #icon>
|
||||||
|
<ClockCircleOutlined />
|
||||||
|
</template>
|
||||||
|
{{ formatDate(task.lastRun) }}
|
||||||
|
</ATag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加任务模态框 -->
|
||||||
|
<AModal
|
||||||
|
v-model:open="addTaskModalVisible"
|
||||||
|
:title="isEditing ? '编辑任务' : '新增任务'"
|
||||||
|
@ok="handleAddTaskOk"
|
||||||
|
@cancel="handleAddTaskCancel"
|
||||||
|
:okText="isEditing ? '保存' : '添加'"
|
||||||
|
cancelText="取消"
|
||||||
|
:width="650"
|
||||||
|
class="task-modal"
|
||||||
|
:maskClosable="false"
|
||||||
|
:destroyOnClose="true"
|
||||||
|
>
|
||||||
|
<!-- 第一步:选择任务类型 -->
|
||||||
|
<div v-if="!selectedTaskType && !isEditing" class="task-type-selection">
|
||||||
|
<p class="selection-tip">请选择要创建的任务类型:</p>
|
||||||
|
<div class="task-type-options">
|
||||||
|
<CheckCard
|
||||||
|
class="task-type-card"
|
||||||
|
value="backup"
|
||||||
|
v-model="tempSelectedType"
|
||||||
|
selectionMode="single"
|
||||||
|
iconPosition="top-right"
|
||||||
|
borderRadius="16px"
|
||||||
|
:showHoverCircle="true"
|
||||||
|
:iconSize="28"
|
||||||
|
>
|
||||||
|
<div class="task-type-content">
|
||||||
|
<AAvatar shape="square" :size="40" :src="backupIcon"/>
|
||||||
|
<span>定时备份</span>
|
||||||
|
<p>定期备份您的照片和相册数据</p>
|
||||||
|
</div>
|
||||||
|
</CheckCard>
|
||||||
|
<CheckCard
|
||||||
|
class="task-type-card"
|
||||||
|
value="cleanup"
|
||||||
|
v-model="tempSelectedType"
|
||||||
|
selectionMode="single"
|
||||||
|
iconPosition="top-right"
|
||||||
|
borderRadius="16px"
|
||||||
|
margin="12px"
|
||||||
|
:showHoverCircle="true"
|
||||||
|
:iconSize="28"
|
||||||
|
>
|
||||||
|
<div class="task-type-content">
|
||||||
|
<AAvatar shape="square" :size="40" :src="cleanupIcon"/>
|
||||||
|
<span>定时清理</span>
|
||||||
|
<p>定期清理重复或未使用的文件</p>
|
||||||
|
</div>
|
||||||
|
</CheckCard>
|
||||||
|
<CheckCard
|
||||||
|
class="task-type-card"
|
||||||
|
value="recycle"
|
||||||
|
v-model="tempSelectedType"
|
||||||
|
selectionMode="single"
|
||||||
|
iconPosition="top-right"
|
||||||
|
borderRadius="16px"
|
||||||
|
:showHoverCircle="true"
|
||||||
|
:iconSize="28"
|
||||||
|
>
|
||||||
|
<div class="task-type-content">
|
||||||
|
<AAvatar shape="square" :size="40" :src="recycleIcon"/>
|
||||||
|
<span>回收站管理</span>
|
||||||
|
<p>定期清空回收站中的文件</p>
|
||||||
|
</div>
|
||||||
|
</CheckCard>
|
||||||
|
</div>
|
||||||
|
<div class="task-type-actions">
|
||||||
|
<AButton type="primary" :disabled="!tempSelectedType" @click="confirmTaskType">下一步</AButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第二步:填写任务详情 -->
|
||||||
|
<div v-else>
|
||||||
|
<!-- 备份任务表单 -->
|
||||||
|
<AForm v-if="selectedTaskType === 'backup'" layout="vertical">
|
||||||
|
<AFormItem label="备份频率" name="backupFrequency">
|
||||||
|
<ASelect
|
||||||
|
v-model:value="currentTask.frequency"
|
||||||
|
placeholder="请选择备份频率"
|
||||||
|
>
|
||||||
|
<ASelectOption value="daily">每天</ASelectOption>
|
||||||
|
<ASelectOption value="weekly">每周</ASelectOption>
|
||||||
|
<ASelectOption value="monthly">每月</ASelectOption>
|
||||||
|
</ASelect>
|
||||||
|
</AFormItem>
|
||||||
|
|
||||||
|
<AFormItem label="备份时间" name="backupTime">
|
||||||
|
<ATimePicker
|
||||||
|
v-model:value="currentTask.time"
|
||||||
|
format="HH:mm"
|
||||||
|
placeholder="请选择备份时间"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</AFormItem>
|
||||||
|
|
||||||
|
<AFormItem label="备份存储" name="backupStorage">
|
||||||
|
<ASelect
|
||||||
|
v-model:value="currentTask.storageId"
|
||||||
|
placeholder="请选择备份存储"
|
||||||
|
>
|
||||||
|
<ASelectOption
|
||||||
|
v-for="item in storageList"
|
||||||
|
:key="item.id"
|
||||||
|
:value="item.id"
|
||||||
|
>
|
||||||
|
<AFlex align="center" gap="small">
|
||||||
|
<AAvatar :size="20" shape="circle" :src="ProviderIcon[item.provider]"/>
|
||||||
|
<span>{{ ProviderNameMap[item.provider] }} - {{ item.bucket }}</span>
|
||||||
|
</AFlex>
|
||||||
|
</ASelectOption>
|
||||||
|
</ASelect>
|
||||||
|
</AFormItem>
|
||||||
|
|
||||||
|
<AFormItem label="备份内容" name="backupContent">
|
||||||
|
<ACheckboxGroup
|
||||||
|
v-model:value="currentTask.content"
|
||||||
|
>
|
||||||
|
<ACheckbox value="photos">照片</ACheckbox>
|
||||||
|
<ACheckbox value="albums">相册</ACheckbox>
|
||||||
|
<ACheckbox value="settings">设置</ACheckbox>
|
||||||
|
</ACheckboxGroup>
|
||||||
|
</AFormItem>
|
||||||
|
</AForm>
|
||||||
|
|
||||||
|
<!-- 清理任务表单 -->
|
||||||
|
<AForm v-if="selectedTaskType === 'cleanup'" layout="vertical">
|
||||||
|
<AFormItem label="清理频率" name="cleanupFrequency">
|
||||||
|
<ASelect
|
||||||
|
v-model:value="currentTask.frequency"
|
||||||
|
placeholder="请选择清理频率"
|
||||||
|
>
|
||||||
|
<ASelectOption value="daily">每天</ASelectOption>
|
||||||
|
<ASelectOption value="weekly">每周</ASelectOption>
|
||||||
|
<ASelectOption value="monthly">每月</ASelectOption>
|
||||||
|
</ASelect>
|
||||||
|
</AFormItem>
|
||||||
|
|
||||||
|
<AFormItem label="清理时间" name="cleanupTime">
|
||||||
|
<ATimePicker
|
||||||
|
v-model:value="currentTask.time"
|
||||||
|
format="HH:mm"
|
||||||
|
placeholder="请选择清理时间"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</AFormItem>
|
||||||
|
|
||||||
|
<AFormItem label="清理策略" name="cleanupStrategy">
|
||||||
|
<ARadioGroup
|
||||||
|
v-model:value="currentTask.strategy"
|
||||||
|
>
|
||||||
|
<ARadio value="duplicates">仅清理重复文件</ARadio>
|
||||||
|
<ARadio value="unused">清理长期未使用文件</ARadio>
|
||||||
|
<ARadio value="both">两者都清理</ARadio>
|
||||||
|
</ARadioGroup>
|
||||||
|
</AFormItem>
|
||||||
|
|
||||||
|
<AFormItem
|
||||||
|
label="未使用时间阈值(天)"
|
||||||
|
name="unusedThreshold"
|
||||||
|
v-if="currentTask.strategy !== 'duplicates'"
|
||||||
|
>
|
||||||
|
<AInputNumber
|
||||||
|
v-model:value="currentTask.unusedThreshold"
|
||||||
|
:min="1"
|
||||||
|
:max="365"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</AFormItem>
|
||||||
|
</AForm>
|
||||||
|
|
||||||
|
<!-- 回收站管理表单 -->
|
||||||
|
<AForm v-if="selectedTaskType === 'recycle'" layout="vertical">
|
||||||
|
<AFormItem label="自动清空频率" name="recycleFrequency">
|
||||||
|
<ASelect
|
||||||
|
v-model:value="currentTask.frequency"
|
||||||
|
placeholder="请选择清空频率"
|
||||||
|
>
|
||||||
|
<ASelectOption value="weekly">每周</ASelectOption>
|
||||||
|
<ASelectOption value="biweekly">每两周</ASelectOption>
|
||||||
|
<ASelectOption value="monthly">每月</ASelectOption>
|
||||||
|
</ASelect>
|
||||||
|
</AFormItem>
|
||||||
|
|
||||||
|
<AFormItem label="执行时间" name="recycleTime">
|
||||||
|
<ATimePicker
|
||||||
|
v-model:value="currentTask.time"
|
||||||
|
format="HH:mm"
|
||||||
|
placeholder="请选择执行时间"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</AFormItem>
|
||||||
|
|
||||||
|
<AFormItem label="保留策略" name="retentionPolicy">
|
||||||
|
<ARadioGroup
|
||||||
|
v-model:value="currentTask.retentionPolicy"
|
||||||
|
>
|
||||||
|
<ARadio value="all">清空所有文件</ARadio>
|
||||||
|
<ARadio value="time">按时间清空</ARadio>
|
||||||
|
</ARadioGroup>
|
||||||
|
</AFormItem>
|
||||||
|
|
||||||
|
<AFormItem
|
||||||
|
label="保留时间(天)"
|
||||||
|
name="retentionDays"
|
||||||
|
v-if="currentTask.retentionPolicy === 'time'"
|
||||||
|
>
|
||||||
|
<AInputNumber
|
||||||
|
v-model:value="currentTask.retentionDays"
|
||||||
|
:min="1"
|
||||||
|
:max="90"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</AFormItem>
|
||||||
|
|
||||||
|
<AFormItem label="清空前通知" name="notifyBeforeEmpty">
|
||||||
|
<ASwitch
|
||||||
|
v-model:checked="currentTask.notifyBeforeEmpty"
|
||||||
|
/>
|
||||||
|
</AFormItem>
|
||||||
|
</AForm>
|
||||||
|
</div>
|
||||||
|
</AModal>
|
||||||
|
|
||||||
|
<!-- 删除确认对话框 -->
|
||||||
|
<AModal
|
||||||
|
v-model:open="deleteConfirmVisible"
|
||||||
|
title="确认删除"
|
||||||
|
@ok="confirmDelete"
|
||||||
|
@cancel="cancelDelete"
|
||||||
|
okText="删除"
|
||||||
|
cancelText="取消"
|
||||||
|
:okButtonProps="{ danger: true, size: 'middle' }"
|
||||||
|
class="delete-confirm-modal"
|
||||||
|
:maskClosable="false"
|
||||||
|
:closable="true"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<ExclamationCircleOutlined style="color: #ff4d4f; font-size: 22px;" />
|
||||||
|
</template>
|
||||||
|
<div class="delete-confirm-content">
|
||||||
|
<p>确定要删除这个定时任务吗?</p>
|
||||||
|
<p class="warning-text">此操作不可恢复,请谨慎操作。</p>
|
||||||
|
</div>
|
||||||
|
</AModal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {message} from 'ant-design-vue';
|
||||||
|
import {PlusOutlined, EditOutlined, DeleteOutlined, ClockCircleOutlined, ExclamationCircleOutlined} from '@ant-design/icons-vue';
|
||||||
|
import {listUserStorageConfigApi} from "@/api/storage";
|
||||||
|
import backupIcon from "@/assets/svgs/source-storage.svg";
|
||||||
|
import cleanupIcon from "@/assets/svgs/cleanup.svg";
|
||||||
|
import recycleIcon from "@/assets/svgs/recycle.svg";
|
||||||
|
import {ProviderIcon, ProviderNameMap} from "@/constant/provider_map";
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import CheckCard from '@/components/CheckCard/CheckCard.vue';
|
||||||
|
|
||||||
|
// 存储列表
|
||||||
|
const storageList = ref<any[]>([]);
|
||||||
|
|
||||||
|
// 任务列表
|
||||||
|
const taskList = ref<any[]>([]);
|
||||||
|
|
||||||
|
// 模态框相关
|
||||||
|
const addTaskModalVisible = ref(false);
|
||||||
|
const selectedTaskType = ref<any>();
|
||||||
|
const tempSelectedType = ref([]);
|
||||||
|
const isEditing = ref(false);
|
||||||
|
const editingIndex = ref(-1);
|
||||||
|
|
||||||
|
// 删除确认
|
||||||
|
const deleteConfirmVisible = ref(false);
|
||||||
|
const deletingIndex = ref(-1);
|
||||||
|
|
||||||
|
// 当前编辑的任务
|
||||||
|
const currentTask = reactive<any>({
|
||||||
|
type: '',
|
||||||
|
enabled: true,
|
||||||
|
frequency: '',
|
||||||
|
time: null,
|
||||||
|
// 备份任务特有
|
||||||
|
storageId: '',
|
||||||
|
content: [],
|
||||||
|
// 清理任务特有
|
||||||
|
strategy: 'duplicates',
|
||||||
|
unusedThreshold: 30,
|
||||||
|
// 回收站任务特有
|
||||||
|
retentionPolicy: 'time',
|
||||||
|
retentionDays: 30,
|
||||||
|
notifyBeforeEmpty: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取存储列表
|
||||||
|
const getStorageList = async () => {
|
||||||
|
try {
|
||||||
|
const res: any = await listUserStorageConfigApi();
|
||||||
|
if (res.code === 200) {
|
||||||
|
storageList.value = res.data.records;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取存储列表失败', error);
|
||||||
|
message.error('获取存储列表失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取任务列表
|
||||||
|
const getTaskList = async () => {
|
||||||
|
try {
|
||||||
|
// 这里应该调用获取任务列表的API
|
||||||
|
// 模拟数据
|
||||||
|
taskList.value = [
|
||||||
|
{
|
||||||
|
type: 'backup',
|
||||||
|
enabled: true,
|
||||||
|
frequency: 'daily',
|
||||||
|
time: dayjs('2023-01-01 03:00:00'),
|
||||||
|
storageId: '1',
|
||||||
|
content: ['photos', 'albums'],
|
||||||
|
lastRun: '2023-05-15 03:00:00'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'cleanup',
|
||||||
|
enabled: false,
|
||||||
|
frequency: 'weekly',
|
||||||
|
time: dayjs('2023-01-01 04:00:00'),
|
||||||
|
strategy: 'both',
|
||||||
|
unusedThreshold: 60,
|
||||||
|
lastRun: '2023-05-10 04:00:00'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
message.success('获取任务列表成功');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取任务列表失败', error);
|
||||||
|
message.error('获取任务列表失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 刷新任务列表
|
||||||
|
const refreshTasks = () => {
|
||||||
|
getTaskList();
|
||||||
|
getStorageList();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 显示添加任务模态框
|
||||||
|
const showAddTaskModal = () => {
|
||||||
|
isEditing.value = false;
|
||||||
|
selectedTaskType.value = '';
|
||||||
|
tempSelectedType.value = [];
|
||||||
|
resetCurrentTask();
|
||||||
|
addTaskModalVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认任务类型
|
||||||
|
const confirmTaskType = () => {
|
||||||
|
selectedTaskType.value = tempSelectedType.value[0];
|
||||||
|
currentTask.type = tempSelectedType.value;
|
||||||
|
|
||||||
|
// 设置默认值
|
||||||
|
if (selectedTaskType.value === 'backup') {
|
||||||
|
currentTask.frequency = 'daily';
|
||||||
|
currentTask.content = ['photos'];
|
||||||
|
} else if (selectedTaskType.value === 'cleanup') {
|
||||||
|
currentTask.frequency = 'weekly';
|
||||||
|
currentTask.strategy = 'duplicates';
|
||||||
|
} else if (selectedTaskType.value === 'recycle') {
|
||||||
|
currentTask.frequency = 'monthly';
|
||||||
|
currentTask.retentionPolicy = 'time';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置当前任务
|
||||||
|
const resetCurrentTask = () => {
|
||||||
|
Object.assign(currentTask, {
|
||||||
|
type: '',
|
||||||
|
enabled: true,
|
||||||
|
frequency: '',
|
||||||
|
time: null,
|
||||||
|
storageId: '',
|
||||||
|
content: [],
|
||||||
|
strategy: 'duplicates',
|
||||||
|
unusedThreshold: 30,
|
||||||
|
retentionPolicy: 'time',
|
||||||
|
retentionDays: 30,
|
||||||
|
notifyBeforeEmpty: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理添加任务确认
|
||||||
|
const handleAddTaskOk = () => {
|
||||||
|
// 验证表单
|
||||||
|
if (!currentTask.time) {
|
||||||
|
message.warning('请选择执行时间');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTask.type === 'backup') {
|
||||||
|
if (!currentTask.storageId) {
|
||||||
|
message.warning('请选择备份存储');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentTask.content.length === 0) {
|
||||||
|
message.warning('请选择备份内容');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是编辑模式
|
||||||
|
if (isEditing.value) {
|
||||||
|
taskList.value[editingIndex.value] = {...currentTask};
|
||||||
|
message.success('任务更新成功');
|
||||||
|
} else {
|
||||||
|
// 添加新任务
|
||||||
|
taskList.value.push({...currentTask});
|
||||||
|
message.success('任务添加成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭模态框
|
||||||
|
addTaskModalVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理添加任务取消
|
||||||
|
const handleAddTaskCancel = () => {
|
||||||
|
addTaskModalVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑任务
|
||||||
|
const editTask = (index: number) => {
|
||||||
|
try {
|
||||||
|
isEditing.value = true;
|
||||||
|
editingIndex.value = index;
|
||||||
|
const task = taskList.value[index];
|
||||||
|
selectedTaskType.value = task.type;
|
||||||
|
|
||||||
|
// 重置当前任务对象
|
||||||
|
resetCurrentTask();
|
||||||
|
|
||||||
|
// 复制基本属性
|
||||||
|
Object.keys(task).forEach(key => {
|
||||||
|
if (key === 'time' && task[key]) {
|
||||||
|
// 确保时间字段正确处理
|
||||||
|
currentTask.time = dayjs(task[key]);
|
||||||
|
} else {
|
||||||
|
currentTask[key] = task[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 打开模态框
|
||||||
|
addTaskModalVisible.value = true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('编辑任务出错', error);
|
||||||
|
message.error('编辑任务出错');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除任务
|
||||||
|
const deleteTask = (index: number) => {
|
||||||
|
try {
|
||||||
|
deletingIndex.value = index;
|
||||||
|
deleteConfirmVisible.value = true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除任务出错', error);
|
||||||
|
message.error('删除任务出错');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认删除
|
||||||
|
const confirmDelete = () => {
|
||||||
|
if (deletingIndex.value > -1) {
|
||||||
|
taskList.value.splice(deletingIndex.value, 1);
|
||||||
|
message.success('任务删除成功');
|
||||||
|
}
|
||||||
|
deleteConfirmVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消删除
|
||||||
|
const cancelDelete = () => {
|
||||||
|
deleteConfirmVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换任务状态
|
||||||
|
const toggleTaskStatus = (index: number, status: boolean) => {
|
||||||
|
try {
|
||||||
|
// 更新任务状态
|
||||||
|
taskList.value[index].enabled = status;
|
||||||
|
|
||||||
|
// 这里可以添加API调用来更新后端数据
|
||||||
|
// 例如: updateTaskStatusApi(taskList.value[index].id, status);
|
||||||
|
|
||||||
|
// 显示成功消息
|
||||||
|
message.success(`任务已${status ? '启用' : '禁用'}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('切换任务状态出错', error);
|
||||||
|
message.error('切换任务状态出错');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取任务图标
|
||||||
|
const getTaskIcon = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'backup':
|
||||||
|
return backupIcon;
|
||||||
|
case 'cleanup':
|
||||||
|
return cleanupIcon;
|
||||||
|
case 'recycle':
|
||||||
|
return recycleIcon;
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取任务类型名称
|
||||||
|
const getTaskTypeName = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'backup':
|
||||||
|
return '定时备份';
|
||||||
|
case 'cleanup':
|
||||||
|
return '定时清理';
|
||||||
|
case 'recycle':
|
||||||
|
return '回收站管理';
|
||||||
|
default:
|
||||||
|
return '未知任务';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取任务类型样式类
|
||||||
|
const getTaskTypeClass = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'backup':
|
||||||
|
return 'backup-task';
|
||||||
|
case 'cleanup':
|
||||||
|
return 'cleanup-task';
|
||||||
|
case 'recycle':
|
||||||
|
return 'recycle-task';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取任务类型颜色
|
||||||
|
const getTaskTypeColor = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'backup':
|
||||||
|
return 'blue';
|
||||||
|
case 'cleanup':
|
||||||
|
return 'green';
|
||||||
|
case 'recycle':
|
||||||
|
return 'orange';
|
||||||
|
default:
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取任务类型简称
|
||||||
|
const getTaskTypeShortName = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'backup':
|
||||||
|
return '备份';
|
||||||
|
case 'cleanup':
|
||||||
|
return '清理';
|
||||||
|
case 'recycle':
|
||||||
|
return '回收';
|
||||||
|
default:
|
||||||
|
return '未知';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取频率颜色
|
||||||
|
const getFrequencyColor = (frequency: string) => {
|
||||||
|
switch (frequency) {
|
||||||
|
case 'daily':
|
||||||
|
return 'magenta';
|
||||||
|
case 'weekly':
|
||||||
|
return 'purple';
|
||||||
|
case 'biweekly':
|
||||||
|
return 'geekblue';
|
||||||
|
case 'monthly':
|
||||||
|
return 'cyan';
|
||||||
|
default:
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取清理策略颜色
|
||||||
|
const getCleanupStrategyColor = (strategy: string) => {
|
||||||
|
switch (strategy) {
|
||||||
|
case 'duplicates':
|
||||||
|
return 'green';
|
||||||
|
case 'unused':
|
||||||
|
return 'orange';
|
||||||
|
case 'both':
|
||||||
|
return 'volcano';
|
||||||
|
default:
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取保留策略颜色
|
||||||
|
const getRetentionPolicyColor = (policy: string) => {
|
||||||
|
switch (policy) {
|
||||||
|
case 'all':
|
||||||
|
return 'red';
|
||||||
|
case 'time':
|
||||||
|
return 'blue';
|
||||||
|
default:
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取内容项名称
|
||||||
|
const getContentItemName = (item: string) => {
|
||||||
|
const contentMap: Record<string, string> = {
|
||||||
|
photos: '照片',
|
||||||
|
albums: '相册',
|
||||||
|
settings: '设置'
|
||||||
|
};
|
||||||
|
return contentMap[item] || item;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取频率文本
|
||||||
|
const getFrequencyText = (frequency: string) => {
|
||||||
|
switch (frequency) {
|
||||||
|
case 'daily':
|
||||||
|
return '每天';
|
||||||
|
case 'weekly':
|
||||||
|
return '每周';
|
||||||
|
case 'biweekly':
|
||||||
|
return '每两周';
|
||||||
|
case 'monthly':
|
||||||
|
return '每月';
|
||||||
|
default:
|
||||||
|
return '未知';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
const formatTime = (time: any) => {
|
||||||
|
try {
|
||||||
|
if (!time) return '未设置';
|
||||||
|
return dayjs(time).format('HH:mm');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('格式化时间出错', error);
|
||||||
|
return '时间格式错误';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (date: string) => {
|
||||||
|
try {
|
||||||
|
if (!date) return '未执行';
|
||||||
|
return dayjs(date).format('YYYY-MM-DD HH:mm');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('格式化日期出错', error);
|
||||||
|
return '日期格式错误';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取清理策略文本
|
||||||
|
const getCleanupStrategyText = (strategy: string) => {
|
||||||
|
switch (strategy) {
|
||||||
|
case 'duplicates':
|
||||||
|
return '仅清理重复文件';
|
||||||
|
case 'unused':
|
||||||
|
return '清理长期未使用文件';
|
||||||
|
case 'both':
|
||||||
|
return '清理重复和未使用文件';
|
||||||
|
default:
|
||||||
|
return '未知';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取保留策略文本
|
||||||
|
const getRetentionPolicyText = (policy: string) => {
|
||||||
|
switch (policy) {
|
||||||
|
case 'all':
|
||||||
|
return '清空所有文件';
|
||||||
|
case 'time':
|
||||||
|
return '按时间清空';
|
||||||
|
default:
|
||||||
|
return '未知';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据ID获取存储信息
|
||||||
|
const getStorageById = (id: string) => {
|
||||||
|
return storageList.value.find(item => item.id === id);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getStorageList();
|
||||||
|
getTaskList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss" src="./index.scss">
|
||||||
|
|
||||||
|
</style>
|
@@ -0,0 +1,721 @@
|
|||||||
|
.account-setting-task {
|
||||||
|
width: calc(100% - 72px);
|
||||||
|
background-color: var(--white-color, #fff);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-setting-task-header {
|
||||||
|
padding: 0 36px;
|
||||||
|
width: 100%;
|
||||||
|
height: 70px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid #eaeaea;
|
||||||
|
background: linear-gradient(135deg, #f8f9fa 0%, #e9f2ff 100%);
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #222222;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 16px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 5px;
|
||||||
|
height: 24px;
|
||||||
|
background: linear-gradient(to bottom, #1890ff, #36cfc9);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-setting-task-body {
|
||||||
|
padding: 36px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
background-color: var(--white-color, #fff);
|
||||||
|
border-radius: 0 0 16px 16px;
|
||||||
|
|
||||||
|
.task-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card {
|
||||||
|
background-color: #fafafa;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.backup-task {
|
||||||
|
border-left: 4px solid #1890ff;
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(90deg, #1890ff, #36cfc9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cleanup-task {
|
||||||
|
border-left: 4px solid #52c41a;
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(90deg, #52c41a, #b7eb8f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.recycle-task {
|
||||||
|
border-left: 4px solid #fa8c16;
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(90deg, #fa8c16, #ffd666);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card-header {
|
||||||
|
padding: 16px 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
.task-card-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.task-icon {
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-type-tag {
|
||||||
|
margin-left: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
padding: 0 8px;
|
||||||
|
|
||||||
|
&.edit-btn:hover {
|
||||||
|
color: #1890ff;
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.delete-btn:hover {
|
||||||
|
color: #ff4d4f;
|
||||||
|
background-color: #fff1f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-switch {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card-content {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
background-color: #fafafa;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.task-info-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-bottom: 1px dashed #f0f0f0;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 12px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 4px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.backup-info .group-title::before {
|
||||||
|
background: linear-gradient(to bottom, #1890ff, #36cfc9);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cleanup-info .group-title::before {
|
||||||
|
background: linear-gradient(to bottom, #52c41a, #b7eb8f);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.recycle-info .group-title::before {
|
||||||
|
background: linear-gradient(to bottom, #fa8c16, #ffd666);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-info-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #666;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: #333;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.info-tag {
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-tag {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.content-tag {
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.storage-info {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-status {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
margin-left: 16px;
|
||||||
|
|
||||||
|
.status-tag {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #d9d9d9;
|
||||||
|
margin-right: 6px;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: #52c41a;
|
||||||
|
box-shadow: 0 0 6px rgba(82, 196, 26, 0.5);
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-last-run {
|
||||||
|
.last-run-tag {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 任务类型选择样式
|
||||||
|
.task-type-selection {
|
||||||
|
padding: 32px 20px;
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #f0f2f5 100%);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.selection-tip {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
color: #1a1a1a;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
animation: fadeInDown 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-type-options {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
animation: fadeInUp 0.6s ease-out;
|
||||||
|
|
||||||
|
.task-type-card {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
.task-type-content {
|
||||||
|
padding: 32px 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 6px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(1)::before {
|
||||||
|
background: linear-gradient(90deg, #1890ff, #36cfc9);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2)::before {
|
||||||
|
background: linear-gradient(90deg, #52c41a, #b7eb8f);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3)::before {
|
||||||
|
background: linear-gradient(90deg, #fa8c16, #ffd666);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .task-type-content::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected .task-type-content::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-card.selected {
|
||||||
|
box-shadow: 0 12px 28px rgba(24, 144, 255, 0.2);
|
||||||
|
background: linear-gradient(145deg, #e6f7ff 0%, #ffffff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-type-option {
|
||||||
|
flex: 1;
|
||||||
|
padding: 32px 24px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: linear-gradient(145deg, #ffffff 0%, #f8f9fa 100%);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 6px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
background: radial-gradient(circle, rgba(24, 144, 255, 0.2) 0%, rgba(24, 144, 255, 0) 60%);
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 24px rgba(24, 144, 255, 0.15);
|
||||||
|
background: linear-gradient(145deg, #ffffff 0%, #f0f7ff 100%);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: #1890ff;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #4096ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active::after {
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
opacity: 0;
|
||||||
|
transition: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
border-color: #1890ff;
|
||||||
|
background: linear-gradient(145deg, #e6f7ff 0%, #ffffff 100%);
|
||||||
|
box-shadow: 0 12px 28px rgba(24, 144, 255, 0.2);
|
||||||
|
transform: translateY(-4px) scale(1.02);
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
opacity: 1;
|
||||||
|
background: linear-gradient(90deg, #1890ff, #36cfc9);
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #4096ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #666;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-type-tag {
|
||||||
|
margin-left: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 12px 28px rgba(24, 144, 255, 0.2);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 12px 28px rgba(24, 144, 255, 0.4);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 12px 28px rgba(24, 144, 255, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-type-actions {
|
||||||
|
margin-top: 16px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
height: 44px;
|
||||||
|
padding: 0 32px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 22px;
|
||||||
|
background: linear-gradient(90deg, #1890ff, #36cfc9);
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(24, 144, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #bfbfbf;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(1)::before {
|
||||||
|
background: linear-gradient(90deg, #1890ff, #36cfc9);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2)::before {
|
||||||
|
background: linear-gradient(90deg, #52c41a, #b7eb8f);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3)::before {
|
||||||
|
background: linear-gradient(90deg, #fa8c16, #ffd666);
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-type-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 32px;
|
||||||
|
animation: fadeInUp 0.8s ease-out;
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
min-width: 120px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
background: linear-gradient(135deg, #1890ff 0%, #36cfc9 100%);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 16px rgba(24, 144, 255, 0.4);
|
||||||
|
background: linear-gradient(135deg, #40a9ff 0%, #5cdbd3 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 4px 8px rgba(24, 144, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background: linear-gradient(135deg, #bfbfbf 0%, #d9d9d9 100%);
|
||||||
|
box-shadow: none;
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除确认对话框样式
|
||||||
|
.delete-confirm-modal {
|
||||||
|
.delete-confirm-content {
|
||||||
|
padding: 12px 0;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-text {
|
||||||
|
color: #ff4d4f;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 任务模态框样式
|
||||||
|
.task-modal {
|
||||||
|
:deep(.ant-modal-content) {
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-modal-header) {
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
background: linear-gradient(135deg, #f8f9fa 0%, #e9f2ff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-modal-title) {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-modal-body) {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-modal-footer) {
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
padding: 12px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-form-item-label) {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-select), :deep(.ant-input-number), :deep(.ant-picker) {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-radio-wrapper), :deep(.ant-checkbox-wrapper) {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(82, 196, 26, 0.5);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
box-shadow: 0 0 0 6px rgba(82, 196, 26, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(82, 196, 26, 0);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,49 +1,74 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="personal-center">
|
<div class="personal-center">
|
||||||
<div class="personal-center-header">
|
<Header :logo-color="'#333'" class="fixed-header" />
|
||||||
<Header :logo-color="'#fff'" style="background: transparent;box-shadow: none;backdrop-filter: none;"/>
|
<div class="personal-center-container">
|
||||||
<div class="personal-center-header-info">
|
<!-- 左侧边栏 - 用户信息和导航 -->
|
||||||
<div class="personal-center-header-info-container">
|
<div class="personal-center-sidebar">
|
||||||
<div class="personal-center-header-info-container-avatar">
|
<div class="user-profile">
|
||||||
<AAvatar :size="80" :text-size="1.5" :round="true" :src="userStore.user.avatar"/>
|
<div class="user-avatar">
|
||||||
|
<AAvatar :size="100" :text-size="1.8" :round="true" :src="userStore.user.avatar"/>
|
||||||
|
<div class="user-level">
|
||||||
|
<img src="/level_icon/icon/lv1.png" alt="level">
|
||||||
</div>
|
</div>
|
||||||
<div class="personal-center-header-info-container-description">
|
|
||||||
<div class="personal-center-header-info-container-description-name">
|
|
||||||
<span>{{ userStore.user.nickname }}</span>
|
|
||||||
<img src="/level_icon/icon/lv1.png" class="personal-level" alt="level">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="personal-center-header-info-container-description-introduce">
|
<div class="user-info">
|
||||||
<span>描述信息</span>
|
<h2 class="user-name">{{ userStore.user.nickname }}</h2>
|
||||||
|
<p class="user-description">描述信息</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="user-stats">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value">128</div>
|
||||||
|
<div class="stat-label">照片</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value">56</div>
|
||||||
|
<div class="stat-label">分享</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value">24</div>
|
||||||
|
<div class="stat-label">收藏</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
<AMenu
|
||||||
<div class="personal-center-content">
|
class="sidebar-menu"
|
||||||
<AMenu :selectedKeys="[menuStore.userCenterMenu]" mode="horizontal" :selectable="true" :multiple="false"
|
:selectedKeys="[menuStore.userCenterMenu]"
|
||||||
@select="handleClick">
|
mode="inline"
|
||||||
<AMenuItem key="home" :style="menuCSSStyle" title="主页">
|
:selectable="true"
|
||||||
|
:multiple="false"
|
||||||
|
@select="handleClick"
|
||||||
|
>
|
||||||
|
<AMenuItem key="home" title="主页">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<AAvatar shape="square" size="small" :src="home"/>
|
<AAvatar shape="square" size="small" :src="home"/>
|
||||||
</template>
|
</template>
|
||||||
<span class="ant-menu-item-title">主页</span>
|
<span class="menu-item-title">主页</span>
|
||||||
</AMenuItem>
|
</AMenuItem>
|
||||||
<AMenuItem key="dynamic" :style="menuCSSStyle" title="动态">
|
<AMenuItem key="dynamic" title="动态">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<AAvatar shape="square" size="small" :src="dynamic"/>
|
<AAvatar shape="square" size="small" :src="dynamic"/>
|
||||||
</template>
|
</template>
|
||||||
<span class="ant-menu-item-title">动态</span>
|
<span class="menu-item-title">动态</span>
|
||||||
</AMenuItem>
|
</AMenuItem>
|
||||||
<AMenuItem key="setting" :style="menuCSSStyle" title="设置">
|
<AMenuItem key="setting" title="设置">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<AAvatar shape="square" size="small" :src="setting"/>
|
<AAvatar shape="square" size="small" :src="setting"/>
|
||||||
</template>
|
</template>
|
||||||
<span class="ant-menu-item-title">设置</span>
|
<span class="menu-item-title">设置</span>
|
||||||
</AMenuItem>
|
</AMenuItem>
|
||||||
</AMenu>
|
</AMenu>
|
||||||
<div class="personal-center-content-container">
|
</div>
|
||||||
<router-view>
|
|
||||||
</router-view>
|
<!-- 右侧内容区域 -->
|
||||||
|
<div class="personal-center-content">
|
||||||
|
<div class="content-header">
|
||||||
|
<h1>{{ getPageTitle() }}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="content-container">
|
||||||
|
<router-view></router-view>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,106 +80,227 @@ import home from "@/assets/svgs/home.svg";
|
|||||||
import dynamic from "@/assets/svgs/dynamic.svg";
|
import dynamic from "@/assets/svgs/dynamic.svg";
|
||||||
import setting from "@/assets/svgs/setting.svg";
|
import setting from "@/assets/svgs/setting.svg";
|
||||||
|
|
||||||
|
|
||||||
const userStore = useStore().user;
|
const userStore = useStore().user;
|
||||||
const menuStore = useStore().menu;
|
const menuStore = useStore().menu;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const menuCSSStyle: any = reactive({
|
|
||||||
display: 'flex',
|
// 页面标题映射
|
||||||
alignItems: 'center',
|
const pageTitles = {
|
||||||
});
|
'home': '个人主页',
|
||||||
|
'dynamic': '我的动态',
|
||||||
|
'setting': '账户设置'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取当前页面标题
|
||||||
|
function getPageTitle() {
|
||||||
|
return pageTitles[menuStore.userCenterMenu] || '个人中心';
|
||||||
|
}
|
||||||
|
|
||||||
function handleClick({key}) {
|
function handleClick({key}) {
|
||||||
menuStore.userCenterMenu = key;
|
menuStore.userCenterMenu = key;
|
||||||
router.push(`/main/user/center/${key}`);
|
router.push(`/main/user/center/${key}`);
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.personal-center {
|
.personal-center {
|
||||||
//background-color: #eaeef6;
|
//min-height: 100vh;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
|
||||||
.personal-center-header {
|
.fixed-header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 200px;
|
z-index: 1000;
|
||||||
background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.2)), url("@/assets/images/bg_1.png");
|
}
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
.personal-center-container {
|
||||||
|
display: flex;
|
||||||
|
width: calc(100vw - 50px);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
gap: 30px;
|
||||||
|
margin-top: 70px; /* 为固定的Header腾出空间 */
|
||||||
|
position: relative; /* 为绝对定位的子元素提供参考 */
|
||||||
|
overflow: visible; /* 允许内容溢出,以便右侧内容可以滚动 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧边栏样式 */
|
||||||
|
.personal-center-sidebar {
|
||||||
|
width: 300px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: sticky;
|
||||||
|
top: 90px; /* 为固定的Header腾出空间,比margin-top多一点以确保有足够空间 */
|
||||||
|
height: calc(100vh - 110px); /* 设置固定高度,减去header和上下margin的高度 */
|
||||||
|
align-self: flex-start; /* 确保在flex容器中靠上对齐 */
|
||||||
|
|
||||||
|
.user-profile {
|
||||||
|
padding: 30px 20px;
|
||||||
|
text-align: center;
|
||||||
|
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||||
|
color: white;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.personal-center-header-info {
|
.user-avatar {
|
||||||
width: 100%;
|
position: relative;
|
||||||
height: 130px;
|
width: 100px;
|
||||||
display: flex;
|
height: 100px;
|
||||||
flex-direction: row;
|
margin: 0 auto 15px;
|
||||||
|
|
||||||
.personal-center-header-info-container {
|
.user-level {
|
||||||
width: 95%;
|
position: absolute;
|
||||||
height: 110px;
|
bottom: 0;
|
||||||
margin: 0 auto;
|
right: 0;
|
||||||
display: flex;
|
background: white;
|
||||||
flex-direction: row;
|
|
||||||
align-items: flex-end;
|
|
||||||
justify-content: flex-start;
|
|
||||||
gap: 20px;
|
|
||||||
|
|
||||||
.personal-center-header-info-container-avatar {
|
|
||||||
height: 80px;
|
|
||||||
width: 80px;
|
|
||||||
border: 2px solid #fff;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
padding: 3px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.personal-center-header-info-container-description {
|
.user-info {
|
||||||
height: 70px;
|
margin-bottom: 20px;
|
||||||
width: 80%;
|
|
||||||
|
.user-name {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-description {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-stats {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-menu {
|
||||||
|
border-right: none;
|
||||||
|
|
||||||
|
:deep(.ant-menu-item) {
|
||||||
|
height: 50px;
|
||||||
|
line-height: 50px;
|
||||||
|
margin: 4px 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(99, 102, 241, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ant-menu-item-selected {
|
||||||
|
background-color: rgba(99, 102, 241, 0.1);
|
||||||
|
color: #6366f1;
|
||||||
|
border-right: 3px solid #6366f1;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-title {
|
||||||
|
font-size: 15px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧内容区域样式 */
|
||||||
|
.personal-center-content {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
overflow: auto; /* 允许内容区域独立滚动 */
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
/* 移除最大高度限制,允许内容无限滚动 */
|
||||||
align-items: flex-start;
|
|
||||||
|
|
||||||
.personal-center-header-info-container-description-name {
|
.content-header {
|
||||||
font-size: 24px;
|
padding: 20px 30px;
|
||||||
font-weight: 700;
|
border-bottom: 1px solid #eee;
|
||||||
color: #fff;
|
|
||||||
text-shadow: 0px 1px 2px rgba(0, 0, 0, .4);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 20px;
|
|
||||||
|
|
||||||
.personal-level {
|
h1 {
|
||||||
width: auto;
|
font-size: 22px;
|
||||||
height: 20px;
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.personal-center-header-info-container-description-introduce {
|
.content-container {
|
||||||
font-size: 16px;
|
flex: 1;
|
||||||
color: #fff;
|
padding: 20px 30px;
|
||||||
background-color: rgba(255, 255, 255, .2);
|
|
||||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, .5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.personal-center-content {
|
|
||||||
|
|
||||||
.ant-menu-item-title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.personal-center-content-container {
|
|
||||||
width: calc(100% - 40px);
|
|
||||||
height: calc(100vh - 290px);
|
|
||||||
padding: 20px;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.personal-center-container {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 10px;
|
||||||
|
overflow: auto; /* 在小屏幕上允许整个容器滚动 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.personal-center-sidebar {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
position: relative; /* 在小屏幕上取消sticky定位 */
|
||||||
|
top: 0;
|
||||||
|
height: auto; /* 在小屏幕上高度自适应 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-menu {
|
||||||
|
display: flex;
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
:deep(.ant-menu-item) {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.user-profile {
|
||||||
|
padding: 20px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header {
|
||||||
|
padding: 15px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
padding: 15px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -1,7 +1,39 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="user-center-dynamic" ref="chartRef">
|
<div class="user-center-dynamic">
|
||||||
|
<div class="section-header">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="chart-container" ref="chartRef">
|
||||||
|
<!-- 图表将在这里渲染 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-summary">
|
||||||
|
<div class="summary-item">
|
||||||
|
<div class="summary-icon visits"></div>
|
||||||
|
<div class="summary-content">
|
||||||
|
<div class="summary-value">{{ totalVisits }}</div>
|
||||||
|
<div class="summary-label">总访问量</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="summary-item">
|
||||||
|
<div class="summary-icon visitors"></div>
|
||||||
|
<div class="summary-content">
|
||||||
|
<div class="summary-value">{{ totalVisitors }}</div>
|
||||||
|
<div class="summary-label">总访客数</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="summary-item">
|
||||||
|
<div class="summary-icon publishes"></div>
|
||||||
|
<div class="summary-content">
|
||||||
|
<div class="summary-value">{{ totalPublishes }}</div>
|
||||||
|
<div class="summary-label">总发布数</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import * as echarts from 'echarts';
|
import * as echarts from 'echarts';
|
||||||
@@ -19,8 +51,24 @@ const dates = ref<string[]>([]);
|
|||||||
const visitCounts = ref<number[]>([]);
|
const visitCounts = ref<number[]>([]);
|
||||||
const visitorCounts = ref<number[]>([]);
|
const visitorCounts = ref<number[]>([]);
|
||||||
const publishCounts = ref<number[]>([]);
|
const publishCounts = ref<number[]>([]);
|
||||||
|
// 移除时间范围选择
|
||||||
|
let chartInstance: echarts.ECharts | null = null;
|
||||||
|
|
||||||
|
// 计算总访问量、总访客数和总发布数
|
||||||
|
const totalVisits = computed(() => {
|
||||||
|
return visitCounts.value.reduce((sum, count) => sum + count, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalVisitors = computed(() => {
|
||||||
|
return visitorCounts.value.reduce((sum, count) => sum + count, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPublishes = computed(() => {
|
||||||
|
return publishCounts.value.reduce((sum, count) => sum + count, 0);
|
||||||
|
});
|
||||||
|
|
||||||
async function getData() {
|
async function getData() {
|
||||||
|
try {
|
||||||
const res: any = await getShareStatisticsInfoApi();
|
const res: any = await getShareStatisticsInfoApi();
|
||||||
if (res && res.code === 200) {
|
if (res && res.code === 200) {
|
||||||
const data: DataItem[] = res.data.records;
|
const data: DataItem[] = res.data.records;
|
||||||
@@ -28,54 +76,97 @@ async function getData() {
|
|||||||
visitCounts.value = data.map((item: DataItem) => item.visit_count);
|
visitCounts.value = data.map((item: DataItem) => item.visit_count);
|
||||||
visitorCounts.value = data.map((item: DataItem) => item.visitor_count);
|
visitorCounts.value = data.map((item: DataItem) => item.visitor_count);
|
||||||
publishCounts.value = data.map((item: DataItem) => item.publish_count);
|
publishCounts.value = data.map((item: DataItem) => item.publish_count);
|
||||||
|
|
||||||
|
// 数据加载完成后初始化或更新图表
|
||||||
|
initChart();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取分享统计数据失败:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
// 初始化图表
|
||||||
await nextTick();
|
function initChart() {
|
||||||
const chartInstance = echarts.init(chartRef.value);
|
if (!chartRef.value) return;
|
||||||
|
|
||||||
|
// 如果图表实例已存在,销毁它
|
||||||
|
if (chartInstance) {
|
||||||
|
chartInstance.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新的图表实例
|
||||||
|
chartInstance = echarts.init(chartRef.value);
|
||||||
|
|
||||||
|
// 使用防抖函数处理resize事件
|
||||||
|
const resizeHandler = () => {
|
||||||
|
if (chartInstance) {
|
||||||
|
nextTick(() => {
|
||||||
|
chartInstance.resize();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听窗口大小变化,调整图表大小
|
||||||
|
window.addEventListener('resize', resizeHandler);
|
||||||
|
|
||||||
|
// 设置图表配置
|
||||||
|
updateChartOption();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新图表配置
|
||||||
|
function updateChartOption() {
|
||||||
|
if (!chartInstance) return;
|
||||||
|
|
||||||
|
const colorList = ["#6366f1", "#22c55e", "#f97316"];
|
||||||
|
|
||||||
const colorList = ["#9E87FF", "#73DDFF", "#fe9a8b", "#F56948", "#9E87FF"];
|
|
||||||
const option = {
|
const option = {
|
||||||
backgroundColor: "#fff",
|
backgroundColor: "#fff",
|
||||||
title: {
|
title: {
|
||||||
text: "最近七天分享统计",
|
text: "分享统计概览",
|
||||||
fontSize: 12,
|
textStyle: {
|
||||||
fontWeight: 400,
|
fontSize: 16,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: '#333'
|
||||||
|
},
|
||||||
left: "center",
|
left: "center",
|
||||||
top: "5%",
|
top: "5%",
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
icon: "circle",
|
icon: "circle",
|
||||||
top: "5%",
|
bottom: "0%",
|
||||||
right: "5%",
|
itemWidth: 8,
|
||||||
itemWidth: 6,
|
itemHeight: 8,
|
||||||
itemGap: 20,
|
itemGap: 20,
|
||||||
color: "#556677",
|
textStyle: {
|
||||||
|
color: "#333",
|
||||||
|
fontSize: 12
|
||||||
|
}
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: "axis",
|
trigger: "axis",
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
borderRadius: 4,
|
||||||
|
shadowColor: 'rgba(0, 0, 0, 0.1)',
|
||||||
|
shadowBlur: 10,
|
||||||
|
textStyle: {
|
||||||
|
color: "#333",
|
||||||
|
},
|
||||||
|
padding: [10, 15],
|
||||||
axisPointer: {
|
axisPointer: {
|
||||||
label: {
|
type: 'line',
|
||||||
show: true,
|
|
||||||
backgroundColor: "#fff",
|
|
||||||
color: "#556677",
|
|
||||||
borderColor: "rgba(0,0,0,0)",
|
|
||||||
shadowColor: "rgba(0,0,0,0)",
|
|
||||||
shadowOffsetY: 0,
|
|
||||||
},
|
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: 0,
|
color: '#6366f1',
|
||||||
},
|
width: 1,
|
||||||
},
|
type: 'dashed'
|
||||||
backgroundColor: "#fff",
|
}
|
||||||
color: "#5c6c7c",
|
}
|
||||||
padding: [10, 10],
|
|
||||||
extraCssText: "box-shadow: 1px 0 2px 0 rgba(163,163,163,0.5)",
|
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
top: "15%",
|
top: "15%",
|
||||||
|
left: "3%",
|
||||||
|
right: "3%",
|
||||||
|
bottom: "12%",
|
||||||
|
containLabel: true
|
||||||
},
|
},
|
||||||
xAxis: [
|
xAxis: [
|
||||||
{
|
{
|
||||||
@@ -83,7 +174,7 @@ onMounted(async () => {
|
|||||||
data: dates.value,
|
data: dates.value,
|
||||||
axisLine: {
|
axisLine: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: "#DCE2E8",
|
color: "#e0e0e0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
axisTick: {
|
axisTick: {
|
||||||
@@ -91,68 +182,10 @@ onMounted(async () => {
|
|||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
interval: 0,
|
interval: 0,
|
||||||
color: "#556677",
|
color: "#666",
|
||||||
// 默认x轴字体大小
|
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
// margin:文字到x轴的距离
|
|
||||||
margin: 15,
|
margin: 15,
|
||||||
},
|
rotate: dates.value.length > 10 ? 45 : 0
|
||||||
axisPointer: {
|
|
||||||
label: {
|
|
||||||
// padding: [11, 5, 7],
|
|
||||||
padding: [0, 0, 10, 0],
|
|
||||||
/*
|
|
||||||
除了padding[0]建议必须是0之外,其他三项可随意设置
|
|
||||||
|
|
||||||
和CSSpadding相同,[上,右,下,左]
|
|
||||||
|
|
||||||
如果需要下边线超出文字,设左右padding即可,注:左右padding最好相同
|
|
||||||
|
|
||||||
padding[2]的10:
|
|
||||||
|
|
||||||
10 = 文字距下边线的距离 + 下边线的宽度
|
|
||||||
|
|
||||||
如:UI图中文字距下边线距离为7 下边线宽度为2
|
|
||||||
|
|
||||||
则padding: [0, 0, 9, 0]
|
|
||||||
|
|
||||||
*/
|
|
||||||
// 这里的margin和axisLabel的margin要一致!
|
|
||||||
margin: 15,
|
|
||||||
// 移入时的字体大小
|
|
||||||
fontSize: 12,
|
|
||||||
backgroundColor: {
|
|
||||||
type: "linear",
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
x2: 0,
|
|
||||||
y2: 1,
|
|
||||||
colorStops: [
|
|
||||||
{
|
|
||||||
offset: 0,
|
|
||||||
color: "#fff", // 0% 处的颜色
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// offset: 0.9,
|
|
||||||
offset: 0.86,
|
|
||||||
/*
|
|
||||||
0.86 = (文字 + 文字距下边线的距离)/(文字 + 文字距下边线的距离 + 下边线的宽度)
|
|
||||||
|
|
||||||
*/
|
|
||||||
color: "#fff", // 0% 处的颜色
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 0.86,
|
|
||||||
color: "#33c0cd", // 0% 处的颜色
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 1,
|
|
||||||
color: "#33c0cd", // 100% 处的颜色
|
|
||||||
},
|
|
||||||
],
|
|
||||||
global: false, // 缺省为 false
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
boundaryGap: false,
|
boundaryGap: false,
|
||||||
},
|
},
|
||||||
@@ -160,42 +193,33 @@ onMounted(async () => {
|
|||||||
yAxis: [
|
yAxis: [
|
||||||
{
|
{
|
||||||
type: "value",
|
type: "value",
|
||||||
|
name: "数量",
|
||||||
|
nameTextStyle: {
|
||||||
|
color: "#666",
|
||||||
|
fontSize: 12,
|
||||||
|
padding: [0, 0, 0, 5]
|
||||||
|
},
|
||||||
axisTick: {
|
axisTick: {
|
||||||
show: false,
|
show: false,
|
||||||
},
|
},
|
||||||
axisLine: {
|
axisLine: {
|
||||||
show: true,
|
show: true,
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: "#DCE2E8",
|
color: "#e0e0e0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: "#556677",
|
color: "#666",
|
||||||
|
fontSize: 12
|
||||||
},
|
},
|
||||||
splitLine: {
|
splitLine: {
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "value",
|
|
||||||
position: "right",
|
|
||||||
axisTick: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
axisLabel: {
|
|
||||||
color: "#556677",
|
|
||||||
formatter: "{value}",
|
|
||||||
},
|
|
||||||
axisLine: {
|
|
||||||
show: true,
|
show: true,
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: "#DCE2E8",
|
color: "#f5f5f5",
|
||||||
},
|
type: "dashed"
|
||||||
},
|
}
|
||||||
splitLine: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
}
|
||||||
],
|
],
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
@@ -263,7 +287,7 @@ onMounted(async () => {
|
|||||||
type: "line",
|
type: "line",
|
||||||
data: publishCounts.value,
|
data: publishCounts.value,
|
||||||
symbolSize: 1,
|
symbolSize: 1,
|
||||||
yAxisIndex: 1,
|
yAxisIndex: 0,
|
||||||
symbol: "circle",
|
symbol: "circle",
|
||||||
smooth: true,
|
smooth: true,
|
||||||
showSymbol: false,
|
showSymbol: false,
|
||||||
@@ -272,7 +296,7 @@ onMounted(async () => {
|
|||||||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||||
{
|
{
|
||||||
offset: 0,
|
offset: 0,
|
||||||
color: "#fe9a",
|
color: "#fe9a8b",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
offset: 1,
|
offset: 1,
|
||||||
@@ -291,8 +315,7 @@ onMounted(async () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
chartInstance.setOption(option);
|
chartInstance.setOption(option);
|
||||||
|
}
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
getData();
|
getData();
|
||||||
@@ -308,12 +331,115 @@ onBeforeUnmount(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.user-center-dynamic {
|
.user-center-dynamic {
|
||||||
width: calc(100vw - 40px);
|
width: 100%;
|
||||||
height: calc(100vh - 290px);
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
flex-direction: column;
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 350px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-summary {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
.summary-item {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
font-family: "Font Awesome 5 Free";
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 18px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.visits {
|
||||||
|
background-color: #6366f1;
|
||||||
|
&::before { content: '\f201'; } /* chart line icon */
|
||||||
|
}
|
||||||
|
|
||||||
|
&.visitors {
|
||||||
|
background-color: #22c55e;
|
||||||
|
&::before { content: '\f007'; } /* user icon */
|
||||||
|
}
|
||||||
|
|
||||||
|
&.publishes {
|
||||||
|
background-color: #f97316;
|
||||||
|
&::before { content: '\f093'; } /* upload icon */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-content {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.summary-value {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.stats-summary {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
min-height: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -1,206 +1,613 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="user-center-home">
|
<div class="user-center-home">
|
||||||
<div class="user-center-home-left">
|
<!-- 数据卡片区域 -->
|
||||||
<div class="user-center-home-left-top" v-if="chartData">
|
<div class="stats-cards" v-if="chartData">
|
||||||
<div class="user-center-home-left-top-card"
|
<div class="stats-card" data-type="images">
|
||||||
style="background: linear-gradient(102.74deg, rgb(66, 230, 171) -7.03%, rgb(103, 235, 187) 97.7%);">
|
<div class="stats-card-icon">
|
||||||
<div class="user-center-home-left-top-card-top">
|
<AAvatar :size="50" shape="square" :src="imageIcon"/>
|
||||||
<div class="user-center-home-left-top-card-top-avatar">
|
|
||||||
<AAvatar :size="60" shape="square" :src="imageIcon"/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="user-center-home-left-top-card-top-name">
|
<div class="stats-card-content">
|
||||||
<span style="font-size: 2.8vh;color: rgba(255, 255, 255, 0.6); font-weight: bold;">图片总数</span>
|
<div class="stats-card-title">图片总数</div>
|
||||||
<span style="font-size: 3.8vh;font-weight: bold;color: #ffffff">{{ chartData.image_count }}</span>
|
<div class="stats-card-value">{{ chartData.image_count }}</div>
|
||||||
</div>
|
<div class="stats-card-footer">
|
||||||
</div>
|
<span class="stats-card-label">今日上传</span>
|
||||||
<div class="user-center-home-left-top-card-bottom">
|
<span class="stats-card-change">+{{ chartData.today_upload_count }}</span>
|
||||||
<span style="font-size: 2.3vh;color: rgba(255, 255, 255, 0.6); font-weight: bold;">今日上传</span>
|
|
||||||
<span style="font-size: 3vh;font-weight: bold;color: #ffffff">+{{ chartData.today_upload_count }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="user-center-home-left-top-card"
|
|
||||||
style="background: linear-gradient(101.63deg, rgb(82, 138, 250) -12.83%, rgb(122, 167, 255) 100%);">
|
|
||||||
<div class="user-center-home-left-top-card-top">
|
|
||||||
<div class="user-center-home-left-top-card-top-avatar">
|
|
||||||
<AAvatar :size="60" shape="square" :src="shareIcon"/>
|
|
||||||
</div>
|
|
||||||
<div class="user-center-home-left-top-card-top-name">
|
|
||||||
<span style="font-size: 2.8vh;color: rgba(255, 255, 255, 0.6); font-weight: bold;">分享总数</span>
|
|
||||||
<span style="font-size: 3.8vh;font-weight: bold;color: white">{{ chartData.share_count }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="user-center-home-left-top-card-bottom">
|
|
||||||
<span style="font-size: 2.3vh;color: rgba(255, 255, 255, 0.6); font-weight: bold;">今日上传</span>
|
|
||||||
<span style="font-size: 2.8vh;font-weight: bold;color: white">+{{ chartData.today_share_count }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="user-center-home-left-top-card"
|
|
||||||
style="background: linear-gradient(102.99deg, rgb(126, 92, 255) 3.18%, rgb(162, 139, 255) 102.52%);">
|
|
||||||
<div class="user-center-home-left-top-card-top">
|
|
||||||
<div class="user-center-home-left-top-card-top-avatar">
|
|
||||||
<AAvatar :size="60" shape="square" :src="fileSize"/>
|
|
||||||
</div>
|
|
||||||
<div class="user-center-home-left-top-card-top-name">
|
|
||||||
<span style="font-size: 2.8vh;color: rgba(255, 255, 255, 0.6); font-weight: bold;">文件总量</span>
|
|
||||||
<span
|
|
||||||
style="font-size: 3.8vh;font-weight: bold;color: white">{{
|
|
||||||
bytesToSize(chartData.file_size_count)
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="user-center-home-left-top-card-bottom">
|
|
||||||
<span style="font-size: 2.3vh;color: rgba(255, 255, 255, 0.6);font-weight: bold;">今日上传</span>
|
|
||||||
<span
|
|
||||||
style="font-size: 2.8vh;font-weight: bold;color: white">+{{
|
|
||||||
bytesToSize(chartData.today_file_size_count)
|
|
||||||
}}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-center-home-left-bottom" v-if="chartData">
|
|
||||||
<span style="font-size: 16px; font-weight: bold; margin-left: 20px;">文件上传热力图</span>
|
|
||||||
<HeatmapPro :contributions="chartData.heatmap"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="user-center-home-right">
|
|
||||||
<ACard class="user-center-home-right-card" :hoverable="false">
|
|
||||||
|
|
||||||
</ACard>
|
<div class="stats-card" data-type="shares">
|
||||||
|
<div class="stats-card-icon">
|
||||||
|
<AAvatar :size="50" shape="square" :src="shareIcon"/>
|
||||||
|
</div>
|
||||||
|
<div class="stats-card-content">
|
||||||
|
<div class="stats-card-title">分享总数</div>
|
||||||
|
<div class="stats-card-value">{{ chartData.share_count }}</div>
|
||||||
|
<div class="stats-card-footer">
|
||||||
|
<span class="stats-card-label">今日分享</span>
|
||||||
|
<span class="stats-card-change">+{{ chartData.today_share_count }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-card" data-type="storage">
|
||||||
|
<div class="stats-card-icon">
|
||||||
|
<AAvatar :size="50" shape="square" :src="fileSize"/>
|
||||||
|
</div>
|
||||||
|
<div class="stats-card-content">
|
||||||
|
<div class="stats-card-title">文件总量</div>
|
||||||
|
<div class="stats-card-value">{{ bytesToSize(chartData.file_size_count) }}</div>
|
||||||
|
<div class="stats-card-footer">
|
||||||
|
<span class="stats-card-label">今日上传</span>
|
||||||
|
<span class="stats-card-change">+{{ bytesToSize(chartData.today_file_size_count) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主要内容区域 -->
|
||||||
|
<div class="content-grid">
|
||||||
|
<!-- 月度上传图表区域 -->
|
||||||
|
<div class="monthly-chart-section" v-if="chartData">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>月度上传数量统计</h3>
|
||||||
|
</div>
|
||||||
|
<div class="section-body" ref="chartRef">
|
||||||
|
<!-- ECharts将在这里渲染 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 最近活动区域 -->
|
||||||
|
<div class="recent-activity-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>最近活动</h3>
|
||||||
|
<div class="section-actions">
|
||||||
|
<AButton type="link">查看全部</AButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section-body">
|
||||||
|
<div class="activity-list">
|
||||||
|
<div class="activity-item" v-for="(item, index) in recentActivities" :key="index">
|
||||||
|
<div class="activity-icon" :class="item.type"></div>
|
||||||
|
<div class="activity-content">
|
||||||
|
<div class="activity-title">{{ item.title }}</div>
|
||||||
|
<div class="activity-time">{{ item.time }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
import HeatmapPro from "@/components/HeatmapPro/HeatmapPro.vue";
|
|
||||||
import imageIcon from "@/assets/svgs/image-icon.svg";
|
import imageIcon from "@/assets/svgs/image-icon.svg";
|
||||||
import shareIcon from "@/assets/svgs/share-icon.svg";
|
import shareIcon from "@/assets/svgs/share-icon.svg";
|
||||||
import fileSize from "@/assets/svgs/file-size.svg";
|
import fileSize from "@/assets/svgs/file-size.svg";
|
||||||
import {getUserUploadInfoApi} from "@/api/storage";
|
import {getUserUploadInfoApi} from "@/api/storage";
|
||||||
import bytesToSize from "@/utils/imageUtils/bytesToSize";
|
import bytesToSize from "@/utils/imageUtils/bytesToSize";
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
|
||||||
const chartData = ref<any>();
|
const chartData = ref<any>();
|
||||||
|
|
||||||
|
// 模拟最近活动数据
|
||||||
|
const recentActivities = ref([
|
||||||
|
{
|
||||||
|
type: 'upload',
|
||||||
|
title: '上传了5张照片到「旅行相册」',
|
||||||
|
time: '今天 14:30'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'share',
|
||||||
|
title: '分享了相册「家庭聚会」',
|
||||||
|
time: '昨天 18:45'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'like',
|
||||||
|
title: '收藏了「风景摄影」',
|
||||||
|
time: '3天前'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'comment',
|
||||||
|
title: '评论了照片「日落」',
|
||||||
|
time: '上周'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'system',
|
||||||
|
title: '系统自动备份了您的照片',
|
||||||
|
time: '2周前'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const chartRef = ref<HTMLElement | null>(null);
|
||||||
|
let chartInstance: echarts.ECharts | null = null;
|
||||||
|
|
||||||
|
// 处理热力图数据,转换为月度上传数量
|
||||||
|
const processMonthlyData = (heatmapData: any[]) => {
|
||||||
|
if (!heatmapData || !Array.isArray(heatmapData)) return { months: [], counts: [] };
|
||||||
|
|
||||||
|
// 创建一个对象来存储每个月的上传数量
|
||||||
|
const monthlyUploads: Record<string, number> = {};
|
||||||
|
|
||||||
|
// 遍历热力图数据,按月份汇总上传数量
|
||||||
|
heatmapData.forEach(item => {
|
||||||
|
if (item && item.date) {
|
||||||
|
// 从日期中提取年月,格式为YYYY-MM
|
||||||
|
const date = new Date(item.date);
|
||||||
|
const yearMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
||||||
|
|
||||||
|
// 累加该月份的上传数量
|
||||||
|
if (!monthlyUploads[yearMonth]) {
|
||||||
|
monthlyUploads[yearMonth] = 0;
|
||||||
|
}
|
||||||
|
monthlyUploads[yearMonth] += item.count || 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取所有月份并排序
|
||||||
|
const sortedMonths = Object.keys(monthlyUploads).sort();
|
||||||
|
|
||||||
|
// 提取排序后的月份和对应的上传数量
|
||||||
|
const months = sortedMonths.map(month => {
|
||||||
|
// 将YYYY-MM格式转换为更友好的显示格式,如"2023年1月"
|
||||||
|
const [year, month2] = month.split('-');
|
||||||
|
return `${year}年${parseInt(month2)}月`;
|
||||||
|
});
|
||||||
|
const counts = sortedMonths.map(month => monthlyUploads[month]);
|
||||||
|
|
||||||
|
return { months, counts };
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化图表
|
||||||
|
const initChart = () => {
|
||||||
|
if (!chartRef.value) return;
|
||||||
|
|
||||||
|
// 如果图表实例已存在,销毁它
|
||||||
|
if (chartInstance) {
|
||||||
|
chartInstance.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新的图表实例
|
||||||
|
chartInstance = echarts.init(chartRef.value);
|
||||||
|
|
||||||
|
// 使用防抖函数处理resize事件
|
||||||
|
const resizeHandler = () => {
|
||||||
|
if (chartInstance) {
|
||||||
|
nextTick(() => {
|
||||||
|
chartInstance.resize();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听窗口大小变化,调整图表大小
|
||||||
|
window.addEventListener('resize', resizeHandler);
|
||||||
|
|
||||||
|
// 更新图表配置
|
||||||
|
updateChartOption();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新图表配置
|
||||||
|
const updateChartOption = () => {
|
||||||
|
if (!chartInstance || !chartData.value || !chartData.value.heatmap) return;
|
||||||
|
|
||||||
|
// 处理数据,获取月度上传数量
|
||||||
|
const { months, counts } = processMonthlyData(chartData.value.heatmap);
|
||||||
|
|
||||||
|
// 设置图表配置
|
||||||
|
const option = {
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
title: {
|
||||||
|
text: "月度上传数量统计",
|
||||||
|
textStyle: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: '#333'
|
||||||
|
},
|
||||||
|
left: "center",
|
||||||
|
top: "5%",
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: "axis",
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
borderRadius: 4,
|
||||||
|
shadowColor: 'rgba(0, 0, 0, 0.1)',
|
||||||
|
shadowBlur: 10,
|
||||||
|
textStyle: {
|
||||||
|
color: "#333",
|
||||||
|
},
|
||||||
|
padding: [10, 15],
|
||||||
|
formatter: '{b}: {c} 次上传'
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
top: "15%",
|
||||||
|
left: "3%",
|
||||||
|
right: "3%",
|
||||||
|
bottom: "12%",
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: [
|
||||||
|
{
|
||||||
|
type: "category",
|
||||||
|
data: months,
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: "#e0e0e0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
interval: 0,
|
||||||
|
color: "#666",
|
||||||
|
fontSize: 12,
|
||||||
|
margin: 15,
|
||||||
|
rotate: months.length > 6 ? 45 : 0
|
||||||
|
},
|
||||||
|
boundaryGap: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: "value",
|
||||||
|
name: "上传数量",
|
||||||
|
nameTextStyle: {
|
||||||
|
color: "#666",
|
||||||
|
fontSize: 12,
|
||||||
|
padding: [0, 0, 0, 5]
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: "#e0e0e0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: "#666",
|
||||||
|
fontSize: 12
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: "#f5f5f5",
|
||||||
|
type: "dashed"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "上传数量",
|
||||||
|
type: "bar",
|
||||||
|
barWidth: '40%',
|
||||||
|
data: counts,
|
||||||
|
itemStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: '#42e6ab' },
|
||||||
|
{ offset: 1, color: '#67ebbb' }
|
||||||
|
])
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: '#3ad598' },
|
||||||
|
{ offset: 1, color: '#5ae9a9' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// 应用配置
|
||||||
|
chartInstance.setOption(option);
|
||||||
|
};
|
||||||
|
|
||||||
async function getData() {
|
async function getData() {
|
||||||
const res: any = await getUserUploadInfoApi();
|
const res: any = await getUserUploadInfoApi();
|
||||||
if (res && res.code === 200) {
|
if (res && res.code === 200) {
|
||||||
chartData.value = res.data;
|
chartData.value = res.data;
|
||||||
|
// 数据加载完成后初始化图表
|
||||||
|
nextTick(() => {
|
||||||
|
initChart();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getData();
|
getData();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 组件卸载时清理图表实例和事件监听
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (chartInstance) {
|
||||||
|
chartInstance.dispose();
|
||||||
|
chartInstance = null;
|
||||||
|
}
|
||||||
|
window.removeEventListener('resize', () => {});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.user-center-home {
|
.user-center-home {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 40px;
|
|
||||||
|
|
||||||
.user-center-home-left {
|
|
||||||
width: 60%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
gap: 24px;
|
||||||
|
|
||||||
|
/* 数据卡片区域 */
|
||||||
|
.stats-cards {
|
||||||
|
display: flex;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
|
||||||
.user-center-home-left-top {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 25vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
|
.stats-card {
|
||||||
.user-center-home-left-top-card {
|
flex: 1;
|
||||||
width: 28%;
|
background-color: white;
|
||||||
height: 90%;
|
border-radius: 12px;
|
||||||
background-color: #fff;
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
border-radius: 1.8vh;
|
padding: 20px;
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
overflow: hidden;
|
||||||
align-items: flex-start;
|
position: relative;
|
||||||
justify-content: flex-start;
|
transition: all 0.3s ease;
|
||||||
padding-inline: 10px;
|
|
||||||
padding-block: 10px;
|
|
||||||
gap: 10px;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-center-home-left-top-card-top {
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 60%;
|
height: 100%;
|
||||||
|
opacity: 0.8;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-type="images"]::before {
|
||||||
|
background: linear-gradient(135deg, rgba(66, 230, 171, 0.1) 0%, rgba(103, 235, 187, 0.1) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-type="shares"]::before {
|
||||||
|
background: linear-gradient(135deg, rgba(82, 138, 250, 0.1) 0%, rgba(122, 167, 255, 0.1) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-type="storage"]::before {
|
||||||
|
background: linear-gradient(135deg, rgba(126, 92, 255, 0.1) 0%, rgba(162, 139, 255, 0.1) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card-icon {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
margin-right: 16px;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
left: -15px;
|
||||||
|
top: -15px;
|
||||||
|
z-index: -1;
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-type="images"] .stats-card-icon::after {
|
||||||
|
background-color: #42e6ab;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-type="shares"] .stats-card-icon::after {
|
||||||
|
background-color: #528afa;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-type="storage"] .stats-card-icon::after {
|
||||||
|
background-color: #7e5cff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card-content {
|
||||||
|
flex: 1;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
.stats-card-title {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
align-items: center;
|
||||||
align-items: flex-start;
|
gap: 8px;
|
||||||
justify-content: flex-start;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
.user-center-home-left-top-card-top-avatar {
|
.stats-card-label {
|
||||||
width: 60px;
|
font-size: 13px;
|
||||||
height: 60px;
|
color: #888;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-center-home-left-top-card-top-name {
|
.stats-card-change {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #42e6ab;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容网格区域 */
|
||||||
|
.content-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 1fr;
|
||||||
|
gap: 24px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 60px;
|
flex: 1;
|
||||||
|
|
||||||
|
/* 通用区块样式 */
|
||||||
|
.monthly-chart-section,
|
||||||
|
.recent-activity-section {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
height: 100%;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-center-home-left-top-card-bottom {
|
|
||||||
width: 100%;
|
|
||||||
height: 40%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
margin-bottom: 16px;
|
||||||
gap: 10px;
|
padding-bottom: 12px;
|
||||||
}
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-center-home-left-bottom {
|
.section-body {
|
||||||
width: 100%;
|
flex: 1;
|
||||||
height: 58%;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 月度上传图表区域特定样式 */
|
||||||
|
.monthly-chart-section {
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.section-body {
|
||||||
|
height: 300px; /* 设置固定高度以确保图表正确渲染 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 最近活动区域特定样式 */
|
||||||
|
.recent-activity-section {
|
||||||
|
.activity-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
gap: 16px;
|
||||||
justify-content: flex-start;
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 1.8vh;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
padding-block: 20px;
|
|
||||||
|
|
||||||
}
|
.activity-item {
|
||||||
}
|
|
||||||
|
|
||||||
.user-center-home-right {
|
|
||||||
width: 39%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #f5f5f5;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
.user-center-home-right-card {
|
.activity-icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
font-family: "Font Awesome 5 Free";
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 16px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.upload {
|
||||||
|
background-color: #42e6ab;
|
||||||
|
&::before { content: '\f093'; } /* upload icon */
|
||||||
|
}
|
||||||
|
|
||||||
|
&.share {
|
||||||
|
background-color: #528afa;
|
||||||
|
&::before { content: '\f064'; } /* share icon */
|
||||||
|
}
|
||||||
|
|
||||||
|
&.like {
|
||||||
|
background-color: #ff6b6b;
|
||||||
|
&::before { content: '\f004'; } /* heart icon */
|
||||||
|
}
|
||||||
|
|
||||||
|
&.comment {
|
||||||
|
background-color: #ffc107;
|
||||||
|
&::before { content: '\f075'; } /* comment icon */
|
||||||
|
}
|
||||||
|
|
||||||
|
&.system {
|
||||||
|
background-color: #6c757d;
|
||||||
|
&::before { content: '\f013'; } /* gear icon */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-content {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.activity-title {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.stats-cards {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.stats-card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
}
|
||||||
background-color: #fff;
|
}
|
||||||
//margin-top: 20px;
|
|
||||||
|
.content-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.stats-cards {
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-grid {
|
||||||
|
gap: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,15 +5,55 @@
|
|||||||
<p class="setting-description">您可以在这里管理您的账户设置和偏好</p>
|
<p class="setting-description">您可以在这里管理您的账户设置和偏好</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="setting-section">
|
<div class="settings-container">
|
||||||
|
<!-- 左侧导航 -->
|
||||||
|
<div class="settings-nav">
|
||||||
|
<div
|
||||||
|
class="nav-item"
|
||||||
|
:class="{ active: activeSection === 'account' }"
|
||||||
|
@click="activeSection = 'account'"
|
||||||
|
>
|
||||||
|
<div class="nav-icon account"></div>
|
||||||
|
<span>账户安全</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="nav-item"
|
||||||
|
:class="{ active: activeSection === 'privacy' }"
|
||||||
|
@click="activeSection = 'privacy'"
|
||||||
|
>
|
||||||
|
<div class="nav-icon privacy"></div>
|
||||||
|
<span>隐私设置</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="nav-item"
|
||||||
|
:class="{ active: activeSection === 'notification' }"
|
||||||
|
@click="activeSection = 'notification'"
|
||||||
|
>
|
||||||
|
<div class="nav-icon notification"></div>
|
||||||
|
<span>通知设置</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="nav-item"
|
||||||
|
:class="{ active: activeSection === 'appearance' }"
|
||||||
|
@click="activeSection = 'appearance'"
|
||||||
|
>
|
||||||
|
<div class="nav-icon appearance"></div>
|
||||||
|
<span>外观设置</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧内容区 -->
|
||||||
|
<div class="settings-content">
|
||||||
|
<!-- 账户安全设置 -->
|
||||||
|
<div v-if="activeSection === 'account'" class="settings-section">
|
||||||
<div class="section-title">账户安全</div>
|
<div class="section-title">账户安全</div>
|
||||||
<div class="user-center-setting-content">
|
<div class="settings-list">
|
||||||
<div class="user-center-setting-item">
|
<div class="setting-item">
|
||||||
<div class="setting-item-left">
|
<div class="setting-item-left">
|
||||||
<img src="@/assets/svgs/account_security.svg" alt="AI识别" class="setting-icon" />
|
<div class="setting-icon account-ai"></div>
|
||||||
<div class="setting-text">
|
<div class="setting-text">
|
||||||
<span class="user-center-setting-item-title">开启AI识别</span>
|
<span class="setting-item-title">开启AI识别</span>
|
||||||
<span class="setting-description">允许系统使用AI技术识别您的照片内容</span>
|
<span class="setting-item-desc">允许系统使用AI技术识别您的照片内容</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ASwitch v-model:checked="userStore.settings.enableAI">
|
<ASwitch v-model:checked="userStore.settings.enableAI">
|
||||||
@@ -26,12 +66,12 @@
|
|||||||
</ASwitch>
|
</ASwitch>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="user-center-setting-item">
|
<div class="setting-item">
|
||||||
<div class="setting-item-left">
|
<div class="setting-item-left">
|
||||||
<img src="@/assets/svgs/login_security.svg" alt="手机上传" class="setting-icon" />
|
<div class="setting-icon account-mobile"></div>
|
||||||
<div class="setting-text">
|
<div class="setting-text">
|
||||||
<span class="user-center-setting-item-title">开启手机上传</span>
|
<span class="setting-item-title">开启手机上传</span>
|
||||||
<span class="setting-description">允许从移动设备上传照片到您的相册</span>
|
<span class="setting-item-desc">允许从移动设备上传照片到您的相册</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ASwitch v-model:checked="userStore.settings.enableMobileUpload">
|
<ASwitch v-model:checked="userStore.settings.enableMobileUpload">
|
||||||
@@ -43,18 +83,30 @@
|
|||||||
</template>
|
</template>
|
||||||
</ASwitch>
|
</ASwitch>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-item-left">
|
||||||
|
<div class="setting-icon account-password"></div>
|
||||||
|
<div class="setting-text">
|
||||||
|
<span class="setting-item-title">修改密码</span>
|
||||||
|
<span class="setting-item-desc">定期更改密码可以提高账户安全性</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<AButton type="primary" size="small">修改</AButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="setting-section">
|
<!-- 隐私设置 -->
|
||||||
|
<div v-if="activeSection === 'privacy'" class="settings-section">
|
||||||
<div class="section-title">隐私设置</div>
|
<div class="section-title">隐私设置</div>
|
||||||
<div class="user-center-setting-content">
|
<div class="settings-list">
|
||||||
<div class="user-center-setting-item">
|
<div class="setting-item">
|
||||||
<div class="setting-item-left">
|
<div class="setting-item-left">
|
||||||
<img src="@/assets/svgs/privacy.svg" alt="个人资料" class="setting-icon" />
|
<div class="setting-icon privacy-profile"></div>
|
||||||
<div class="setting-text">
|
<div class="setting-text">
|
||||||
<span class="user-center-setting-item-title">公开个人资料</span>
|
<span class="setting-item-title">公开个人资料</span>
|
||||||
<span class="setting-description">允许其他用户查看您的个人资料信息</span>
|
<span class="setting-item-desc">允许其他用户查看您的个人资料信息</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ASwitch v-model:checked="userStore.settings.publicProfile">
|
<ASwitch v-model:checked="userStore.settings.publicProfile">
|
||||||
@@ -67,12 +119,12 @@
|
|||||||
</ASwitch>
|
</ASwitch>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="user-center-setting-item">
|
<div class="setting-item">
|
||||||
<div class="setting-item-left">
|
<div class="setting-item-left">
|
||||||
<img src="@/assets/svgs/community.svg" alt="评论" class="setting-icon" />
|
<div class="setting-icon privacy-comment"></div>
|
||||||
<div class="setting-text">
|
<div class="setting-text">
|
||||||
<span class="user-center-setting-item-title">开启评论功能</span>
|
<span class="setting-item-title">开启评论功能</span>
|
||||||
<span class="setting-description">允许其他用户在您的照片下方评论</span>
|
<span class="setting-item-desc">允许其他用户在您的照片下方评论</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ASwitch v-model:checked="userStore.settings.enableComment">
|
<ASwitch v-model:checked="userStore.settings.enableComment">
|
||||||
@@ -85,12 +137,12 @@
|
|||||||
</ASwitch>
|
</ASwitch>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="user-center-setting-item">
|
<div class="setting-item">
|
||||||
<div class="setting-item-left">
|
<div class="setting-item-left">
|
||||||
<img src="@/assets/svgs/search.svg" alt="搜索记录" class="setting-icon" />
|
<div class="setting-icon privacy-search"></div>
|
||||||
<div class="setting-text">
|
<div class="setting-text">
|
||||||
<span class="user-center-setting-item-title">保存搜索记录</span>
|
<span class="setting-item-title">保存搜索记录</span>
|
||||||
<span class="setting-description">保存您的搜索历史以提供更好的推荐</span>
|
<span class="setting-item-desc">保存您的搜索历史以提供更好的推荐</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ASwitch v-model:checked="userStore.settings.saveSearchHistory">
|
<ASwitch v-model:checked="userStore.settings.saveSearchHistory">
|
||||||
@@ -104,22 +156,224 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 通知设置 -->
|
||||||
|
<div v-if="activeSection === 'notification'" class="settings-section">
|
||||||
|
<div class="section-title">通知设置</div>
|
||||||
|
<div class="settings-list">
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-item-left">
|
||||||
|
<div class="setting-icon notification-email"></div>
|
||||||
|
<div class="setting-text">
|
||||||
|
<span class="setting-item-title">邮件通知</span>
|
||||||
|
<span class="setting-item-desc">接收重要活动和更新的邮件通知</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ASwitch v-model:checked="notificationSettings.email">
|
||||||
|
<template #checkedChildren>
|
||||||
|
<check-outlined />
|
||||||
|
</template>
|
||||||
|
<template #unCheckedChildren>
|
||||||
|
<close-outlined />
|
||||||
|
</template>
|
||||||
|
</ASwitch>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-item-left">
|
||||||
|
<div class="setting-icon notification-comment"></div>
|
||||||
|
<div class="setting-text">
|
||||||
|
<span class="setting-item-title">评论通知</span>
|
||||||
|
<span class="setting-item-desc">当有人评论您的照片时接收通知</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ASwitch v-model:checked="notificationSettings.comment">
|
||||||
|
<template #checkedChildren>
|
||||||
|
<check-outlined />
|
||||||
|
</template>
|
||||||
|
<template #unCheckedChildren>
|
||||||
|
<close-outlined />
|
||||||
|
</template>
|
||||||
|
</ASwitch>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-item-left">
|
||||||
|
<div class="setting-icon notification-system"></div>
|
||||||
|
<div class="setting-text">
|
||||||
|
<span class="setting-item-title">系统通知</span>
|
||||||
|
<span class="setting-item-desc">接收系统更新和维护信息</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ASwitch v-model:checked="notificationSettings.system">
|
||||||
|
<template #checkedChildren>
|
||||||
|
<check-outlined />
|
||||||
|
</template>
|
||||||
|
<template #unCheckedChildren>
|
||||||
|
<close-outlined />
|
||||||
|
</template>
|
||||||
|
</ASwitch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 外观设置 -->
|
||||||
|
<div v-if="activeSection === 'appearance'" class="settings-section">
|
||||||
|
<div class="section-title">外观设置</div>
|
||||||
|
<div class="settings-list">
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-item-left">
|
||||||
|
<div class="setting-icon appearance-theme"></div>
|
||||||
|
<div class="setting-text">
|
||||||
|
<span class="setting-item-title">主题模式</span>
|
||||||
|
<span class="setting-item-desc">选择您喜欢的界面主题</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ARadioGroup v-model:value="appearanceSettings.theme">
|
||||||
|
<ARadioButton value="light">浅色</ARadioButton>
|
||||||
|
<ARadioButton value="dark">深色</ARadioButton>
|
||||||
|
<ARadioButton value="system">跟随系统</ARadioButton>
|
||||||
|
</ARadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-item-left">
|
||||||
|
<div class="setting-icon appearance-layout"></div>
|
||||||
|
<div class="setting-text">
|
||||||
|
<span class="setting-item-title">布局密度</span>
|
||||||
|
<span class="setting-item-desc">调整界面元素的紧凑程度</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ASelect v-model:value="appearanceSettings.density" style="width: 120px">
|
||||||
|
<ASelectOption value="compact">紧凑</ASelectOption>
|
||||||
|
<ASelectOption value="default">默认</ASelectOption>
|
||||||
|
<ASelectOption value="comfortable">宽松</ASelectOption>
|
||||||
|
</ASelect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-actions">
|
||||||
|
<AButton type="primary" @click="saveSettings">保存设置</AButton>
|
||||||
|
<AButton @click="resetSettings">重置</AButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
import { CheckOutlined, CloseOutlined } from '@ant-design/icons-vue';
|
import { CheckOutlined, CloseOutlined } from '@ant-design/icons-vue';
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { message as AMessage } from 'ant-design-vue';
|
||||||
|
|
||||||
const userStore = useStore().user;
|
const userStore = useStore().user;
|
||||||
|
const activeSection = ref('account');
|
||||||
|
|
||||||
|
// 通知设置(模拟数据)
|
||||||
|
const notificationSettings = ref({
|
||||||
|
email: true,
|
||||||
|
comment: true,
|
||||||
|
system: false
|
||||||
|
});
|
||||||
|
|
||||||
// const saveSettings = () => {
|
// 外观设置(模拟数据)
|
||||||
// // 保存设置到后端
|
const appearanceSettings = ref({
|
||||||
// };
|
theme: 'light',
|
||||||
|
density: 'default'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保存设置
|
||||||
|
const saveSettings = () => {
|
||||||
|
// 这里可以添加保存到后端的逻辑
|
||||||
|
// 模拟保存成功
|
||||||
|
AMessage.success('设置已保存');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置设置
|
||||||
|
const resetSettings = () => {
|
||||||
|
// 重置为默认值
|
||||||
|
notificationSettings.value = {
|
||||||
|
email: true,
|
||||||
|
comment: true,
|
||||||
|
system: false
|
||||||
|
};
|
||||||
|
|
||||||
|
appearanceSettings.value = {
|
||||||
|
theme: 'light',
|
||||||
|
density: 'default'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 模拟重置成功
|
||||||
|
AMessage.success('设置已重置');
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
/* 图标样式 */
|
||||||
|
.setting-icon {
|
||||||
|
&.account-ai {
|
||||||
|
background-image: url('@/assets/svgs/ai-icon.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.account-mobile {
|
||||||
|
background-image: url('@/assets/svgs/qr-phone.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.account-password {
|
||||||
|
background-image: url('@/assets/svgs/lock.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.privacy-profile {
|
||||||
|
background-image: url('@/assets/svgs/personal-center.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.privacy-comment {
|
||||||
|
background-image: url('@/assets/svgs/community.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.privacy-search {
|
||||||
|
background-image: url('@/assets/svgs/search.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.notification-email {
|
||||||
|
background-image: url('@/assets/svgs/email_security.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.notification-comment {
|
||||||
|
background-image: url('@/assets/svgs/community.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.notification-system {
|
||||||
|
background-image: url('@/assets/svgs/setting.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.appearance-theme {
|
||||||
|
background-image: url('@/assets/svgs/thing-album.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.appearance-layout {
|
||||||
|
background-image: url('@/assets/svgs/thing-album.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
&.account {
|
||||||
|
background-image: url('@/assets/svgs/account_security.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.privacy {
|
||||||
|
background-image: url('@/assets/svgs/privacy.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.notification {
|
||||||
|
background-image: url('@/assets/svgs/notice.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.appearance {
|
||||||
|
background-image: url('@/assets/svgs/thing-album.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
.user-center-setting {
|
.user-center-setting {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -145,10 +399,66 @@ const userStore = useStore().user;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-section {
|
.settings-container {
|
||||||
width: calc(100% - 80px);
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
gap: 30px;
|
||||||
|
|
||||||
|
.settings-nav {
|
||||||
|
width: 200px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(99, 102, 241, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: rgba(99, 102, 241, 0.1);
|
||||||
|
border-left-color: #6366f1;
|
||||||
|
color: #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin-right: 12px;
|
||||||
|
background-size: contain;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active .nav-icon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section {
|
||||||
|
width: calc(100% - 50px);
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
background-color: var(--white-color, #fff);
|
background-color: #fff;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
@@ -162,26 +472,21 @@ const userStore = useStore().user;
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
color: var(--text-color, #333);
|
color: #333;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.user-center-setting-content {
|
.settings-list {
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
|
||||||
.user-center-setting-item {
|
.setting-item {
|
||||||
width: 100%;
|
|
||||||
min-height: 60px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 10px 0;
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.03);
|
border-bottom: 1px solid rgba(0, 0, 0, 0.03);
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
@@ -191,58 +496,91 @@ const userStore = useStore().user;
|
|||||||
.setting-item-left {
|
.setting-item-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 15px;
|
||||||
|
|
||||||
.setting-icon {
|
.setting-icon {
|
||||||
width: 28px;
|
width: 36px;
|
||||||
height: 28px;
|
height: 36px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: rgba(99, 102, 241, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #6366f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-text {
|
.setting-text {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.user-center-setting-item-title {
|
.setting-item-title {
|
||||||
display: inline-block;
|
font-size: 15px;
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 22px;
|
color: #333;
|
||||||
color: var(--text-color, #333);
|
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-description {
|
.setting-item-desc {
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
color: #999;
|
color: #666;
|
||||||
line-height: 1.5;
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.ant-switch) {
|
.settings-actions {
|
||||||
background-color: rgba(0, 0, 0, 0.25);
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
&.ant-switch-checked {
|
gap: 12px;
|
||||||
background-color: var(--blue, rgba(96,165,250,.9));
|
margin-top: 20px;
|
||||||
}
|
width: 100%;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
||||||
.setting-section {
|
.settings-container {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.settings-nav {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 12px 8px;
|
||||||
|
border-left: none;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-left-color: transparent;
|
||||||
|
border-bottom-color: #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-center-setting-item {
|
.setting-item {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|
||||||
.setting-item-left {
|
.setting-item-left {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -64,6 +64,9 @@ export default defineConfig(({mode}: { mode: string }): object => {
|
|||||||
force: false,
|
force: false,
|
||||||
needsInterop: [".vite"],
|
needsInterop: [".vite"],
|
||||||
},
|
},
|
||||||
|
worker: {
|
||||||
|
format: 'es'
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
vitePluginBundleObfuscator(defaultObfuscatorConfig),
|
vitePluginBundleObfuscator(defaultObfuscatorConfig),
|
||||||
@@ -177,6 +180,7 @@ export default defineConfig(({mode}: { mode: string }): object => {
|
|||||||
watch: null, // 设置为 {} 则会启用 rollup 的监听器
|
watch: null, // 设置为 {} 则会启用 rollup 的监听器
|
||||||
rollupOptions: { // 自定义底层的 Rollup 打包配置
|
rollupOptions: { // 自定义底层的 Rollup 打包配置
|
||||||
output: {
|
output: {
|
||||||
|
format: 'es',
|
||||||
chunkFileNames: 'js/[name]-[hash].js', // 引入文件名的名称
|
chunkFileNames: 'js/[name]-[hash].js', // 引入文件名的名称
|
||||||
entryFileNames: 'js/[name]-[hash].js', // 包的入口文件名称
|
entryFileNames: 'js/[name]-[hash].js', // 包的入口文件名称
|
||||||
assetFileNames: '[ext]/[name]-[hash].[ext]',// 资源文件像 字体,图片等
|
assetFileNames: '[ext]/[name]-[hash].[ext]',// 资源文件像 字体,图片等
|
||||||
|
Reference in New Issue
Block a user