💄 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']
|
||||
AButton: typeof import('ant-design-vue/es')['Button']
|
||||
ACard: typeof import('ant-design-vue/es')['Card']
|
||||
ACardMeta: typeof import('ant-design-vue/es')['CardMeta']
|
||||
ACascader: typeof import('ant-design-vue/es')['Cascader']
|
||||
AccountSetting: typeof import('./src/views/User/AccountSetting/AccountSetting.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']
|
||||
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']
|
||||
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']
|
||||
ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
|
||||
ACol: typeof import('ant-design-vue/es')['Col']
|
||||
@@ -67,7 +68,6 @@ declare module 'vue' {
|
||||
ASelect: typeof import('ant-design-vue/es')['Select']
|
||||
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
||||
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
|
||||
ASlider: typeof import('ant-design-vue/es')['Slider']
|
||||
ASpace: typeof import('ant-design-vue/es')['Space']
|
||||
ASpin: typeof import('ant-design-vue/es')['Spin']
|
||||
AStatistic: typeof import('ant-design-vue/es')['Statistic']
|
||||
@@ -78,9 +78,10 @@ declare module 'vue' {
|
||||
ATabs: typeof import('ant-design-vue/es')['Tabs']
|
||||
ATag: typeof import('ant-design-vue/es')['Tag']
|
||||
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']
|
||||
ATree: typeof import('ant-design-vue/es')['Tree']
|
||||
ATreeSelect: typeof import('ant-design-vue/es')['TreeSelect']
|
||||
ATypography: typeof import('ant-design-vue/es')['Typography']
|
||||
ATypographyParagraph: typeof import('ant-design-vue/es')['TypographyParagraph']
|
||||
AUpload: typeof import('ant-design-vue/es')['Upload']
|
||||
@@ -94,6 +95,7 @@ declare module 'vue' {
|
||||
CheckCard: typeof import('./src/components/CheckCard/CheckCard.vue')['default']
|
||||
CheckCircleOutlined: typeof import('@ant-design/icons-vue')['CheckCircleOutlined']
|
||||
CheckOutlined: typeof import('@ant-design/icons-vue')['CheckOutlined']
|
||||
ClockCircleOutlined: typeof import('@ant-design/icons-vue')['ClockCircleOutlined']
|
||||
CloseCircleOutlined: typeof import('@ant-design/icons-vue')['CloseCircleOutlined']
|
||||
CloseOutlined: typeof import('@ant-design/icons-vue')['CloseOutlined']
|
||||
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']
|
||||
CompareImage: typeof import('./src/views/Upscale/CompareImage.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']
|
||||
DownloadOutlined: typeof import('@ant-design/icons-vue')['DownloadOutlined']
|
||||
DownOutlined: typeof import('@ant-design/icons-vue')['DownOutlined']
|
||||
DynamicTitle: typeof import('./src/components/DynamicTitle/DynamicTitle.vue')['default']
|
||||
EditOutlined: typeof import('@ant-design/icons-vue')['EditOutlined']
|
||||
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']
|
||||
EyeOutlined: typeof import('@ant-design/icons-vue')['EyeOutlined']
|
||||
FileImageOutlined: typeof import('@ant-design/icons-vue')['FileImageOutlined']
|
||||
FilerobotImageEditor: typeof import('./src/components/FilerobotImageEditor/FilerobotImageEditor.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']
|
||||
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']
|
||||
ImageBed: typeof import('./src/views/ImageBed/index.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']
|
||||
ImageToolbar: typeof import('./src/components/ImageToolbar/ImageToolbar.vue')['default']
|
||||
ImageUpload: typeof import('./src/components/ImageUpload/ImageUpload.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']
|
||||
InfoCircleOutlined: typeof import('@ant-design/icons-vue')['InfoCircleOutlined']
|
||||
LandingPage: typeof import('./src/views/Landing/LandingPage.vue')['default']
|
||||
LeftOutlined: typeof import('@ant-design/icons-vue')['LeftOutlined']
|
||||
LinkOutlined: typeof import('@ant-design/icons-vue')['LinkOutlined']
|
||||
@@ -147,7 +148,6 @@ declare module 'vue' {
|
||||
LogoutOutlined: typeof import('@ant-design/icons-vue')['LogoutOutlined']
|
||||
MainPage: typeof import('./src/views/Main/MainPage.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']
|
||||
OrderedListOutlined: typeof import('@ant-design/icons-vue')['OrderedListOutlined']
|
||||
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']
|
||||
PreviewBlurDetect: typeof import('./src/views/Preview/PreviewBlurDetect/PreviewBlurDetect.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']
|
||||
PrivacySpace: typeof import('./src/views/Photograph/PrivacySpace/PrivacySpace.vue')['default']
|
||||
QrcodeOutlined: typeof import('@ant-design/icons-vue')['QrcodeOutlined']
|
||||
@@ -200,12 +201,14 @@ declare module 'vue' {
|
||||
StarButton: typeof import('./src/components/StarButton/StarButton.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']
|
||||
System: typeof import('./src/views/Admin/System/index.vue')['default']
|
||||
SystemHeader: typeof import('./src/views/Admin/System/Components/SystemHeader.vue')['default']
|
||||
SystemLogs: typeof import('./src/views/Admin/System/Pages/SystemLogs.vue')['default']
|
||||
SystemSidebar: typeof import('./src/views/Admin/System/Components/SystemSidebar.vue')['default']
|
||||
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']
|
||||
ThingAlbumIndex: typeof import('./src/views/Album/ThingAlbum/ThingAlbumIndex.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']
|
||||
UserOutlined: typeof import('@ant-design/icons-vue')['UserOutlined']
|
||||
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']
|
||||
}
|
||||
}
|
||||
|
@@ -12,6 +12,8 @@ server {
|
||||
proxy_set_header Connection "upgrade";
|
||||
access_log /var/log/nginx/host.access.log main;
|
||||
error_log /var/log/nginx/error.log error;
|
||||
client_max_body_size 120M;
|
||||
client_body_buffer_size 10m;
|
||||
|
||||
|
||||
location / {
|
||||
@@ -39,6 +41,8 @@ server {
|
||||
proxy_send_timeout 3600s; # 设置为1小时
|
||||
send_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;
|
||||
|
14
package.json
14
package.json
@@ -33,8 +33,8 @@
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/json-stringify-safe": "^5.0.3",
|
||||
"@types/leaflet": "^1.9.16",
|
||||
"@types/node": "^22.13.11",
|
||||
"@types/leaflet": "^1.9.17",
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@vladmandic/face-api": "^1.7.15",
|
||||
"@vuepic/vue-datepicker": "^11.0.2",
|
||||
@@ -66,12 +66,12 @@
|
||||
"nsfwjs": "^4.2.1",
|
||||
"opencv-qr": "^0.7.0",
|
||||
"pinia": "^3.0.1",
|
||||
"pinia-plugin-persistedstate-2": "^2.0.29",
|
||||
"pinia-plugin-persistedstate-2": "^2.0.30",
|
||||
"qr-scanner-wechat": "^0.1.3",
|
||||
"rimraf": "^6.0.1",
|
||||
"seedrandom": "^3.0.5",
|
||||
"swiper": "^11.2.6",
|
||||
"unplugin-auto-import": "^19.1.1",
|
||||
"unplugin-auto-import": "^19.1.2",
|
||||
"upscaler": "^1.0.0-beta.19",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-html": "^3.2.2",
|
||||
@@ -91,16 +91,16 @@
|
||||
"globals": "^16.0.0",
|
||||
"sass": "^1.86.0",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript-eslint": "^8.27.0",
|
||||
"typescript-eslint": "^8.28.0",
|
||||
"unplugin-vue-components": "^28.4.1",
|
||||
"vite": "^6.2.2",
|
||||
"vite": "^6.2.3",
|
||||
"vite-plugin-bundle-obfuscator": "1.4.2",
|
||||
"vite-plugin-chunk-split": "^0.5.0",
|
||||
"vue-tsc": "2.2.8"
|
||||
},
|
||||
"overrides": {
|
||||
"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 sourceBucket 源存储桶
|
||||
* @param targetProvider 目标存储商
|
||||
* @param targetBucket 目标存储桶
|
||||
* @param sourceStorage 源存储商
|
||||
* @param targetStorage 目标存储商
|
||||
*/
|
||||
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', {
|
||||
source_provider: sourceProvider,
|
||||
source_bucket: sourceBucket,
|
||||
target_provider: targetProvider,
|
||||
target_bucket: targetBucket,
|
||||
source_storage: sourceStorage,
|
||||
target_storage: targetStorage,
|
||||
|
||||
}, {
|
||||
meta: {
|
||||
ignoreToken: false,
|
||||
@@ -52,4 +49,4 @@ export const cancelBackupTaskApi = (taskId: string) => {
|
||||
},
|
||||
name: "cancel-backup-task",
|
||||
});
|
||||
};
|
||||
};
|
||||
|
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>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, onMounted, onBeforeUnmount} from 'vue';
|
||||
import {ref, computed, onMounted, onBeforeUnmount, nextTick} from 'vue';
|
||||
import {
|
||||
format,
|
||||
startOfWeek,
|
||||
@@ -73,7 +73,6 @@ import {
|
||||
parseISO,
|
||||
getYear
|
||||
} from 'date-fns';
|
||||
import {debounce} from 'lodash-es';
|
||||
import type {PropType} from 'vue';
|
||||
import {zhCN} from 'date-fns/locale';
|
||||
|
||||
@@ -137,16 +136,17 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const containerRef = ref<HTMLElement>();
|
||||
// 设置固定的初始值,避免从小到大的变化
|
||||
const cellSize = ref(12);
|
||||
const cellGap = ref(3);
|
||||
const weekdays = ['Mon', 'Wed', 'Fri'];
|
||||
const visibleWeeks = ref<Array<Array<{ date: Date; count: number }>>>([]);
|
||||
|
||||
// 新增响应式变量
|
||||
// 响应式变量,用于控制图表宽度
|
||||
const chartMaxWidth = ref(0);
|
||||
|
||||
// 修改后的updateSize函数
|
||||
const updateSize = debounce(() => {
|
||||
// 优化updateSize函数,移除debounce,避免延迟导致的动画效果
|
||||
const updateSize = () => {
|
||||
if (!containerRef.value) return;
|
||||
|
||||
const container = containerRef.value;
|
||||
@@ -156,15 +156,16 @@ const updateSize = debounce(() => {
|
||||
// 动态计算最大宽度
|
||||
chartMaxWidth.value = containerWidth - 40;
|
||||
|
||||
// 重新计算单元格尺寸
|
||||
// 计算单元格尺寸,确保合理的显示效果
|
||||
const maxCellSize = Math.min(
|
||||
(containerWidth - 40) / 54, // 更精确的计算
|
||||
containerHeight / 7 - cellGap.value
|
||||
containerHeight / 7 - 3 // 使用固定的间距值,避免循环依赖
|
||||
);
|
||||
|
||||
// 设置单元格大小和间距
|
||||
cellSize.value = Math.max(8, Math.min(14, maxCellSize));
|
||||
cellGap.value = Math.max(2, cellSize.value * 0.15);
|
||||
}, 150);
|
||||
cellGap.value = Math.max(2, Math.min(4, cellSize.value * 0.15));
|
||||
};
|
||||
|
||||
// 日期有效性检查
|
||||
const isValidDate = (date: Date) => {
|
||||
@@ -260,13 +261,20 @@ const formatTooltip = (date: Date) => {
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
// 先处理数据,再更新尺寸,避免不必要的重新计算
|
||||
visibleWeeks.value = processContributions();
|
||||
updateSize();
|
||||
|
||||
const observer = new ResizeObserver(debounce(() => {
|
||||
// 立即执行一次更新尺寸,确保初始渲染正确
|
||||
nextTick(() => {
|
||||
updateSize();
|
||||
visibleWeeks.value = processContributions();
|
||||
}, 200));
|
||||
});
|
||||
|
||||
// 使用ResizeObserver监听容器大小变化
|
||||
const observer = new ResizeObserver(() => {
|
||||
// 直接调用updateSize,不使用debounce,避免动画效果
|
||||
updateSize();
|
||||
// 不需要重新处理贡献数据,因为数据不会因为大小变化而改变
|
||||
});
|
||||
|
||||
if (containerRef.value) observer.observe(containerRef.value);
|
||||
onBeforeUnmount(() => observer.disconnect());
|
||||
@@ -279,13 +287,15 @@ onMounted(() => {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 300px;
|
||||
padding: 20px 15px; /* 调整左右padding */
|
||||
padding: 20px 15px;
|
||||
box-sizing: border-box;
|
||||
overflow-x: auto; /* 允许横向滚动 */
|
||||
/* 防止内容溢出 */
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.legend {
|
||||
position: absolute; /* 粘性定位 */
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
display: flex;
|
||||
@@ -295,16 +305,15 @@ onMounted(() => {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 15px; /* 增加底部间距 */
|
||||
z-index: 2; /* 确保在图上层级 */
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
& + & {
|
||||
margin-left: 4px; /* 增加色块间距 */
|
||||
}
|
||||
.legend-item + .legend-item {
|
||||
margin-left: 4px; /* 增加色块间距 */
|
||||
}
|
||||
|
||||
.legend-block {
|
||||
@@ -317,7 +326,8 @@ onMounted(() => {
|
||||
position: relative;
|
||||
margin-top: 45px; /* 增加顶部间距 */
|
||||
margin-left: 40px;
|
||||
min-width: 520px; /* 最小宽度保证布局 */
|
||||
/* 移除固定最小宽度,使用更灵活的方式 */
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.month-axis {
|
||||
@@ -382,7 +392,6 @@ onMounted(() => {
|
||||
.chart-wrapper {
|
||||
margin-top: 35px;
|
||||
margin-left: 30px;
|
||||
min-width: 480px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,7 +402,7 @@ onMounted(() => {
|
||||
|
||||
.day-cell {
|
||||
border-radius: 15%;
|
||||
transition: all 0.2s ease;
|
||||
/* 移除过渡效果,避免加载时的动画 */
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -402,5 +411,7 @@ onMounted(() => {
|
||||
transform: scale(1.15);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
|
||||
z-index: 1;
|
||||
/* 只在悬停时添加过渡效果 */
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
</style>
|
||||
|
@@ -13,14 +13,23 @@
|
||||
placeholder="选择存储桶">
|
||||
</ACascader>
|
||||
</template>
|
||||
<ATooltip title="选择存储桶" color="orange">
|
||||
<AButton type="text" shape="circle" size="large" class="header-menu-item-btn">
|
||||
<template #icon>
|
||||
<AAvatar size="default" shape="circle"
|
||||
:src="ProviderIcon[uploadStore.storageSelected?.[0]]? ProviderIcon[uploadStore.storageSelected?.[0]] : wenhao"/>
|
||||
</template>
|
||||
</AButton>
|
||||
</ATooltip>
|
||||
<ABadge dot
|
||||
color="green"
|
||||
:offset="[-9, 12]">
|
||||
<ATooltip title="选择存储桶" color="orange">
|
||||
<AButton type="text" shape="circle" size="large" class="header-menu-item-btn"
|
||||
:class="{'breathing': !uploadStore.storageSelected?.length}">
|
||||
<template #icon>
|
||||
<AAvatar size="default" shape="circle"
|
||||
:src="ProviderIcon[uploadStore.storageSelected?.[0]] ? ProviderIcon[uploadStore.storageSelected?.[0]] : wenhao"/>
|
||||
</template>
|
||||
</AButton>
|
||||
</ATooltip>
|
||||
<template #count>
|
||||
<ExclamationCircleOutlined style="color: red" v-if="!uploadStore.storageSelected?.length"/>
|
||||
</template>
|
||||
</ABadge>
|
||||
|
||||
</APopover>
|
||||
</div>
|
||||
|
||||
@@ -53,6 +62,12 @@
|
||||
<span class="tool-box-card-title">OCR文字识别</span>
|
||||
</AFlex>
|
||||
</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>
|
||||
</template>
|
||||
<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 blur from "@/assets/svgs/blur.svg";
|
||||
import scanIcon from "@/assets/svgs/scan.svg";
|
||||
import qr from "@/assets/svgs/qr.svg";
|
||||
import imgBed from "@/assets/svgs/img_bed.svg";
|
||||
|
||||
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>
|
||||
|
@@ -17,4 +17,13 @@ export default [
|
||||
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,
|
||||
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: '执行记录'
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
@@ -2,29 +2,29 @@
|
||||
<div class="basic-settings">
|
||||
<a-card title="基础设置" :bordered="false">
|
||||
<a-form
|
||||
:model="settingsForm"
|
||||
:rules="settingsRules"
|
||||
:label-col="{ span: 4 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
ref="settingsFormRef"
|
||||
:model="settingsForm"
|
||||
:rules="settingsRules"
|
||||
:label-col="{ span: 4 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
ref="settingsFormRef"
|
||||
>
|
||||
<a-form-item label="系统名称" name="systemName">
|
||||
<a-input v-model:value="settingsForm.systemName" placeholder="请输入系统名称" />
|
||||
<a-input v-model:value="settingsForm.systemName" placeholder="请输入系统名称"/>
|
||||
</a-form-item>
|
||||
|
||||
|
||||
<a-form-item label="系统Logo" name="logoUrl">
|
||||
<div class="logo-upload-wrapper">
|
||||
<div class="logo-preview" v-if="settingsForm.logoUrl">
|
||||
<img :src="settingsForm.logoUrl" alt="系统Logo" />
|
||||
<img :src="settingsForm.logoUrl" alt="系统Logo"/>
|
||||
<a-button type="link" danger @click="removeLogo">移除</a-button>
|
||||
</div>
|
||||
<a-upload
|
||||
v-if="!settingsForm.logoUrl"
|
||||
name="logo"
|
||||
list-type="picture-card"
|
||||
:show-upload-list="false"
|
||||
:before-upload="beforeLogoUpload"
|
||||
@change="handleLogoChange"
|
||||
v-if="!settingsForm.logoUrl"
|
||||
name="logo"
|
||||
list-type="picture-card"
|
||||
:show-upload-list="false"
|
||||
:before-upload="beforeLogoUpload"
|
||||
@change="handleLogoChange"
|
||||
>
|
||||
<div>
|
||||
<div style="margin-top: 8px">上传Logo</div>
|
||||
@@ -33,7 +33,7 @@
|
||||
</div>
|
||||
<div class="upload-hint">建议尺寸: 200px * 60px,格式: PNG, JPG, SVG</div>
|
||||
</a-form-item>
|
||||
|
||||
|
||||
<a-form-item label="系统主题" name="theme">
|
||||
<a-radio-group v-model:value="settingsForm.theme">
|
||||
<a-radio value="light">浅色主题</a-radio>
|
||||
@@ -41,20 +41,20 @@
|
||||
<a-radio value="auto">跟随系统</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
|
||||
<a-form-item label="主题色" name="primaryColor">
|
||||
<div class="color-picker-wrapper">
|
||||
<div
|
||||
v-for="color in predefinedColors"
|
||||
:key="color"
|
||||
class="color-block"
|
||||
:style="{ backgroundColor: color }"
|
||||
:class="{ active: settingsForm.primaryColor === color }"
|
||||
@click="settingsForm.primaryColor = color"
|
||||
<div
|
||||
v-for="color in predefinedColors"
|
||||
:key="color"
|
||||
class="color-block"
|
||||
:style="{ backgroundColor: color }"
|
||||
:class="{ active: settingsForm.primaryColor === color }"
|
||||
@click="settingsForm.primaryColor = color"
|
||||
></div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
|
||||
<a-form-item label="页面布局" name="layout">
|
||||
<a-radio-group v-model:value="settingsForm.layout">
|
||||
<a-radio value="side">侧边菜单</a-radio>
|
||||
@@ -62,22 +62,22 @@
|
||||
<a-radio value="mix">混合菜单</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
|
||||
<a-form-item label="系统语言" name="language">
|
||||
<a-select v-model:value="settingsForm.language" placeholder="请选择系统语言">
|
||||
<a-select-option value="zh-CN">简体中文</a-select-option>
|
||||
<a-select-option value="en-US">English</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
|
||||
<a-form-item label="版权信息" name="copyright">
|
||||
<a-input v-model:value="settingsForm.copyright" placeholder="请输入版权信息" />
|
||||
<a-input v-model:value="settingsForm.copyright" placeholder="请输入版权信息"/>
|
||||
</a-form-item>
|
||||
|
||||
|
||||
<a-form-item label="备案信息" name="icp">
|
||||
<a-input v-model:value="settingsForm.icp" placeholder="请输入备案信息" />
|
||||
<a-input v-model:value="settingsForm.icp" placeholder="请输入备案信息"/>
|
||||
</a-form-item>
|
||||
|
||||
|
||||
<a-form-item :wrapper-col="{ span: 16, offset: 4 }">
|
||||
<a-button type="primary" @click="handleSaveSettings">保存设置</a-button>
|
||||
<a-button style="margin-left: 10px" @click="resetSettings">重置</a-button>
|
||||
@@ -88,8 +88,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import {ref, reactive, onMounted} from 'vue';
|
||||
import {message} from 'ant-design-vue';
|
||||
|
||||
// 预定义的主题色
|
||||
const predefinedColors = [
|
||||
@@ -121,14 +121,14 @@ const settingsForm = reactive({
|
||||
// 表单验证规则
|
||||
const settingsRules = {
|
||||
systemName: [
|
||||
{ required: true, message: '请输入系统名称', trigger: 'blur' },
|
||||
{ max: 50, message: '系统名称不能超过50个字符', trigger: 'blur' },
|
||||
{required: true, message: '请输入系统名称', trigger: 'blur'},
|
||||
{max: 50, message: '系统名称不能超过50个字符', trigger: 'blur'},
|
||||
],
|
||||
copyright: [
|
||||
{ max: 100, message: '版权信息不能超过100个字符', trigger: 'blur' },
|
||||
{max: 100, message: '版权信息不能超过100个字符', trigger: 'blur'},
|
||||
],
|
||||
icp: [
|
||||
{ max: 50, message: '备案信息不能超过50个字符', trigger: 'blur' },
|
||||
{max: 50, message: '备案信息不能超过50个字符', trigger: 'blur'},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -170,7 +170,7 @@ const removeLogo = () => {
|
||||
};
|
||||
|
||||
// 将文件转换为Base64
|
||||
const getBase64 = (img: File, callback: Function) => {
|
||||
const getBase64 = (img: File, callback: () => void) => {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', () => callback(reader.result));
|
||||
reader.readAsDataURL(img);
|
||||
@@ -211,12 +211,12 @@ onMounted(() => {
|
||||
.logo-upload-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
|
||||
.logo-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
|
||||
img {
|
||||
max-width: 200px;
|
||||
max-height: 60px;
|
||||
@@ -224,18 +224,18 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.upload-hint {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
|
||||
.color-picker-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
|
||||
|
||||
.color-block {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
@@ -243,16 +243,16 @@ onMounted(() => {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
border: 2px solid transparent;
|
||||
|
||||
|
||||
&.active {
|
||||
border-color: #000;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@@ -331,8 +331,8 @@ const handlePermissionModalOk = () => {
|
||||
const newId: any = Math.max(...permissionList.value.map(item => item.id)) + 1;
|
||||
const statusText = permissionForm.status === 'active' ? '启用' : '禁用';
|
||||
const now = new Date().toLocaleString();
|
||||
permissionForm.id = newId;
|
||||
permissionList.value.push({
|
||||
newId,
|
||||
...permissionForm,
|
||||
statusText,
|
||||
createTime: now
|
||||
|
@@ -5,12 +5,12 @@
|
||||
<a-button type="primary" @click="handleAddRole">新增角色</a-button>
|
||||
</template>
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="roleList"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@change="handleTableChange"
|
||||
rowKey="id"
|
||||
:columns="columns"
|
||||
:data-source="roleList"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@change="handleTableChange"
|
||||
rowKey="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
@@ -23,10 +23,10 @@
|
||||
<a-button type="link" @click="handleEditRole(record)">编辑</a-button>
|
||||
<a-button type="link" @click="handleSetPermissions(record)">权限设置</a-button>
|
||||
<a-popconfirm
|
||||
title="确定要删除此角色吗?"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="handleDeleteRole(record)"
|
||||
title="确定要删除此角色吗?"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="handleDeleteRole(record)"
|
||||
>
|
||||
<a-button type="link" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
@@ -38,27 +38,27 @@
|
||||
|
||||
<!-- 角色表单对话框 -->
|
||||
<a-modal
|
||||
v-model:visible="roleModalVisible"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="modalLoading"
|
||||
@ok="handleRoleModalOk"
|
||||
@cancel="handleRoleModalCancel"
|
||||
v-model:visible="roleModalVisible"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="modalLoading"
|
||||
@ok="handleRoleModalOk"
|
||||
@cancel="handleRoleModalCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="roleFormRef"
|
||||
:model="roleForm"
|
||||
:rules="roleRules"
|
||||
:label-col="{ span: 4 }"
|
||||
:wrapper-col="{ span: 20 }"
|
||||
ref="roleFormRef"
|
||||
:model="roleForm"
|
||||
:rules="roleRules"
|
||||
:label-col="{ span: 4 }"
|
||||
:wrapper-col="{ span: 20 }"
|
||||
>
|
||||
<a-form-item label="角色名称" name="name">
|
||||
<a-input v-model:value="roleForm.name" placeholder="请输入角色名称" />
|
||||
<a-input v-model:value="roleForm.name" placeholder="请输入角色名称"/>
|
||||
</a-form-item>
|
||||
<a-form-item label="角色编码" name="code">
|
||||
<a-input v-model:value="roleForm.code" placeholder="请输入角色编码" />
|
||||
<a-input v-model:value="roleForm.code" placeholder="请输入角色编码"/>
|
||||
</a-form-item>
|
||||
<a-form-item label="角色描述" name="description">
|
||||
<a-textarea v-model:value="roleForm.description" placeholder="请输入角色描述" />
|
||||
<a-textarea v-model:value="roleForm.description" placeholder="请输入角色描述"/>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="roleForm.status">
|
||||
@@ -71,24 +71,24 @@
|
||||
|
||||
<!-- 权限设置对话框 -->
|
||||
<a-modal
|
||||
v-model:visible="permissionModalVisible"
|
||||
title="权限设置"
|
||||
:confirm-loading="permissionModalLoading"
|
||||
@ok="handlePermissionModalOk"
|
||||
@cancel="handlePermissionModalCancel"
|
||||
width="800px"
|
||||
v-model:visible="permissionModalVisible"
|
||||
title="权限设置"
|
||||
:confirm-loading="permissionModalLoading"
|
||||
@ok="handlePermissionModalOk"
|
||||
@cancel="handlePermissionModalCancel"
|
||||
width="800px"
|
||||
>
|
||||
<template v-if="currentRole">
|
||||
<p>为角色 <strong>{{ currentRole.name }}</strong> 设置权限:</p>
|
||||
<a-table
|
||||
:columns="permissionColumns"
|
||||
:data-source="permissionList"
|
||||
:row-selection="{
|
||||
:columns="permissionColumns"
|
||||
:data-source="permissionList"
|
||||
:row-selection="{
|
||||
selectedRowKeys: selectedPermissions,
|
||||
onChange: onPermissionSelectionChange
|
||||
}"
|
||||
rowKey="id"
|
||||
size="small"
|
||||
rowKey="id"
|
||||
size="small"
|
||||
></a-table>
|
||||
</template>
|
||||
</a-modal>
|
||||
@@ -96,8 +96,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import {ref, reactive, onMounted} from 'vue';
|
||||
import {message} from 'ant-design-vue';
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
@@ -182,9 +182,9 @@ const roleForm = reactive({
|
||||
});
|
||||
|
||||
const roleRules = {
|
||||
name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入角色编码', trigger: 'blur' }],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
|
||||
name: [{required: true, message: '请输入角色名称', trigger: 'blur'}],
|
||||
code: [{required: true, message: '请输入角色编码', trigger: 'blur'}],
|
||||
status: [{required: true, message: '请选择状态', trigger: 'change'}],
|
||||
};
|
||||
|
||||
// 权限设置相关
|
||||
@@ -275,11 +275,11 @@ const handleRoleModalOk = () => {
|
||||
}
|
||||
} 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 now = new Date().toLocaleString();
|
||||
roleForm.id = id;
|
||||
roleList.value.push({
|
||||
id,
|
||||
...roleForm,
|
||||
statusText,
|
||||
createTime: now,
|
||||
|
@@ -239,7 +239,7 @@ import {
|
||||
} from '@ant-design/icons-vue';
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
const searchForm = reactive<any>({
|
||||
timeRange: [],
|
||||
userType: undefined,
|
||||
registerSource: undefined,
|
||||
|
@@ -5,7 +5,7 @@
|
||||
<div class="user-list-header-right">
|
||||
<AButton type="primary" @click="handleAddUser">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
<PlusOutlined/>
|
||||
</template>
|
||||
新增用户
|
||||
</AButton>
|
||||
@@ -16,10 +16,10 @@
|
||||
<div class="table-search-wrapper">
|
||||
<AForm layout="inline" :model="searchForm">
|
||||
<AFormItem label="用户名">
|
||||
<AInput v-model:value="searchForm.username" placeholder="请输入用户名" allowClear />
|
||||
<AInput v-model:value="searchForm.username" placeholder="请输入用户名" allowClear/>
|
||||
</AFormItem>
|
||||
<AFormItem label="手机号">
|
||||
<AInput v-model:value="searchForm.phone" placeholder="请输入手机号" allowClear />
|
||||
<AInput v-model:value="searchForm.phone" placeholder="请输入手机号" allowClear/>
|
||||
</AFormItem>
|
||||
<AFormItem label="状态">
|
||||
<ASelect v-model:value="searchForm.status" placeholder="请选择状态" style="width: 120px" allowClear>
|
||||
@@ -38,13 +38,13 @@
|
||||
<AFormItem>
|
||||
<AButton type="primary" @click="handleSearch">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
<SearchOutlined/>
|
||||
</template>
|
||||
搜索
|
||||
</AButton>
|
||||
<AButton style="margin-left: 8px" @click="resetSearch">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
<ReloadOutlined/>
|
||||
</template>
|
||||
重置
|
||||
</AButton>
|
||||
@@ -53,12 +53,12 @@
|
||||
</div>
|
||||
|
||||
<ATable
|
||||
:columns="columns"
|
||||
:data-source="userList"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@change="handleTableChange"
|
||||
rowKey="id"
|
||||
:columns="columns"
|
||||
:data-source="userList"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@change="handleTableChange"
|
||||
rowKey="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
@@ -71,10 +71,10 @@
|
||||
<AButton type="link" size="small" @click="handleEditUser(record)">编辑</AButton>
|
||||
<AButton type="link" size="small" @click="handleViewUser(record)">查看</AButton>
|
||||
<APopconfirm
|
||||
title="确定要删除此用户吗?"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="handleDeleteUser(record)"
|
||||
title="确定要删除此用户吗?"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="handleDeleteUser(record)"
|
||||
>
|
||||
<AButton type="link" danger size="small">删除</AButton>
|
||||
</APopconfirm>
|
||||
@@ -86,21 +86,22 @@
|
||||
|
||||
<!-- 用户编辑对话框 -->
|
||||
<AModal
|
||||
v-model:visible="userModalVisible"
|
||||
:title="modalTitle"
|
||||
@ok="handleUserModalOk"
|
||||
@cancel="handleUserModalCancel"
|
||||
:confirmLoading="modalLoading"
|
||||
v-model:visible="userModalVisible"
|
||||
:title="modalTitle"
|
||||
@ok="handleUserModalOk"
|
||||
@cancel="handleUserModalCancel"
|
||||
: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">
|
||||
<AInput v-model:value="userForm.username" placeholder="请输入用户名" />
|
||||
<AInput v-model:value="userForm.username" placeholder="请输入用户名"/>
|
||||
</AFormItem>
|
||||
<AFormItem label="手机号" name="phone">
|
||||
<AInput v-model:value="userForm.phone" placeholder="请输入手机号" />
|
||||
<AInput v-model:value="userForm.phone" placeholder="请输入手机号"/>
|
||||
</AFormItem>
|
||||
<AFormItem label="邮箱" name="email">
|
||||
<AInput v-model:value="userForm.email" placeholder="请输入邮箱" />
|
||||
<AInput v-model:value="userForm.email" placeholder="请输入邮箱"/>
|
||||
</AFormItem>
|
||||
<AFormItem label="角色" name="role">
|
||||
<ASelect v-model:value="userForm.role" placeholder="请选择角色">
|
||||
@@ -122,8 +123,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import {ref, reactive, onMounted} from 'vue';
|
||||
import {message} from 'ant-design-vue';
|
||||
import {
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
@@ -272,22 +273,22 @@ const userForm = reactive({
|
||||
// 表单验证规则
|
||||
const userFormRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 3, max: 20, message: '用户名长度应为3-20个字符', trigger: 'blur' }
|
||||
{required: true, message: '请输入用户名', trigger: 'blur'},
|
||||
{min: 3, max: 20, message: '用户名长度应为3-20个字符', trigger: 'blur'}
|
||||
],
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
||||
{required: true, message: '请输入手机号', trigger: 'blur'},
|
||||
{pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur'}
|
||||
],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
|
||||
{required: true, message: '请输入邮箱', trigger: 'blur'},
|
||||
{type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur'}
|
||||
],
|
||||
role: [
|
||||
{ required: true, message: '请选择角色', trigger: 'change' }
|
||||
{required: true, message: '请选择角色', trigger: 'change'}
|
||||
],
|
||||
status: [
|
||||
{ required: true, message: '请选择状态', trigger: 'change' }
|
||||
{required: true, message: '请选择状态', trigger: 'change'}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -383,12 +384,12 @@ const handleUserModalOk = () => {
|
||||
}
|
||||
} 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 statusText = userForm.status === 'active' ? '正常' : (userForm.status === 'inactive' ? '禁用' : '待审核');
|
||||
const now = new Date().toLocaleString();
|
||||
userForm.id = id;
|
||||
userList.value.push({
|
||||
id,
|
||||
...userForm,
|
||||
roleText,
|
||||
statusText,
|
||||
|
@@ -437,7 +437,7 @@ onMounted(() => {
|
||||
|
||||
.image-bed-upload-container {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
//padding: 10px;
|
||||
}
|
||||
|
||||
.image-bed-upload-content {
|
||||
|
@@ -528,7 +528,7 @@ const handleRecognize = async () => {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="scss">
|
||||
.ocr-detection {
|
||||
padding: 20px;
|
||||
}
|
||||
@@ -656,7 +656,6 @@ const handleRecognize = async () => {
|
||||
}
|
||||
|
||||
.result-list {
|
||||
max-height: 350px;
|
||||
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">
|
||||
<AFormItem label="存储商" name="sourceProvider">
|
||||
<ASelect
|
||||
v-model:value="sourceStorage.provider"
|
||||
placeholder="请选择源存储商"
|
||||
@change="handleSourceProviderChange"
|
||||
v-model:value="sourceStorage.id"
|
||||
placeholder="请选择源存储商"
|
||||
@change="handleSourceProviderChange"
|
||||
>
|
||||
<ASelectOption
|
||||
v-for="item in storageList"
|
||||
:key="item.provider + '-' + item.bucket"
|
||||
:value="item.provider + '-' + item.bucket"
|
||||
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]" />
|
||||
@@ -41,17 +41,39 @@
|
||||
<AAvatar :size="40" shape="circle" :src="ProviderIcon[selectedSourceStorage.provider]" />
|
||||
<div class="storage-info">
|
||||
<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 class="storage-card-content">
|
||||
<div class="storage-detail">
|
||||
<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 class="storage-detail">
|
||||
<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>
|
||||
@@ -71,15 +93,15 @@
|
||||
<AForm layout="vertical">
|
||||
<AFormItem label="存储商" name="targetProvider">
|
||||
<ASelect
|
||||
v-model:value="targetStorage.provider"
|
||||
placeholder="请选择目标存储商"
|
||||
@change="handleTargetProviderChange"
|
||||
:disabled="!sourceStorage.provider"
|
||||
v-model:value="targetStorage.id"
|
||||
placeholder="请选择目标存储商"
|
||||
@change="handleTargetProviderChange"
|
||||
:disabled="!sourceStorage.id"
|
||||
>
|
||||
<ASelectOption
|
||||
v-for="item in availableTargetStorages"
|
||||
:key="item.provider + '-' + item.bucket"
|
||||
:value="item.provider + '-' + item.bucket"
|
||||
v-for="item in availableTargetStorages"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
<AFlex align="center" gap="small">
|
||||
<AAvatar :size="20" shape="circle" :src="ProviderIcon[item.provider]" />
|
||||
@@ -94,17 +116,39 @@
|
||||
<AAvatar :size="40" shape="circle" :src="ProviderIcon[selectedTargetStorage.provider]" />
|
||||
<div class="storage-info">
|
||||
<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 class="storage-card-content">
|
||||
<div class="storage-detail">
|
||||
<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 class="storage-detail">
|
||||
<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>
|
||||
@@ -114,11 +158,11 @@
|
||||
|
||||
<div class="backup-action">
|
||||
<AButton
|
||||
type="primary"
|
||||
size="large"
|
||||
:disabled="!isBackupReady"
|
||||
@click="startBackup"
|
||||
:loading="backupInProgress"
|
||||
type="primary"
|
||||
size="large"
|
||||
:disabled="!isBackupReady"
|
||||
@click="startBackup"
|
||||
:loading="backupInProgress"
|
||||
>
|
||||
开始备份
|
||||
</AButton>
|
||||
@@ -154,8 +198,8 @@ import targetIcon from "@/assets/svgs/target-storage.svg";
|
||||
|
||||
const router = useRouter();
|
||||
const storageList = ref<any[]>([]);
|
||||
const sourceStorage = ref({ provider: '', bucket: '' });
|
||||
const targetStorage = ref({ provider: '', bucket: '' });
|
||||
const sourceStorage = ref({ id: null });
|
||||
const targetStorage = ref({ id: null });
|
||||
const selectedSourceStorage = ref<any>(null);
|
||||
const selectedTargetStorage = ref<any>(null);
|
||||
const backupModalVisible = ref(false);
|
||||
@@ -164,14 +208,25 @@ const backupStatus = ref('准备开始备份...');
|
||||
const backupInProgress = ref(false);
|
||||
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() {
|
||||
const res: any = await listUserStorageConfigApi();
|
||||
if (res && res.code === 200) {
|
||||
storageList.value = res.data.records;
|
||||
// 重置选择
|
||||
sourceStorage.value = { provider: '', bucket: '' };
|
||||
targetStorage.value = { provider: '', bucket: '' };
|
||||
sourceStorage.value = { id: null };
|
||||
targetStorage.value = { id: null };
|
||||
selectedSourceStorage.value = null;
|
||||
selectedTargetStorage.value = null;
|
||||
}
|
||||
@@ -179,12 +234,8 @@ async function getStorageList() {
|
||||
|
||||
// 计算可用的目标存储(排除已选择的源存储)
|
||||
const availableTargetStorages = computed(() => {
|
||||
if (!sourceStorage.value.provider) return [];
|
||||
return storageList.value.filter(item => {
|
||||
const sourceKey = sourceStorage.value.provider;
|
||||
const itemKey = item.provider + '-' + item.bucket;
|
||||
return sourceKey !== itemKey;
|
||||
});
|
||||
if (!sourceStorage.value.id) return [];
|
||||
return storageList.value.filter(item => item.id !== sourceStorage.value.id);
|
||||
});
|
||||
|
||||
// 判断是否可以开始备份
|
||||
@@ -199,22 +250,17 @@ function handleSourceProviderChange(value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [provider, bucket] = value.split('-');
|
||||
const selected = storageList.value.find(item =>
|
||||
item.provider === provider && item.bucket === bucket
|
||||
);
|
||||
const selected = storageList.value.find(item => item.id === value);
|
||||
|
||||
if (selected) {
|
||||
selectedSourceStorage.value = selected;
|
||||
|
||||
// 如果目标存储与源存储相同,则清空目标存储
|
||||
if (targetStorage.value.provider) {
|
||||
const [targetProvider, targetBucket] = targetStorage.value.provider.split('-');
|
||||
if (targetProvider === provider && targetBucket === bucket) {
|
||||
targetStorage.value.provider = '';
|
||||
selectedTargetStorage.value = null;
|
||||
}
|
||||
if (targetStorage.value.id === value) {
|
||||
targetStorage.value.id = null;
|
||||
selectedTargetStorage.value = null;
|
||||
}
|
||||
} else {
|
||||
selectedSourceStorage.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,13 +271,12 @@ function handleTargetProviderChange(value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [provider, bucket] = value.split('-');
|
||||
const selected = storageList.value.find(item =>
|
||||
item.provider === provider && item.bucket === bucket
|
||||
);
|
||||
const selected = storageList.value.find(item => item.id === value);
|
||||
|
||||
if (selected) {
|
||||
selectedTargetStorage.value = selected;
|
||||
} else {
|
||||
selectedTargetStorage.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,17 +290,9 @@ async function startBackup() {
|
||||
backupStatus.value = '正在准备备份...';
|
||||
|
||||
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(
|
||||
sourceProviderInfo,
|
||||
sourceBucketInfo,
|
||||
targetProviderInfo,
|
||||
targetBucketInfo
|
||||
selectedSourceStorage.value.id,
|
||||
selectedTargetStorage.value.id
|
||||
);
|
||||
|
||||
if (res && res.code === 200) {
|
||||
@@ -343,10 +380,11 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 样式保持不变 */
|
||||
.account-setting-backup {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
//height: 100%;
|
||||
//overflow: auto;
|
||||
|
||||
.account-setting-backup-header {
|
||||
display: flex;
|
||||
@@ -459,13 +497,34 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.storage-card-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
|
||||
.storage-detail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
padding: 8px;
|
||||
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-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -537,7 +537,7 @@ const handleThirdPartySuccess = (_type: string) => {
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
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;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
|
@@ -63,7 +63,7 @@
|
||||
@finish="handleUpdatePhone"
|
||||
>
|
||||
<AFormItem label="当前手机">
|
||||
<AInput :value="currentPhone" disabled />
|
||||
<AInput :value="userStore.user.phone" disabled />
|
||||
</AFormItem>
|
||||
|
||||
<AFormItem
|
||||
@@ -120,13 +120,6 @@ import { message } from 'ant-design-vue';
|
||||
import { sendMessage } from '@/api/user';
|
||||
import useStore from "@/store";
|
||||
|
||||
defineProps({
|
||||
currentPhone: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
// 获取用户store
|
||||
|
@@ -7,6 +7,7 @@
|
||||
:width="500"
|
||||
:maskClosable="false"
|
||||
@cancel="handleCancel"
|
||||
:footer="null"
|
||||
>
|
||||
<div class="third-party-container">
|
||||
<div class="third-party-item">
|
||||
@@ -22,18 +23,18 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="third-party-action">
|
||||
<AButton
|
||||
type="primary"
|
||||
size="small"
|
||||
<AButton
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="thirdPartyStatus.wechat"
|
||||
@click="handleBindThirdParty('wechat')"
|
||||
>
|
||||
{{ thirdPartyStatus.wechat ? '已绑定' : '绑定' }}
|
||||
</AButton>
|
||||
<AButton
|
||||
v-if="thirdPartyStatus.wechat"
|
||||
type="link"
|
||||
danger
|
||||
<AButton
|
||||
v-if="thirdPartyStatus.wechat"
|
||||
type="link"
|
||||
danger
|
||||
size="small"
|
||||
@click="handleUnbindThirdParty('wechat')"
|
||||
>
|
||||
@@ -55,18 +56,18 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="third-party-action">
|
||||
<AButton
|
||||
type="primary"
|
||||
size="small"
|
||||
<AButton
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="thirdPartyStatus.qq"
|
||||
@click="handleBindThirdParty('qq')"
|
||||
>
|
||||
{{ thirdPartyStatus.qq ? '已绑定' : '绑定' }}
|
||||
</AButton>
|
||||
<AButton
|
||||
v-if="thirdPartyStatus.qq"
|
||||
type="link"
|
||||
danger
|
||||
<AButton
|
||||
v-if="thirdPartyStatus.qq"
|
||||
type="link"
|
||||
danger
|
||||
size="small"
|
||||
@click="handleUnbindThirdParty('qq')"
|
||||
>
|
||||
@@ -88,18 +89,18 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="third-party-action">
|
||||
<AButton
|
||||
type="primary"
|
||||
size="small"
|
||||
<AButton
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="thirdPartyStatus.github"
|
||||
@click="handleBindThirdParty('github')"
|
||||
>
|
||||
{{ thirdPartyStatus.github ? '已绑定' : '绑定' }}
|
||||
</AButton>
|
||||
<AButton
|
||||
v-if="thirdPartyStatus.github"
|
||||
type="link"
|
||||
danger
|
||||
<AButton
|
||||
v-if="thirdPartyStatus.github"
|
||||
type="link"
|
||||
danger
|
||||
size="small"
|
||||
@click="handleUnbindThirdParty('github')"
|
||||
>
|
||||
@@ -121,18 +122,18 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="third-party-action">
|
||||
<AButton
|
||||
type="primary"
|
||||
size="small"
|
||||
<AButton
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="thirdPartyStatus.gitee"
|
||||
@click="handleBindThirdParty('gitee')"
|
||||
>
|
||||
{{ thirdPartyStatus.gitee ? '已绑定' : '绑定' }}
|
||||
</AButton>
|
||||
<AButton
|
||||
v-if="thirdPartyStatus.gitee"
|
||||
type="link"
|
||||
danger
|
||||
<AButton
|
||||
v-if="thirdPartyStatus.gitee"
|
||||
type="link"
|
||||
danger
|
||||
size="small"
|
||||
@click="handleUnbindThirdParty('gitee')"
|
||||
>
|
||||
@@ -141,10 +142,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<AButton key="back" @click="handleCancel">关闭</AButton>
|
||||
</template>
|
||||
</AModal>
|
||||
|
||||
<!-- 解绑确认模态窗口 -->
|
||||
@@ -242,7 +239,7 @@ const handleBindThirdParty = (type: string) => {
|
||||
// 解绑第三方账号(显示确认窗口)
|
||||
const handleUnbindThirdParty = (type: string) => {
|
||||
currentUnbindType.value = type;
|
||||
|
||||
|
||||
// 设置平台名称
|
||||
switch (type) {
|
||||
case 'wechat':
|
||||
@@ -261,7 +258,7 @@ const handleUnbindThirdParty = (type: string) => {
|
||||
currentUnbindPlatform.value = '';
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
// 显示确认窗口
|
||||
confirmUnbindVisible.value = true;
|
||||
};
|
||||
@@ -276,13 +273,13 @@ const cancelUnbind = () => {
|
||||
// 确认解绑
|
||||
const confirmUnbind = async () => {
|
||||
if (!currentUnbindType.value) return;
|
||||
|
||||
|
||||
try {
|
||||
submitLoading.value = true;
|
||||
// 这里需要实现解绑第三方账号的API调用,暂时模拟成功
|
||||
setTimeout(() => {
|
||||
message.success(`${currentUnbindPlatform.value}账号解绑成功`);
|
||||
|
||||
|
||||
// 更新状态
|
||||
switch (currentUnbindType.value) {
|
||||
case 'wechat':
|
||||
@@ -300,7 +297,7 @@ const confirmUnbind = async () => {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
emit('success', `unbind_${currentUnbindType.value}`);
|
||||
cancelUnbind();
|
||||
submitLoading.value = false;
|
||||
@@ -362,4 +359,4 @@ const confirmUnbind = async () => {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
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>
|
||||
<span class="ant-menu-item-title">图像备份</span>
|
||||
</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>
|
||||
</div>
|
||||
@@ -46,6 +58,8 @@ import home from "@/assets/svgs/home.svg";
|
||||
import peopleAlbum from "@/assets/svgs/people-album.svg";
|
||||
import storage from "@/assets/svgs/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 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>
|
||||
<div class="personal-center">
|
||||
<div class="personal-center-header">
|
||||
<Header :logo-color="'#fff'" style="background: transparent;box-shadow: none;backdrop-filter: none;"/>
|
||||
<div class="personal-center-header-info">
|
||||
<div class="personal-center-header-info-container">
|
||||
<div class="personal-center-header-info-container-avatar">
|
||||
<AAvatar :size="80" :text-size="1.5" :round="true" :src="userStore.user.avatar"/>
|
||||
</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">
|
||||
<Header :logo-color="'#333'" class="fixed-header" />
|
||||
<div class="personal-center-container">
|
||||
<!-- 左侧边栏 - 用户信息和导航 -->
|
||||
<div class="personal-center-sidebar">
|
||||
<div class="user-profile">
|
||||
<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 class="personal-center-header-info-container-description-introduce">
|
||||
<span>描述信息</span>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<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>
|
||||
|
||||
<AMenu
|
||||
class="sidebar-menu"
|
||||
:selectedKeys="[menuStore.userCenterMenu]"
|
||||
mode="inline"
|
||||
:selectable="true"
|
||||
:multiple="false"
|
||||
@select="handleClick"
|
||||
>
|
||||
<AMenuItem key="home" title="主页">
|
||||
<template #icon>
|
||||
<AAvatar shape="square" size="small" :src="home"/>
|
||||
</template>
|
||||
<span class="menu-item-title">主页</span>
|
||||
</AMenuItem>
|
||||
<AMenuItem key="dynamic" title="动态">
|
||||
<template #icon>
|
||||
<AAvatar shape="square" size="small" :src="dynamic"/>
|
||||
</template>
|
||||
<span class="menu-item-title">动态</span>
|
||||
</AMenuItem>
|
||||
<AMenuItem key="setting" title="设置">
|
||||
<template #icon>
|
||||
<AAvatar shape="square" size="small" :src="setting"/>
|
||||
</template>
|
||||
<span class="menu-item-title">设置</span>
|
||||
</AMenuItem>
|
||||
</AMenu>
|
||||
</div>
|
||||
</div>
|
||||
<div class="personal-center-content">
|
||||
<AMenu :selectedKeys="[menuStore.userCenterMenu]" mode="horizontal" :selectable="true" :multiple="false"
|
||||
@select="handleClick">
|
||||
<AMenuItem key="home" :style="menuCSSStyle" title="主页">
|
||||
<template #icon>
|
||||
<AAvatar shape="square" size="small" :src="home"/>
|
||||
</template>
|
||||
<span class="ant-menu-item-title">主页</span>
|
||||
</AMenuItem>
|
||||
<AMenuItem key="dynamic" :style="menuCSSStyle" title="动态">
|
||||
<template #icon>
|
||||
<AAvatar shape="square" size="small" :src="dynamic"/>
|
||||
</template>
|
||||
<span class="ant-menu-item-title">动态</span>
|
||||
</AMenuItem>
|
||||
<AMenuItem key="setting" :style="menuCSSStyle" title="设置">
|
||||
<template #icon>
|
||||
<AAvatar shape="square" size="small" :src="setting"/>
|
||||
</template>
|
||||
<span class="ant-menu-item-title">设置</span>
|
||||
</AMenuItem>
|
||||
</AMenu>
|
||||
<div class="personal-center-content-container">
|
||||
<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>
|
||||
@@ -55,106 +80,227 @@ import home from "@/assets/svgs/home.svg";
|
||||
import dynamic from "@/assets/svgs/dynamic.svg";
|
||||
import setting from "@/assets/svgs/setting.svg";
|
||||
|
||||
|
||||
const userStore = useStore().user;
|
||||
const menuStore = useStore().menu;
|
||||
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}) {
|
||||
menuStore.userCenterMenu = key;
|
||||
router.push(`/main/user/center/${key}`);
|
||||
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.personal-center {
|
||||
//background-color: #eaeef6;
|
||||
//min-height: 100vh;
|
||||
background-color: #f5f7fa;
|
||||
|
||||
.personal-center-header {
|
||||
.fixed-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
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;
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.personal-center-header-info {
|
||||
width: 100%;
|
||||
height: 130px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
.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-header-info-container {
|
||||
width: 95%;
|
||||
height: 110px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-start;
|
||||
gap: 20px;
|
||||
/* 左侧边栏样式 */
|
||||
.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容器中靠上对齐 */
|
||||
|
||||
.personal-center-header-info-container-avatar {
|
||||
height: 80px;
|
||||
width: 80px;
|
||||
border: 2px solid #fff;
|
||||
.user-profile {
|
||||
padding: 30px 20px;
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
color: white;
|
||||
position: relative;
|
||||
|
||||
.user-avatar {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin: 0 auto 15px;
|
||||
|
||||
.user-level {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
}
|
||||
padding: 3px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
|
||||
.personal-center-header-info-container-description {
|
||||
height: 70px;
|
||||
width: 80%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
|
||||
.personal-center-header-info-container-description-name {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
text-shadow: 0px 1px 2px rgba(0, 0, 0, .4);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
|
||||
.personal-level {
|
||||
width: auto;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.personal-center-header-info-container-description-introduce {
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
background-color: rgba(255, 255, 255, .2);
|
||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, .5)
|
||||
img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-info {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.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;
|
||||
flex-direction: column;
|
||||
/* 移除最大高度限制,允许内容无限滚动 */
|
||||
|
||||
.ant-menu-item-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
.content-header {
|
||||
padding: 20px 30px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.personal-center-content-container {
|
||||
width: calc(100% - 40px);
|
||||
height: calc(100vh - 290px);
|
||||
padding: 20px;
|
||||
.content-container {
|
||||
flex: 1;
|
||||
padding: 20px 30px;
|
||||
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>
|
||||
|
@@ -1,6 +1,38 @@
|
||||
<template>
|
||||
<div class="user-center-dynamic" ref="chartRef">
|
||||
<div class="user-center-dynamic">
|
||||
<div class="section-header">
|
||||
|
||||
</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>
|
||||
<script setup lang="ts">
|
||||
@@ -19,63 +51,122 @@ const dates = ref<string[]>([]);
|
||||
const visitCounts = ref<number[]>([]);
|
||||
const visitorCounts = 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() {
|
||||
const res: any = await getShareStatisticsInfoApi();
|
||||
if (res && res.code === 200) {
|
||||
const data: DataItem[] = res.data.records;
|
||||
dates.value = data.map((item: DataItem) => item.date);
|
||||
visitCounts.value = data.map((item: DataItem) => item.visit_count);
|
||||
visitorCounts.value = data.map((item: DataItem) => item.visitor_count);
|
||||
publishCounts.value = data.map((item: DataItem) => item.publish_count);
|
||||
try {
|
||||
const res: any = await getShareStatisticsInfoApi();
|
||||
if (res && res.code === 200) {
|
||||
const data: DataItem[] = res.data.records;
|
||||
dates.value = data.map((item: DataItem) => item.date);
|
||||
visitCounts.value = data.map((item: DataItem) => item.visit_count);
|
||||
visitorCounts.value = data.map((item: DataItem) => item.visitor_count);
|
||||
publishCounts.value = data.map((item: DataItem) => item.publish_count);
|
||||
|
||||
// 数据加载完成后初始化或更新图表
|
||||
initChart();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取分享统计数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
const chartInstance = echarts.init(chartRef.value);
|
||||
// 初始化图表
|
||||
function 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();
|
||||
}
|
||||
|
||||
// 更新图表配置
|
||||
function updateChartOption() {
|
||||
if (!chartInstance) return;
|
||||
|
||||
const colorList = ["#6366f1", "#22c55e", "#f97316"];
|
||||
|
||||
const colorList = ["#9E87FF", "#73DDFF", "#fe9a8b", "#F56948", "#9E87FF"];
|
||||
const option = {
|
||||
backgroundColor: "#fff",
|
||||
title: {
|
||||
text: "最近七天分享统计",
|
||||
fontSize: 12,
|
||||
fontWeight: 400,
|
||||
text: "分享统计概览",
|
||||
textStyle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
color: '#333'
|
||||
},
|
||||
left: "center",
|
||||
top: "5%",
|
||||
},
|
||||
legend: {
|
||||
icon: "circle",
|
||||
top: "5%",
|
||||
right: "5%",
|
||||
itemWidth: 6,
|
||||
bottom: "0%",
|
||||
itemWidth: 8,
|
||||
itemHeight: 8,
|
||||
itemGap: 20,
|
||||
color: "#556677",
|
||||
|
||||
textStyle: {
|
||||
color: "#333",
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: {
|
||||
label: {
|
||||
show: true,
|
||||
backgroundColor: "#fff",
|
||||
color: "#556677",
|
||||
borderColor: "rgba(0,0,0,0)",
|
||||
shadowColor: "rgba(0,0,0,0)",
|
||||
shadowOffsetY: 0,
|
||||
},
|
||||
lineStyle: {
|
||||
width: 0,
|
||||
},
|
||||
},
|
||||
backgroundColor: "#fff",
|
||||
color: "#5c6c7c",
|
||||
padding: [10, 10],
|
||||
extraCssText: "box-shadow: 1px 0 2px 0 rgba(163,163,163,0.5)",
|
||||
borderRadius: 4,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.1)',
|
||||
shadowBlur: 10,
|
||||
textStyle: {
|
||||
color: "#333",
|
||||
},
|
||||
padding: [10, 15],
|
||||
axisPointer: {
|
||||
type: 'line',
|
||||
lineStyle: {
|
||||
color: '#6366f1',
|
||||
width: 1,
|
||||
type: 'dashed'
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
top: "15%",
|
||||
left: "3%",
|
||||
right: "3%",
|
||||
bottom: "12%",
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
@@ -83,7 +174,7 @@ onMounted(async () => {
|
||||
data: dates.value,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#DCE2E8",
|
||||
color: "#e0e0e0",
|
||||
},
|
||||
},
|
||||
axisTick: {
|
||||
@@ -91,68 +182,10 @@ onMounted(async () => {
|
||||
},
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
color: "#556677",
|
||||
// 默认x轴字体大小
|
||||
color: "#666",
|
||||
fontSize: 12,
|
||||
// margin:文字到x轴的距离
|
||||
margin: 15,
|
||||
},
|
||||
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
|
||||
},
|
||||
},
|
||||
rotate: dates.value.length > 10 ? 45 : 0
|
||||
},
|
||||
boundaryGap: false,
|
||||
},
|
||||
@@ -160,42 +193,33 @@ onMounted(async () => {
|
||||
yAxis: [
|
||||
{
|
||||
type: "value",
|
||||
name: "数量",
|
||||
nameTextStyle: {
|
||||
color: "#666",
|
||||
fontSize: 12,
|
||||
padding: [0, 0, 0, 5]
|
||||
},
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: "#DCE2E8",
|
||||
color: "#e0e0e0",
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#556677",
|
||||
color: "#666",
|
||||
fontSize: 12
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "value",
|
||||
position: "right",
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#556677",
|
||||
formatter: "{value}",
|
||||
},
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: "#DCE2E8",
|
||||
},
|
||||
color: "#f5f5f5",
|
||||
type: "dashed"
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
@@ -263,7 +287,7 @@ onMounted(async () => {
|
||||
type: "line",
|
||||
data: publishCounts.value,
|
||||
symbolSize: 1,
|
||||
yAxisIndex: 1,
|
||||
yAxisIndex: 0,
|
||||
symbol: "circle",
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
@@ -272,7 +296,7 @@ onMounted(async () => {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{
|
||||
offset: 0,
|
||||
color: "#fe9a",
|
||||
color: "#fe9a8b",
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
@@ -291,8 +315,7 @@ onMounted(async () => {
|
||||
],
|
||||
};
|
||||
chartInstance.setOption(option);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
getData();
|
||||
@@ -308,12 +331,115 @@ onBeforeUnmount(() => {
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-center-dynamic {
|
||||
width: calc(100vw - 40px);
|
||||
height: calc(100vh - 290px);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
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>
|
||||
|
@@ -1,206 +1,613 @@
|
||||
<template>
|
||||
<div class="user-center-home">
|
||||
<div class="user-center-home-left">
|
||||
<div class="user-center-home-left-top" v-if="chartData">
|
||||
<div class="user-center-home-left-top-card"
|
||||
style="background: linear-gradient(102.74deg, rgb(66, 230, 171) -7.03%, rgb(103, 235, 187) 97.7%);">
|
||||
<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="imageIcon"/>
|
||||
</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: #ffffff">{{ chartData.image_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: 3vh;font-weight: bold;color: #ffffff">+{{ chartData.today_upload_count }}</span>
|
||||
</div>
|
||||
<!-- 数据卡片区域 -->
|
||||
<div class="stats-cards" v-if="chartData">
|
||||
<div class="stats-card" data-type="images">
|
||||
<div class="stats-card-icon">
|
||||
<AAvatar :size="50" shape="square" :src="imageIcon"/>
|
||||
</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 class="stats-card-content">
|
||||
<div class="stats-card-title">图片总数</div>
|
||||
<div class="stats-card-value">{{ chartData.image_count }}</div>
|
||||
<div class="stats-card-footer">
|
||||
<span class="stats-card-label">今日上传</span>
|
||||
<span class="stats-card-change">+{{ chartData.today_upload_count }}</span>
|
||||
</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 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="user-center-home-right">
|
||||
<ACard class="user-center-home-right-card" :hoverable="false">
|
||||
|
||||
</ACard>
|
||||
<!-- 主要内容区域 -->
|
||||
<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>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
import HeatmapPro from "@/components/HeatmapPro/HeatmapPro.vue";
|
||||
import imageIcon from "@/assets/svgs/image-icon.svg";
|
||||
import shareIcon from "@/assets/svgs/share-icon.svg";
|
||||
import fileSize from "@/assets/svgs/file-size.svg";
|
||||
import {getUserUploadInfoApi} from "@/api/storage";
|
||||
import bytesToSize from "@/utils/imageUtils/bytesToSize";
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
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() {
|
||||
const res: any = await getUserUploadInfoApi();
|
||||
if (res && res.code === 200) {
|
||||
chartData.value = res.data;
|
||||
// 数据加载完成后初始化图表
|
||||
nextTick(() => {
|
||||
initChart();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getData();
|
||||
});
|
||||
|
||||
// 组件卸载时清理图表实例和事件监听
|
||||
onBeforeUnmount(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
window.removeEventListener('resize', () => {});
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.user-center-home {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: 40px;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
.user-center-home-left {
|
||||
width: 60%;
|
||||
height: 100%;
|
||||
/* 数据卡片区域 */
|
||||
.stats-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
|
||||
.user-center-home-left-top {
|
||||
width: 100%;
|
||||
height: 25vh;
|
||||
.stats-card {
|
||||
flex: 1;
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.user-center-home-left-top-card {
|
||||
width: 28%;
|
||||
height: 90%;
|
||||
background-color: #fff;
|
||||
border-radius: 1.8vh;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
padding-inline: 10px;
|
||||
padding-block: 10px;
|
||||
gap: 10px;
|
||||
border: 1px solid #eee;
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.8;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
&[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;
|
||||
}
|
||||
|
||||
.user-center-home-left-top-card-top {
|
||||
width: 100%;
|
||||
height: 60%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
|
||||
.user-center-home-left-top-card-top-avatar {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.user-center-home-left-top-card-top-name {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.stats-card-value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.user-center-home-left-top-card-bottom {
|
||||
width: 100%;
|
||||
height: 40%;
|
||||
.stats-card-footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
|
||||
.stats-card-label {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.stats-card-change {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #42e6ab;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-center-home-left-bottom {
|
||||
width: 100%;
|
||||
height: 58%;
|
||||
/* 内容网格区域 */
|
||||
.content-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 24px;
|
||||
width: 100%;
|
||||
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;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
background-color: #fff;
|
||||
border-radius: 1.8vh;
|
||||
border: 1px solid #eee;
|
||||
padding-block: 20px;
|
||||
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;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.section-body {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 月度上传图表区域特定样式 */
|
||||
.monthly-chart-section {
|
||||
overflow: hidden;
|
||||
|
||||
.section-body {
|
||||
height: 300px; /* 设置固定高度以确保图表正确渲染 */
|
||||
}
|
||||
}
|
||||
|
||||
/* 最近活动区域特定样式 */
|
||||
.recent-activity-section {
|
||||
.activity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-center-home-right {
|
||||
width: 39%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1024px) {
|
||||
.stats-cards {
|
||||
flex-direction: column;
|
||||
|
||||
.stats-card {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.user-center-home-right-card {
|
||||
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,121 +5,375 @@
|
||||
<p class="setting-description">您可以在这里管理您的账户设置和偏好</p>
|
||||
</div>
|
||||
|
||||
<div class="setting-section">
|
||||
<div class="section-title">账户安全</div>
|
||||
<div class="user-center-setting-content">
|
||||
<div class="user-center-setting-item">
|
||||
<div class="setting-item-left">
|
||||
<img src="@/assets/svgs/account_security.svg" alt="AI识别" class="setting-icon" />
|
||||
<div class="setting-text">
|
||||
<span class="user-center-setting-item-title">开启AI识别</span>
|
||||
<span class="setting-description">允许系统使用AI技术识别您的照片内容</span>
|
||||
<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="settings-list">
|
||||
<div class="setting-item">
|
||||
<div class="setting-item-left">
|
||||
<div class="setting-icon account-ai"></div>
|
||||
<div class="setting-text">
|
||||
<span class="setting-item-title">开启AI识别</span>
|
||||
<span class="setting-item-desc">允许系统使用AI技术识别您的照片内容</span>
|
||||
</div>
|
||||
</div>
|
||||
<ASwitch v-model:checked="userStore.settings.enableAI">
|
||||
<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 account-mobile"></div>
|
||||
<div class="setting-text">
|
||||
<span class="setting-item-title">开启手机上传</span>
|
||||
<span class="setting-item-desc">允许从移动设备上传照片到您的相册</span>
|
||||
</div>
|
||||
</div>
|
||||
<ASwitch v-model:checked="userStore.settings.enableMobileUpload">
|
||||
<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 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>
|
||||
<ASwitch v-model:checked="userStore.settings.enableAI">
|
||||
<template #checkedChildren>
|
||||
<check-outlined />
|
||||
</template>
|
||||
<template #unCheckedChildren>
|
||||
<close-outlined />
|
||||
</template>
|
||||
</ASwitch>
|
||||
</div>
|
||||
|
||||
<div class="user-center-setting-item">
|
||||
<div class="setting-item-left">
|
||||
<img src="@/assets/svgs/login_security.svg" alt="手机上传" class="setting-icon" />
|
||||
<div class="setting-text">
|
||||
<span class="user-center-setting-item-title">开启手机上传</span>
|
||||
<span class="setting-description">允许从移动设备上传照片到您的相册</span>
|
||||
<!-- 隐私设置 -->
|
||||
<div v-if="activeSection === 'privacy'" 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 privacy-profile"></div>
|
||||
<div class="setting-text">
|
||||
<span class="setting-item-title">公开个人资料</span>
|
||||
<span class="setting-item-desc">允许其他用户查看您的个人资料信息</span>
|
||||
</div>
|
||||
</div>
|
||||
<ASwitch v-model:checked="userStore.settings.publicProfile">
|
||||
<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 privacy-comment"></div>
|
||||
<div class="setting-text">
|
||||
<span class="setting-item-title">开启评论功能</span>
|
||||
<span class="setting-item-desc">允许其他用户在您的照片下方评论</span>
|
||||
</div>
|
||||
</div>
|
||||
<ASwitch v-model:checked="userStore.settings.enableComment">
|
||||
<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 privacy-search"></div>
|
||||
<div class="setting-text">
|
||||
<span class="setting-item-title">保存搜索记录</span>
|
||||
<span class="setting-item-desc">保存您的搜索历史以提供更好的推荐</span>
|
||||
</div>
|
||||
</div>
|
||||
<ASwitch v-model:checked="userStore.settings.saveSearchHistory">
|
||||
<template #checkedChildren>
|
||||
<check-outlined />
|
||||
</template>
|
||||
<template #unCheckedChildren>
|
||||
<close-outlined />
|
||||
</template>
|
||||
</ASwitch>
|
||||
</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>
|
||||
<ASwitch v-model:checked="userStore.settings.enableMobileUpload">
|
||||
<template #checkedChildren>
|
||||
<check-outlined />
|
||||
</template>
|
||||
<template #unCheckedChildren>
|
||||
<close-outlined />
|
||||
</template>
|
||||
</ASwitch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-section">
|
||||
<div class="section-title">隐私设置</div>
|
||||
<div class="user-center-setting-content">
|
||||
<div class="user-center-setting-item">
|
||||
<div class="setting-item-left">
|
||||
<img src="@/assets/svgs/privacy.svg" alt="个人资料" class="setting-icon" />
|
||||
<div class="setting-text">
|
||||
<span class="user-center-setting-item-title">公开个人资料</span>
|
||||
<span class="setting-description">允许其他用户查看您的个人资料信息</span>
|
||||
</div>
|
||||
</div>
|
||||
<ASwitch v-model:checked="userStore.settings.publicProfile">
|
||||
<template #checkedChildren>
|
||||
<check-outlined />
|
||||
</template>
|
||||
<template #unCheckedChildren>
|
||||
<close-outlined />
|
||||
</template>
|
||||
</ASwitch>
|
||||
</div>
|
||||
|
||||
<div class="user-center-setting-item">
|
||||
<div class="setting-item-left">
|
||||
<img src="@/assets/svgs/community.svg" alt="评论" class="setting-icon" />
|
||||
<div class="setting-text">
|
||||
<span class="user-center-setting-item-title">开启评论功能</span>
|
||||
<span class="setting-description">允许其他用户在您的照片下方评论</span>
|
||||
</div>
|
||||
</div>
|
||||
<ASwitch v-model:checked="userStore.settings.enableComment">
|
||||
<template #checkedChildren>
|
||||
<check-outlined />
|
||||
</template>
|
||||
<template #unCheckedChildren>
|
||||
<close-outlined />
|
||||
</template>
|
||||
</ASwitch>
|
||||
</div>
|
||||
|
||||
<div class="user-center-setting-item">
|
||||
<div class="setting-item-left">
|
||||
<img src="@/assets/svgs/search.svg" alt="搜索记录" class="setting-icon" />
|
||||
<div class="setting-text">
|
||||
<span class="user-center-setting-item-title">保存搜索记录</span>
|
||||
<span class="setting-description">保存您的搜索历史以提供更好的推荐</span>
|
||||
</div>
|
||||
</div>
|
||||
<ASwitch v-model:checked="userStore.settings.saveSearchHistory">
|
||||
<template #checkedChildren>
|
||||
<check-outlined />
|
||||
</template>
|
||||
<template #unCheckedChildren>
|
||||
<close-outlined />
|
||||
</template>
|
||||
</ASwitch>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-actions">
|
||||
<AButton type="primary" @click="saveSettings">保存设置</AButton>
|
||||
<AButton @click="resetSettings">重置</AButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import { CheckOutlined, CloseOutlined } from '@ant-design/icons-vue';
|
||||
import useStore from "@/store";
|
||||
import { ref } from 'vue';
|
||||
import { message as AMessage } from 'ant-design-vue';
|
||||
|
||||
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>
|
||||
<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 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -145,10 +399,66 @@ const userStore = useStore().user;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-section {
|
||||
width: calc(100% - 80px);
|
||||
.settings-container {
|
||||
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;
|
||||
background-color: var(--white-color, #fff);
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
padding: 20px;
|
||||
@@ -162,87 +472,115 @@ const userStore = useStore().user;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
color: var(--text-color, #333);
|
||||
color: #333;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
}
|
||||
|
||||
.user-center-setting-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.user-center-setting-item {
|
||||
width: 100%;
|
||||
min-height: 60px;
|
||||
.settings-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.03);
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.setting-item-left {
|
||||
.setting-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.03);
|
||||
|
||||
.setting-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.setting-text {
|
||||
.setting-item-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
|
||||
.user-center-setting-item-title {
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
color: var(--text-color, #333);
|
||||
margin-bottom: 4px;
|
||||
.setting-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(99, 102, 241, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
.setting-description {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
line-height: 1.5;
|
||||
.setting-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.setting-item-title {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.setting-item-desc {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-switch) {
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
|
||||
&.ant-switch-checked {
|
||||
background-color: var(--blue, rgba(96,165,250,.9));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
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;
|
||||
}
|
||||
|
||||
.user-center-setting-item {
|
||||
.setting-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
|
||||
.setting-item-left {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -64,6 +64,9 @@ export default defineConfig(({mode}: { mode: string }): object => {
|
||||
force: false,
|
||||
needsInterop: [".vite"],
|
||||
},
|
||||
worker: {
|
||||
format: 'es'
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
vitePluginBundleObfuscator(defaultObfuscatorConfig),
|
||||
@@ -177,6 +180,7 @@ export default defineConfig(({mode}: { mode: string }): object => {
|
||||
watch: null, // 设置为 {} 则会启用 rollup 的监听器
|
||||
rollupOptions: { // 自定义底层的 Rollup 打包配置
|
||||
output: {
|
||||
format: 'es',
|
||||
chunkFileNames: 'js/[name]-[hash].js', // 引入文件名的名称
|
||||
entryFileNames: 'js/[name]-[hash].js', // 包的入口文件名称
|
||||
assetFileNames: '[ext]/[name]-[hash].[ext]',// 资源文件像 字体,图片等
|
||||
|
Reference in New Issue
Block a user