From 9e6dd550872a4b0b1a0f73b2694b1fe1e4ff6153 Mon Sep 17 00:00:00 2001 From: landaiqing Date: Wed, 12 Mar 2025 16:12:19 +0800 Subject: [PATCH] :sparkles: add image blur detection and background management pages --- .eslintrc-auto-import.json | 7 +- auto-import.d.ts | 2 + components.d.ts | 42 +- package.json | 41 +- src/api/share/index.ts | 6 +- src/assets/svgs/background.svg | 66 ++ src/assets/svgs/blur.svg | 1 + src/components/ImageUpload/ImageUpload.vue | 2 +- src/components/ImageUpload/UploadSetting.vue | 31 +- .../VueCompareImage/VueCompareImage.vue | 446 ----------- src/components/VueCompareImage/index.scss | 93 --- src/components/VueCompareImage/index.ts | 3 - src/constant/coco_ssd_label_category.ts | 18 +- src/main.ts | 3 - src/router/modules/preview.ts | 11 + src/router/modules/system.ts | 122 +++ src/router/router.ts | 6 +- src/store/index.ts | 2 + src/store/modules/commentStore.ts | 2 +- src/store/modules/imageStore.ts | 3 +- src/store/modules/langStore.ts | 2 +- src/store/modules/menuStore.ts | 2 +- src/store/modules/searchStore.ts | 2 +- src/store/modules/shareStore.ts | 2 +- src/store/modules/systemStore.ts | 19 + src/store/modules/themeStore.ts | 2 +- src/store/modules/uploadStore.ts | 317 +++++++- src/store/modules/upscaleStore.ts | 3 +- src/store/modules/userStore.ts | 2 +- src/utils/file/image-converter.ts | 132 ++++ src/utils/imageBlurDetect/blurDetect.ts | 168 ++++ src/utils/imageBlurDetect/blur_detect.c | 76 ++ src/utils/imageBlurDetect/blur_detect.js | 18 + src/utils/imageBlurDetect/blur_detect.wasm | Bin 0 -> 7514 bytes src/utils/tfjs/anime_classifier_pro.ts | 60 +- src/utils/tfjs/landscape_recognition.ts | 48 +- src/utils/tfjs/mobilenet.ts | 5 +- src/views/Admin/Auth/Login.vue | 243 ++++++ src/views/Admin/Error/PageError403.vue | 35 + src/views/Admin/Error/PageError404.vue | 35 + src/views/Admin/Error/PageError500.vue | 35 + .../Admin/System/Components/SystemHeader.vue | 172 ++++ .../Admin/System/Components/SystemSidebar.vue | 132 ++++ src/views/Admin/System/Index.vue | 36 + .../Admin/System/Pages/BasicSettings.vue | 258 ++++++ src/views/Admin/System/Pages/Dashboard.vue | 343 ++++++++ .../Admin/System/Pages/PermissionSetting.vue | 380 +++++++++ .../Admin/System/Pages/RoleManagement.vue | 427 ++++++++++ .../Admin/System/Pages/SecuritySettings.vue | 215 +++++ .../Admin/System/Pages/StorageManagement.vue | 747 ++++++++++++++++++ src/views/Admin/System/Pages/SystemLogs.vue | 410 ++++++++++ src/views/Admin/System/Pages/UserAnalysis.vue | 590 ++++++++++++++ src/views/Admin/System/Pages/UserList.vue | 438 ++++++++++ .../Admin/System/Pages/VisitStatistics.vue | 602 ++++++++++++++ src/views/Preview/PreviewBlurDetect.vue | 298 +++++++ src/views/Upscale/CompareImage.vue | 1 + .../AccountSettingStorage/StorageCard.vue | 1 - .../image-analysis/image-analysis.worker.ts | 122 +++ src/workers/tfjs/tfjs.worker.ts | 11 - tsconfig.node.json | 1 - 60 files changed, 6647 insertions(+), 650 deletions(-) create mode 100644 src/assets/svgs/background.svg create mode 100644 src/assets/svgs/blur.svg delete mode 100644 src/components/VueCompareImage/VueCompareImage.vue delete mode 100644 src/components/VueCompareImage/index.scss delete mode 100644 src/components/VueCompareImage/index.ts create mode 100644 src/router/modules/preview.ts create mode 100644 src/router/modules/system.ts create mode 100644 src/store/modules/systemStore.ts create mode 100644 src/utils/file/image-converter.ts create mode 100644 src/utils/imageBlurDetect/blurDetect.ts create mode 100644 src/utils/imageBlurDetect/blur_detect.c create mode 100644 src/utils/imageBlurDetect/blur_detect.js create mode 100644 src/utils/imageBlurDetect/blur_detect.wasm create mode 100644 src/views/Admin/Auth/Login.vue create mode 100644 src/views/Admin/Error/PageError403.vue create mode 100644 src/views/Admin/Error/PageError404.vue create mode 100644 src/views/Admin/Error/PageError500.vue create mode 100644 src/views/Admin/System/Components/SystemHeader.vue create mode 100644 src/views/Admin/System/Components/SystemSidebar.vue create mode 100644 src/views/Admin/System/Index.vue create mode 100644 src/views/Admin/System/Pages/BasicSettings.vue create mode 100644 src/views/Admin/System/Pages/Dashboard.vue create mode 100644 src/views/Admin/System/Pages/PermissionSetting.vue create mode 100644 src/views/Admin/System/Pages/RoleManagement.vue create mode 100644 src/views/Admin/System/Pages/SecuritySettings.vue create mode 100644 src/views/Admin/System/Pages/StorageManagement.vue create mode 100644 src/views/Admin/System/Pages/SystemLogs.vue create mode 100644 src/views/Admin/System/Pages/UserAnalysis.vue create mode 100644 src/views/Admin/System/Pages/UserList.vue create mode 100644 src/views/Admin/System/Pages/VisitStatistics.vue create mode 100644 src/views/Preview/PreviewBlurDetect.vue create mode 100644 src/workers/image-analysis/image-analysis.worker.ts delete mode 100644 src/workers/tfjs/tfjs.worker.ts diff --git a/.eslintrc-auto-import.json b/.eslintrc-auto-import.json index 924f94b..4a33ff5 100644 --- a/.eslintrc-auto-import.json +++ b/.eslintrc-auto-import.json @@ -307,6 +307,11 @@ "watchThrottled": true, "watchTriggerable": true, "watchWithFilter": true, - "whenever": true + "whenever": true, + "createRef": true, + "onElementRemoval": true, + "useCountdown": true, + "usePreferredReducedTransparency": true, + "useSSRWidth": true } } diff --git a/auto-import.d.ts b/auto-import.d.ts index e7e67d7..23310ab 100644 --- a/auto-import.d.ts +++ b/auto-import.d.ts @@ -26,6 +26,7 @@ declare global { const createInjectionState: typeof import('@vueuse/core')['createInjectionState'] const createPinia: typeof import('pinia')['createPinia'] const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn'] + const createRef: typeof import('@vueuse/core')['createRef'] const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate'] const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable'] const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise'] @@ -337,6 +338,7 @@ declare module 'vue' { readonly createInjectionState: UnwrapRef readonly createPinia: UnwrapRef readonly createReactiveFn: UnwrapRef + readonly createRef: UnwrapRef readonly createReusableTemplate: UnwrapRef readonly createSharedComposable: UnwrapRef readonly createTemplatePromise: UnwrapRef diff --git a/components.d.ts b/components.d.ts index 92d712e..9c87abc 100644 --- a/components.d.ts +++ b/components.d.ts @@ -8,6 +8,7 @@ export {} /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { + AAlert: typeof import('ant-design-vue/es')['Alert'] AAvatar: typeof import('ant-design-vue/es')['Avatar'] AAvatarGroup: typeof import('ant-design-vue/es')['AvatarGroup'] ABadge: typeof import('ant-design-vue/es')['Badge'] @@ -21,8 +22,11 @@ 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'] + ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup'] ACol: typeof import('ant-design-vue/es')['Col'] AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider'] + ADescriptions: typeof import('ant-design-vue/es')['Descriptions'] + ADescriptionsItem: typeof import('ant-design-vue/es')['DescriptionsItem'] ADivider: typeof import('ant-design-vue/es')['Divider'] ADrawer: typeof import('ant-design-vue/es')['Drawer'] ADropdown: typeof import('ant-design-vue/es')['Dropdown'] @@ -37,6 +41,7 @@ declare module 'vue' { AInput: typeof import('ant-design-vue/es')['Input'] AInputNumber: typeof import('ant-design-vue/es')['InputNumber'] AInputPassword: typeof import('ant-design-vue/es')['InputPassword'] + AInputSearch: typeof import('ant-design-vue/es')['InputSearch'] AlbumCard: typeof import('./src/views/Album/Phoalbum/AlbumCard.vue')['default'] AlbumShareModal: typeof import('./src/views/Album/Phoalbum/AlbumShareModal.vue')['default'] AList: typeof import('ant-design-vue/es')['List'] @@ -53,14 +58,19 @@ declare module 'vue' { AProgress: typeof import('ant-design-vue/es')['Progress'] AQrcode: typeof import('ant-design-vue/es')['QRCode'] ARadio: typeof import('ant-design-vue/es')['Radio'] + ARadioButton: typeof import('ant-design-vue/es')['RadioButton'] ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup'] ARangePicker: typeof import('ant-design-vue/es')['RangePicker'] + AResult: typeof import('ant-design-vue/es')['Result'] 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'] + 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'] + ASubMenu: typeof import('ant-design-vue/es')['SubMenu'] ASwitch: typeof import('ant-design-vue/es')['Switch'] ATable: typeof import('ant-design-vue/es')['Table'] ATabPane: typeof import('ant-design-vue/es')['TabPane'] @@ -68,9 +78,14 @@ declare module 'vue' { ATag: typeof import('ant-design-vue/es')['Tag'] ATextarea: typeof import('ant-design-vue/es')['Textarea'] 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'] AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger'] BackgroundAnimation: typeof import('./src/components/BackgroundAnimation/BackgroundAnimation.vue')['default'] + BasicSettings: typeof import('./src/views/Admin/System/Pages/BasicSettings.vue')['default'] BgColorsOutlined: typeof import('@ant-design/icons-vue')['BgColorsOutlined'] BlockOutlined: typeof import('@ant-design/icons-vue')['BlockOutlined'] BoxDog: typeof import('./src/components/BoxDog/BoxDog.vue')['default'] @@ -89,12 +104,12 @@ declare module 'vue' { CommentReply: typeof import('./src/components/CommentReply/index.vue')['default'] 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'] 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'] - 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'] @@ -102,12 +117,14 @@ 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'] + ImageEnhancer: typeof import('./src/components/ImageEnhancer/ImageEnhancer.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'] 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'] @@ -117,17 +134,22 @@ declare module 'vue' { LocationAlbumIndex: typeof import('./src/views/Album/LocationAlbum/LocationAlbumIndex.vue')['default'] LocationAlbumList: typeof import('./src/views/Album/LocationAlbum/LocationAlbumList.vue')['default'] LockOutlined: typeof import('@ant-design/icons-vue')['LockOutlined'] + Login: typeof import('./src/views/Admin/Auth/Login.vue')['default'] LoginFooter: typeof import('./src/views/Login/LoginFooter.vue')['default'] LoginPage: typeof import('./src/views/Login/LoginPage.vue')['default'] MainPage: typeof import('./src/views/Main/MainPage.vue')['default'] MessageReport: typeof import('./src/components/CommentReply/src/MessageReport/MessageReport.vue')['default'] 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'] + PageError404: typeof import('./src/views/Admin/Error/PageError404.vue')['default'] + PageError500: typeof import('./src/views/Admin/Error/PageError500.vue')['default'] ParameterSetting: typeof import('./src/views/Upscale/ParameterSetting.vue')['default'] PeopleAlbumDetail: typeof import('./src/views/Album/PeopleAlbum/PeopleAlbumDetail.vue')['default'] PeopleAlbumIndex: typeof import('./src/views/Album/PeopleAlbum/PeopleAlbumIndex.vue')['default'] PeopleAlbumList: typeof import('./src/views/Album/PeopleAlbum/PeopleAlbumList.vue')['default'] PeopleAlbumToolbar: typeof import('./src/views/Album/PeopleAlbum/PeopleAlbumToolbar.vue')['default'] + PermissionSetting: typeof import('./src/views/Admin/System/Pages/PermissionSetting.vue')['default'] PersonalCenter: typeof import('./src/views/User/PersonalCenter/PersonalCenter.vue')['default'] PhoalbumDetail: typeof import('./src/views/Album/Phoalbum/PhoalbumDetail.vue')['default'] PhoalbumIndex: typeof import('./src/views/Album/Phoalbum/PhoalbumIndex.vue')['default'] @@ -136,6 +158,7 @@ declare module 'vue' { PlusOutlined: typeof import('@ant-design/icons-vue')['PlusOutlined'] PlusSquareOutlined: typeof import('@ant-design/icons-vue')['PlusSquareOutlined'] Popover: typeof import('./src/components/MyUI/Popover/Popover.vue')['default'] + PreviewBlurDetect: typeof import('./src/views/Preview/PreviewBlurDetect.vue')['default'] QrcodeOutlined: typeof import('@ant-design/icons-vue')['QrcodeOutlined'] QRLogin: typeof import('./src/views/QRLogin/QRLogin.vue')['default'] QRLoginFooter: typeof import('./src/views/QRLogin/QRLoginFooter.vue')['default'] @@ -147,13 +170,14 @@ declare module 'vue' { 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'] + RoleManagement: typeof import('./src/views/Admin/System/Pages/RoleManagement.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] SafetyOutlined: typeof import('@ant-design/icons-vue')['SafetyOutlined'] SearchOutlined: typeof import('@ant-design/icons-vue')['SearchOutlined'] SearchResult: typeof import('./src/views/Photograph/SearchResult/SearchResult.vue')['default'] + SecuritySettings: typeof import('./src/views/Admin/System/Pages/SecuritySettings.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'] @@ -161,7 +185,13 @@ declare module 'vue' { 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'] + 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'] 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'] @@ -170,14 +200,14 @@ declare module 'vue' { UploadSetting: typeof import('./src/components/ImageUpload/UploadSetting.vue')['default'] Upscale: typeof import('./src/views/Upscale/index.vue')['default'] UpscalePhoneUpload: typeof import('./src/views/Phone/UpscalePhoneUpload/UpscalePhoneUpload.vue')['default'] - UserCenterAnalysis: typeof import('./src/views/User/PersonalCenter/components/UserCenterAnalysis/UserCenterAnalysis.vue')['default'] + UserAnalysis: typeof import('./src/views/Admin/System/Pages/UserAnalysis.vue')['default'] UserCenterDynamic: typeof import('./src/views/User/PersonalCenter/components/UserCenterDynamic/UserCenterDynamic.vue')['default'] UserCenterHome: typeof import('./src/views/User/PersonalCenter/components/UserCenterHome/UserCenterHome.vue')['default'] - UserCenterInfo: typeof import('./src/views/User/PersonalCenter/components/UserCenterInfo/UserCenterInfo.vue')['default'] UserCenterSetting: typeof import('./src/views/User/PersonalCenter/components/UserCenterSetting/UserCenterSetting.vue')['default'] - UserCenterShare: typeof import('./src/views/User/PersonalCenter/components/UserCenterShare/UserCenterShare.vue')['default'] UserInfoCard: typeof import('./src/components/CommentReply/src/UserInfoCard/UserInfoCard.vue')['default'] + 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'] } diff --git a/package.json b/package.json index 712c654..119540a 100644 --- a/package.json +++ b/package.json @@ -12,8 +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", + "@intlify/eslint-plugin-vue-i18n": "^4.0.0", "@mediapipe/face_detection": "^0.4.1646425229", "@mediapipe/face_mesh": "^0.4.1633559619", "@tensorflow-models/coco-ssd": "^2.2.3", @@ -32,27 +31,28 @@ "@types/crypto-js": "^4.2.2", "@types/file-saver": "^2.0.7", "@types/json-stringify-safe": "^5.0.3", - "@types/node": "^22.13.5", + "@types/node": "^22.13.10", "@types/nprogress": "^0.2.3", "@vladmandic/face-api": "^1.7.15", "@vuepic/vue-datepicker": "^11.0.1", - "@vueuse/core": "^12.7.0", - "@vueuse/integrations": "^12.7.0", - "alova": "^3.2.9", + "@vueuse/core": "^13.0.0", + "@vueuse/integrations": "^13.0.0", + "alova": "^3.2.10", "animejs": "^3.2.2", "ant-design-vue": "^4.2.6", - "axios": "^1.8.1", + "axios": "^1.8.3", "browser-image-compression": "^2.0.2", "buffer": "^6.0.3", "crypto-js": "^4.2.0", "echarts": "^5.6.0", - "eslint": "9.21.0", + "eslint": "9.22.0", "exifr": "^7.1.3", "file-saver": "^2.0.5", "go-captcha-vue": "^2.0.6", "gsap": "^3.12.7", "jsencrypt": "^3.3.2", "json-stringify-safe": "^5.0.1", + "jsonc-eslint-parser": "^2.4.0", "jszip": "^3.10.1", "less": "^4.2.2", "localforage": "^1.10.0", @@ -62,34 +62,37 @@ "nsfwjs": "^4.2.1", "opencv-qr": "^0.7.0", "pinia": "^3.0.1", - "pinia-plugin-persistedstate-2": "^2.0.28", + "pinia-plugin-persistedstate-2": "^2.0.29", "qr-scanner-wechat": "^0.1.3", "rimraf": "^6.0.1", "seedrandom": "^3.0.5", - "swiper": "^11.2.4", + "swiper": "^11.2.5", "unplugin-auto-import": "^19.1.1", + "upscaler": "^1.0.0-beta.19", "vite-plugin-compression": "^0.5.1", "vite-plugin-html": "^3.2.2", "vite-plugin-node-polyfills": "^0.23.0", "vue": "^3.5.13", "vue-dompurify-html": "^5.2.0", - "vue-i18n": "^11.1.1", + "vue-eslint-parser": "^10.1.1", + "vue-i18n": "^11.1.2", "vue-router": "^4.5.0", - "ws": "^8.18.1" + "ws": "^8.18.1", + "yaml-eslint-parser": "^1.3.0" }, "devDependencies": { - "@eslint/js": "^9.21.0", + "@eslint/js": "^9.22.0", "@vitejs/plugin-vue": "^5.2.1", - "eslint-plugin-vue": "^9.32.0", + "eslint-plugin-vue": "^10.0.0", "globals": "^16.0.0", "sass": "^1.85.1", - "typescript": "^5.7.3", - "typescript-eslint": "^8.25.0", + "typescript": "^5.8.2", + "typescript-eslint": "^8.26.1", "unplugin-vue-components": "^28.4.1", - "vite": "^6.2.0", - "vite-plugin-bundle-obfuscator": "1.4.1", + "vite": "^6.2.1", + "vite-plugin-bundle-obfuscator": "1.4.2", "vite-plugin-chunk-split": "^0.5.0", - "vue-tsc": "2.2.4" + "vue-tsc": "2.2.8" }, "overrides": { "vite-plugin-chunk-split": { diff --git a/src/api/share/index.ts b/src/api/share/index.ts index 988bc77..70ea0d7 100644 --- a/src/api/share/index.ts +++ b/src/api/share/index.ts @@ -4,7 +4,7 @@ import {service} from "@/utils/alova/service.ts"; * 上传分享图片 * @param formData */ -export const shareImageUploadApi = (formData) => { +export const shareImageUploadApi = (formData: any) => { return service.Post('/api/auth/share/upload', {...formData}, { meta: { ignoreToken: false, @@ -48,11 +48,13 @@ export const queryShareRecordListApi = (dataRequest: string[]) => { ignoreToken: false, signature: false, }, + hitSource: ["upload-share-image", "delete-share-record"] }); }; /** * 查询分享信息 * @param invite_code + * @param access_password */ export const queryShareInfoApi = (invite_code: string, access_password: string) => { return service.Post('/api/auth/share/info', { @@ -82,6 +84,7 @@ export const queryShareOverviewApi = () => { ignoreToken: false, signature: false, }, + hitSource: ["upload-share-image", "delete-share-record"] }); }; /** @@ -100,5 +103,6 @@ export const deleteShareRecordApi = (id: number, invite_code: string, album_id: ignoreToken: false, signature: false, }, + name: 'delete-share-record', }); }; diff --git a/src/assets/svgs/background.svg b/src/assets/svgs/background.svg new file mode 100644 index 0000000..57be5b7 --- /dev/null +++ b/src/assets/svgs/background.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/svgs/blur.svg b/src/assets/svgs/blur.svg new file mode 100644 index 0000000..ed32a0b --- /dev/null +++ b/src/assets/svgs/blur.svg @@ -0,0 +1 @@ + diff --git a/src/components/ImageUpload/ImageUpload.vue b/src/components/ImageUpload/ImageUpload.vue index 4082a6d..29d5172 100644 --- a/src/components/ImageUpload/ImageUpload.vue +++ b/src/components/ImageUpload/ImageUpload.vue @@ -45,7 +45,7 @@
+
+ + + 图像加密 + + + + + +
+
+ + + 模糊检测 + + + + + +
- - - diff --git a/src/components/VueCompareImage/index.scss b/src/components/VueCompareImage/index.scss deleted file mode 100644 index 0670405..0000000 --- a/src/components/VueCompareImage/index.scss +++ /dev/null @@ -1,93 +0,0 @@ -.vci--container { - box-sizing: border-box; - position: relative; - display: flex; - overflow: hidden; - width: 100%; -} - -.vci--right-image { - display: flex; - position: absolute; - object-fit: cover; - height: 100%; - width: 100%; -} - -.vci--left-image { - display: flex; - position: absolute; - object-fit: cover; - height: 100%; - width: 100%; -} - -.vci--slider { - position: absolute; - display: flex; - align-items: center; - justify-content: center; -} - -.vci--slider-line { - flex: 0 1 auto; - box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12); -} - -.vci--custom-handle { - box-sizing: border-box; - display: flex; - flex: 1 0 auto; - justify-content: center; - align-items: center; - height: auto; - width: auto; -} - -.vci--default-handle { - box-sizing: border-box; - display: flex; - flex: 1 0 auto; - justify-content: center; - align-items: center; - border-radius: 100%; - box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12); -} - -.vci--left-arrow { - height: 0px; - width: 0px; -} - -.vci--right-arrow { - height: 0px; - width: 0px; -} - -.vci--left-label { - position: absolute; - padding: 10px 20px; - background: rgba(0, 0, 0, 0.5); - color: white; - transition: opacity 0.1s ease-out; -} - -.vci--right-label { - position: absolute; - padding: 10px 20px; - background: rgba(0, 0, 0, 0.5); - color: white; - transition: opacity 0.1s ease-out; -} - -.vci--right-label-container { - position: absolute; - height: 100%; - width: 100%; -} - -.vci--left-label-container { - position: absolute; - height: 100%; - width: 100%; -} diff --git a/src/components/VueCompareImage/index.ts b/src/components/VueCompareImage/index.ts deleted file mode 100644 index ae47065..0000000 --- a/src/components/VueCompareImage/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import VueCompareImage from './VueCompareImage.vue'; - -export {VueCompareImage}; diff --git a/src/constant/coco_ssd_label_category.ts b/src/constant/coco_ssd_label_category.ts index f51e20c..4106a7f 100644 --- a/src/constant/coco_ssd_label_category.ts +++ b/src/constant/coco_ssd_label_category.ts @@ -220,30 +220,30 @@ export const LABEL_TO_CATEGORY = new Map([ ]); // 获取标签所属大类的函数,支持英文和中文返回 -export function getCategoryByLabel(label: string, lang: 'en' | 'zh' = 'en'): string | undefined { +export function getCategoryByLabel(label: string, lang: 'en' | 'zh' = 'en'): string | null { const category = LABEL_TO_CATEGORY.get(label); - return category ? category[lang] : undefined; + return category ? category[lang] : null; } // 获取标签所属大类的函数,支持英文和中文返回 -export function getCategoryName(label: string, lang: 'en' | 'zh' = 'en'): string | undefined { +export function getCategoryName(label: string, lang: 'en' | 'zh' = 'en'): string | null { const category = CATEGORIES[label]; - return category ? category[lang] : undefined; + return category ? category[lang] : null; } // 获取标签的小分类名称 -export function getLabelName(label: string, lang: 'en' | 'zh' = 'en'): string | undefined { +export function getLabelName(label: string, lang: 'en' | 'zh' = 'en'): string | null { const labelInfo = LABELS[label]; - return labelInfo ? labelInfo[lang] : undefined; + return labelInfo ? labelInfo[lang] : null; } // 获取标签的中文名称 -export const getZhCategoryNameByEnName = (enName: string): string | undefined => { +export const getZhCategoryNameByEnName = (enName: string): string | null => { const category = Object.values(CATEGORIES).find(cat => cat.en.toLowerCase() === enName.toLowerCase()); - return category?.zh; + return category?.zh || null; }; // 获取标签的中文名称 -export const getZhLabelNameByEnName = (enName: string): string | undefined => { +export const getZhLabelNameByEnName = (enName: string): string | null => { const labelInfo = LABELS[enName.toLowerCase()]; return labelInfo?.zh; }; diff --git a/src/main.ts b/src/main.ts index e97391a..cf1ca81 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,8 +10,6 @@ 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)); @@ -22,5 +20,4 @@ app.use(router); app.use(i18n); app.use(GoCaptcha); app.use(VueDOMPurifyHTML); -app.use(VueCalendarMap); app.mount('#app'); diff --git a/src/router/modules/preview.ts b/src/router/modules/preview.ts new file mode 100644 index 0000000..678d89c --- /dev/null +++ b/src/router/modules/preview.ts @@ -0,0 +1,11 @@ +export default [ + { + path: '/preview/blur-detect', + name: 'PreviewBlurDetect', + component: () => import('@/views/Preview/PreviewBlurDetect.vue'), + meta: { + requiresAuth: false, + title: 'PreviewBlurDetect', + } + }, +]; diff --git a/src/router/modules/system.ts b/src/router/modules/system.ts new file mode 100644 index 0000000..58876e4 --- /dev/null +++ b/src/router/modules/system.ts @@ -0,0 +1,122 @@ +import Dashboard from '@/views/Admin/System/Pages/Dashboard.vue'; +import UserList from '@/views/Admin/System/Pages/UserList.vue'; +import PermissionSetting from '@/views/Admin/System/Pages/PermissionSetting.vue'; +import RoleManagement from '@/views/Admin/System/Pages/RoleManagement.vue'; + +export default [ + { + path: '/admin/login', + name: 'admin-login', + component: () => import('@/views/Admin/Auth/Login.vue'), + meta: { + requiresAuth: false, + title: 'Auth Login', + } + }, + { + path: '/admin/system/index', + name: 'admin-system', + redirect: '/admin/system/dashboard', + component: () => import('@/views/Admin/System/Index.vue'), + meta: { + requiresAuth: false, + title: '管理首页', + }, + children: [ + { + path: '/admin/system/dashboard', + name: 'admin-system-dashboard', + component: Dashboard, + meta: { + requiresAuth: false, + title: '管理首页', + } + }, { + path: '/admin/system/user', + name: 'admin-system-user', + component: UserList, + meta: { + requiresAuth: false, + title: '用户列表', + } + }, + { + path: '/admin/system/permission', + name: 'admin-system-permission', + component: PermissionSetting, + meta: { + requiresAuth: false, + title: '权限列表', + } + }, + { + path: '/admin/system/role', + name: 'admin-system-role', + component: RoleManagement, + meta: { + requiresAuth: false, + title: '角色列表', + } + }, + { + path: '/admin/system/basic', + name: 'admin-system-basic-setting', + component: () => import('@/views/Admin/System/Pages/BasicSettings.vue'), + meta: { + requiresAuth: false, + title: '基本设置', + } + }, + { + path: '/admin/system/security', + name: 'admin-system-security-setting', + component: () => import('@/views/Admin/System/Pages/SecuritySettings.vue'), + meta: { + requiresAuth: false, + title: '安全设置', + } + }, + { + path: '/admin/system/log', + name: 'admin-system-log', + component: () => import('@/views/Admin/System/Pages/SystemLogs.vue'), + meta: { + requiresAuth: false, + title: '系统日志', + } + }, + { + path: '/admin/system/analysis', + name: 'admin-system-analysis', + component: () => import('@/views/Admin/System/Pages/UserAnalysis.vue'), + meta: { + requiresAuth: false, + title: '用户分析', + } + }, + { + path: '/admin/system/visit', + name: 'admin-system-visit', + component: () => import('@/views/Admin/System/Pages/VisitStatistics.vue'), + meta: { + requiresAuth: false, + title: '访问统计', + } + }, + { + path: '/admin/system/storage', + name: 'admin-system-storage', + component: () => import('@/views/Admin/System/Pages/StorageManagement.vue'), + meta: { + requiresAuth: false, + title: '存储管理', + } + }, + + ] + } + + +]; + + diff --git a/src/router/router.ts b/src/router/router.ts index cd1581e..d0537e0 100644 --- a/src/router/router.ts +++ b/src/router/router.ts @@ -9,6 +9,8 @@ import mainRouter from "./modules/main_router.ts"; import i18n from "@/locales"; import phone_upload from "@/router/modules/phone_upload.ts"; import user from "@/router/modules/user.ts"; +import system from "@/router/modules/system.ts"; +import preview from "@/router/modules/preview.ts"; const routes: Array = [ ...login, @@ -17,10 +19,12 @@ const routes: Array = [ ...mainRouter, ...phone_upload, ...user, + ...system, + ...preview, { path: '/:pathMatch(.*)', redirect: '/404', - } + }, ]; const router: Router = createRouter({ diff --git a/src/store/index.ts b/src/store/index.ts index 478b780..939e8ce 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -9,6 +9,7 @@ import {useUploadStore} from "@/store/modules/uploadStore.ts"; import {useImageStore} from "@/store/modules/imageStore.ts"; import {useShareStore} from "@/store/modules/shareStore.ts"; import {useSearchStore} from "@/store/modules/searchStore.ts"; +import {useSystemStore} from "@/store/modules/systemStore.ts"; export default function useStore() { return { @@ -23,5 +24,6 @@ export default function useStore() { image: useImageStore(), share: useShareStore(), search: useSearchStore(), + system: useSystemStore(), }; } diff --git a/src/store/modules/commentStore.ts b/src/store/modules/commentStore.ts index 6f8ed96..5df35dc 100644 --- a/src/store/modules/commentStore.ts +++ b/src/store/modules/commentStore.ts @@ -342,7 +342,7 @@ export const useCommentStore = defineStore( persistedState: { persist: true, storage: localForage, - key: 'comment', + key: 'STORE-COMMENT', includePaths: ["emojiList", "commentList", "replyVisibility", "commentMap"] } } diff --git a/src/store/modules/imageStore.ts b/src/store/modules/imageStore.ts index dd755d0..04a85d2 100644 --- a/src/store/modules/imageStore.ts +++ b/src/store/modules/imageStore.ts @@ -55,7 +55,6 @@ export const useImageStore = defineStore( const imageEditVisible = ref(false); - /** * 获取人脸列表 */ @@ -138,7 +137,7 @@ export const useImageStore = defineStore( persistedState: { persist: true, storage: localForage, - key: 'image', + key: 'STORE-IMAGE', includePaths: [ "tabActiveKey", "tabMap", diff --git a/src/store/modules/langStore.ts b/src/store/modules/langStore.ts index 014965d..1041a1c 100644 --- a/src/store/modules/langStore.ts +++ b/src/store/modules/langStore.ts @@ -15,7 +15,7 @@ export const langStore = defineStore( persistedState: { persist: true, storage: localStorage, - key: 'lang', + key: 'STORE-LANGUAGE', includePaths: ['lang'] } } diff --git a/src/store/modules/menuStore.ts b/src/store/modules/menuStore.ts index d022879..3f1c61f 100644 --- a/src/store/modules/menuStore.ts +++ b/src/store/modules/menuStore.ts @@ -17,7 +17,7 @@ export const useMenuStore = defineStore( persistedState: { persist: true, storage: localStorage, - key: 'menu', + key: 'STORE-MENU', includePaths: ['currentMenu', 'userCenterMenu', 'accountSettingMenu'] } } diff --git a/src/store/modules/searchStore.ts b/src/store/modules/searchStore.ts index 6355a53..998e982 100644 --- a/src/store/modules/searchStore.ts +++ b/src/store/modules/searchStore.ts @@ -57,7 +57,7 @@ export const useSearchStore = defineStore( persistedState: { persist: true, storage: localStorage, - key: 'search', + key: 'STORE-SEARCH', includePaths: ['searchOption', 'options'] } } diff --git a/src/store/modules/shareStore.ts b/src/store/modules/shareStore.ts index 7d7ce62..85e9cd2 100644 --- a/src/store/modules/shareStore.ts +++ b/src/store/modules/shareStore.ts @@ -31,7 +31,7 @@ export const useShareStore = defineStore( persistedState: { persist: true, storage: sessionStorage, - key: 'share', + key: 'STORE-SHARE', includePaths: ['sharePassword'] } } diff --git a/src/store/modules/systemStore.ts b/src/store/modules/systemStore.ts new file mode 100644 index 0000000..948056b --- /dev/null +++ b/src/store/modules/systemStore.ts @@ -0,0 +1,19 @@ +export const useSystemStore = defineStore( + 'system', + () => { + const isCollapsed = ref(false); + + return { + isCollapsed, + }; + }, + { + // 开启数据持久化 + persistedState: { + persist: true, + storage: localStorage, + key: 'STORE-SYSTEM', + includePaths: ['isCollapsed'] + } + } +); diff --git a/src/store/modules/themeStore.ts b/src/store/modules/themeStore.ts index 6a2606a..68cb076 100644 --- a/src/store/modules/themeStore.ts +++ b/src/store/modules/themeStore.ts @@ -40,7 +40,7 @@ export const useThemeStore = defineStore( persistedState: { persist: true, storage: localStorage, - key: 'theme', + key: 'STORE-THEME', includePaths: ['themeName', 'darkMode'] } } diff --git a/src/store/modules/uploadStore.ts b/src/store/modules/uploadStore.ts index e201a9a..f547314 100644 --- a/src/store/modules/uploadStore.ts +++ b/src/store/modules/uploadStore.ts @@ -1,7 +1,7 @@ // import localforage from 'localforage'; import imageCompression from "browser-image-compression"; -import {message, type UploadProps} from "ant-design-vue"; +import {message, notification, type UploadProps} from "ant-design-vue"; import {initNSFWJs, predictNSFW} from "@/utils/tfjs/nsfw.ts"; import i18n from "@/locales"; @@ -13,13 +13,25 @@ import {getCategoryByLabel} from "@/constant/coco_ssd_label_category.ts"; import exifr from "exifr"; import isScreenshot from "@/utils/imageUtils/isScreenshot.ts"; import {ready, scan} from 'qr-scanner-wechat'; +import {initBlurDetect, detectBlurFromFile} from '@/utils/imageBlurDetect/blurDetect'; +import {fileToImageData} from "@/utils/file/image-converter.ts"; + +// Web Worker图像分析响应接口 +interface ImageAnalysisResponse { + isNSFW?: boolean; + isAnime?: boolean; + landscape?: string | null; + tagName?: string | null; + topCategory?: string | undefined; + error?: string; +} interface UploadPredictResult { isAnime: boolean; tagName: string | null; landscape: 'building' | 'forest' | 'glacier' | 'mountain' | 'sea' | 'street' | null; isScreenshot: boolean; - topCategory: string | undefined; + topCategory: string | null; width: number | null; height: number | null; latitude: number | null; @@ -60,8 +72,36 @@ export const useUploadStore = defineStore( target_detection: false, qrcode_detection: true, face_detection: true, + encrypt: false, + blur_detection: true, }); + // 用于控制模糊检测后的流程控制 + const blurDetectionControl = reactive({ + isPaused: false, + continuePromise: null as Promise | null, + continueResolve: null as ((value: boolean) => void) | null, + // 继续执行上传流程 + continueUpload(enhance = false) { + if (this.continueResolve) { + this.continueResolve(enhance); + this.isPaused = false; + this.continuePromise = null; + this.continueResolve = null; + } + }, + // 创建一个新的暂停Promise + createPausePromise() { + this.isPaused = true; + this.continuePromise = new Promise((resolve) => { + this.continueResolve = resolve; + }); + return this.continuePromise; + } + }); + // 模糊检测阈值 + const thresholdValue = ref(200); + const storageSelected = ref([]); const albumSelected = ref(); @@ -72,7 +112,7 @@ export const useUploadStore = defineStore( const image = new Image(); // 压缩图片配置 const options = reactive({ - maxSizeMB: 0.2, + maxSizeMB: 0.4, maxWidthOrHeight: 750, maxIteration: 2, useWebWorker: true, @@ -205,6 +245,48 @@ export const useUploadStore = defineStore( currentProgress += stepIncrement; } + // 模糊检测 + if (uploadSetting.blur_detection) { + const result = await detectBlurFromFile(file, thresholdValue.value); + if (result.isBlurry) { + // 显示通知并暂停执行 + notification.open({ + message: "检测到图片模糊,是否继续上传?", + type: "warning", + style: { + color: "rgba(96,165,250,.9)", + cursor: "pointer", + }, + duration: 3, + btn: + h('a-button', { + type: 'primary', + onClick: () => { + // 继续上传 + blurDetectionControl.continueUpload(false); + notification.close('blur-notification'); + } + }, '继续上传'), + key: 'blur-notification', + onClose: () => { + // 如果通知被关闭但没有点击按钮,默认继续上传 + if (blurDetectionControl.isPaused) { + blurDetectionControl.continueUpload(false); + } + } + }); + + // 创建暂停Promise并等待用户操作 + const shouldEnhance = await blurDetectionControl.createPausePromise(); + + // 这里可以添加处理的代码 + if (shouldEnhance) { /* empty */ + } + } + await smoothUpdateProgress(currentProgress + stepIncrement, 500); + currentProgress += stepIncrement; + } + // 动漫检测 if (uploadSetting.anime_detection) { const prediction = await animePredictImagePro(image); @@ -222,17 +304,11 @@ export const useUploadStore = defineStore( if (uploadSetting.target_detection) { const cocoResults: any = await cocoSsdPredict(image); if (cocoResults.length > 0) { - // 取置信度最高的结果 - // 如果只有一个结果,直接取第一个 - if (cocoResults.length === 1) { - predictResult.topCategory = getCategoryByLabel(cocoResults[0].class); - predictResult.tagName = cocoResults[0].class; - } else { - // 多个结果时,按 score 排序,取置信度最高的结果 - const sortedResults = cocoResults.sort((a, b) => b.score - a.score); - predictResult.topCategory = getCategoryByLabel(sortedResults[0].class); - predictResult.tagName = sortedResults[0].class; - } + // 多个结果时,按 score 排序,取置信度最高的结果 + const sortedResults = cocoResults.sort((a, b) => b.score - a.score); + predictResult.topCategory = getCategoryByLabel(sortedResults[0].class); + predictResult.tagName = sortedResults[0].class; + } await smoothUpdateProgress(currentProgress + stepIncrement, 500); currentProgress += stepIncrement; @@ -260,6 +336,214 @@ export const useUploadStore = defineStore( } } + /** + * 上传前的预处理 + * @param file + * @param _fileList + */ + async function beforeUploadWithWebWorker(file: File, _fileList: File[]) { + predicting.value = true; + clearPredictResult(); + progressPercent.value = 0; // 初始化进度条 + progressStatus.value = 'active'; // 开始状态 + // 压缩图片 + // const compressedFile = await imageCompression(file, options); + + const imageData: ImageData = await fileToImageData(file); + + predictResult.width = imageData.width; + predictResult.height = imageData.height; + + // 更新进度条函数,逐步增加 + const smoothUpdateProgress = async (targetPercent, duration) => { + const increment = (targetPercent - progressPercent.value) / (duration / 50); + return new Promise((resolve) => { + const interval = setInterval(() => { + if (progressPercent.value >= targetPercent) { + clearInterval(interval); + resolve(false); + } else { + progressPercent.value = Math.min(progressPercent.value + increment, targetPercent); + } + }, 50); + }); + }; + + try { + // 创建Web Worker进行图像分析 + const worker = new Worker(new URL('@/workers/image-analysis/image-analysis.worker.ts', import.meta.url), {type: 'module'}); + + // 计算启用的检测步骤及进度分配 + const enabledSteps = [ + uploadSetting.nsfw_detection, + uploadSetting.anime_detection, + uploadSetting.landscape_detection, + uploadSetting.target_detection, + uploadSetting.gps_detection, + uploadSetting.screenshot_detection, + uploadSetting.qrcode_detection, + uploadSetting.blur_detection + ].filter(Boolean).length; + + const stepIncrement = enabledSteps > 0 ? Math.floor(100 / (enabledSteps + 1)) : 100; + let currentProgress = 0; + + // 二维码检测 + if (uploadSetting.qrcode_detection) { + await ready(); + const result = await scan(imageData); + if (result.text) { + predictResult.hasQrcode = true; + } + await smoothUpdateProgress(currentProgress + stepIncrement, 500); + currentProgress += stepIncrement; + } + + // GPS 数据提取 + if (uploadSetting.gps_detection) { + const gpsData = await extractGPSExifData(file); + if (gpsData) { + predictResult.longitude = gpsData.longitude; + predictResult.latitude = gpsData.latitude; + } + await smoothUpdateProgress(currentProgress + stepIncrement, 500); + currentProgress += stepIncrement; + } + + // 截图检测 + if (uploadSetting.screenshot_detection) { + predictResult.isScreenshot = await isScreenshot(file); + await smoothUpdateProgress(currentProgress + stepIncrement, 500); + currentProgress += stepIncrement; + } + + // 模糊检测 + if (uploadSetting.blur_detection) { + const result = await detectBlurFromFile(file, thresholdValue.value); + if (result.isBlurry) { + // 显示通知并暂停执行 + notification.open({ + message: "检测到图片模糊,是否继续上传?", + type: "warning", + style: { + color: "rgba(96,165,250,.9)", + cursor: "pointer", + }, + duration: 3, + btn: + h('a-button', { + type: 'primary', + onClick: () => { + // 继续上传 + blurDetectionControl.continueUpload(false); + notification.close('blur-notification'); + } + }, '继续上传'), + key: 'blur-notification', + onClose: () => { + // 如果通知被关闭但没有点击按钮,默认继续上传 + if (blurDetectionControl.isPaused) { + blurDetectionControl.continueUpload(false); + } + } + }); + + // 创建暂停Promise并等待用户操作 + const shouldEnhance = await blurDetectionControl.createPausePromise(); + + // 这里可以添加处理的代码 + if (shouldEnhance) { /* empty */ + } + } + await smoothUpdateProgress(currentProgress + stepIncrement, 500); + currentProgress += stepIncrement; + } + + // 使用Web Worker进行图像分析(NSFW、动漫、景观、目标检测) + if (uploadSetting.nsfw_detection || uploadSetting.anime_detection || + uploadSetting.landscape_detection || uploadSetting.target_detection) { + + // 创建一个Promise来处理Worker的响应 + const workerPromise = new Promise((resolve, reject) => { + worker.onmessage = (e) => { + resolve(e.data); + }; + + worker.onerror = (error) => { + reject(error); + }; + }); + + // 发送数据到Worker + worker.postMessage({ + imageData: imageData.data.buffer, + width: imageData.width, + height: imageData.height, + settings: { + nsfw_detection: uploadSetting.nsfw_detection, + anime_detection: uploadSetting.anime_detection, + landscape_detection: uploadSetting.landscape_detection, + target_detection: uploadSetting.target_detection + }, + }, [imageData.data.buffer]); + + // 等待Worker处理结果 + const analysisResult = await workerPromise; + + // 处理Worker返回的结果 + if (analysisResult.error) { + console.error('Worker处理图像时出错:', analysisResult.error); + } else { + // NSFW检测结果处理 + if (uploadSetting.nsfw_detection && analysisResult.isNSFW) { + message.error(i18n.global.t('comment.illegalImage')); + progressStatus.value = 'exception'; + fileList.value.pop(); + predicting.value = false; + worker.terminate(); + return false; + } + + // 动漫检测结果处理 + if (uploadSetting.anime_detection) { + predictResult.isAnime = analysisResult.isAnime || false; + } + + // 景观识别结果处理 + if (uploadSetting.landscape_detection && analysisResult.landscape) { + predictResult.landscape = analysisResult.landscape as 'building' | 'forest' | 'glacier' | 'mountain' | 'sea' | 'street' | null; + } + + // 目标检测结果处理 + if (uploadSetting.target_detection) { + predictResult.tagName = analysisResult.tagName || null; + predictResult.topCategory = analysisResult.topCategory || null; + } + } + + // 终止Worker + worker.terminate(); + + await smoothUpdateProgress(currentProgress + stepIncrement, 500); + currentProgress += stepIncrement; + } + + // 确保进度条到100% + if (currentProgress < 100) { + await smoothUpdateProgress(100, 500); + } + + predicting.value = false; + return true; + + } catch (error) { + console.error('识别过程中发生错误:', error); + predicting.value = false; + progressPercent.value = 0; // 重置进度条 + return false; + } + } + /** * 提取 EXIF 数据 @@ -311,6 +595,7 @@ export const useUploadStore = defineStore( onMounted(async () => { await ready(); + await initBlurDetect(); }); @@ -331,6 +616,8 @@ export const useUploadStore = defineStore( fileList, rejectFile, removeFile, + blurDetectionControl, + beforeUploadWithWebWorker }; }, { @@ -338,7 +625,7 @@ export const useUploadStore = defineStore( persistedState: { persist: true, storage: localStorage, - key: 'upload', + key: 'STORE-UPLOAD', includePaths: ["storageSelected", "albumSelected", "uploadSetting"] } } diff --git a/src/store/modules/upscaleStore.ts b/src/store/modules/upscaleStore.ts index fe90b06..c4c2ae5 100644 --- a/src/store/modules/upscaleStore.ts +++ b/src/store/modules/upscaleStore.ts @@ -187,7 +187,6 @@ export const useUpscaleStore = defineStore( wasmModule.value._free(sourcePtr); wasmModule.value._free(targetPtr); } - } @@ -232,7 +231,7 @@ export const useUpscaleStore = defineStore( persistedState: { persist: false, storage: localForage, - key: 'upscale', + key: 'STORE-UPSCALE', includePaths: [], } } diff --git a/src/store/modules/userStore.ts b/src/store/modules/userStore.ts index 1795fe6..4f32b50 100644 --- a/src/store/modules/userStore.ts +++ b/src/store/modules/userStore.ts @@ -170,7 +170,7 @@ export const useAuthStore = defineStore( persistedState: { persist: true, storage: localStorage, - key: 'user', + key: 'STORE-USER', includePaths: ['user', 'token', "clientId"] } } diff --git a/src/utils/file/image-converter.ts b/src/utils/file/image-converter.ts new file mode 100644 index 0000000..59f46e7 --- /dev/null +++ b/src/utils/file/image-converter.ts @@ -0,0 +1,132 @@ +// image-converter.ts +import {ref} from 'vue'; + + +/** + * 图片加载状态 + */ +export const imageLoading = ref(false); + +/** + * 将File类型的图片转换为ArrayBuffer + * @param file 图片文件 + * @returns Promise 包含图片的宽度、高度和ArrayBuffer数据 + */ +export const fileToArrayBuffer = (file: File): Promise<{ width: number; height: number; buffer: ArrayBuffer }> => { + imageLoading.value = true; + + return new Promise((resolve, reject) => { + // 检查文件类型 + if (!file.type.startsWith('image/')) { + imageLoading.value = false; + reject(new Error('文件类型必须是图片')); + return; + } + + // 创建FileReader实例读取文件 + const reader = new FileReader(); + + // 读取完成后的处理 + reader.onload = (event) => { + const arrayBuffer = event.target?.result as ArrayBuffer; + + // 创建一个临时的Image对象来获取图片尺寸 + const img = new Image(); + const url = URL.createObjectURL(file); + + img.onload = () => { + // 获取图片尺寸 + const width = img.width; + const height = img.height; + + // 释放URL对象 + URL.revokeObjectURL(url); + imageLoading.value = false; + + // 返回包含尺寸和数据的对象 + resolve({ + width, + height, + buffer: arrayBuffer + }); + }; + + img.onerror = () => { + URL.revokeObjectURL(url); + imageLoading.value = false; + reject(new Error('图片加载失败')); + }; + + img.src = url; + }; + + reader.onerror = () => { + imageLoading.value = false; + reject(new Error('文件读取失败')); + }; + + // 开始读取文件为ArrayBuffer + reader.readAsArrayBuffer(file); + }); +}; + + +/** + * 将 File 对象转换为 ImageData + * @param {File} file - 用户上传的文件 + * @returns {Promise} - 返回解析后的 ImageData + */ +export async function fileToImageData(file: File): Promise { + // 1. 验证文件类型 + if (!file.type.startsWith('image/')) { + throw new Error('文件类型必须是图像'); + } + + // 2. 创建 Image 对象 + const img = await loadImage(file); + + // 3. 创建 Canvas 并绘制图像 + const canvas = document.createElement('canvas'); + canvas.width = img.naturalWidth; + canvas.height = img.naturalHeight; + + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw new Error('无法创建 Canvas 上下文'); + } + + // 确保 img 是 HTMLImageElement + if (!(img instanceof HTMLImageElement)) { + throw new Error('加载的图像不是有效的 HTMLImageElement'); + } + + // 绘制图像到 Canvas + ctx.drawImage(img, 0, 0); + + // 4. 提取 ImageData + return ctx.getImageData(0, 0, canvas.width, canvas.height); +} + +/** + * 加载 File 为 Image 对象 + * @param {File} file - 用户上传的文件 + * @returns {Promise} - 返回加载完成的 Image 对象 + */ +function loadImage(file: File): Promise { + return new Promise((resolve, reject) => { + const img = new Image(); + const url = URL.createObjectURL(file); + + img.onload = () => { + URL.revokeObjectURL(url); // 释放内存 + resolve(img); + }; + + img.onerror = (err) => { + URL.revokeObjectURL(url); + reject(new Error(`图像加载失败: ${err}`)); + }; + + img.src = url; + }); +} diff --git a/src/utils/imageBlurDetect/blurDetect.ts b/src/utils/imageBlurDetect/blurDetect.ts new file mode 100644 index 0000000..876fadf --- /dev/null +++ b/src/utils/imageBlurDetect/blurDetect.ts @@ -0,0 +1,168 @@ +// blurDetect.ts +import Module from './blur_detect.js'; + +/** + * 模糊检测模块 - 使用WebAssembly实现的图像模糊度检测 + */ + +// 模块初始化状态 +let isModuleInitialized = false; +let moduleInstance: any = null; + +/** + * 初始化模糊检测模块 + * @returns Promise + */ +export async function initBlurDetect(): Promise { + if (isModuleInitialized) return; + + try { + moduleInstance = await Module(); + isModuleInitialized = true; + console.log('模糊检测模块初始化成功'); + } catch (error) { + console.error('模糊检测模块初始化失败:', error); + throw new Error('模糊检测模块初始化失败'); + } +} + +/** + * 检测图像是否模糊 + * @param imageData - Canvas ImageData对象 + * @param threshold - 模糊度阈值,默认为200,值越小越容易被判定为模糊 + * @returns boolean - true表示图像模糊,false表示图像清晰 + */ +export function isBlur(imageData: ImageData, threshold = 200): boolean { + if (!isModuleInitialized || !moduleInstance) { + throw new Error('模糊检测模块未初始化,请先调用initBlurDetect()'); + } + + const ptr = moduleInstance._malloc(imageData.data.length); + if (!ptr) throw new Error('内存分配失败'); + + try { + moduleInstance.HEAPU8.set(imageData.data, ptr); + const variance = moduleInstance._laplacian_blur_detect( + ptr, + imageData.width, + imageData.height + ); + + if (variance < 0) throw new Error('模糊检测失败'); + return variance < threshold; + } finally { + moduleInstance._free(ptr); + } +} + +/** + * 获取图像的模糊度值 + * @param imageData - Canvas ImageData对象 + * @returns number - 模糊度值,值越小表示图像越模糊 + */ +export function getBlurValue(imageData: ImageData): number { + if (!isModuleInitialized || !moduleInstance) { + throw new Error('模糊检测模块未初始化,请先调用initBlurDetect()'); + } + + const ptr = moduleInstance._malloc(imageData.data.length); + if (!ptr) throw new Error('内存分配失败'); + + try { + moduleInstance.HEAPU8.set(imageData.data, ptr); + const variance = moduleInstance._laplacian_blur_detect( + ptr, + imageData.width, + imageData.height + ); + + if (variance < 0) throw new Error('模糊检测失败'); + return variance; + } finally { + moduleInstance._free(ptr); + } +} + +/** + * 从Image或Canvas元素创建ImageData + * @param source - HTMLImageElement或HTMLCanvasElement + * @returns ImageData + */ +export function createImageDataFromSource(source: HTMLImageElement | HTMLCanvasElement): ImageData { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + if (!ctx) { + throw new Error('无法创建Canvas上下文'); + } + + canvas.width = source.width || (source as HTMLImageElement).naturalWidth || 300; + canvas.height = source.height || (source as HTMLImageElement).naturalHeight || 200; + + ctx.drawImage(source, 0, 0, canvas.width, canvas.height); + return ctx.getImageData(0, 0, canvas.width, canvas.height); +} + +/** + * 从URL加载图像并检测模糊度 + * @param url - 图像URL + * @param threshold - 模糊度阈值 + * @returns Promise<{isBlurry: boolean, blurValue: number}> + */ +export async function detectBlurFromUrl(url: string, threshold = 200): Promise<{isBlurry: boolean, blurValue: number}> { + return new Promise((resolve, reject) => { + const img = new Image(); + img.crossOrigin = 'anonymous'; + + img.onload = () => { + try { + const imageData = createImageDataFromSource(img); + const blurValue = getBlurValue(imageData); + const isBlurry = blurValue < threshold; + + resolve({ + isBlurry, + blurValue + }); + } catch (error) { + reject(error); + } + }; + + img.onerror = () => { + reject(new Error('图像加载失败')); + }; + + img.src = url; + }); +} + +/** + * 从File对象检测模糊度 + * @param file - 图像文件 + * @param threshold - 模糊度阈值 + * @returns Promise<{isBlurry: boolean, blurValue: number}> + */ +export async function detectBlurFromFile(file: File, threshold = 200): Promise<{isBlurry: boolean, blurValue: number}> { + return new Promise((resolve, reject) => { + if (!file.type.startsWith('image/')) { + reject(new Error('文件不是图像类型')); + return; + } + + const reader = new FileReader(); + + reader.onload = (event) => { + const url = event.target?.result as string; + detectBlurFromUrl(url, threshold) + .then(resolve) + .catch(reject); + }; + + reader.onerror = () => { + reject(new Error('文件读取失败')); + }; + + reader.readAsDataURL(file); + }); +} diff --git a/src/utils/imageBlurDetect/blur_detect.c b/src/utils/imageBlurDetect/blur_detect.c new file mode 100644 index 0000000..73ff8d6 --- /dev/null +++ b/src/utils/imageBlurDetect/blur_detect.c @@ -0,0 +1,76 @@ +// blur_detect.c +// emcc blur_detect.c -O3 -o blur_detect.js -s WASM=1 -s EXPORTED_FUNCTIONS="['_laplacian_blur_detect', '_malloc', '_free']" -s EXPORTED_RUNTIME_METHODS="['HEAPU8', 'setValue']" -s ALLOW_MEMORY_GROWTH=1 -s MODULARIZE=1 -s EXPORT_ES6=1 -s ENVIRONMENT='web' +// blur_detect.c +#include +#include +#include + +#define RESTRICT __restrict__ // 兼容各编译器的限制指针 + +// 内存池优化(线程不安全,单线程环境专用) +static uint8_t* gray_buffer = NULL; +static size_t buffer_size = 0; + +// 灰度转换优化(循环展开+寄存器重用) +static inline void rgb2gray(uint8_t* RESTRICT gray, + const uint8_t* RESTRICT rgba, + int total_pixels) { + for (int i = 0; i < total_pixels; i += 4) { + // 一次处理4个像素(16字节输入) + const uint8_t* p = rgba + i*4; + gray[i] = (9798*p[0] + 19235*p[1] + 3736*p[2]) >> 15; + gray[i+1] = (9798*p[4] + 19235*p[5] + 3736*p[6]) >> 15; + gray[i+2] = (9798*p[8] + 19235*p[9] + 3736*p[10]) >> 15; + gray[i+3] = (9798*p[12] + 19235*p[13] + 3736*p[14]) >> 15; + } +} + +// 主检测函数(内存访问优化版) +double laplacian_blur_detect(const uint8_t* RESTRICT rgba, + int width, int height) { + const int gray_size = width * height; + + // 内存池管理(避免重复分配) + if (gray_size > buffer_size) { + free(gray_buffer); + gray_buffer = (uint8_t*)malloc(gray_size); + if (!gray_buffer) return NAN; // 使用NaN表示错误 + buffer_size = gray_size; + } + + // 灰度转换 + rgb2gray(gray_buffer, rgba, width * height); + + // 卷积计算参数 + const int8_t kernel[9] = {0,1,0,1,-4,1,0,1,0}; + const int stride = width; + int64_t variance = 0; + const int h_bound = height-1, w_bound = width-1; + + // 内存局部性优化:按行缓存指针 + for (int y = 1; y < h_bound; ++y) { + const uint8_t* row_prev = gray_buffer + (y-1)*stride; + const uint8_t* row_curr = row_prev + stride; + const uint8_t* row_next = row_curr + stride; + + // 列计算(展开循环减少分支预测) + for (int x = 1; x < w_bound; ++x) { + // 寄存器缓存邻域像素 + const uint8_t p0 = row_prev[x-1], p1 = row_prev[x], p2 = row_prev[x+1]; + const uint8_t p3 = row_curr[x-1], p4 = row_curr[x], p5 = row_curr[x+1]; + const uint8_t p6 = row_next[x-1], p7 = row_next[x], p8 = row_next[x+1]; + + // 展开卷积运算 + const int sum = + p0*kernel[0] + p1*kernel[1] + p2*kernel[2] + + p3*kernel[3] + p4*kernel[4] + p5*kernel[5] + + p6*kernel[6] + p7*kernel[7] + p8*kernel[8]; + + variance += sum * sum; + } + } + + return variance / (double)((width-2)*(height-2)); +} + +// emcc blur_detect.c -O3 -o blur_detect.js -s WASM=1 -s EXPORTED_FUNCTIONS="['_laplacian_blur_detect', '_malloc', '_free']" -s EXPORTED_RUNTIME_METHODS="['HEAPU8', 'setValue']" -s ALLOW_MEMORY_GROWTH=1 -s MODULARIZE=1 -s EXPORT_ES6=1 -s ENVIRONMENT='web' -s STRICT=1 -flto diff --git a/src/utils/imageBlurDetect/blur_detect.js b/src/utils/imageBlurDetect/blur_detect.js new file mode 100644 index 0000000..361cd35 --- /dev/null +++ b/src/utils/imageBlurDetect/blur_detect.js @@ -0,0 +1,18 @@ +/* @vite-ignore */ +/* eslint-disable */ +// @ts-nocheck +var Module = (() => { + var _scriptName = import.meta.url; + + return ( +async function(moduleArg = {}) { + var moduleRtn; + +var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_WEB=true;var ENVIRONMENT_IS_WORKER=false;var moduleOverrides=Object.assign({},Module);var quit_=(status,toThrow)=>{throw toThrow};var scriptDirectory="";function locateFile(path){return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.slice(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;var wasmBinary;var wasmMemory;var ABORT=false;var EXITSTATUS;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAP64,HEAPU64,HEAPF64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){}function initRuntime(){runtimeInitialized=true;wasmExports["g"]()}function postRun(){}var runDependencies=0;var dependenciesFulfilled=null;function addRunDependency(id){runDependencies++}function removeRunDependency(id){runDependencies--;if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("blur_detect.wasm")}return new URL("blur_detect.wasm",import.meta.url).href}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){return{a:wasmImports}}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["f"];updateMemoryViews();removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();wasmBinaryFile??=findWasmBinary();try{var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}catch(e){readyPromiseReject(e);return Promise.reject(e)}}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}function setValue(ptr,value,type="i8"){if(type.endsWith("*"))type="*";switch(type){case"i1":HEAP8[ptr]=value;break;case"i8":HEAP8[ptr]=value;break;case"i16":HEAP16[ptr>>1]=value;break;case"i32":HEAP32[ptr>>2]=value;break;case"i64":HEAP64[ptr>>3]=BigInt(value);break;case"float":HEAPF32[ptr>>2]=value;break;case"double":HEAPF64[ptr>>3]=value;break;case"*":HEAPU32[ptr>>2]=value;break;default:abort(`invalid type for setValue: ${type}`)}}var __abort_js=()=>abort("");var runtimeKeepaliveCounter=0;var __emscripten_runtime_keepalive_clear=()=>{runtimeKeepaliveCounter=0};var timers={};var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var keepRuntimeAlive=()=>true;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;var maybeExit=()=>{if(!keepRuntimeAlive()){try{_exit(EXITSTATUS)}catch(e){handleException(e)}}};var callUserCallback=func=>{if(ABORT){return}try{func();maybeExit()}catch(e){handleException(e)}};var _emscripten_get_now=()=>performance.now();var __setitimer_js=(which,timeout_ms)=>{if(timers[which]){clearTimeout(timers[which].id);delete timers[which]}if(!timeout_ms)return 0;var id=setTimeout(()=>{delete timers[which];callUserCallback(()=>__emscripten_timeout(which,_emscripten_get_now()))},timeout_ms);timers[which]={id,timeout_ms};return 0};var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var wasmImports={c:__abort_js,b:__emscripten_runtime_keepalive_clear,d:__setitimer_js,e:_emscripten_resize_heap,a:_proc_exit};var wasmExports=await createWasm();var ___wasm_call_ctors=wasmExports["g"];var _laplacian_blur_detect=Module["_laplacian_blur_detect"]=wasmExports["h"];var _free=Module["_free"]=wasmExports["i"];var _malloc=Module["_malloc"]=wasmExports["j"];var __emscripten_timeout=wasmExports["k"];Module["setValue"]=setValue;function run(){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve(Module);postRun()}{doRun()}}run();moduleRtn=readyPromise; + + + return moduleRtn; +} +); +})(); +export default Module; diff --git a/src/utils/imageBlurDetect/blur_detect.wasm b/src/utils/imageBlurDetect/blur_detect.wasm new file mode 100644 index 0000000000000000000000000000000000000000..8b5f6789a5590660715b18933f568de6886f20d0 GIT binary patch literal 7514 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 { return getCategory(results.dataSync().indexOf(results.max().dataSync()[0])); }; +export const predictLandscapeTensor = async (tensor: tf.Tensor3D | tf.Tensor4D) => { + if (!tensor) return; + const model = await loadLandscapeRecognitionModel(); + + // 确保输入张量的尺寸正确 + let processedTensor = tensor; + if (tensor.shape[0] !== 150 || tensor.shape[1] !== 150) { + processedTensor = tf.image.resizeBilinear(tensor, [150, 150]); + } + + // 确保张量是float32类型 + if (processedTensor.dtype !== 'float32') { + processedTensor = tf.cast(processedTensor, 'float32'); + } + + // 归一化处理 + const offset = tf.scalar(127.5); + const normalized = processedTensor.sub(offset).div(offset); + + // 确保张量形状正确,添加批次维度 + let batched = normalized; + if (normalized.shape.length === 3) { + batched = normalized.reshape([1, 150, 150, 3]); + } + + const results: any = model.predict(batched); + return getCategory(results.dataSync().indexOf(results.max().dataSync()[0])); +}; + +// 接收ImageData进行推理的函数 +export const predictLandscapeImageData = async (imageData: ImageData) => { + if (!imageData) return; + const model = await loadLandscapeRecognitionModel(); + + // 将ImageData转换为张量 + const img = tf.cast(tf.browser.fromPixels(imageData), 'float32').resizeBilinear([150, 150]); + + // 归一化处理 + const offset = tf.scalar(127.5); + const normalized = img.sub(offset).div(offset); + const batched = normalized.reshape([1, 150, 150, 3]); + + const results: any = model.predict(batched); + return getCategory(results.dataSync().indexOf(results.max().dataSync()[0])); +}; + const getCategory = (index: number) => { switch (index) { case 0: diff --git a/src/utils/tfjs/mobilenet.ts b/src/utils/tfjs/mobilenet.ts index 4e07964..8e40ca2 100644 --- a/src/utils/tfjs/mobilenet.ts +++ b/src/utils/tfjs/mobilenet.ts @@ -77,10 +77,7 @@ export async function loadCocoSsdModel() { } // 加载 COCO SSD 模型的工具函数 -// 使用提取的加载模型工具函数 -export async function cocoSsdPredict(image) { - // 初始化 TensorFlow.js - tf.setBackend('webgl'); +export async function cocoSsdPredict(image: any) { if (!(await initializeTensorFlow())) { return []; } diff --git a/src/views/Admin/Auth/Login.vue b/src/views/Admin/Auth/Login.vue new file mode 100644 index 0000000..122defa --- /dev/null +++ b/src/views/Admin/Auth/Login.vue @@ -0,0 +1,243 @@ + + + + + diff --git a/src/views/Admin/Error/PageError403.vue b/src/views/Admin/Error/PageError403.vue new file mode 100644 index 0000000..db5b3cc --- /dev/null +++ b/src/views/Admin/Error/PageError403.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/src/views/Admin/Error/PageError404.vue b/src/views/Admin/Error/PageError404.vue new file mode 100644 index 0000000..f7ccddd --- /dev/null +++ b/src/views/Admin/Error/PageError404.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/src/views/Admin/Error/PageError500.vue b/src/views/Admin/Error/PageError500.vue new file mode 100644 index 0000000..7776cee --- /dev/null +++ b/src/views/Admin/Error/PageError500.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/src/views/Admin/System/Components/SystemHeader.vue b/src/views/Admin/System/Components/SystemHeader.vue new file mode 100644 index 0000000..fe10b12 --- /dev/null +++ b/src/views/Admin/System/Components/SystemHeader.vue @@ -0,0 +1,172 @@ + + + + + diff --git a/src/views/Admin/System/Components/SystemSidebar.vue b/src/views/Admin/System/Components/SystemSidebar.vue new file mode 100644 index 0000000..6752f2a --- /dev/null +++ b/src/views/Admin/System/Components/SystemSidebar.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/src/views/Admin/System/Index.vue b/src/views/Admin/System/Index.vue new file mode 100644 index 0000000..7ae616e --- /dev/null +++ b/src/views/Admin/System/Index.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/src/views/Admin/System/Pages/BasicSettings.vue b/src/views/Admin/System/Pages/BasicSettings.vue new file mode 100644 index 0000000..100d67d --- /dev/null +++ b/src/views/Admin/System/Pages/BasicSettings.vue @@ -0,0 +1,258 @@ + + + + + \ No newline at end of file diff --git a/src/views/Admin/System/Pages/Dashboard.vue b/src/views/Admin/System/Pages/Dashboard.vue new file mode 100644 index 0000000..651ba65 --- /dev/null +++ b/src/views/Admin/System/Pages/Dashboard.vue @@ -0,0 +1,343 @@ + + + + + diff --git a/src/views/Admin/System/Pages/PermissionSetting.vue b/src/views/Admin/System/Pages/PermissionSetting.vue new file mode 100644 index 0000000..f94253d --- /dev/null +++ b/src/views/Admin/System/Pages/PermissionSetting.vue @@ -0,0 +1,380 @@ + + + + + diff --git a/src/views/Admin/System/Pages/RoleManagement.vue b/src/views/Admin/System/Pages/RoleManagement.vue new file mode 100644 index 0000000..cf9f4bb --- /dev/null +++ b/src/views/Admin/System/Pages/RoleManagement.vue @@ -0,0 +1,427 @@ + + + + + diff --git a/src/views/Admin/System/Pages/SecuritySettings.vue b/src/views/Admin/System/Pages/SecuritySettings.vue new file mode 100644 index 0000000..611b41f --- /dev/null +++ b/src/views/Admin/System/Pages/SecuritySettings.vue @@ -0,0 +1,215 @@ + + + + + \ No newline at end of file diff --git a/src/views/Admin/System/Pages/StorageManagement.vue b/src/views/Admin/System/Pages/StorageManagement.vue new file mode 100644 index 0000000..5762722 --- /dev/null +++ b/src/views/Admin/System/Pages/StorageManagement.vue @@ -0,0 +1,747 @@ + + + + + diff --git a/src/views/Admin/System/Pages/SystemLogs.vue b/src/views/Admin/System/Pages/SystemLogs.vue new file mode 100644 index 0000000..9059ef8 --- /dev/null +++ b/src/views/Admin/System/Pages/SystemLogs.vue @@ -0,0 +1,410 @@ + + + + + \ No newline at end of file diff --git a/src/views/Admin/System/Pages/UserAnalysis.vue b/src/views/Admin/System/Pages/UserAnalysis.vue new file mode 100644 index 0000000..280d96b --- /dev/null +++ b/src/views/Admin/System/Pages/UserAnalysis.vue @@ -0,0 +1,590 @@ + + + + + diff --git a/src/views/Admin/System/Pages/UserList.vue b/src/views/Admin/System/Pages/UserList.vue new file mode 100644 index 0000000..8389b72 --- /dev/null +++ b/src/views/Admin/System/Pages/UserList.vue @@ -0,0 +1,438 @@ + + + + + diff --git a/src/views/Admin/System/Pages/VisitStatistics.vue b/src/views/Admin/System/Pages/VisitStatistics.vue new file mode 100644 index 0000000..4d87d98 --- /dev/null +++ b/src/views/Admin/System/Pages/VisitStatistics.vue @@ -0,0 +1,602 @@ + + + + + diff --git a/src/views/Preview/PreviewBlurDetect.vue b/src/views/Preview/PreviewBlurDetect.vue new file mode 100644 index 0000000..0ff15d6 --- /dev/null +++ b/src/views/Preview/PreviewBlurDetect.vue @@ -0,0 +1,298 @@ + + + + + diff --git a/src/views/Upscale/CompareImage.vue b/src/views/Upscale/CompareImage.vue index 5b8e091..474600b 100644 --- a/src/views/Upscale/CompareImage.vue +++ b/src/views/Upscale/CompareImage.vue @@ -151,6 +151,7 @@ const updateQrcodeSize = () => { }; function generateQrCodeUrl(): string { + console.log(import.meta.env.VITE_APP_WEB_URL + "/main/upscale/phone/app?user_id=" + user.user.uid + "&token=" + user.token.accessToken); return import.meta.env.VITE_APP_WEB_URL + "/main/upscale/phone/app?user_id=" + user.user.uid + "&token=" + user.token.accessToken; } diff --git a/src/views/User/AccountSetting/components/AccountSettingStorage/StorageCard.vue b/src/views/User/AccountSetting/components/AccountSettingStorage/StorageCard.vue index 2e7797c..d87776b 100644 --- a/src/views/User/AccountSetting/components/AccountSettingStorage/StorageCard.vue +++ b/src/views/User/AccountSetting/components/AccountSettingStorage/StorageCard.vue @@ -51,7 +51,6 @@