add user dashboard page

This commit is contained in:
2025-03-05 17:44:43 +08:00
parent 08d4bbfbf9
commit b77217f724
38 changed files with 2126 additions and 155 deletions

10
components.d.ts vendored
View File

@@ -13,6 +13,7 @@ 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']
AccountSettingHome: typeof import('./src/views/User/AccountSetting/components/AccountSettingHome/AccountSettingHome.vue')['default']
@@ -20,6 +21,7 @@ declare module 'vue' {
AccountSettingSidebar: typeof import('./src/views/User/AccountSetting/components/AccountSettingSidebar/AccountSettingSidebar.vue')['default']
AccountSettingStorage: typeof import('./src/views/User/AccountSetting/components/AccountSettingStorage/AccountSettingStorage.vue')['default']
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
ACol: typeof import('ant-design-vue/es')['Col']
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
ADivider: typeof import('ant-design-vue/es')['Divider']
ADrawer: typeof import('ant-design-vue/es')['Drawer']
@@ -53,6 +55,7 @@ declare module 'vue' {
ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
ARow: typeof import('ant-design-vue/es')['Row']
ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
@@ -91,6 +94,7 @@ declare module 'vue' {
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']
EllipsisOutlined: typeof import('@ant-design/icons-vue')['EllipsisOutlined']
EyeInvisibleOutlined: typeof import('@ant-design/icons-vue')['EyeInvisibleOutlined']
EyeOutlined: typeof import('@ant-design/icons-vue')['EyeOutlined']
FileImageOutlined: typeof import('@ant-design/icons-vue')['FileImageOutlined']
@@ -98,6 +102,8 @@ declare module 'vue' {
Folder: typeof import('./src/components/Folder/Folder.vue')['default']
ForgetPage: typeof import('./src/views/Forget/ForgetPage.vue')['default']
GradientText: typeof import('./src/components/MyUI/GradientText/GradientText.vue')['default']
Heatmap: typeof import('./src/components/Heatmap/Heatmap.vue')['default']
HeatmapPro: typeof import('./src/components/HeatmapPro/HeatmapPro.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']
@@ -106,6 +112,7 @@ declare module 'vue' {
LeftOutlined: typeof import('@ant-design/icons-vue')['LeftOutlined']
LinkOutlined: typeof import('@ant-design/icons-vue')['LinkOutlined']
LoadingGraphic: typeof import('./src/components/LoadingGraphic/LoadingGraphic.vue')['default']
LoadingOutlined: typeof import('@ant-design/icons-vue')['LoadingOutlined']
LocationAlbumDetail: typeof import('./src/views/Album/LocationAlbum/LocationAlbumDetail.vue')['default']
LocationAlbumIndex: typeof import('./src/views/Album/LocationAlbum/LocationAlbumIndex.vue')['default']
LocationAlbumList: typeof import('./src/views/Album/LocationAlbum/LocationAlbumList.vue')['default']
@@ -136,6 +143,7 @@ declare module 'vue' {
Rate: typeof import('./src/components/MyUI/Rate/Rate.vue')['default']
RecentUpload: typeof import('./src/views/Photograph/RecentUpload/RecentUpload.vue')['default']
RecyclingBin: typeof import('./src/views/RecyclingBin/RecyclingBin.vue')['default']
RedoOutlined: typeof import('@ant-design/icons-vue')['RedoOutlined']
ReplyInput: typeof import('./src/components/CommentReply/src/ReplyInput/ReplyInput.vue')['default']
ReplyList: typeof import('./src/components/CommentReply/src/ReplyList/ReplyList.vue')['default']
ReplyReply: typeof import('./src/components/CommentReply/src/ReplyReplyInput/ReplyReply.vue')['default']
@@ -145,12 +153,14 @@ declare module 'vue' {
SearchOutlined: typeof import('@ant-design/icons-vue')['SearchOutlined']
SearchResult: typeof import('./src/views/Photograph/SearchResult/SearchResult.vue')['default']
SendOutlined: typeof import('@ant-design/icons-vue')['SendOutlined']
SettingOutlined: typeof import('@ant-design/icons-vue')['SettingOutlined']
SharePhoneUpload: typeof import('./src/views/Phone/SharePhoneUpload/SharePhoneUpload.vue')['default']
ShareSidebar: typeof import('./src/views/Share/ShareViewList/ShareSidebar.vue')['default']
ShareUpload: typeof import('./src/views/Share/ImageShare/ShareUpload.vue')['default']
ShareViewList: typeof import('./src/views/Share/ShareViewList/index.vue')['default']
Spin: typeof import('./src/components/MyUI/Spin/Spin.vue')['default']
StarButton: typeof import('./src/components/StarButton/StarButton.vue')['default']
StorageCard: typeof import('./src/views/User/AccountSetting/components/AccountSettingStorage/StorageCard.vue')['default']
TabletOutlined: typeof import('@ant-design/icons-vue')['TabletOutlined']
ThingAlbumDetail: typeof import('./src/views/Album/ThingAlbum/ThingAlbumDetail.vue')['default']
ThingAlbumIndex: typeof import('./src/views/Album/ThingAlbum/ThingAlbumIndex.vue')['default']

View File

@@ -12,6 +12,7 @@
"dependencies": {
"@alova/adapter-axios": "^2.0.13",
"@ant-design/icons-vue": "^7.0.1",
"@fcli/vue-calendar-map": "^1.0.2",
"@intlify/eslint-plugin-vue-i18n": "^3.2.0",
"@mediapipe/face_detection": "^0.4.1646425229",
"@mediapipe/face_mesh": "^0.4.1633559619",
@@ -55,6 +56,7 @@
"jszip": "^3.10.1",
"less": "^4.2.2",
"localforage": "^1.10.0",
"lodash-es": "^4.17.21",
"moment": "^2.30.1",
"nprogress": "^0.2.0",
"nsfwjs": "^4.2.1",

View File

@@ -1,4 +1,4 @@
import {service} from "@/utils/alova/service.ts";
import {service} from "@/utils/alova/service_app.ts";
export const uploadImage = (data: any) => {
return service.Post('/api/auth/phone/upload', {
@@ -26,3 +26,37 @@ export const sharePhoneUploadApi = (data: any) => {
}
});
};
/**
* 上传文件
* @param formData
* @param headers
*/
export const uploadFile = (formData: any, headers: any) => {
return service.Post('/api/auth/storage/uploads', formData, {
headers: headers,
meta: {
ignoreToken: false,
signature: false,
},
name: "upload-file",
});
};
export const albumListApi = (type: number, sort: boolean, headers: any) => {
return service.Post('/api/auth/storage/album/list', {
type: type,
sort: sort,
}, {
headers: headers,
cacheFor: {
expire: 60 * 60 * 24 * 7,
mode: "restore",
},
meta: {
ignoreToken: false,
signature: false,
},
hitSource: ["create-album", "rename-album", "delete-album", "album-share"],
});
};

View File

@@ -350,6 +350,7 @@ export const getStorageConfigListApi = () => {
signature: false,
},
name: "storage-config-list",
hitSource: ["delete-storage-config", "add-storage-config"]
});
};
/**
@@ -509,3 +510,54 @@ export const downloadAlbumImagesApi = (id: number, provider: string, bucket: str
name: "download-album-images",
});
};
/**
* 获取用户存储配置
*/
export const listUserStorageConfigApi = () => {
return service.Post('/api/auth/storage/user/storage/list', {}, {
cacheFor: {
expire: 60 * 60 * 24 * 7,
mode: "restore",
},
meta: {
ignoreToken: false,
signature: false,
},
name: "list-user-storage-config",
hitSource: ["delete-storage-config", "add-storage-config"],
});
};
/**
* 创建存储配置
* @param id
* @param provider
* @param bucket
*/
export const deleteStorageConfigApi = (id: number, provider: string, bucket: string) => {
return service.Post('/api/auth/storage/config/delete', {
id: id,
provider: provider,
bucket: bucket,
}, {
meta: {
ignoreToken: false,
signature: false,
},
name: "delete-storage-config",
});
};
/**
* 创建存储配置
* @param params
*/
export const addStorageConfigApi = (params: any) => {
return service.Post('/api/auth/storage/config/add', {
...params,
}, {
meta: {
ignoreToken: false,
signature: false,
},
name: "add-storage-config",
});
};

View File

@@ -76,10 +76,6 @@ html {
}
}
// 取消antd table 最后一行的边框
.ant-table-wrapper .ant-table:not(.ant-table-bordered) .ant-table-tbody > tr:last-child > td {
border-bottom: none !important;
}
// 空白内容样式
.empty-content {
@@ -90,7 +86,3 @@ html {
height: 100%;
width: 100%;
}
//:not(button) > svg:not([color]) {
// color: var(--text-color) !important;
//}

View File

@@ -0,0 +1 @@
<svg t="1741150116560" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="18568" width="200" height="200"><path d="M512 231.575273a729.774545 729.774545 0 0 1-109.882182 55.924363 482.059636 482.059636 0 0 1-126.417454 31.115637v239.848727c0 115.397818 167.773091 247.330909 236.299636 247.330909S748.299636 673.861818 748.299636 558.464V318.615273a476.555636 476.555636 0 0 1-126.033454-31.115637A719.127273 719.127273 0 0 1 512 231.575273z m176.046545 218.577454L496.244364 669.521455l-125.672728-105.89091 39.389091-45.288727L488.727273 586.426182l152.808727-175.255273z" fill="#4E8CEE" p-id="18569"></path><path d="M512 0a512 512 0 1 0 512 512A512 512 0 0 0 512 0z m0 866.443636c-103.563636 0-296.168727-157.533091-296.168727-306.804363V261.504h29.533091a415.499636 415.499636 0 0 0 134.295272-29.090909 584.448 584.448 0 0 0 114.606546-60.648727L512 157.533091l17.722182 12.602182a581.306182 581.306182 0 0 0 114.932363 62.231272 415.895273 415.895273 0 0 0 133.899637 28.753455h29.533091v296.948364C808.157091 708.910545 615.563636 866.443636 512 866.443636z" fill="#B4D1FF" p-id="18570"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<svg t="1741079927040" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5318" width="200" height="200"><path d="M512 512m-512 0a512 512 0 1 0 1024 0 512 512 0 1 0-1024 0Z" fill="#FDEBED" p-id="5319"></path><path d="M729.6 384H294.4c-7.68 0-12.8-5.12-12.8-12.8v-25.6c0-7.68 5.12-12.8 12.8-12.8h115.2v-25.6c0-14.08 11.52-25.6 25.6-25.6h153.6c14.08 0 25.6 11.52 25.6 25.6v25.6h115.2c7.68 0 12.8 5.12 12.8 12.8v25.6c0 7.68-5.12 12.8-12.8 12.8z m-371.2 38.4h307.2c28.16 0 51.2 23.04 51.2 51.2v217.6c0 28.16-23.04 51.2-51.2 51.2H358.4c-28.16 0-51.2-23.04-51.2-51.2V473.6c0-28.16 23.04-51.2 51.2-51.2z m192 243.2c0 7.68 5.12 12.8 12.8 12.8s12.8-5.12 12.8-12.8V537.6c0-7.68-5.12-12.8-12.8-12.8s-12.8 5.12-12.8 12.8v128z m-102.4 0c0 7.68 5.12 12.8 12.8 12.8s12.8-5.12 12.8-12.8V537.6c0-7.68-5.12-12.8-12.8-12.8s-12.8 5.12-12.8 12.8v128z" fill="#EC3A4E" p-id="5320"></path></svg>

After

Width:  |  Height:  |  Size: 913 B

View File

@@ -0,0 +1 @@
<svg t="1741150908905" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="23961" width="200" height="200"><path d="M922.88 164.522667a42.666667 42.666667 0 0 0-35.84-8.533334 116.906667 116.906667 0 0 1-93.866667-19.626666 42.666667 42.666667 0 0 0-50.346666 0 116.906667 116.906667 0 0 1-93.866667 19.626666 42.666667 42.666667 0 0 0-51.626667 41.813334v141.226666a197.12 197.12 0 0 0 78.506667 157.866667l66.986667 49.493333a42.666667 42.666667 0 0 0 50.346666 0l66.986667-49.493333a197.12 197.12 0 0 0 78.506667-157.866667v-141.226666a42.666667 42.666667 0 0 0-15.786667-33.28zM853.333333 339.029333a111.36 111.36 0 0 1-42.666666 89.173334l-42.666667 29.866666-42.666667-30.72a111.36 111.36 0 0 1-42.666666-89.173333v-93.44a199.253333 199.253333 0 0 0 85.333333-23.04 199.253333 199.253333 0 0 0 85.333333 23.04z m42.666667 256a42.666667 42.666667 0 0 0-42.666667 42.666667v128a42.666667 42.666667 0 0 1-42.666666 42.666667H213.333333a42.666667 42.666667 0 0 1-42.666666-42.666667v-407.04l250.88 250.88a127.744 127.744 0 0 0 181.76-0.853333l-29.866667-30.293334-31.573333-29.013333a42.666667 42.666667 0 0 1-59.733334 0l-251.306666-250.88H469.333333a42.666667 42.666667 0 0 0 0-85.333333H213.333333a128 128 0 0 0-128 128v426.666666a128 128 0 0 0 128 128h597.333334a128 128 0 0 0 128-128v-128a42.666667 42.666667 0 0 0-42.666667-42.666666z" fill="#0070FF" p-id="23962"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
<svg t="1741144027158" class="icon" viewBox="0 0 1032 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10401" width="200" height="200"><path d="M779.491772 953.616478H254.137244a177.907534 177.907534 0 0 1-177.678272-177.678271V249.437367a177.907534 177.907534 0 0 1 177.678272-177.678272H779.491772a177.907534 177.907534 0 0 1 177.678272 177.678272v526.50084A177.907534 177.907534 0 0 1 779.491772 953.616478zM254.137244 152.000896a97.551103 97.551103 0 0 0-97.436472 97.436471v526.50084a97.551103 97.551103 0 0 0 97.436472 97.436471H779.491772a97.551103 97.551103 0 0 0 97.436472-97.436471V249.437367a97.551103 97.551103 0 0 0-97.436472-97.436471z" fill="#FDD345" p-id="10402"></path><path d="M483.858054 751.407142h-66.944587L285.431546 608.118213h-1.146311v143.288929h-47.801187v-441.3299h47.801187v279.699988h1.146311l125.177208-136.41106h62.588604L334.837569 597.113624zM581.638419 708.305832h-1.146311v43.10131h-47.686556v-441.3299h47.686556v195.675361h1.146311a112.911676 112.911676 0 0 1 103.168029-59.378932 109.243479 109.243479 0 0 1 89.756185 40.006269q32.440613 40.006269 32.440613 107.294749 0 74.854136-36.338072 119.789545a121.165118 121.165118 0 0 1-99.499832 44.935408 98.009627 98.009627 0 0 1-89.526923-50.09381z m-1.146311-120.248069v41.611105a88.609874 88.609874 0 0 0 23.957909 62.703235A85.629464 85.629464 0 0 0 733.639315 685.03571q24.645696-33.1284 24.645696-91.704914a120.133438 120.133438 0 0 0-22.926229-77.949177 76.115079 76.115079 0 0 0-62.244711-28.199261 84.597783 84.597783 0 0 0-67.28848 28.657785 106.377701 106.377701 0 0 0-25.333483 72.21762z" fill="#FDD345" p-id="10403"></path></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1 @@
<svg t="1741144431170" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14627" width="200" height="200"><path d="M739.9936 878.8992H364.7488c-113.1008 0-204.8-91.6992-204.8-204.8V417.7408c0-113.1008 91.6992-204.8 204.8-204.8h375.2448c113.1008 0 204.8 91.6992 204.8 204.8v256.3584c0 113.1008-91.6992 204.8-204.8 204.8z" fill="#FFF7E6" p-id="14628"></path><path d="M944.7936 444.928c-14.1312 0-25.6-11.4688-25.6-25.6v-10.24c0-14.1312 11.4688-25.6 25.6-25.6s25.6 11.4688 25.6 25.6v10.24c0 14.1312-11.4688 25.6-25.6 25.6z" fill="#44454A" p-id="14629"></path><path d="M290.7648 878.8992h443.5456c116.224 0 210.4832-94.2592 210.4832-210.4832v-123.904c-61.44-55.296-151.0912-95.1808-284.16-76.7488-160.7168 22.272-249.9072 137.0112-296.2432 263.9872-80.128-99.1744-208.7936-97.5872-284.16-86.0672v22.6816c0.0512 116.2752 94.2592 210.5344 210.5344 210.5344z" fill="#FD973F" p-id="14630"></path><path d="M944.7936 464.896c-14.1312 0-25.6 11.4688-25.6 25.6v2.2528c-72.7552-47.4112-160.6144-64.3584-262.0416-50.3296-135.936 18.8416-239.5648 102.5536-301.6192 242.944-77.056-68.864-179.0464-75.0592-249.6512-68.5568V352.9728c0-100.352 81.664-182.016 182.016-182.016h449.28c95.3856 0 175.1552 74.4448 181.6064 169.472a25.61536 25.61536 0 0 0 27.2896 23.808 25.61536 25.61536 0 0 0 23.808-27.2896c-8.2432-121.8048-110.4896-217.2416-232.704-217.2416H287.8976c-128.6144 0-233.216 104.6016-233.216 233.216v318.3104c0 128.6144 104.6016 233.216 233.216 233.216h449.3312c128.6144 0 233.216-104.6016 233.216-233.216V490.496c-0.0512-14.1312-11.52-25.6-25.6512-25.6zM290.7648 853.2992c-101.9392 0-184.8832-82.944-184.8832-184.8832v-0.1536c68.352-7.0656 173.0048-1.536 238.6432 79.616a25.66656 25.66656 0 0 0 24.1152 9.1648 25.7024 25.7024 0 0 0 19.8656-16.4864c53.3504-146.2272 146.1248-229.4784 275.712-247.3984 102.1952-14.1312 187.904 7.0656 255.0272 63.0784v112.1792c0 101.9392-82.944 184.8832-184.8832 184.8832H290.7648z" fill="#44454A" p-id="14631"></path><path d="M299.9296 369.8688m-79.4624 0a79.4624 79.4624 0 1 0 158.9248 0 79.4624 79.4624 0 1 0-158.9248 0Z" fill="#FD973F" p-id="14632"></path><path d="M299.9296 474.9312c-57.9072 0-105.0624-47.104-105.0624-105.0624 0-57.9072 47.104-105.0624 105.0624-105.0624 57.9072 0 105.0624 47.104 105.0624 105.0624s-47.1552 105.0624-105.0624 105.0624z m0-158.8736c-29.696 0-53.8624 24.1664-53.8624 53.8624s24.1664 53.8624 53.8624 53.8624S353.792 399.616 353.792 369.92c0-29.7472-24.1664-53.8624-53.8624-53.8624z" fill="#44454A" p-id="14633"></path></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1 @@
<svg t="1741152058412" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="43893" width="200" height="200"><path d="M793.144889 220.302222q-8.049778 0.398222-16.213333 0.398222a353.735111 353.735111 0 0 1-241.009778-94.378666 47.530667 47.530667 0 0 0-64.256 0 353.735111 353.735111 0 0 1-241.009778 94.378666c-3.413333 0-6.798222 0-10.183111-0.170666a48.355556 48.355556 0 0 0-50.119111 48.355555v295.310223c0 178.232889 136.533333 230.883556 313.656889 324.266666a48.981333 48.981333 0 0 0 45.653333 0c25.6-13.454222 50.062222-26.026667 73.585778-38.257778a145.351111 145.351111 0 0 1-11.690667-35.413333 28.444444 28.444444 0 0 0 0-55.523555 146.176 146.176 0 0 1 47.786667-82.773334 28.444444 28.444444 0 0 0 48.042666-27.790222 147.143111 147.143111 0 0 1 95.544889 0 28.444444 28.444444 0 0 0 31.544889 36.494222 243.911111 243.911111 0 0 0 28.842667-121.002666V268.657778a48.184889 48.184889 0 0 0-50.176-48.355556z m-231.054222 239.985778a66.417778 66.417778 0 0 1-18.517334 18.545778v135.736889a36.750222 36.750222 0 0 1-73.500444 0v-135.736889a66.389333 66.389333 0 1 1 92.017778-18.545778z" fill="#2172F6" p-id="43894"></path><path d="M773.006222 903.480889a23.978667 23.978667 0 0 1 40.448-23.409778 122.88 122.88 0 0 0 40.220445-69.660444 23.978667 23.978667 0 0 1 0-46.734223 122.908444 122.908444 0 0 0-40.220445-69.660444 23.978667 23.978667 0 0 1-40.448-23.409778 123.790222 123.790222 0 0 0-80.384 0 23.978667 23.978667 0 0 1-40.448 23.409778 123.050667 123.050667 0 0 0-40.220444 69.660444 23.978667 23.978667 0 0 1 0 46.734223 123.079111 123.079111 0 0 0 40.220444 69.660444 23.978667 23.978667 0 0 1 40.448 23.409778 123.221333 123.221333 0 0 0 80.384 0z m-92.928-116.423111a52.736 52.736 0 1 1 52.764445 52.764444 52.764444 52.764444 0 0 1-52.764445-52.764444z" fill="#2172F6" p-id="43895"></path></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1 @@
<svg t="1741151981616" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="36372" width="200" height="200"><path d="M128 0h768C981.357714 0 1024 42.642286 1024 128v768c0 85.357714-42.642286 128-128 128H128C42.642286 1024 0 981.357714 0 896V128C0 42.642286 42.642286 0 128 0z" fill="#4C84FF" p-id="36373"></path><path d="M704 398.189714h-31.963429v-64.950857c0-89.746286-71.68-162.596571-160.036571-162.596571-88.283429 0-159.963429 72.850286-159.963429 162.596571v64.950857h-32.036571c-35.181714 0-64 29.257143-64 65.097143v282.331429c0 35.766857 28.818286 65.097143 64 65.097143h384c35.181714 0 64-29.257143 64-65.097143v-282.331429a64.731429 64.731429 0 0 0-64-65.097143zM512 682.715429a85.577143 85.577143 0 0 1-85.357714-85.357715A85.577143 85.577143 0 0 1 512 512a85.577143 85.577143 0 0 1 85.357714 85.357714A85.577143 85.577143 0 0 1 512 682.642286z m-95.963429-284.525715v-64.950857c0-53.979429 42.861714-97.572571 95.963429-97.572571 53.101714 0 96.036571 43.52 96.036571 97.572571v64.950857H416.036571z" fill="#FFFFFF" p-id="36374"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<svg t="1741151722442" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="31745" width="200" height="200"><path d="M511.982392 0s-225.676102 228.353615-424.237072 153.000747v484.927368a649.870673 649.870673 0 0 0 424.237072 386.071885 649.870673 649.870673 0 0 0 424.237071-386.071885v-484.927368c-198.60347 75.352868-424.237071-153.000747-424.237071-153.000747z" fill="#3388FF" p-id="31746"></path><path d="M332.971518 266.2213m62.092803 0l233.836142 0q62.092803 0 62.092803 62.092803l0 345.824189q0 62.092803-62.092803 62.092803l-233.836142 0q-62.092803 0-62.092803-62.092803l0-345.824189q0-62.092803 62.092803-62.092803Z" fill="#FFFFFF" p-id="31747"></path><path d="M509.049877 614.127999m-37.697684 0a37.697684 37.697684 0 1 0 75.395368 0 37.697684 37.697684 0 1 0-75.395368 0Z" fill="#3388FF" p-id="31748"></path><path d="M403.649363 316.966548m22.56761 0l165.623309 0q22.56761 0 22.56761 22.56761l0 0q0 22.56761-22.56761 22.56761l-165.623309 0q-22.56761 0-22.56761-22.56761l0 0q0-22.56761 22.56761-22.56761Z" fill="#3388FF" p-id="31749"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<svg t="1741143907669" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5846" width="200" height="200"><path d="M758.8352 945.2032H272.5376c-107.264 0-194.2016-86.9376-194.2016-194.2016V264.7552c0-107.264 86.9376-194.2016 194.2016-194.2016h486.2464c107.264 0 194.2016 86.9376 194.2016 194.2016v486.2464c0.0512 107.264-86.8864 194.2016-194.1504 194.2016z" fill="#4FE8CB" p-id="5847"></path><path d="M722.7392 623.1552c-57.344 0-104.0384 46.6432-104.0384 103.9872s46.6432 103.9872 104.0384 103.9872c57.344 0 103.9872-46.6432 103.9872-103.9872s-46.6432-103.9872-103.9872-103.9872z m0 156.8256c-29.1328 0-52.8384-23.7056-52.8384-52.7872s23.7056-52.7872 52.8384-52.7872 52.7872 23.7056 52.7872 52.7872-23.6544 52.7872-52.7872 52.7872zM595.8656 631.3472L457.472 556.032c4.3008-12.4928 6.7072-25.9072 6.7072-39.8848 0-13.6704-2.2528-26.7776-6.4-39.0144l190.5664-105.2672c18.8928 19.4048 45.2608 31.5392 74.4448 31.5392 22.1696 0 43.3152-6.8608 61.1328-19.8656a25.62048 25.62048 0 0 0 5.632-35.7888 25.66656 25.66656 0 0 0-35.7888-5.632 52.27008 52.27008 0 0 1-31.0272 10.0864c-29.1328 0-52.8384-23.7056-52.8384-52.7872s23.7056-52.7872 52.8384-52.7872 52.7872 23.7056 52.7872 52.7872c0 14.1312 11.4688 25.6 25.6 25.6s25.6-11.4688 25.6-25.6c0-57.344-46.6432-103.9872-103.9872-103.9872s-104.0384 46.6432-104.0384 103.9872c0 9.7792 1.4336 19.2 3.9936 28.16L431.616 433.0496c-22.4256-24.3712-54.5792-39.6288-90.2144-39.6288-67.6864 0-122.7264 55.04-122.7264 122.7264s55.04 122.7264 122.7264 122.7264c35.328 0 67.1744-15.0016 89.6-38.9632l140.4416 76.3904c3.8912 2.0992 8.0896 3.1232 12.1856 3.1232 9.0624 0 17.8688-4.8128 22.528-13.3632 6.7584-12.3904 2.1504-27.9552-10.2912-34.7136z m-254.464-43.6736c-39.4752 0-71.5264-32.1024-71.5264-71.5264 0-39.424 32.1024-71.5264 71.5264-71.5264 39.424 0 71.5264 32.1024 71.5264 71.5264 0 39.424-32.1024 71.5264-71.5264 71.5264z" fill="#F7F8F8" p-id="5848"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,406 @@
<template>
<div ref="containerRef" class="contribution-container">
<!-- 颜色图例 -->
<div class="legend">
<span style="color: #999999"></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 style="color: #999999"></span>
</div>
<!-- 主图表区域 -->
<div class="chart-wrapper">
<!-- 月份坐标轴 -->
<div class="month-axis" v-if="monthLabels.length">
<div
v-for="(month, index) in monthLabels"
:key="index"
class="month-label"
:style="{ left: month.position + '%' }"
>
{{ month.name }}
</div>
</div>
<!-- 贡献图主体 -->
<div class="chart-body">
<!-- 星期坐标轴 -->
<div class="weekday-axis">
<div v-for="(weekday, index) in weekdays" :key="index">{{ 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} from 'vue';
import {
format,
startOfWeek,
addDays,
eachDayOfInterval,
getMonth,
parseISO,
getYear
} from 'date-fns';
import {debounce} from 'lodash-es';
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+ 上传'
}
]
}
});
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(() => {
if (!containerRef.value) return;
const container = containerRef.value;
const containerWidth = container.offsetWidth - 60; // 增加边距
const containerHeight = container.offsetHeight - 80;
// 动态计算最大宽度
chartMaxWidth.value = containerWidth - 40;
// 重新计算单元格尺寸
const maxCellSize = Math.min(
(containerWidth - 40) / 54, // 更精确的计算
containerHeight / 7 - cellGap.value
);
cellSize.value = Math.max(8, Math.min(14, maxCellSize));
cellGap.value = Math.max(2, cellSize.value * 0.15);
}, 150);
// 日期有效性检查
const isValidDate = (date: Date) => {
return date instanceof Date && !isNaN(date.getTime());
};
// 生成标准的GitHub风格日期网格
const generateDateGrid = () => {
const today = new Date();
const startDate = startOfWeek(addDays(today, -358)); // 52周前
const endDate = today;
const weeksArray: Date[][] = [];
let currentWeekStart = startOfWeek(startDate, {weekStartsOn: 0}); // 从周日开始
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.slice(-52); // 严格52周
};
// 处理贡献数据
const processContributions = () => {
const contributionMap = new Map(
props.contributions
.filter(c => {
try {
const date = parseISO(c.date);
return isValidDate(date);
} 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 labels = new Map<string, MonthLabel>();
const totalWeeks = visibleWeeks.value.length;
visibleWeeks.value.forEach((week, weekIndex) => {
if (week.length === 0) return;
const firstDate = week[0].date;
if (!isValidDate(firstDate)) return;
const monthYear = `${getYear(firstDate)}-${getMonth(firstDate)}`;
if (!labels.has(monthYear)) {
labels.set(monthYear, {
name: format(firstDate, 'MMM'),
position: (weekIndex / totalWeeks) * 100
});
}
});
return Array.from(labels.values());
});
// 颜色匹配逻辑
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});
};
// 初始化
onMounted(() => {
visibleWeeks.value = processContributions();
updateSize();
const observer = new ResizeObserver(debounce(() => {
updateSize();
visibleWeeks.value = processContributions();
}, 200));
if (containerRef.value) observer.observe(containerRef.value);
onBeforeUnmount(() => observer.disconnect());
});
</script>
<style scoped>
.contribution-container {
position: relative;
width: 100%;
height: 100%;
min-width: 300px;
padding: 20px 15px; /* 调整左右padding */
box-sizing: border-box;
overflow-x: auto; /* 允许横向滚动 */
}
.legend {
position: absolute; /* 粘性定位 */
top: 10px;
right: 10px;
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);
margin-bottom: 15px; /* 增加底部间距 */
z-index: 2; /* 确保在图上层级 */
}
.legend-item {
display: flex;
& + & {
margin-left: 4px; /* 增加色块间距 */
}
}
.legend-block {
width: 16px;
height: 16px;
border-radius: 3px;
}
.chart-wrapper {
position: relative;
margin-top: 45px; /* 增加顶部间距 */
margin-left: 40px;
min-width: 520px; /* 最小宽度保证布局 */
}
.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;
}
.weekday-axis {
position: absolute;
left: -35px;
top: 0;
bottom: 0;
width: 30px;
display: flex;
flex-direction: column;
justify-content: space-between;
font-size: 10px;
color: #586069;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
}
.weeks-container {
display: flex;
flex-wrap: nowrap;
padding-right: 20px; /* 增加右侧padding */
}
/* 移动端适配 */
@media (max-width: 768px) {
.contribution-container {
padding: 15px 10px;
}
.legend {
top: 5px;
padding: 6px;
gap: 4px;
}
.legend-block {
width: 14px;
height: 14px;
}
.legend-text {
font-size: 0.8em;
}
.chart-wrapper {
margin-top: 35px;
margin-left: 30px;
min-width: 480px;
}
}
.week-column {
display: flex;
flex-direction: column;
}
.day-cell {
border-radius: 15%;
transition: all 0.2s ease;
flex-shrink: 0;
cursor: pointer;
}
.day-cell:hover {
transform: scale(1.15);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
z-index: 1;
}
</style>

