From 91fef896571fe5efd840cc0ad7dfa2ad6d0e02c8 Mon Sep 17 00:00:00 2001 From: landaiqing Date: Tue, 10 Dec 2024 12:17:02 +0800 Subject: [PATCH] :sparkles: add image upscale --- components.d.ts | 8 + package.json | 4 +- src/assets/svgs/download.svg | 1 + src/assets/svgs/empty.svg | 1 + src/assets/svgs/file.svg | 2 +- src/assets/svgs/package-download.svg | 1 + src/assets/svgs/qr-phone.svg | 1 + src/assets/svgs/remove.svg | 1 + src/assets/svgs/run.svg | 1 + src/assets/svgs/save.svg | 1 + src/main.ts | 5 +- src/store/index.ts | 2 + src/store/modules/commentStore.ts | 13 +- src/store/modules/langStore.ts | 8 +- src/store/modules/themeStore.ts | 7 +- src/store/modules/upscaleStore.ts | 88 +++++++++++ src/store/modules/userStore.ts | 12 +- src/store/modules/websocketStore.ts | 4 +- src/utils/nsfw/nsfw.ts | 2 +- src/views/Main/index.scss | 19 +-- src/views/Upscale/CompareResult.vue | 184 +++++++++++++++++++++++ src/views/Upscale/UploadImage.vue | 211 +++++++++++++++++++++++++++ src/views/Upscale/Upscale.vue | 109 +++++++++++++- src/views/Upscale/index.scss | 80 ++++++++++ 24 files changed, 734 insertions(+), 31 deletions(-) create mode 100644 src/assets/svgs/download.svg create mode 100644 src/assets/svgs/empty.svg create mode 100644 src/assets/svgs/package-download.svg create mode 100644 src/assets/svgs/qr-phone.svg create mode 100644 src/assets/svgs/remove.svg create mode 100644 src/assets/svgs/run.svg create mode 100644 src/assets/svgs/save.svg create mode 100644 src/store/modules/upscaleStore.ts create mode 100644 src/views/Upscale/CompareResult.vue create mode 100644 src/views/Upscale/UploadImage.vue diff --git a/components.d.ts b/components.d.ts index 1d0609b..6ec1a46 100644 --- a/components.d.ts +++ b/components.d.ts @@ -47,6 +47,8 @@ declare module 'vue' { ARadio: typeof import('ant-design-vue/es')['Radio'] ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup'] 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'] ASpace: typeof import('ant-design-vue/es')['Space'] ASpin: typeof import('ant-design-vue/es')['Spin'] @@ -57,6 +59,7 @@ declare module 'vue' { ATextarea: typeof import('ant-design-vue/es')['Textarea'] ATooltip: typeof import('ant-design-vue/es')['Tooltip'] AUpload: typeof import('ant-design-vue/es')['Upload'] + AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger'] Avatar: typeof import('./src/components/MyUI/Avatar/Avatar.vue')['default'] BackgroundAnimation: typeof import('./src/components/BackgroundAnimation/BackgroundAnimation.vue')['default'] BackTop: typeof import('./src/components/MyUI/BackTop/BackTop.vue')['default'] @@ -74,6 +77,7 @@ declare module 'vue' { CloseCircleOutlined: typeof import('@ant-design/icons-vue')['CloseCircleOutlined'] Clouds: typeof import('./src/components/Clouds/Clouds.vue')['default'] CloudServerOutlined: typeof import('@ant-design/icons-vue')['CloudServerOutlined'] + CloudUploadOutlined: typeof import('@ant-design/icons-vue')['CloudUploadOutlined'] Col: typeof import('./src/components/MyUI/Grid/Col.vue')['default'] Collapse: typeof import('./src/components/MyUI/Collapse/Collapse.vue')['default'] CommentInput: typeof import('./src/components/CommentReply/src/CommentInput/CommentInput.vue')['default'] @@ -81,6 +85,7 @@ declare module 'vue' { CommentOutlined: typeof import('@ant-design/icons-vue')['CommentOutlined'] CommentReply: typeof import('./src/components/CommentReply/index.vue')['default'] CompareImage: typeof import('./src/components/ImageCompare/CompareImage.vue')['default'] + CompareResult: typeof import('./src/views/Upscale/CompareResult.vue')['default'] Countdown: typeof import('./src/components/MyUI/Countdown/Countdown.vue')['default'] CustomerServiceOutlined: typeof import('@ant-design/icons-vue')['CustomerServiceOutlined'] DatePicker: typeof import('./src/components/MyUI/DatePicker/DatePicker.vue')['default'] @@ -104,6 +109,7 @@ declare module 'vue' { ImageCompare: typeof import('./src/components/ImageCompare/ImageCompare.vue')['default'] ImageInPainting: typeof import('./src/views/Upscale/Upscale.vue')['default'] ImageShare: typeof import('./src/views/ImageShare/ImageShare.vue')['default'] + InboxOutlined: typeof import('@ant-design/icons-vue')['InboxOutlined'] Input: typeof import('./src/components/MyUI/Input/Input.vue')['default'] InputSearch: typeof import('./src/components/MyUI/InputSearch/InputSearch.vue')['default'] LandingPage: typeof import('./src/views/Landing/LandingPage.vue')['default'] @@ -140,6 +146,7 @@ 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'] + RestOutlined: typeof import('@ant-design/icons-vue')['RestOutlined'] Result: typeof import('./src/components/MyUI/Result/Result.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] @@ -174,6 +181,7 @@ declare module 'vue' { TreeChart: typeof import('./src/components/MyUI/TreeChart/TreeChart.vue')['default'] UngroupOutlined: typeof import('@ant-design/icons-vue')['UngroupOutlined'] Upload: typeof import('./src/components/MyUI/Upload/Upload.vue')['default'] + UploadImage: typeof import('./src/views/Upscale/UploadImage.vue')['default'] Upscale: typeof import('./src/views/Upscale/Upscale.vue')['default'] UserInfoCard: typeof import('./src/components/CommentReply/src/UserInfoCard/UserInfoCard.vue')['default'] UserOutlined: typeof import('@ant-design/icons-vue')['UserOutlined'] diff --git a/package.json b/package.json index 02f76ae..1d55dec 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "nprogress": "^0.2.0", "nsfwjs": "^4.2.1", "pinia": "^2.3.0", - "pinia-plugin-persistedstate": "^4.1.3", + "pinia-plugin-persistedstate-2": "^2.0.27", "qrcode": "^1", "seedrandom": "^3.0.5", "swiper": "^11.1.15", @@ -63,7 +63,7 @@ "globals": "^15.13.0", "sass": "^1.82.0", "typescript": "^5.7.2", - "typescript-eslint": "^8.17.0", + "typescript-eslint": "^8.18.0", "unplugin-vue-components": "^0.27.5", "vite": "^6.0.3", "vite-plugin-bundle-obfuscator": "1.3.2", diff --git a/src/assets/svgs/download.svg b/src/assets/svgs/download.svg new file mode 100644 index 0000000..c478d64 --- /dev/null +++ b/src/assets/svgs/download.svg @@ -0,0 +1 @@ + diff --git a/src/assets/svgs/empty.svg b/src/assets/svgs/empty.svg new file mode 100644 index 0000000..f8b7977 --- /dev/null +++ b/src/assets/svgs/empty.svg @@ -0,0 +1 @@ + diff --git a/src/assets/svgs/file.svg b/src/assets/svgs/file.svg index 3101494..d149e82 100644 --- a/src/assets/svgs/file.svg +++ b/src/assets/svgs/file.svg @@ -1 +1 @@ - + diff --git a/src/assets/svgs/package-download.svg b/src/assets/svgs/package-download.svg new file mode 100644 index 0000000..72b9e17 --- /dev/null +++ b/src/assets/svgs/package-download.svg @@ -0,0 +1 @@ + diff --git a/src/assets/svgs/qr-phone.svg b/src/assets/svgs/qr-phone.svg new file mode 100644 index 0000000..35bd4a7 --- /dev/null +++ b/src/assets/svgs/qr-phone.svg @@ -0,0 +1 @@ + diff --git a/src/assets/svgs/remove.svg b/src/assets/svgs/remove.svg new file mode 100644 index 0000000..9563079 --- /dev/null +++ b/src/assets/svgs/remove.svg @@ -0,0 +1 @@ + diff --git a/src/assets/svgs/run.svg b/src/assets/svgs/run.svg new file mode 100644 index 0000000..85e0c57 --- /dev/null +++ b/src/assets/svgs/run.svg @@ -0,0 +1 @@ + diff --git a/src/assets/svgs/save.svg b/src/assets/svgs/save.svg new file mode 100644 index 0000000..67966fe --- /dev/null +++ b/src/assets/svgs/save.svg @@ -0,0 +1 @@ + diff --git a/src/main.ts b/src/main.ts index f6220fe..cf1ca81 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,12 +6,13 @@ import router from "@/router/router.ts"; import "go-captcha-vue/dist/style.css"; import GoCaptcha from "go-captcha-vue"; import {createPinia, Pinia} from "pinia"; -import piniaPluginPersistedstate from "pinia-plugin-persistedstate"; +import {createPersistedStatePlugin} from 'pinia-plugin-persistedstate-2'; import VueDOMPurifyHTML from 'vue-dompurify-html'; import {registerDirectives} from "@/directives"; const pinia: Pinia = createPinia(); -pinia.use(piniaPluginPersistedstate); +const installPersistedStatePlugin = createPersistedStatePlugin(); +pinia.use((context) => installPersistedStatePlugin(context)); const app = createApp(App); registerDirectives(app); app.use(pinia); diff --git a/src/store/index.ts b/src/store/index.ts index c19727a..4e1fdcd 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -3,6 +3,7 @@ import {useThemeStore} from "@/store/modules/themeStore.ts"; import {langStore} from "@/store/modules/langStore.ts"; import {useCommentStore} from "@/store/modules/commentStore.ts"; import {useWebSocketStore} from "@/store/modules/websocketStore.ts"; +import {useUpscaleStore} from "@/store/modules/upscaleStore.ts"; export default function useStore() { return { @@ -11,5 +12,6 @@ export default function useStore() { lang: langStore(), comment: useCommentStore(), websocket: useWebSocketStore(), + upscale: useUpscaleStore(), }; } diff --git a/src/store/modules/commentStore.ts b/src/store/modules/commentStore.ts index 9400574..ae3f751 100644 --- a/src/store/modules/commentStore.ts +++ b/src/store/modules/commentStore.ts @@ -8,6 +8,7 @@ import QQ_EMOJI from "@/constant/qq_emoji.ts"; import {initNSFWJs, predictNSFW} from "@/utils/nsfw/nsfw.ts"; import {NSFWJS} from "nsfwjs"; import i18n from "@/locales"; +import localForage from "localforage"; export const useCommentStore = defineStore( 'comment', @@ -325,10 +326,16 @@ export const useCommentStore = defineStore( }, { // 开启数据持久化 - persist: { + // persist: { + // key: 'comment', + // storage: localStorage, + // pick: ["emojiList", "commentList", "replyVisibility", "commentMap"], + // } + persistedState: { + persist: true, + storage: localForage, key: 'comment', - storage: localStorage, - pick: ["emojiList", "commentList", "replyVisibility", "commentMap"], + includePaths: ["emojiList", "commentList", "replyVisibility", "commentMap"] } } ); diff --git a/src/store/modules/langStore.ts b/src/store/modules/langStore.ts index ed231cd..014965d 100644 --- a/src/store/modules/langStore.ts +++ b/src/store/modules/langStore.ts @@ -1,6 +1,7 @@ import {defineStore} from 'pinia'; import {ref} from "vue"; + export const langStore = defineStore( 'lang', () => { @@ -11,10 +12,11 @@ export const langStore = defineStore( }, { // 开启数据持久化 - persist: { - key: 'lang', + persistedState: { + persist: true, storage: localStorage, - pick: ["lang"], + key: 'lang', + includePaths: ['lang'] } } ); diff --git a/src/store/modules/themeStore.ts b/src/store/modules/themeStore.ts index cd86b2f..6a2606a 100644 --- a/src/store/modules/themeStore.ts +++ b/src/store/modules/themeStore.ts @@ -37,10 +37,11 @@ export const useThemeStore = defineStore( return {themeName, themeConfig, darkMode, setThemeName, toggleDarkMode}; }, { - persist: { - key: 'theme', + persistedState: { + persist: true, storage: localStorage, - pick: ["themeName", "darkMode"], + key: 'theme', + includePaths: ['themeName', 'darkMode'] } } ); diff --git a/src/store/modules/upscaleStore.ts b/src/store/modules/upscaleStore.ts new file mode 100644 index 0000000..e4c7875 --- /dev/null +++ b/src/store/modules/upscaleStore.ts @@ -0,0 +1,88 @@ +import {defineStore} from 'pinia'; +import {initNSFWJs, predictNSFW} from "@/utils/nsfw/nsfw.ts"; +import i18n from "@/locales"; +import message from "@/components/MyUI/Message/Message.vue"; +import {NSFWJS} from "nsfwjs"; +import localForage from "localforage"; + +export const useUpscaleStore = defineStore( + 'upscale', + () => { + const imageList = ref([]); + const fileList = ref([]); + const uploading = ref(false); + + /** + * 图片上传前的校验 + * @param file + */ + async function beforeUpload(file: any) { + if (fileList.value.length >= 5) { + return false; + } + uploading.value = true; + if (!window.FileReader) return false; + const reader = new FileReader(); + reader.readAsDataURL(file); // 文件转换 + reader.onloadend = async function () { + const img: HTMLImageElement = document.createElement('img'); + img.src = reader.result as string; + img.onload = async () => { + // 图片 NSFW 检测 + const nsfw: NSFWJS = await initNSFWJs(); + const isNSFW: boolean = await predictNSFW(nsfw, img); + if (isNSFW) { + message.error(i18n.global.t('comment.illegalImage')); + fileList.value.pop(); + return false; + } + fileList.value.push(img.src); + }; + + uploading.value = false; + return true; + }; + } + + /** + * 自定义上传图片请求 + */ + async function customUploadRequest() { + imageList.value = fileList.value; + + } + + /** + * 移除图片 + * @param index + */ + async function removeImage(index: number) { + fileList.value.splice(index, 1); + imageList.value.splice(index, 1); + } + + return { + imageList, + fileList, + uploading, + beforeUpload, + customUploadRequest, + removeImage + }; + } + + , + { + // 开启数据持久化 + persistedState: { + persist: true, + storage: + localForage, + key: + 'upscale', + includePaths: + ['imageList', 'fileList'] + } + } + ) +; diff --git a/src/store/modules/userStore.ts b/src/store/modules/userStore.ts index 2e3306e..3ff72ea 100644 --- a/src/store/modules/userStore.ts +++ b/src/store/modules/userStore.ts @@ -144,10 +144,16 @@ export const useAuthStore = defineStore( }, { // 开启数据持久化 - persist: { - key: 'user', + // persist: { + // key: 'user', + // storage: localStorage, + // pick: ['user', "clientId", "githubRedirectUrl", "giteeRedirectUrl", "qqRedirectUrl"], + // } + persistedState: { + persist: true, storage: localStorage, - pick: ['user', "clientId", "githubRedirectUrl", "giteeRedirectUrl", "qqRedirectUrl"], + key: 'user', + includePaths: ['user', "clientId", "githubRedirectUrl", "giteeRedirectUrl", "qqRedirectUrl"] } } ); diff --git a/src/store/modules/websocketStore.ts b/src/store/modules/websocketStore.ts index c7b235a..a894b57 100644 --- a/src/store/modules/websocketStore.ts +++ b/src/store/modules/websocketStore.ts @@ -49,5 +49,7 @@ export const useWebSocketStore = defineStore('websocket', () => { getReadyState }; }, { - persist: false, + persistedState: { + persist: false, + } }); diff --git a/src/utils/nsfw/nsfw.ts b/src/utils/nsfw/nsfw.ts index 974fc26..02060c9 100644 --- a/src/utils/nsfw/nsfw.ts +++ b/src/utils/nsfw/nsfw.ts @@ -9,7 +9,7 @@ let isInit: boolean = false; const initNSFWJs = async (): Promise => { tf.enableProdMode(); if (!isInit) { - const initialLoad: nsfwjs.NSFWJS = await nsfwjs.load("/nsfw/model/mobilenet_v2_mid/", { + const initialLoad: nsfwjs.NSFWJS = await nsfwjs.load("/nsfw/mobilenet_v2_mid/", { size: 224, type: "graph" }); diff --git a/src/views/Main/index.scss b/src/views/Main/index.scss index e4a5b8a..c1ae92e 100644 --- a/src/views/Main/index.scss +++ b/src/views/Main/index.scss @@ -3,23 +3,20 @@ flex-direction: column; background-color: #eaeef6; color: var(--text-color); - width: 100%; - min-height: 100vh; - - .main-header { - - } + width: 100vw; .main-content { display: flex; flex-direction: row; + width: 100%; + height: calc(100vh - 70px); .main-content-container { - width: calc(100% - 230px); - height: calc(100% - 110px); - padding: 20px; - overflow: scroll; - + width: calc(100vw - 230px); + height: calc(100vh - 100px); + max-height: calc(100vh - 100px); + padding: 15px; + overflow: auto; } } diff --git a/src/views/Upscale/CompareResult.vue b/src/views/Upscale/CompareResult.vue new file mode 100644 index 0000000..47698d9 --- /dev/null +++ b/src/views/Upscale/CompareResult.vue @@ -0,0 +1,184 @@ + + + diff --git a/src/views/Upscale/UploadImage.vue b/src/views/Upscale/UploadImage.vue new file mode 100644 index 0000000..2e5f21b --- /dev/null +++ b/src/views/Upscale/UploadImage.vue @@ -0,0 +1,211 @@ + + + diff --git a/src/views/Upscale/Upscale.vue b/src/views/Upscale/Upscale.vue index 8e222e3..b40b555 100644 --- a/src/views/Upscale/Upscale.vue +++ b/src/views/Upscale/Upscale.vue @@ -1,15 +1,122 @@