View File

@@ -216,6 +216,11 @@ const editImages = async () => {
image.onload = () => {
imageStore.imageEditVisible = true;
};
image.onerror = () => {
message.warning("图片已过期,请刷新后重试");
imageStore.selected = [];
imageStore.imageEditVisible = false;
};
};
@@ -285,7 +290,6 @@ async function addImagesToAlbum(albumId: number) {
*/
function handleImageEditClose() {
imageStore.selected = [];
image.src = '';
imageStore.imageEditVisible = false;
}

View File

@@ -1,6 +0,0 @@
export const ProviderIcon = {
"ali": "/provider_icon/ali.svg",
"tencent": "/provider_icon/tencent.svg",
"minio": "/provider_icon/minio.svg",
"huawei": "/provider_icon/huawei.svg",
};

View File

@@ -0,0 +1,50 @@
export const ProviderIcon = {
"ali": "/provider_icon/ali.svg",
"tencent": "/provider_icon/tencent.svg",
"minio": "/provider_icon/minio.svg",
"huawei": "/provider_icon/huawei.svg",
};
export const ProviderNameMap = {
"ali": "阿里云",
"tencent": "腾讯云",
"minio": "MinIO",
"huawei": "华为云",
};
export const ProviderColorMap = {
'ali': 'orange',
'tencent': 'blue',
'minio': 'red',
'huawei': 'red',
};
export const AliRegionMap = {
"cn-hangzhou": "杭州",
"cn-shanghai": "上海",
"cn-qingdao": "青岛",
"cn-beijing": "北京",
"cn-zhangjiakou": "张家口",
"cn-huhehaote": "呼和浩特",
"cn-shenzhen": "深圳",
"cn-chengdu": "成都",
"cn-heyuan": "河源",
"cn-wulanchabu": "乌兰察布",
"cn-ningxia": "宁夏",
"cn-shaoxing": "绍兴",
"cn-jiaxing": "嘉兴",
"cn-zhengzhou": "郑州",
"cn-hongkong": "香港",
"ap-southeast-1": "新加坡",
"ap-southeast-2": "悉尼",
"ap-southeast-3": "吉隆坡",
"ap-southeast-5": "雅加达",
"ap-northeast-1": "东京",
"ap-south-1": "孟买",
"eu-west-1": "伦敦",
"eu-central-1": "法兰克福",
"us-west-1": "硅谷",
"us-east-1": "弗吉尼亚",
"us-east-2": "米兰",
"us-west-2": "弗吉尼亚",
"me-east-1": "迪拜",
};

View File

@@ -155,7 +155,7 @@ import wenhao from "@/assets/svgs/wenhao.svg";
import useStore from "@/store";
import ImageUpload from "@/components/ImageUpload/ImageUpload.vue";
import {getStorageConfigListApi} from "@/api/storage";
import {ProviderIcon} from "@/constant/provider_icon.ts";
import {ProviderIcon} from "@/constant/provider_map.ts";
const router = useRouter();

View File

@@ -9,6 +9,7 @@
:style="{borderRadius: borderRadius, boxShadow: boxShadow}"
:placeholder="searchStore.searchOption[0]? '#'+searchStore.searchOption[0] : '搜索...'"
@pressEnter="search"
:allowClear="true "
>
<template #suffix>
<AButton size="small" type="text" shape="circle" @click.prevent>
@@ -55,6 +56,38 @@
style="border-radius: 20px"
/>
</div>
<div class="header-search-picture" v-if="searchStore.searchOption[0] === 'picture'">
<AUpload
v-model:file-list="fileList"
name="avatar"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
:before-upload="beforeUpload"
:custom-request="customRequest"
>
<img v-if="iconUrl" :src="iconUrl" alt="avatar" style="width: 100%; height: 100%;border-radius: 8px"/>
<div v-else>
<loading-outlined v-if="loading"></loading-outlined>
<plus-outlined v-else></plus-outlined>
<div class="ant-upload-text">Upload</div>
</div>
</AUpload>
<div class="header-search-picture-info" v-if="fileInfo">
<AFlex :vertical="false" align="center" gap="small">
<span style="font-size: 13px; color: #999999">文件名:</span>
<span style="font-size: 16px; font-weight: bolder">{{ fileInfo.name }}</span>
</AFlex>
<AFlex :vertical="false" align="center" gap="small">
<span style="font-size: 13px; color: #999999">类型:</span>
<ATag>{{ fileInfo.type }}</ATag>
</AFlex>
<AFlex :vertical="false" align="center" gap="small">
<span style="font-size: 13px; color: #999999">大小:</span>
<ATag>{{ bytesToSize(fileInfo.size) }}</ATag>
</AFlex>
</div>
</div>
<!-- <AFlex :vertical="false" align="center" justify="space-between" class="header-search-content-header">-->
<!-- <span>搜索历史</span>-->
<!-- <AButton type="text" size="small" style="color: #707072">清空搜索历史</AButton>-->
@@ -79,6 +112,12 @@ import useStore from "@/store";
import 'dayjs/locale/zh-cn';
import dayjs, {Dayjs} from 'dayjs';
import {imageSearchApi} from "@/api/storage";
import {NSFWJS} from "nsfwjs";
import {initNSFWJs, predictNSFW} from "@/utils/tfjs/nsfw.ts";
import {message} from "ant-design-vue";
import i18n from "@/locales";
import {bytesToSize} from "@/utils/imageUtils/bytesToSize.ts";
import {cocoSsdPredict} from "@/utils/tfjs/mobilenet.ts";
dayjs.locale('zh-cn');
const borderRadius = ref('20px');
@@ -152,7 +191,6 @@ async function search() {
keyword: searchStore.searchValue,
provider: uploadStore.storageSelected?.[0],
bucket: uploadStore.storageSelected?.[1],
input_image: "123"
};
const res: any = await imageSearchApi(params);
if (res && res.code === 200) {
@@ -161,11 +199,44 @@ async function search() {
path: '/main/photo/search/list', query: {
type: searchStore.searchOption[0],
keyword: searchStore.searchValue,
provider: uploadStore.storageSelected?.[0],
bucket: uploadStore.storageSelected?.[1],
}
});
}
}
const fileList = ref([]);
const loading = ref<boolean>(false);
const iconUrl = ref<string>('');
const image = new Image();
const beforeUpload = async (file: any) => {
loading.value = true;
image.src = URL.createObjectURL(file);
// 图片 NSFW 检测
const nsfw: NSFWJS = await initNSFWJs();
const isNSFW: boolean = await predictNSFW(nsfw, image);
if (isNSFW) {
message.error(i18n.global.t('comment.illegalImage'));
return false;
}
image.onload = async () => {
};
const cocoResults: any = await cocoSsdPredict(image);
searchStore.searchValue = cocoResults.map(item => item.class).join(',');
iconUrl.value = URL.createObjectURL(file);
loading.value = false;
return true;
};
const fileInfo = ref<any>();
function customRequest(file: any) {
fileInfo.value = file.file;
}
</script>

View File

@@ -10,6 +10,8 @@ import {createPersistedStatePlugin} from 'pinia-plugin-persistedstate-2';
import VueDOMPurifyHTML from 'vue-dompurify-html';
import {registerDirectives} from "@/directives";
import VueCalendarMap from '@fcli/vue-calendar-map';
const pinia: Pinia = createPinia();
const installPersistedStatePlugin = createPersistedStatePlugin();
pinia.use((context) => installPersistedStatePlugin(context));
@@ -20,4 +22,5 @@ app.use(router);
app.use(i18n);
app.use(GoCaptcha);
app.use(VueDOMPurifyHTML);
app.use(VueCalendarMap);
app.mount('#app');

View File

@@ -1,13 +1,12 @@
import UserCenterHome from "@/views/User/PersonalCenter/components/UserCenterHome/UserCenterHome.vue";
import UserCenterDynamic from "@/views/User/PersonalCenter/components/UserCenterDynamic/UserCenterDynamic.vue";
import UserCenterInfo from "@/views/User/PersonalCenter/components/UserCenterInfo/UserCenterInfo.vue";
import UserCenterAnalysis from "@/views/User/PersonalCenter/components/UserCenterAnalysis/UserCenterAnalysis.vue";
import UserCenterShare from "@/views/User/PersonalCenter/components/UserCenterShare/UserCenterShare.vue";
import UserCenterSetting from "@/views/User/PersonalCenter/components/UserCenterSetting/UserCenterSetting.vue";
import AccountSettingHome from "@/views/User/AccountSetting/components/AccountSettingHome/AccountSettingHome.vue";
import AccountSettingInfo from "@/views/User/AccountSetting/components/AccountSettingInfo/AccountSettingInfo.vue";
import AccountSettingStorage from "@/views/User/AccountSetting/components/AccountSettingStorage/AccountSettingStorage.vue";
import AccountSettingStorage
from "@/views/User/AccountSetting/components/AccountSettingStorage/AccountSettingStorage.vue";
export default [
{
@@ -38,33 +37,6 @@ export default [
title: '动态'
},
},
{
path: '/main/user/center/info',
name: 'UserCenterInfo',
component: UserCenterInfo,
meta: {
requiresAuth: true,
title: '个人信息'
},
},
{
path: '/main/user/center/analysis',
name: 'UserCenterAnalysis',
component: UserCenterAnalysis,
meta: {
requiresAuth: true,
title: '数据分析'
},
},
{
path: '/main/user/center/share',
name: 'UserCenterShare',
component: UserCenterShare,
meta: {
requiresAuth: true,
title: '我的分享'
},
},
{
path: '/main/user/center/setting',
name: 'UserCenterSetting',

View File

@@ -0,0 +1,101 @@
import {createAlova} from 'alova';
import CryptoJS from 'crypto-js';
import VueHook from 'alova/vue';
import useStore from "@/store";
import {localforageStorageAdapter} from "@/utils/alova/adapter/localforageStorageAdapter.ts";
import {createServerTokenAuthentication} from "alova/client";
import {AxiosError, AxiosResponse} from "axios";
import {message, Modal} from "ant-design-vue";
import i18n from "@/locales";
import {axiosRequestAdapter} from "@alova/adapter-axios";
import {refreshToken} from "@/api/user";
import generateKeySecretSignature from "@/utils/signature/signature.ts";
import {handleErrorCode} from "@/utils/errorCode/errorCodeHandler.ts";
const {onAuthRequired, onResponseRefreshToken} = createServerTokenAuthentication<typeof VueHook,
typeof axiosRequestAdapter>({
refreshTokenOnSuccess: {
// 在请求前触发将接收到method参数并返回boolean表示token是否过期
isExpired: (response: AxiosResponse<any, any>, _method: any) => {
const code = response.data.code;
return code === 401;
},
// 当token过期时触发在此函数中触发刷新token
handler: async () => {
// 刷新token
const user = useStore().user;
const res: any = await refreshToken();
if (res && res.code === 200) {
const {access_token, expire_at} = res.data;
user.token.accessToken = access_token;
user.token.expireAt = expire_at;
}
}
}
});
export const service = createAlova({
timeout: 10000,
baseURL: import.meta.env.VITE_APP_BASE_API,
statesHook: VueHook,
// 请求适配器
requestAdapter: axiosRequestAdapter(),
l2Cache: localforageStorageAdapter,
cacheLogger: import.meta.env.VITE_NODE_ENV === 'development',
cacheFor: null,
// 设置全局的请求拦截器
beforeRequest: onAuthRequired(async (method: any) => {
// if (!method.meta?.ignoreToken) {
// const user = useStore().user;
// method.config.headers.Authorization = `${import.meta.env.VITE_APP_TOKEN_KEY} ${user.token.accessToken}`;
// method.config.headers['X-UID'] = user.user.uid;
// // method.config.headers['X-Expire-At'] = user.token.expireAt;
// }
const lang = useStore().lang;
method.config.headers['Accept-Language'] = lang.lang || 'zh';
// 令牌
method.config.headers['X-Nonce'] = CryptoJS.lib.WordArray.random(16).toString();
// 签名
if (method.meta?.signature) {
method.config.headers['X-Content-Security'] = generateKeySecretSignature(0, method.type, method.url, method.config.params, method.data);
}
}),
// 响应拦截器
responded: onResponseRefreshToken({
onSuccess: async (response: AxiosResponse, _method: any) => {
if (response.data instanceof Blob) {
return response;
}
const {code} = response.data;
if (code === 403) {
localStorage.removeItem('user');
Modal.warning({
title: i18n.global.t('error.loginExpired'),
content: i18n.global.t('error.authTokenExpired'),
onOk() {
setTimeout(() => {
window.location.href = '/login';
}, 1000);
},
});
return Promise.reject();
}
return response.data;
},
onError:
(error: AxiosError, _method: any) => {
const {response} = error;
if (response) {
handleErrorCode(response.status);
}
if (!window.navigator.onLine) {
message.error(i18n.global.t('error.networkError')).then();
return Promise.reject(error);
}
},
}),
});

View File

@@ -3,7 +3,7 @@
<div class="upload-controller">
<AButton type="text" shape="circle" size="middle">
<template #icon>
<APopover placement="bottom" trigger="click">
<APopover placement="bottomRight" trigger="click">
<template #content>
<UploadSetting/>
</template>
@@ -66,7 +66,7 @@
</template>
<script setup lang="ts">
import setting from "@/assets/svgs/setting.svg";
import {albumListApi, uploadFile} from "@/api/storage";
import {albumListApi, uploadFile} from "@/api/phone";
import useStore from "@/store";
import empty from "@/assets/svgs/empty.svg";
import {useRequest} from "alova/client";
@@ -74,21 +74,21 @@ import imageCompression from "browser-image-compression";
import {generateThumbnail} from "@/utils/imageUtils/generateThumb.ts";
import UploadSetting from "@/components/ImageUpload/UploadSetting.vue";
// const route = useRoute();
const route = useRoute();
const fileList = ref([]);
const predicting = ref<boolean>(false);
const progressPercent = ref<number>(0);
const progressStatus = ref<string>('active');
// const accessToken = computed(() => {
// const token = route.query.token;
// return Array.isArray(token) ? token[0] : token;
// });
// const userId = computed(() => {
// const uid = route.query.user_id;
// return Array.isArray(uid) ? uid[0] : uid;
// });
const accessToken = computed(() => {
const token = route.query.token;
return Array.isArray(token) ? token[0] : token;
});
const userId = computed(() => {
const uid = route.query.user_id;
return Array.isArray(uid) ? uid[0] : uid;
});
const upload = useStore().upload;
@@ -100,8 +100,8 @@ const {send: getAlbumList} = useRequest(albumListApi, {
immediate: false,
debounce: 500,
}).onSuccess((res: any) => {
if (res && res.code === 200) {
albumList.value = res.data.albums;
if (res.data && res.data.code === 200) {
albumList.value = res.data.data.albums;
}
});
@@ -142,7 +142,10 @@ async function customUploadRequest(file: any) {
}
},
);
submitFile(formData).then((response: any) => {
submitFile(formData, {
'Authorization': `Bearer ${accessToken.value}`,
'X-UID': userId.value,
}).then((response: any) => {
if (response && response.code === 200) {
file.onSuccess(response.data, file);
} else {
@@ -153,7 +156,10 @@ async function customUploadRequest(file: any) {
onMounted(() => {
getAlbumList(0, true);
getAlbumList(0, true, {
'Authorization': `Bearer ${accessToken.value}`,
'X-UID': userId.value,
});
});

View File

@@ -24,10 +24,29 @@ import useStore from "@/store";
import ImageToolbar from "@/components/ImageToolbar/ImageToolbar.vue";
import ImageWaterfallList from "@/components/ImageWaterfallList/ImageWaterfallList.vue";
import {imageSearchApi} from "@/api/storage";
const searchStore = useStore().search;
const imageStore = useStore().image;
const router = useRouter();
const route = useRoute();
async function search() {
const params: any = {
type: route.query.type as string,
keyword: route.query.keyword as string,
provider: route.query.provider as string,
bucket: route.query.bucket as string,
};
const res: any = await imageSearchApi(params);
if (res && res.code === 200) {
searchStore.searchResult = res.data.records;
}
}
onMounted(() => {
search();
});
onBeforeUnmount(() => {
searchStore.searchResult = [];
});

View File

@@ -1,11 +1,212 @@
<template>
<div class="account-setting-home">
<div class="account-setting-home-info">
<div class="account-setting-home-info-avatar">
<AAvatar :size="80" :src="userStore.user.avatar"/>
</div>
<div class="account-setting-home-info-content">
<AFlex :vertical="false" align="center" justify="space-between" gap="small">
<span class="account-setting-home-info-content-name">{{ userStore.user.nickname }}</span>
<ATag color="success" :bordered="false">正式会员</ATag>
<img src="/level_icon/icon/lv1.png" style="width: 40px" alt="level">
</AFlex>
<AProgress :percent="50" :stroke-color="{
'0%': '#108ee9',
'100%': '#87d068',
}" :showInfo="false" style="width: 500px" :size="15"/>
<AFlex :vertical="false" align="flex-start" justify="space-between" style="width: 500px">
<img src="/level_icon/icon/lv1.png" class="avatar-level-icon" alt="level">
<img src="/level_icon/icon/lv2.png" class="avatar-level-icon" alt="level">
</AFlex>
</div>
</div>
<ADivider/>
<div class="account-setting-home-content">
<div class="account-setting-home-content-header">
<AAvatar size="default" :src="accountSecurity"/>
<span style="font-size: 20px; font-weight: bold;color: #333">账号安全</span>
</div>
<div class="account-setting-home-content-section">
<AFlex :vertical="false" align="center" justify="space-between" :gap="50">
<div class="account-setting-home-content-section-item">
<div class="account-setting-home-content-section-item-avatar">
<AAvatar shape="circle" :size="80" :src="emailSecurity"/>
</div>
<div class="account-setting-home-content-section-item-content">
<span style="font-size: 18px; font-weight: bold;color: #333;margin-top: 10px">我的邮箱</span>
<span style="font-size: 14px; color: #666;">绑定邮箱后即可使用邮箱登录</span>
<AFlex :vertical="false" align="center" justify="space-between" gap="large">
<AButton type="primary" size="small" :disabled="true">已绑定邮箱</AButton>
<AButton type="link" size="small">更改邮箱</AButton>
</AFlex>
</div>
</div>
<div class="account-setting-home-content-section-item">
<div class="account-setting-home-content-section-item-avatar">
<AAvatar shape="circle" :size="80" :src="phoneSecurity"/>
</div>
<div class="account-setting-home-content-section-item-content">
<span style="font-size: 18px; font-weight: bold;color: #333;margin-top: 10px">我的手机</span>
<span style="font-size: 14px; color: #666;">绑定手机后即可使用手机号登录</span>
<AFlex :vertical="false" align="center" justify="space-between" gap="large">
<AButton type="primary" size="small" :disabled="true">已绑定手机</AButton>
<AButton type="link" size="small">更改手机</AButton>
</AFlex>
</div>
</div>
</AFlex>
<AFlex :vertical="false" align="center" justify="space-between" :gap="50">
<div class="account-setting-home-content-section-item">
<div class="account-setting-home-content-section-item-avatar">
<AAvatar shape="circle" :size="80" :src="passwordSecurity"/>
</div>
<div class="account-setting-home-content-section-item-content">
<span style="font-size: 18px; font-weight: bold;color: #333;margin-top: 10px">我的密保</span>
<span style="font-size: 14px; color: #666;">设置密保账号更安全</span>
<AFlex :vertical="false" align="center" justify="space-between" gap="large">
<AButton type="primary" size="small" :disabled="true">已绑定密保</AButton>
<AButton type="link" size="small">查看并设置</AButton>
</AFlex>
</div>
</div>
<div class="account-setting-home-content-section-item">
<div class="account-setting-home-content-section-item-avatar">
<AAvatar shape="circle" :size="80" :src="loginSecurity"/>
</div>
<div class="account-setting-home-content-section-item-content">
<span style="font-size: 18px; font-weight: bold;color: #333;margin-top: 10px">三方登录</span>
<span style="font-size: 14px; color: #666;">绑定三方账号安全登录</span>
<AFlex :vertical="false" align="center" justify="space-between" gap="large">
<AButton type="primary" size="small" :disabled="true">已绑定三方账号</AButton>
<AButton type="link" size="small">取消绑定</AButton>
</AFlex>
</div>
</div>
</AFlex>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import useStore from "@/store";
import accountSecurity from "@/assets/svgs/account_security.svg";
import emailSecurity from "@/assets/svgs/email_security.svg";
import phoneSecurity from "@/assets/svgs/phone_security.svg";
import passwordSecurity from "@/assets/svgs/password_security.svg";
import loginSecurity from "@/assets/svgs/login_security.svg";
const userStore = useStore().user;
</script>
<template>
666
</template>
<style scoped lang="scss">
.account-setting-home {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
.account-setting-home-info {
width: 100%;
height: 150px;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
padding-top: 40px;
.account-setting-home-info-avatar {
width: 130px;
height: 100px;
display: flex;
justify-content: center;
align-items: flex-start;
}
.account-setting-home-info-content {
width: calc(100% - 100px);
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
gap: 10px;
.account-setting-home-info-content-name {
font-size: 24px;
font-weight: bold;
}
.avatar-level-icon {
width: 40px;
}
}
}
.account-setting-home-content {
width: 100%;
height: 70%;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
gap: 20px;
.account-setting-home-content-header {
width: 100%;
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 10px;
}
.account-setting-home-content-section {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
flex-wrap: wrap;
gap: 20px;
.account-setting-home-content-section-item {
width: 330px;
height: 100px;
background-color: #fff;
border-radius: 10px;
padding: 10px;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
gap: 10px;
.account-setting-home-content-section-item-avatar {
width: 100px;
height: 100%;
display: flex;
justify-content: center;
align-items: flex-start;
}
.account-setting-home-content-section-item-content {
width: 300px;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
gap: 10px;
}
}
}
}
}
</style>

View File

@@ -1,11 +1,163 @@
<script setup lang="ts">
</script>
<template>
<div class="account-setting-info">
<div class="account-setting-info-header">
<span style="font-size: 20px;font-weight: bold;color: #333333;">我的信息</span>
</div>
<div class="account-setting-info-body">
<AForm :model="formState" layout="vertical">
<AFormItem label="用户名" name="username">
<AInput v-model:value="formState.username" placeholder="请输入用户名" :clearable="true">
<template #prefix>
<UserOutlined/>
</template>
</AInput>
</AFormItem>
<AFormItem label="昵称" name="nickname">
<AInput v-model:value="formState.nickname" placeholder="请输入昵称">
<template #prefix>
<SmileOutlined/>
</template>
</AInput>
</AFormItem>
<AFormItem label="邮箱" name="email">
<AInput v-model:value="formState.email" placeholder="请输入邮箱">
<template #prefix>
<MailOutlined/>
</template>
</AInput>
</AFormItem>
<AFormItem label="电话" name="phone">
<AInput v-model:value="formState.phone" placeholder="请输入电话">
<template #prefix>
<PhoneOutlined/>
</template>
</AInput>
</AFormItem>
<AFormItem label="性别" name="gender">
<ASelect v-model:value="formState.gender" placeholder="请选择性别">
<template #prefix>
<ManOutlined/>
</template>
<ASelectOption :value="0">未知</ASelectOption>
<ASelectOption :value="1"></ASelectOption>
<ASelectOption :value="2"></ASelectOption>
</ASelect>
</AFormItem>
<AFormItem label="介绍" name="introduce">
<ATextarea v-model:value="formState.introduce" placeholder="请输入个人介绍" :rows="4">
<template #prefix>
<EditOutlined/>
</template>
</ATextarea>
</AFormItem>
<AFormItem label="博客" name="blog">
<AInput v-model:value="formState.blog" placeholder="请输入博客地址">
<template #prefix>
<GlobalOutlined/>
</template>
</AInput>
</AFormItem>
<AFormItem label="地址" name="location">
<AInput v-model:value="formState.location" placeholder="请输入地址">
<template #prefix>
<EnvironmentOutlined/>
</template>
</AInput>
</AFormItem>
<AFormItem label="公司" name="company">
<AInput v-model:value="formState.company" placeholder="请输入公司">
<template #prefix>
<BankOutlined/>
</template>
</AInput>
</AFormItem>
<AFormItem :wrapper-col="{ span: 24 }">
<div class="form-actions">
<AButton type="primary" @click="handleSubmit">保存</AButton>
</div>
</AFormItem>
</AForm>
</div>
</div>
<ADivider/>
</template>
<style scoped lang="scss">
<script setup lang="ts">
import {
UserOutlined,
SmileOutlined,
MailOutlined,
PhoneOutlined,
ManOutlined,
EditOutlined,
GlobalOutlined,
EnvironmentOutlined,
BankOutlined,
} from '@ant-design/icons-vue';
const formState = reactive({
username: '',
nickname: '',
email: '',
phone: '',
gender: 0,
introduce: '',
blog: '',
location: '',
company: '',
});
const handleSubmit = () => {
// 提交表单逻辑
console.log('表单数据:', formState);
};
</script>
<style scoped lang="scss">
.account-setting-info {
width: 100%;
height: 100%;
overflow: auto;
.account-setting-info-header {
padding: 0 24px;
width: 100%;
height: 50px;
display: flex;
align-items: center;
border-bottom: 1px solid #e5e5e5;
}
.account-setting-info-body {
padding: 24px;
width: 100%;
height: calc(100% - 50px);
:deep(.ant-form) {
max-width: 800px;
.ant-form-item {
margin-bottom: 16px;
.ant-input,
.ant-input-password,
.ant-select-selector {
border-radius: 8px;
}
}
}
.form-actions {
display: flex;
justify-content: center;
margin-top: 24px;
.ant-btn {
width: 120px;
height: 40px;
border-radius: 8px;
}
}
}
}
</style>

View File

@@ -47,6 +47,9 @@ const menuCSSStyle: any = reactive({
});
const router = useRouter();
// const route = useRoute();
function handleClick({key}) {
menuStore.accountSettingMenu = key;
router.push(`/main/user/setting/${key}`);

View File

@@ -1,11 +1,209 @@
<template>
<div class="account-setting-storage">
<div class="account-setting-storage-header">
<AButton type="primary" size="middle" shape="default" @click="drawerVisible = true">
新增存储
</AButton>
<AButton type="default" size="middle" shape="circle" @click="getStorageList">
<template #icon>
<RedoOutlined />
</template>
</AButton>
</div>
<div class="account-setting-storage-body" v-if="storageList && storageList.length>0">
<StorageCard v-for="(item, index) in storageList" :key="index" :storage="item"/>
</div>
<div class="account-setting-storage-empty" v-else>
<AEmpty description="暂无存储策略"/>
</div>
<ADrawer v-model:open="drawerVisible" placement="right" width="40%" title="新增存储策略">
<AForm :model="formState" layout="vertical" @finish="onFinish" :rules="rules" ref="formRef"
class="two-col-form">
<AFormItem label="存储商" name="provider" class="form-item">
<ASelect v-model:value="formState.provider" placeholder="请选择存储商">
<ASelectOption v-for="provider in providers" :key="provider.value" :value="provider.value">
{{ provider.label }}
</ASelectOption>
</ASelect>
</AFormItem>
<AFormItem label="地址" name="endpoint" class="form-item">
<AInput v-model:value="formState.endpoint" placeholder="请输入地址">
<template #addonBefore>
<ASelect v-model:value="protocol" style="width: 90px">
<ASelectOption value="http://">Http://</ASelectOption>
<ASelectOption value="https://">Https://</ASelectOption>
</ASelect>
</template>
</AInput>
</AFormItem>
<AFormItem label="密钥Key" name="access_key" class="form-item">
<AInput v-model:value="formState.access_key" placeholder="请输入密钥Key"/>
</AFormItem>
<AFormItem label="密钥Secret" name="secret_key" class="form-item">
<AInputPassword v-model:value="formState.secret_key" placeholder="请输入密钥Secret"/>
</AFormItem>
<AFormItem label="存储桶" name="bucket" class="form-item">
<AInput v-model:value="formState.bucket" placeholder="请输入存储桶名称"/>
</AFormItem>
<AFormItem label="地域" name="region" class="form-item">
<ASelect v-model:value="formState.region" placeholder="请选择地域" :disabled="!formState.provider">
<ASelectOption
v-for="region in regionsByProvider[formState.provider] || []"
:key="region.value"
:value="region.value"
>
{{ region.label }}
</ASelectOption>
</ASelect>
</AFormItem>
<AFormItem label="容量" name="capacity" class="form-item">
<AInputNumber style="width: 100%;" v-model:value="formState.capacity" placeholder="请输入容量"/>
</AFormItem>
</AForm>
<template #footer>
<AFlex :vertical="false" align="center" justify="end" gap="large">
<AButton type="default" @click="cancel">取消</AButton>
<AButton type="primary" @click="onFinish()">提交</AButton>
</AFlex>
</template>
</ADrawer>
</div>
</template>
<script setup lang="ts">
import StorageCard from "@/views/User/AccountSetting/components/AccountSettingStorage/StorageCard.vue";
import {addStorageConfigApi, listUserStorageConfigApi} from "@/api/storage";
import {message} from "ant-design-vue";
const drawerVisible = ref(false);
const protocol = ref('https://');
const rules = {
provider: [{required: true, message: '存储商不能为空'}],
bucket: [{required: true, message: '存储桶不能为空'}],
endpoint: [{required: true, message: '地址不能为空'}],
access_key: [{required: true, message: '密钥Key不能为空'}],
secret_key: [{required: true, message: '密钥Secret不能为空'}],
region: [{required: true, message: '地域不能为空'}],
capacity: [{required: true, message: '容量不能为空'}],
};
const providers = [
{value: 'ali', label: '阿里云OSS'},
{value: 'tencent', label: '腾讯云COS'},
{value: 'minio', label: 'Minio'},
{value: 'huawei', label: '华为云OBS'},
];
const regionsByProvider = {
ali: [
{value: 'oss-cn-hangzhou', label: '华东1杭州'},
{value: 'oss-cn-shanghai', label: '华东2上海'},
{value: 'oss-cn-qingdao', label: '华北1青岛'},
{value: 'oss-cn-shenzhen', label: '华南1深圳'},
],
tencent: [
{value: 'ap-beijing', label: '北京'},
{value: 'ap-shanghai', label: '上海'},
{value: 'ap-guangzhou', label: '广州'},
{value: 'ap-chengdu', label: '成都'},
],
huawei: [
{value: 'cn-north-1', label: '华北-北京一'},
{value: 'cn-east-3', label: '华东-上海一'},
{value: 'cn-south-1', label: '华南-广州'},
],
minio: [
{value: 'us-east-1', label: '默认地域'},
{value: 'custom', label: '自定义地域'},
],
};
const formRef = ref();
const formState = ref({
provider: '',
bucket: '',
endpoint: '',
access_key: '',
secret_key: '',
region: '',
capacity: null,
});
const onFinish = async () => {
const valid = await formRef.value.validate();
if (!valid) {
return;
}
const res: any = await addStorageConfigApi(formState.value);
if (res && res.code === 200) {
await getStorageList();
drawerVisible.value = false;
formRef.value.resetFields();
} else {
message.warn("新增存储策略失败");
}
};
function cancel() {
formRef.value.resetFields();
drawerVisible.value = false;
};
const storageList = ref<any[]>([]);
// 获取存储列表
async function getStorageList() {
const res: any = await listUserStorageConfigApi();
if (res && res.code === 200) {
storageList.value = res.data.records;
}
}
onMounted(() => {
getStorageList();
});
</script>
<template>
</template>
<style scoped lang="scss">
.account-setting-storage {
display: flex;
flex-direction: column;
.account-setting-storage-header {
width: 100%;
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 20px;
}
.account-setting-storage-body {
width: 100%;
height: calc(100vh - 150px);
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: flex-start;
align-content: flex-start;
justify-content: flex-start;
gap: 20px;
}
.account-setting-storage-empty {
width: 100%;
height: calc(100vh - 150px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
}
.two-col-form {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
</style>

View File

@@ -0,0 +1,148 @@
<template>
<div class="account-setting-storage-card">
<div class="account-setting-storage-card-header">
<div style="width: 60px; height: 60px;">
<AAvatar :size="60" shape="circle" :src="ProviderIcon[storage.provider]"/>
</div>
<AFlex :vertical="true" align="flex-start" justify="space-between"
style="height: 60px;width: 230px;">
<div style="height: 60px;width: 230px;overflow: auto">
<span
style="font-size: 18px; font-weight: bold;">{{ storage.bucket }}</span>
</div>
<AFlex :vertical="false" align="center" justify="flex-start" style="height: 60px;width: 230px;overflow: auto">
<ATag :color="ProviderColorMap[storage.provider]">{{ ProviderNameMap[storage.provider] }}</ATag>
<ATag v-if="storage.endpoint">{{ storage.endpoint }}</ATag>
</AFlex>
</AFlex>
<APopconfirm
title="确认删除?"
ok-text="确认"
cancel-text="取消"
placement="bottomRight"
@confirm="deleteStorage(storage.id, storage.provider, storage.bucket)"
>
<template #icon>
<question-circle-outlined style="color: red"/>
</template>
<AButton type="text" size="small" class="delete-icon"
>
<template #icon>
<AAvatar size="small" shape="circle" :src="deleted"/>
</template>
</AButton>
</APopconfirm>
</div>
<div class="account-setting-storage-card-content">
<div class="account-setting-storage-card-content-item">
<AAvatar size="small" shape="square" :src="bucket"/>
<span style="color: #999999; font-size: 14px;">{{ storage.capacity }}GB</span>
</div>
<div class="account-setting-storage-card-content-item">
<AAvatar size="small" shape="circle" :src="location"/>
<span style="color: #999999; font-size: 14px;">{{ AliRegionMap[storage.region] }}</span>
</div>
<div class="account-setting-storage-card-content-item">
<AAvatar size="small" shape="circle" :src="time"/>
<span style="color: #999999; font-size: 14px;">{{ storage.created_at }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {defineProps} from 'vue';
import bucket from "@/assets/svgs/bucket.svg";
import time from "@/assets/svgs/time.svg";
import location from "@/assets/svgs/location-album.svg";
import deleted from "@/assets/svgs/deleted-circle.svg";
import {message} from "ant-design-vue";
import {deleteStorageConfigApi} from "@/api/storage";
import {AliRegionMap, ProviderColorMap, ProviderIcon, ProviderNameMap} from "@/constant/provider_map.ts";
defineProps({
storage: {
type: Object,
required: true
}
});
/**
* 删除存储配置
*/
async function deleteStorage(id: number, provider: string, bucket: string) {
const res: any = await deleteStorageConfigApi(id, provider, bucket);
if (res && res.code === 200) {
message.success("删除成功");
} else {
message.error("删除失败");
}
}
</script>
<style scoped lang="scss">
.account-setting-storage-card {
width: 300px;
height: 100px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
border-radius: 10px;
background-color: white;
padding: 10px;
cursor: pointer;
position: relative;
transition: all 0.2s ease-in-out;
&:hover {
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
}
&:hover .delete-icon {
opacity: 1;
transform: scale(1);
}
.account-setting-storage-card-header {
width: 100%;
height: 70px;
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: flex-start;
gap: 10px;
}
.delete-icon {
transform: scale(0);
transition: all 0.3s ease-in-out;
opacity: 0;
position: absolute;
border-radius: 50%;
top: -10px;
right: -10px;
font-size: 15px;
cursor: pointer;
}
.account-setting-storage-card-content {
width: 100%;
height: 30px;
display: flex;
flex-direction: row;
align-items: flex-end;
justify-content: space-between;
.account-setting-storage-card-content-item {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 10px;
}
}
}
</style>

View File

@@ -34,24 +34,6 @@
</template>
<span class="ant-menu-item-title">动态</span>
</AMenuItem>
<AMenuItem key="info" :style="menuCSSStyle" title="个人信息">
<template #icon>
<AAvatar shape="square" size="small" :src="personal_info"/>
</template>
<span class="ant-menu-item-title">个人信息</span>
</AMenuItem>
<AMenuItem key="analysis" :style="menuCSSStyle" title="数据分析">
<template #icon>
<AAvatar shape="square" size="small" :src="data_analysis"/>
</template>
<span class="ant-menu-item-title">数据分析</span>
</AMenuItem>
<AMenuItem key="share" :style="menuCSSStyle" title="我的分享">
<template #icon>
<AAvatar shape="square" size="small" :src="share"/>
</template>
<span class="ant-menu-item-title">我的分享</span>
</AMenuItem>
<AMenuItem key="setting" :style="menuCSSStyle" title="设置">
<template #icon>
<AAvatar shape="square" size="small" :src="setting"/>
@@ -60,7 +42,8 @@
</AMenuItem>
</AMenu>
<div class="personal-center-content-container">
<router-view/>
<router-view>
</router-view>
</div>
</div>
</div>
@@ -69,11 +52,8 @@
import Header from "@/layout/default/Header/Header.vue";
import useStore from "@/store";
import home from "@/assets/svgs/home.svg";
import data_analysis from "@/assets/svgs/data_analysis.svg";
import dynamic from "@/assets/svgs/dynamic.svg";
import share from "@/assets/svgs/share.svg";
import setting from "@/assets/svgs/setting.svg";
import personal_info from "@/assets/svgs/personal-center.svg";
const userStore = useStore().user;
@@ -92,7 +72,7 @@ function handleClick({key}) {
</script>
<style scoped lang="scss">
.personal-center {
background-color: #eaeef6;
//background-color: #eaeef6;
.personal-center-header {
width: 100%;
@@ -169,9 +149,10 @@ function handleClick({key}) {
}
.personal-center-content-container {
width: 100%;
height: calc(100vh - 250px);
display: flex;
width: calc(100% - 40px);
height: calc(100vh - 290px);
padding: 20px;
overflow: auto;
}
}
}

View File

@@ -1,11 +0,0 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped lang="scss">
</style>

View File

@@ -1,11 +1,307 @@
<template>
<div class="user-center-dynamic" ref="chartRef">
</div>
</template>
<script setup lang="ts">
import * as echarts from 'echarts';
const chartRef = ref<any>(null);
onMounted(async () => {
await nextTick();
const chartInstance = echarts.init(chartRef.value);
const colorList = ["#9E87FF", "#73DDFF", "#fe9a8b", "#F56948", "#9E87FF"];
const option = {
backgroundColor: "#fff",
title: {
text: "最近七天分享统计",
textStyle: {
fontSize: 12,
fontWeight: 400,
},
left: "center",
top: "5%",
},
legend: {
icon: "circle",
top: "5%",
right: "5%",
itemWidth: 6,
itemGap: 20,
textStyle: {
color: "#556677",
},
},
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",
textStyle: {
color: "#5c6c7c",
},
padding: [10, 10],
extraCssText: "box-shadow: 1px 0 2px 0 rgba(163,163,163,0.5)",
},
grid: {
top: "15%",
},
xAxis: [
{
type: "category",
data: ["2025-3-5", "2025-3-6", "2025-3-7", "2025-3-8", "2025-3-9", "2025-3-10", "2025-3-11"],
axisLine: {
lineStyle: {
color: "#DCE2E8",
},
},
axisTick: {
show: false,
},
axisLabel: {
interval: 0,
textStyle: {
color: "#556677",
},
// 默认x轴字体大小
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
},
},
},
boundaryGap: false,
},
],
yAxis: [
{
type: "value",
axisTick: {
show: false,
},
axisLine: {
show: true,
lineStyle: {
color: "#DCE2E8",
},
},
axisLabel: {
textStyle: {
color: "#556677",
},
},
splitLine: {
show: false,
},
},
{
type: "value",
position: "right",
axisTick: {
show: false,
},
axisLabel: {
textStyle: {
color: "#556677",
},
formatter: "{value}",
},
axisLine: {
show: true,
lineStyle: {
color: "#DCE2E8",
},
},
splitLine: {
show: false,
},
},
],
series: [
{
name: "浏览次数",
type: "line",
data: [10, 10, 30, 12, 15, 3, 7],
symbolSize: 1,
symbol: "circle",
smooth: true,
yAxisIndex: 0,
showSymbol: false,
lineStyle: {
width: 5,
color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
{
offset: 0,
color: "#9effff",
},
{
offset: 1,
color: "#9E87FF",
},
]),
shadowColor: "rgba(158,135,255, 0.3)",
shadowBlur: 10,
shadowOffsetY: 20,
},
itemStyle: {
normal: {
color: colorList[0],
borderColor: colorList[0],
},
},
},
{
name: "浏览人数",
type: "line",
data: [5, 12, 11, 14, 25, 16, 10],
symbolSize: 1,
symbol: "circle",
smooth: true,
yAxisIndex: 0,
showSymbol: false,
lineStyle: {
width: 5,
color: new echarts.graphic.LinearGradient(1, 1, 0, 0, [
{
offset: 0,
color: "#73DD39",
},
{
offset: 1,
color: "#73DDFF",
},
]),
shadowColor: "rgba(115,221,255, 0.3)",
shadowBlur: 10,
shadowOffsetY: 20,
},
itemStyle: {
normal: {
color: colorList[1],
borderColor: colorList[1],
},
},
},
{
name: "发布次数",
type: "line",
data: [150, 120, 170, 140, 500, 160, 110],
symbolSize: 1,
yAxisIndex: 1,
symbol: "circle",
smooth: true,
showSymbol: false,
lineStyle: {
width: 5,
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{
offset: 0,
color: "#fe9a",
},
{
offset: 1,
color: "#fe9a8b",
},
]),
shadowColor: "rgba(254,154,139, 0.3)",
shadowBlur: 10,
shadowOffsetY: 20,
},
itemStyle: {
normal: {
color: colorList[2],
borderColor: colorList[2],
},
},
},
],
};
chartInstance.setOption(option);
});
onBeforeUnmount(() => {
if (chartRef.value) {
const chartInstance = echarts.getInstanceByDom(chartRef.value);
if (chartInstance) {
chartInstance.dispose();
}
}
});
</script>
<template>
</template>
<style scoped lang="scss">
.user-center-dynamic {
width: calc(100vw - 40px);
height: calc(100vh - 290px);
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

@@ -1,11 +1,256 @@
<template>
<div class="user-center-home">
<div class="user-center-home-left">
<div class="user-center-home-left-top">
<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">50</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">+10</span>
</div>
</div>
<div class="user-center-home-left-top-card"
style="background: linear-gradient(101.63deg, rgb(82, 138, 250) -12.83%, rgb(122, 167, 255) 100%);">
<div class="user-center-home-left-top-card-top">
<div class="user-center-home-left-top-card-top-avatar">
<AAvatar :size="60" shape="square" :src="shareIcon"/>
</div>
<div class="user-center-home-left-top-card-top-name">
<span style="font-size: 2.8vh;color: rgba(255, 255, 255, 0.6); font-weight: bold;">分享总数</span>
<span style="font-size: 3.8vh;font-weight: bold;color: white">50</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">+10</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">50</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">+10</span>
</div>
</div>
</div>
<div class="user-center-home-left-bottom">
<span style="font-size: 16px; font-weight: bold; margin-left: 20px;">文件上传热力图</span>
<HeatmapPro :contributions="timeValue" :width="'100%'" :height="'100%'"/>
</div>
</div>
<div class="user-center-home-right">
<ACard class="user-center-home-right-card" :hoverable="false">
</ACard>
</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";
const timeValue = [
{date: "2024-08-02", count: 1},
{date: "2024-08-03", count: 2},
{date: "2024-08-04", count: 3},
{date: "2024-08-05", count: 4},
{date: "2024-08-06", count: 5},
{date: "2024-08-07", count: 6},
{date: "2024-08-08", count: 5},
{date: "2024-08-15", count: 8},
{date: "2024-08-22", count: 3},
{date: "2024-08-29", count: 4},
{date: "2024-09-05", count: 6},
{date: "2024-09-28", count: 6},
{date: "2024-09-22", count: 6},
{date: "2024-09-23", count: 6},
{date: "2024-09-24", count: 6},
{date: "2024-10-04", count: 6},
{date: "2024-10-02", count: 6},
{date: "2024-10-10", count: 6},
{date: "2024-10-11", count: 6},
{date: "2024-10-17", count: 6},
{date: "2024-10-19", count: 6},
{date: "2024-10-23", count: 6},
{date: "2024-10-27", count: 6},
{date: "2024-10-28", count: 6},
{date: "2024-10-29", count: 6},
{date: "2024-11-22", count: 6},
{date: "2024-11-30", count: 6},
{date: "2024-12-08", count: 6},
{date: "2024-12-16", count: 6},
{date: "2024-12-24", count: 6},
{date: "2025-01-01", count: 6},
{date: "2025-01-09", count: 6},
{date: "2025-01-16", count: 6},
{date: "2025-01-22", count: 6},
{date: "2025-01-28", count: 6},
{date: "2025-02-03", count: 6},
{date: "2025-02-09", count: 6},
{date: "2025-02-15", count: 6},
{date: "2025-02-21", count: 6},
{date: "2025-03-21", count: 6},
{date: "2025-03-22", count: 6},
{date: "2025-03-23", count: 6},
{date: "2025-03-24", count: 6},
{date: "2025-03-25", count: 6},
{date: "2025-03-26", count: 6},
{date: "2025-03-27", count: 6},
{date: "2025-03-28", count: 6},
{date: "2025-03-31", count: 6},
{date: "2025-04-03", count: 6},
{date: "2025-04-07", count: 6},
{date: "2025-04-04", count: 6},
{date: "2025-04-10", count: 6},
{date: "2025-04-11", count: 6},
{date: "2025-04-14", count: 6},
{date: "2025-04-17", count: 6},
{date: "2025-04-18", count: 6},
{date: "2025-04-21", count: 6},
{date: "2025-04-24", count: 6},
{date: "2025-04-25", count: 6},
{date: "2025-04-28", count: 6},
{date: "2025-05-01", count: 6},
{date: "2025-05-02", count: 6},
{date: "2025-05-05", count: 6},
{date: "2025-05-08", count: 6},
{date: "2025-05-09", count: 6},
{date: "2025-05-12", count: 6},
{date: "2025-05-15", count: 6},
];
</script>
<template>
6666
</template>
<style scoped lang="scss">
.user-center-home {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 40px;
.user-center-home-left {
width: 60%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 20px;
.user-center-home-left-top {
width: 100%;
height: 25vh;
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
.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;
&:hover {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.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;
}
}
.user-center-home-left-top-card-bottom {
width: 100%;
height: 40%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 10px;
}
}
}
.user-center-home-left-bottom {
width: 100%;
height: 58%;
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;
}
}
.user-center-home-right {
width: 39%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
.user-center-home-right-card {
width: 100%;
height: 100%;
background-color: #fff;
//margin-top: 20px;
}
}
}
</style>

View File

@@ -1,11 +0,0 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped lang="scss">
</style>

View File

@@ -1,11 +1,67 @@
<template>
<div class="user-center-setting">
<h2>用户设置</h2>
<div class="user-center-setting-content">
<div class="user-center-setting-item">
<span class="user-center-setting-item-title">是否开启AI识别</span>
<ASwitch/>
</div>
<div class="user-center-setting-item">
<span class="user-center-setting-item-title">是否开启手机上传</span>
<ASwitch/>
</div>
<div class="user-center-setting-item">
<span class="user-center-setting-item-title">是否公开个人资料</span>
<ASwitch/>
</div>
<div class="user-center-setting-item">
<span class="user-center-setting-item-title">是否公开个人资料</span>
<ASwitch/>
</div>
<div class="user-center-setting-item">
<span class="user-center-setting-item-title">是否开启搜索记录</span>
<ASwitch/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
</script>
<template>
</template>
<style scoped lang="scss">
.user-center-setting {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
.user-center-setting-content {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-content: flex-start;
gap: 50px;
.user-center-setting-item {
width: 350px;
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
.user-center-setting-item-title {
display: inline-block;
font-size: 16px;
font-weight: bold;
line-height: 22px;
color: #333333;
}
}
}
}
</style>

View File

@@ -1,11 +0,0 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped lang="scss">
</style>

View File

@@ -9,7 +9,6 @@ import {AntDesignVueResolver} from 'unplugin-vue-components/resolvers';
import AutoImport from 'unplugin-auto-import/vite';
import {chunkSplitPlugin} from 'vite-plugin-chunk-split';
import vitePluginBundleObfuscator from 'vite-plugin-bundle-obfuscator';
const defaultObfuscatorConfig: any = {
excludes: [],
enable: true,