complete mobile image upload

This commit is contained in:
2024-12-18 01:07:10 +08:00
parent b11641f62e
commit 68a8e3e5c4
26 changed files with 455 additions and 83 deletions

View File

@@ -3,9 +3,8 @@ VITE_NODE_ENV='development'
# 开发环境 # 开发环境
VITE_APP_BASE_API='/sys' VITE_APP_BASE_API='/sys'
# 网站域名
# 页面 title 前缀 VITE_APP_WEB_URL='http://localhost:5173'
VITE_APP_TITLE=开发环境
# 网络请求公用地址 # 网络请求公用地址
VITE_API_BASE_URL='http://127.0.0.1:80' VITE_API_BASE_URL='http://127.0.0.1:80'
@@ -20,4 +19,6 @@ VITE_QR_SOCKET_URL='ws://127.0.0.1:80/api/ws/qrcode'
VITE_MESSAGE_SOCKET_URL='ws://127.0.0.1:80/api/ws/message' VITE_MESSAGE_SOCKET_URL='ws://127.0.0.1:80/api/ws/message'
VITE_FILE_SOCKET_URL='ws://127.0.0.1:80/api/ws/file'
VITE_FINGERPRINT_KEY='idm0jdoau38lwourb4pbjk4dxkat0kcx' VITE_FINGERPRINT_KEY='idm0jdoau38lwourb4pbjk4dxkat0kcx'

View File

@@ -3,8 +3,8 @@ VITE_NODE_ENV='production'
# 生产环境 # 生产环境
VITE_APP_BASE_API='/sys' VITE_APP_BASE_API='/sys'
# 页面 title 前缀 # 网站域名
VITE_APP_TITLE=生产环境 VITE_APP_WEB_URL='http://localhost:5173'
# 网络请求公用地址 # 网络请求公用地址
VITE_API_BASE_URL='https://landaiqing.cn' VITE_API_BASE_URL='https://landaiqing.cn'
@@ -18,5 +18,8 @@ VITE_APP_TOKEN_KEY='Bearer'
VITE_QR_SOCKET_URL='wss://landaiqing.cn/api/ws/qr_ws' VITE_QR_SOCKET_URL='wss://landaiqing.cn/api/ws/qr_ws'
VITE_MESSAGE_SOCKET_URL='wss://landaiqing.cn/api/ws/message_ws' VITE_MESSAGE_SOCKET_URL='wss://landaiqing.cn/api/ws/message_ws'
VITE_FILE_SOCKET_URL='ws://127.0.0.1:80/api/ws/file'
# 签名密钥 # 签名密钥
VITE_FINGERPRINT_KEY='idm0jdoau38lwourb4pbjk4dxkat0kcx' VITE_FINGERPRINT_KEY='idm0jdoau38lwourb4pbjk4dxkat0kcx'

15
components.d.ts vendored
View File

@@ -27,21 +27,30 @@ declare module 'vue' {
AInput: typeof import('ant-design-vue/es')['Input'] AInput: typeof import('ant-design-vue/es')['Input']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword'] AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
Alert: typeof import('./src/components/MyUI/Alert/Alert.vue')['default'] Alert: typeof import('./src/components/MyUI/Alert/Alert.vue')['default']
AList: typeof import('ant-design-vue/es')['List']
AListItem: typeof import('ant-design-vue/es')['ListItem']
AllPhoto: typeof import('./src/views/Photograph/AllPhoto/AllPhoto.vue')['default'] AllPhoto: typeof import('./src/views/Photograph/AllPhoto/AllPhoto.vue')['default']
AMenu: typeof import('ant-design-vue/es')['Menu'] AMenu: typeof import('ant-design-vue/es')['Menu']
AMenuItem: typeof import('ant-design-vue/es')['MenuItem'] AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
AMenuItemGroup: typeof import('ant-design-vue/es')['MenuItemGroup'] AMenuItemGroup: typeof import('ant-design-vue/es')['MenuItemGroup']
AModal: typeof import('ant-design-vue/es')['Modal'] AModal: typeof import('ant-design-vue/es')['Modal']
AnimatedNature: typeof import('./src/components/AnimatedNature/AnimatedNature.vue')['default'] AnimatedNature: typeof import('./src/components/AnimatedNature/AnimatedNature.vue')['default']
APagination: typeof import('ant-design-vue/es')['Pagination']
APopover: typeof import('ant-design-vue/es')['Popover'] APopover: typeof import('ant-design-vue/es')['Popover']
AProgress: typeof import('ant-design-vue/es')['Progress'] AProgress: typeof import('ant-design-vue/es')['Progress']
AQrcode: typeof import('ant-design-vue/es')['QRCode'] AQrcode: typeof import('ant-design-vue/es')['QRCode']
ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ASelect: typeof import('ant-design-vue/es')['Select'] ASelect: typeof import('ant-design-vue/es')['Select']
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
ASpace: typeof import('ant-design-vue/es')['Space']
ASpin: typeof import('ant-design-vue/es')['Spin'] ASpin: typeof import('ant-design-vue/es')['Spin']
ATabPane: typeof import('ant-design-vue/es')['TabPane'] ATabPane: typeof import('ant-design-vue/es')['TabPane']
ATabs: typeof import('ant-design-vue/es')['Tabs'] ATabs: typeof import('ant-design-vue/es')['Tabs']
ATag: typeof import('ant-design-vue/es')['Tag'] ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATooltip: typeof import('ant-design-vue/es')['Tooltip'] ATooltip: typeof import('ant-design-vue/es')['Tooltip']
AUpload: typeof import('ant-design-vue/es')['Upload']
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger'] AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
Avatar: typeof import('./src/components/MyUI/Avatar/Avatar.vue')['default'] Avatar: typeof import('./src/components/MyUI/Avatar/Avatar.vue')['default']
BackgroundAnimation: typeof import('./src/components/BackgroundAnimation/BackgroundAnimation.vue')['default'] BackgroundAnimation: typeof import('./src/components/BackgroundAnimation/BackgroundAnimation.vue')['default']
@@ -55,6 +64,7 @@ declare module 'vue' {
Carousel: typeof import('./src/components/MyUI/Carousel/Carousel.vue')['default'] Carousel: typeof import('./src/components/MyUI/Carousel/Carousel.vue')['default']
Cascader: typeof import('./src/components/MyUI/Cascader/Cascader.vue')['default'] Cascader: typeof import('./src/components/MyUI/Cascader/Cascader.vue')['default']
Checkbox: typeof import('./src/components/MyUI/Checkbox/Checkbox.vue')['default'] Checkbox: typeof import('./src/components/MyUI/Checkbox/Checkbox.vue')['default']
CloseCircleOutlined: typeof import('@ant-design/icons-vue')['CloseCircleOutlined']
Clouds: typeof import('./src/components/Clouds/Clouds.vue')['default'] Clouds: typeof import('./src/components/Clouds/Clouds.vue')['default']
Col: typeof import('./src/components/MyUI/Grid/Col.vue')['default'] Col: typeof import('./src/components/MyUI/Grid/Col.vue')['default']
Collapse: typeof import('./src/components/MyUI/Collapse/Collapse.vue')['default'] Collapse: typeof import('./src/components/MyUI/Collapse/Collapse.vue')['default']
@@ -71,6 +81,7 @@ declare module 'vue' {
DynamicTitle: typeof import('./src/components/DynamicTitle/DynamicTitle.vue')['default'] DynamicTitle: typeof import('./src/components/DynamicTitle/DynamicTitle.vue')['default']
Ellipsis: typeof import('./src/components/MyUI/Ellipsis/Ellipsis.vue')['default'] Ellipsis: typeof import('./src/components/MyUI/Ellipsis/Ellipsis.vue')['default']
Empty: typeof import('./src/components/MyUI/Empty/Empty.vue')['default'] Empty: typeof import('./src/components/MyUI/Empty/Empty.vue')['default']
EyeOutlined: typeof import('@ant-design/icons-vue')['EyeOutlined']
Flex: typeof import('./src/components/MyUI/Flex/Flex.vue')['default'] Flex: typeof import('./src/components/MyUI/Flex/Flex.vue')['default']
FloatButton: typeof import('./src/components/MyUI/FloatButton/FloatButton.vue')['default'] FloatButton: typeof import('./src/components/MyUI/FloatButton/FloatButton.vue')['default']
Folder: typeof import('./src/components/Folder/Folder.vue')['default'] Folder: typeof import('./src/components/Folder/Folder.vue')['default']
@@ -100,6 +111,8 @@ declare module 'vue' {
ParameterSetting: typeof import('./src/views/Upscale/ParameterSetting.vue')['default'] ParameterSetting: typeof import('./src/views/Upscale/ParameterSetting.vue')['default']
PeopleAlbum: typeof import('./src/views/Album/PeopleAlbum/PeopleAlbum.vue')['default'] PeopleAlbum: typeof import('./src/views/Album/PeopleAlbum/PeopleAlbum.vue')['default']
Phoalbum: typeof import('./src/views/Album/Phoalbum/Phoalbum.vue')['default'] Phoalbum: typeof import('./src/views/Album/Phoalbum/Phoalbum.vue')['default']
PhoneUpload: typeof import('./src/views/PhoneUpload/PhoneUpload.vue')['default']
PlusOutlined: typeof import('@ant-design/icons-vue')['PlusOutlined']
Popconfirm: typeof import('./src/components/MyUI/Popconfirm/Popconfirm.vue')['default'] Popconfirm: typeof import('./src/components/MyUI/Popconfirm/Popconfirm.vue')['default']
Popover: typeof import('./src/components/MyUI/Popover/Popover.vue')['default'] Popover: typeof import('./src/components/MyUI/Popover/Popover.vue')['default']
Progress: typeof import('./src/components/MyUI/Progress/Progress.vue')['default'] Progress: typeof import('./src/components/MyUI/Progress/Progress.vue')['default']
@@ -121,6 +134,7 @@ declare module 'vue' {
Scrollbar: typeof import('./src/components/MyUI/Scrollbar/Scrollbar.vue')['default'] Scrollbar: typeof import('./src/components/MyUI/Scrollbar/Scrollbar.vue')['default']
Segmented: typeof import('./src/components/MyUI/Segmented/Segmented.vue')['default'] Segmented: typeof import('./src/components/MyUI/Segmented/Segmented.vue')['default']
Select: typeof import('./src/components/MyUI/Select/Select.vue')['default'] Select: typeof import('./src/components/MyUI/Select/Select.vue')['default']
SendOutlined: typeof import('@ant-design/icons-vue')['SendOutlined']
Skeleton: typeof import('./src/components/MyUI/Skeleton/Skeleton.vue')['default'] Skeleton: typeof import('./src/components/MyUI/Skeleton/Skeleton.vue')['default']
Slider: typeof import('./src/components/MyUI/Slider/Slider.vue')['default'] Slider: typeof import('./src/components/MyUI/Slider/Slider.vue')['default']
Space: typeof import('./src/components/MyUI/Space/Space.vue')['default'] Space: typeof import('./src/components/MyUI/Space/Space.vue')['default']
@@ -147,6 +161,7 @@ declare module 'vue' {
UserOutlined: typeof import('@ant-design/icons-vue')['UserOutlined'] UserOutlined: typeof import('@ant-design/icons-vue')['UserOutlined']
Video: typeof import('./src/components/MyUI/Video/Video.vue')['default'] Video: typeof import('./src/components/MyUI/Video/Video.vue')['default']
VueCompareImage: typeof import('./src/components/VueCompareImage/VueCompareImage.vue')['default'] VueCompareImage: typeof import('./src/components/VueCompareImage/VueCompareImage.vue')['default']
WarningOutlined: typeof import('@ant-design/icons-vue')['WarningOutlined']
Waterfall: typeof import('./src/components/MyUI/Waterfall/Waterfall.vue')['default'] Waterfall: typeof import('./src/components/MyUI/Waterfall/Waterfall.vue')['default']
} }
} }

View File

@@ -10,7 +10,7 @@
"docker-build": "docker build -t schisandra/schisandra-cloud-album-front ." "docker-build": "docker build -t schisandra/schisandra-cloud-album-front ."
}, },
"dependencies": { "dependencies": {
"@alova/adapter-axios": "^2.0.11", "@alova/adapter-axios": "^2.0.12",
"@ant-design/icons-vue": "^7.0.1", "@ant-design/icons-vue": "^7.0.1",
"@tensorflow/tfjs": "^4.22.0", "@tensorflow/tfjs": "^4.22.0",
"@tensorflow/tfjs-backend-webgl": "^4.22.0", "@tensorflow/tfjs-backend-webgl": "^4.22.0",
@@ -24,7 +24,7 @@
"@vuepic/vue-datepicker": "^10.0.0", "@vuepic/vue-datepicker": "^10.0.0",
"@vueuse/core": "^12.0.0", "@vueuse/core": "^12.0.0",
"@vueuse/integrations": "^12.0.0", "@vueuse/integrations": "^12.0.0",
"alova": "^3.2.6", "alova": "^3.2.7",
"animejs": "^3.2.2", "animejs": "^3.2.2",
"ant-design-vue": "^4.2.6", "ant-design-vue": "^4.2.6",
"autofit.js": "^3.2.2", "autofit.js": "^3.2.2",
@@ -64,7 +64,7 @@
"globals": "^15.13.0", "globals": "^15.13.0",
"sass": "^1.83.0", "sass": "^1.83.0",
"typescript": "^5.6.3", "typescript": "^5.6.3",
"typescript-eslint": "^8.18.0", "typescript-eslint": "^8.18.1",
"unplugin-vue-components": "^0.28.0", "unplugin-vue-components": "^0.28.0",
"vite": "^6.0.3", "vite": "^6.0.3",
"vite-plugin-bundle-obfuscator": "1.4.0", "vite-plugin-bundle-obfuscator": "1.4.0",

15
src/api/upscale/index.ts Normal file
View File

@@ -0,0 +1,15 @@
import {service} from "@/utils/alova/service.ts";
import {uploadImageRequest} from "@/types/upscale";
export const uploadImage = (data: uploadImageRequest) => {
return service.Post('/api/auth/upscale/upload', {
image: data.image,
access_token: data.access_token,
user_id: data.user_id,
}, {
meta: {
ignoreToken: false,
signature: true
}
});
};

View File

@@ -0,0 +1 @@
<svg t="1734406724014" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9426" width="200" height="200"><path d="M714.9568 903.9872H310.8864c-63.2832 0-114.5856-51.3024-114.5856-114.5856V233.0624h633.2416v556.3392c0 63.2832-51.3024 114.5856-114.5856 114.5856z" fill="#FF5F5F" p-id="9427"></path><path d="M905.6768 217.7024H726.528V170.496c0-42.9568-34.9696-77.9264-77.9264-77.9264h-271.36c-42.9568 0-77.9264 34.9696-77.9264 77.9264v47.2064H120.1664c-8.4992 0-15.36 6.8608-15.36 15.36s6.8608 15.36 15.36 15.36h60.7744v540.9792c0 71.6288 58.3168 129.9456 129.9456 129.9456h404.0704c71.6288 0 129.9456-58.3168 129.9456-129.9456V248.4224h60.7744c8.4992 0 15.36-6.8608 15.36-15.36s-6.8608-15.36-15.36-15.36zM330.0352 170.496c0-26.0096 21.1968-47.2064 47.2064-47.2064h271.4112c26.0096 0 47.2064 21.1968 47.2064 47.2064v47.2064H330.0352V170.496z m484.1472 618.9056c0 54.7328-44.4928 99.2256-99.2256 99.2256H310.8864c-54.7328 0-99.2256-44.4928-99.2256-99.2256V248.4224h602.5216v540.9792z" fill="#424242" p-id="9428"></path><path d="M379.3408 734.3104c-8.4992 0-15.36-6.8608-15.36-15.36V402.8928c0-8.4992 6.8608-15.36 15.36-15.36s15.36 6.8608 15.36 15.36v316.0576c0 8.448-6.912 15.36-15.36 15.36zM524.0832 734.3104c-8.4992 0-15.36-6.8608-15.36-15.36V402.8928c0-8.4992 6.8608-15.36 15.36-15.36s15.36 6.8608 15.36 15.36v316.0576c0 8.448-6.912 15.36-15.36 15.36zM670.5152 734.3104c-8.4992 0-15.36-6.8608-15.36-15.36V402.8928c0-8.4992 6.8608-15.36 15.36-15.36s15.36 6.8608 15.36 15.36v316.0576c0 8.448-6.912 15.36-15.36 15.36z" fill="#FFFFFF" p-id="9429"></path></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,11 +1 @@
<svg t="1733761095875" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" <svg t="1734409010371" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16420" width="200" height="200"><path d="M820.409449 797.228346q0 25.19685-10.07874 46.866142t-27.716535 38.299213-41.322835 26.204724-50.897638 9.574803l-357.795276 0q-27.212598 0-50.897638-9.574803t-41.322835-26.204724-27.716535-38.299213-10.07874-46.866142l0-675.275591q0-25.19685 10.07874-47.370079t27.716535-38.80315 41.322835-26.204724 50.897638-9.574803l357.795276 0q27.212598 0 50.897638 9.574803t41.322835 26.204724 27.716535 38.80315 10.07874 47.370079l0 675.275591zM738.771654 170.330709l-455.559055 0 0 577.511811 455.559055 0 0-577.511811zM510.992126 776.062992q-21.165354 0-36.787402 15.11811t-15.622047 37.291339q0 21.165354 15.622047 36.787402t36.787402 15.622047q22.173228 0 37.291339-15.622047t15.11811-36.787402q0-22.173228-15.11811-37.291339t-37.291339-15.11811zM591.622047 84.661417q0-8.062992-5.03937-12.598425t-11.086614-4.535433l-128 0q-5.03937 0-10.582677 4.535433t-5.543307 12.598425 5.03937 12.598425 11.086614 4.535433l128 0q6.047244 0 11.086614-4.535433t5.03937-12.598425z" p-id="16421" fill="#707070"></path></svg>
p-id="50680" width="200" height="200">
<path d="M471.87259 871.430008a41.629505 41.629505 0 0 0 41.629506 41.629506 40.98575 40.98575 0 0 0 38.625314-41.629506 40.55658 40.55658 0 0 0-39.912825-40.98575h-1.072925a40.98575 40.98575 0 0 0-39.26907 40.98575z"
fill="#B8BDCC" p-id="50681"></path>
<path d="M738.601844 55.792121a77.036044 77.036044 0 0 1 77.036044 77.036043v758.343672a77.036044 77.036044 0 0 1-77.036044 77.036043H285.398156a77.036044 77.036044 0 0 1-77.036044-77.036043V132.828164A77.036044 77.036044 0 0 1 285.398156 55.792121h453.203688m0-55.792121H285.398156A132.828164 132.828164 0 0 0 152.569992 132.828164v758.343672a132.828164 132.828164 0 0 0 132.828164 132.828164h453.203688a132.828164 132.828164 0 0 0 132.828164-132.828164V132.828164A132.828164 132.828164 0 0 0 738.601844 0z"
fill="#B8BDCC" p-id="50682"></path>
<path d="M713.49539 793.964795H310.50461a39.483655 39.483655 0 0 1-41.629505-36.694049v-519.295893a39.26907 39.26907 0 0 1 41.629505-36.479463h402.99078a39.26907 39.26907 0 0 1 41.629505 36.479463v518.437553A39.483655 39.483655 0 0 1 713.49539 793.964795z"
fill="#E3E5EB" p-id="50683"></path>
<path d="M425.736798 103.430008m25.964795 0l120.596814 0q25.964795 0 25.964795 25.964795l0 0q0 25.964795-25.964795 25.964795l-120.596814 0q-25.964795 0-25.964795-25.964795l0 0q0-25.964795 25.964795-25.964795Z"
fill="#B8BDCC" p-id="50684"></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -2,7 +2,7 @@
<div> <div>
<div class="sidebar"> <div class="sidebar">
<AMenu <AMenu
:selectedKeys="[router.currentRoute.value.path.split('/').slice(-2).join('/')]" :selectedKeys="[route.path.split('/').slice(-2).join('/')]"
:selectable="true" :selectable="true"
:multiple="false" :multiple="false"
mode="vertical" mode="vertical"
@@ -96,8 +96,11 @@ import recyclingbin from '@/assets/svgs/recyclingbin.svg';
import Folder from "@/components/Folder/Folder.vue"; import Folder from "@/components/Folder/Folder.vue";
import ai from '@/assets/svgs/ai.svg'; import ai from '@/assets/svgs/ai.svg';
import share from '@/assets/svgs/share.svg'; import share from '@/assets/svgs/share.svg';
const {t} = useI18n(); const {t} = useI18n();
const router = useRouter(); const router = useRouter();
const route = useRoute();
/** /**

View File

@@ -131,5 +131,9 @@ export default {
recyclingBin: 'Recycling Bin', recyclingBin: 'Recycling Bin',
upscale: 'Image Inpainting', upscale: 'Image Inpainting',
share: 'Quickly Share' share: 'Quickly Share'
},
upload:{
uploadSuccess: 'upload success',
uploadError: 'upload failed',
} }
}; };

View File

@@ -118,6 +118,10 @@ export default {
recyclingBin: '回收站', recyclingBin: '回收站',
upscale: '图像修复', upscale: '图像修复',
share: '快传' share: '快传'
},
upload:{
uploadSuccess: '上传成功!',
uploadError: '上传失败!',
} }
}; };

View File

@@ -21,16 +21,16 @@ export default [
name: 'upscale', name: 'upscale',
component: () => import('@/views/Upscale/index.vue'), component: () => import('@/views/Upscale/index.vue'),
meta: { meta: {
requiresAuth: false, requiresAuth: true,
title: '图像修复' title: '图像修复'
} },
}, },
{ {
path: '/main/photo/share', path: '/main/photo/share',
name: 'share', name: 'share',
component: () => import('@/views/ImageShare/ImageShare.vue'), component: () => import('@/views/ImageShare/ImageShare.vue'),
meta: { meta: {
requiresAuth: false, requiresAuth: true,
title: '快传' title: '快传'
} }
} }

View File

@@ -0,0 +1,11 @@
export default [
{
path: '/upscale/app',
name: 'upscaleApp',
component: () => import('@/views/PhoneUpload/PhoneUpload.vue'),
meta: {
requiresAuth: false,
title: '手机上传'
}
}
];

View File

@@ -7,12 +7,14 @@ import notFound from "./modules/not_found.ts";
import landing from "./modules/landing.ts"; import landing from "./modules/landing.ts";
import mainRouter from "./modules/main_router.ts"; import mainRouter from "./modules/main_router.ts";
import i18n from "@/locales"; import i18n from "@/locales";
import phone_upload from "@/router/modules/phone_upload.ts";
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
...login, ...login,
...notFound, ...notFound,
...landing, ...landing,
...mainRouter, ...mainRouter,
...phone_upload,
{ {
path: '/:pathMatch(.*)', path: '/:pathMatch(.*)',
redirect: '/404', redirect: '/404',

View File

@@ -54,13 +54,7 @@ export const useUpscaleStore = defineStore(
fileData.value = urlData; fileData.value = urlData;
await loadImg(image); await loadImg(image);
uploading.value = false; uploading.value = false;
await clear();
imageData.value = "";
processedImg.value = "";
isDone.value = false;
msg.value = "";
progressBar.value = 0;
return true; return true;
} }
@@ -68,10 +62,21 @@ export const useUpscaleStore = defineStore(
* 自定义上传图片请求 * 自定义上传图片请求
*/ */
async function customUploadRequest(_file: any) { async function customUploadRequest(_file: any) {
imageData.value = fileData.value; imageData.value = fileData.value;
} }
/**
* 清空数据
*/
async function clear() {
imageData.value = "";
processedImg.value = "";
isDone.value = false;
msg.value = "";
progressBar.value = 0;
isProcessing.value = false;
}
/** /**
* 加载图片 * 加载图片
* @param img * @param img
@@ -117,6 +122,7 @@ export const useUpscaleStore = defineStore(
isProcessing, isProcessing,
msg, msg,
progressBar, progressBar,
loadImg,
beforeUpload, beforeUpload,
customUploadRequest, customUploadRequest,
}; };

View File

@@ -10,6 +10,9 @@ export const useWebSocketStore = defineStore('websocket', () => {
wsService: null as WebSocketService | null, wsService: null as WebSocketService | null,
}); });
const readyState = ref<number>(WebSocket.CLOSED);
function initialize(options: { function initialize(options: {
url: string; url: string;
protocols?: string | string[]; protocols?: string | string[];
@@ -17,6 +20,7 @@ export const useWebSocketStore = defineStore('websocket', () => {
}) { }) {
state.wsService = new WebSocketService(options); state.wsService = new WebSocketService(options);
state.wsService?.open(); state.wsService?.open();
readyState.value = WebSocket.OPEN;
} }
function sendMessage(data: any) { function sendMessage(data: any) {
@@ -33,12 +37,9 @@ export const useWebSocketStore = defineStore('websocket', () => {
function close(isActiveClose: boolean) { function close(isActiveClose: boolean) {
state.wsService?.close(isActiveClose); state.wsService?.close(isActiveClose);
readyState.value = WebSocket.CLOSED;
} }
// 新增的获取 WebSocket 状态的方法
function getReadyState() {
return state.wsService ? state.wsService.getReadyState() : WebSocket.CLOSED;
}
return { return {
initialize, initialize,
@@ -46,7 +47,7 @@ export const useWebSocketStore = defineStore('websocket', () => {
on, on,
onEvent, onEvent,
close, close,
getReadyState readyState,
}; };
}, { }, {
persistedState: { persistedState: {

View File

@@ -1,12 +1,5 @@
export interface ImageData { export interface uploadImageRequest {
model_type: string; image: string;
model: string; access_token: string;
factor: number; user_id: string;
tile_size: number;
backend: string;
width: number;
height: number;
input: any;
hasAlpha: boolean;
min_lap: number;
} }

View File

@@ -6,8 +6,7 @@ export const localforageStorageAdapter = {
await localforage.setItem(key, value); await localforage.setItem(key, value);
}, },
async get(key: string) { async get(key: string) {
const res: any = await localforage.getItem(key); return await localforage.getItem(key);
return res ? JSON.parse(res) : null;
}, },
async remove(key: any) { async remove(key: any) {
await localforage.removeItem(key); await localforage.removeItem(key);

View File

@@ -0,0 +1,22 @@
export async function blobToBase64(blobUrl: string): Promise<string> {
try {
const response = await fetch(blobUrl);
const blob = await response.blob();
const reader = new FileReader();
return new Promise<string>((resolve, reject) => {
reader.onload = function () {
// 直接使用 reader.result包含 MIME 类型前缀
const base64StringWithPrefix = reader.result!.toString();
resolve(base64StringWithPrefix);
};
reader.onerror = function () {
reject("File could not be read");
};
// 读取 Blob 文件到 Data URL 格式
reader.readAsDataURL(blob);
});
} catch (error) {
throw new Error("Error fetching blob from URL: " + error);
}
}

View File

@@ -0,0 +1,146 @@
<template>
<div class="upscale-upload-content">
<AUploadDragger
name="image"
accept="image/*"
:multiple="false"
:directory="false"
:maxCount="1"
:beforeUpload="upscale.beforeUpload"
:custom-request="upscale.customUploadRequest"
:disabled="upscale.uploading"
:showUploadList="false">
<div class="upscale-upload-content-main">
<ABadge :offset="[-10, 10]">
<template #count>
<AAvatar :size="25" :src="sueccess" v-if="upscale.imageData"/>
<AAvatar :size="25" :src="warn" v-if="!upscale.imageData"/>
</template>
<AAvatar shape="square" :size="70" :src="file"/>
</ABadge>
<span class="upscale-upload-text">
点击上传图片
</span>
</div>
</AUploadDragger>
<ADivider orientation="center" :plain="true">
<span class="upscale-divider-title">图片预览</span>
</ADivider>
<div class="upscale-upload-image" v-if="upscale.imageData">
<ABadge>
<template #count>
<AButton type="text" size="small" class="upscale-file-btn" @click="upscale.imageData = ''">
<template #icon>
<AAvatar shape="square" :size="25" :src="remove"/>
</template>
</AButton>
</template>
<AAvatar shape="square" :size="100">
<template #icon>
<AImage :src="upscale.imageData" width="100%" height="100%"/>
</template>
</AAvatar>
</ABadge>
</div>
<AEmpty v-else :image="empty" :description="null"/>
<ADivider/>
<div>
<AButton type="primary" size="large" :disabled="!upscale.imageData" :loading="upscale.uploading"
@click="sendImage()"
class="upscale-upload-btn">
发送图片
</AButton>
</div>
<ADivider/>
</div>
</template>
<script setup lang="ts">
import file from "@/assets/svgs/file.svg";
import useStore from "@/store";
import sueccess from '@/assets/svgs/success.svg';
import warn from '@/assets/svgs/warn.svg';
import remove from '@/assets/svgs/remove.svg';
import empty from '@/assets/svgs/empty.svg';
import {blobToBase64} from "@/utils/imageUtils/blobToBase64.ts";
import {uploadImage} from "@/api/upscale";
import {uploadImageRequest} from "@/types/upscale";
import {message} from "ant-design-vue";
import {useI18n} from "vue-i18n";
const upscale = useStore().upscale;
const route = useRoute();
const {t} = useI18n();
async function sendImage() {
if (!upscale.imageData) {
return;
}
const base64 = await blobToBase64(upscale.imageData);
const data: uploadImageRequest = {
image: base64,
access_token: route.query.token as string,
user_id: route.query.user_id as string,
};
const res: any = await uploadImage(data);
if (res && res.code === 200) {
message.success(t('upload.uploadSuccess'));
} else {
message.error(res.message);
}
}
</script>
<style scoped lang="scss">
.upscale-upload-content {
width: 90%;
height: 40vh;
padding: 15px;
margin: 0 auto;
.upscale-upload-content-main {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
overflow: scroll;
.upscale-upload-text {
font-size: 20px;
font-weight: bold;
}
.upscale-upload-btn {
width: 60%;
}
.upscale-upload-tip {
font-size: 12px;
color: rgba(126, 126, 135, 0.99);
}
}
.upscale-divider-title {
font-size: 13px;
color: rgba(126, 126, 135, 0.99);
}
.upscale-upload-image {
display: flex;
justify-content: center;
align-items: center;
}
.upscale-upload-btn {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
}
</style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<CommentReply/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
</script> </script>

View File

@@ -11,20 +11,24 @@
@touchmove="touchMove" @touchmove="touchMove"
@touchend="touchEnd" @touchend="touchEnd"
> >
<div v-if="store.isProcessing" class="canvas-progressbar"> <!-- 进度条 -->
<div class="canvas-progressbar">
<span class="canvas-progressbar-text"> <span class="canvas-progressbar-text">
{{ store.msg }} {{ store.msg }}
</span> </span>
<AProgress <AProgress
v-if="store.isProcessing"
:stroke-color="{ :stroke-color="{
'0%': '#108ee9', '0%': '#108ee9',
'100%': '#87d068', '100%': '#87d068',}"
}"
:percent="store.progressBar" :percent="store.progressBar"
:showInfo="false" :showInfo="false"
status="active"
/> />
</div> </div>
<canvas ref="canvas" v-if="store.imageData || store.processedImg"></canvas> <!-- 图片 -->
<canvas ref="canvas"></canvas>
<!-- 拖动条 -->
<div <div
class="dragLine" class="dragLine"
v-if="store.isDone" v-if="store.isDone"
@@ -40,20 +44,58 @@
</svg> </svg>
</div> </div>
</div> </div>
<div class="canvas-qr" v-if="!store.isDone && !store.imageData"> <!-- 二维码 -->
<AQrcode :bordered="false" color="rgba(126, 126, 135, 0.48)" :size="200" <div class="canvas-qr">
:value="'https://git.landaiqing.cn'" <AQrcode :bordered="false" color="rgba(126, 126, 135, 0.48)"
:size="qrcodeSize"
:value="generateQrCodeUrl()"
:icon="phone" :icon="phone"
:iconSize="40" :iconSize="iconSize"
/> />
<span class="canvas-qr-text">手机扫码上传</span> <span class="canvas-qr-text">手机扫码上传</span>
</div> </div>
<!-- 菜单 -->
<div class="floating-menu" @mousedown.stop v-if="store.isDone && store.processedImg">
<AFlex :vertical="false" align="center" justify="space-between" :gap="12">
<ATooltip placement="top" title="下载图片">
<AButton type="text" size="large" @click="downloadImage" class="menu-btn">
<template #icon>
<AAvatar :src="download" class="menu-icon"/>
</template>
</AButton>
</ATooltip>
<ATooltip placement="top" title="分享图片">
<AButton type="text" size="large" class="menu-btn">
<template #icon>
<AAvatar :src="share" :size="28" class="menu-icon"/>
</template>
</AButton>
</ATooltip>
<ATooltip placement="top" title="保存图片">
<AButton type="text" size="large" class="menu-btn">
<template #icon>
<AAvatar :src="save" :size="30" class="menu-icon"/>
</template>
</AButton>
</ATooltip>
<ATooltip placement="top" title="删除图片">
<AButton type="text" size="large" danger class="menu-btn" @click="deletedImage">
<template #icon>
<AAvatar :src="deleted" :size="28" class="menu-icon"/>
</template>
</AButton>
</ATooltip>
</AFlex>
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import useStore from "@/store"; import useStore from "@/store";
import phone from '@/assets/svgs/qr-phone.svg'; import phone from '@/assets/svgs/qr-phone.svg';
import download from '@/assets/svgs/download.svg';
import share from '@/assets/svgs/share.svg';
import save from '@/assets/svgs/save.svg';
import deleted from '@/assets/svgs/deleted.svg';
const canvasContainer = ref<HTMLDivElement | null>(null); const canvasContainer = ref<HTMLDivElement | null>(null);
const dragging = ref<boolean>(false); const dragging = ref<boolean>(false);
@@ -76,9 +118,68 @@ const touchStartDistance = ref(0);
const imgScaleStart = ref(1); const imgScaleStart = ref(1);
const store = useStore().upscale; const store = useStore().upscale;
const user = useStore().user;
const img = ref<HTMLImageElement>(new Image()); const img = ref<HTMLImageElement>(new Image());
const processedImg = ref<HTMLImageElement>(new Image()); const processedImg = ref<HTMLImageElement>(new Image());
const qrcodeSize = ref<number>(250);
const iconSize = ref<number>(30);
/**
* 更新二维码大小
*/
const updateQrcodeSize = () => {
if (canvasContainer.value) {
// 设置 QRCode 大小
const containerWidth = canvasContainer.value.clientWidth;
qrcodeSize.value = containerWidth * 0.3; // 设置 QRCode 为父盒子宽度的80%
iconSize.value = Math.min(containerWidth * 0.1, 40); // 设置 icon 大小为父盒子宽度的10%
}
};
function generateQrCodeUrl(): string {
return import.meta.env.VITE_APP_WEB_URL + "/upscale/app?user_id=" + user.user.uid + "&token=" + user.user.access_token;
}
console.log(generateQrCodeUrl());
/**
* 下载图片
*/
function downloadImage() {
if (!store.processedImg) return;
const a = document.createElement("a");
a.href = store.processedImg;
if (store.hasAlpha) a.download = "output.png";
else a.download = "output.jpg";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
/**
* 删除图片
*/
function deletedImage() {
if (store.processedImg && store.imageData) {
store.imageData = '';
store.processedImg = '';
}
store.isDone = false;
store.isProcessing = false;
store.progressBar = 0;
store.msg = "";
draggingLine.value = false;
dragging.value = false;
imgX.value = 0;
imgY.value = 0;
imgScale.value = 1;
imgInitScale.value = 1;
img.value = new Image();
processedImg.value = new Image();
initCanvasSize();
}
/** /**
* 开始拖动 * 开始拖动
* @param event * @param event
@@ -409,6 +510,7 @@ function updateCanvasSize() {
*/ */
function handleResize() { function handleResize() {
updateCanvasSize(); updateCanvasSize();
updateQrcodeSize();
} }
/** /**
@@ -464,6 +566,7 @@ onBeforeUnmount(() => {
width: 100%; width: 100%;
height: 100%; height: 100%;
position: relative; position: relative;
z-index: 0;
} }
.bg { .bg {
@@ -536,13 +639,19 @@ onBeforeUnmount(() => {
canvas { canvas {
height: 100%; height: 100%;
width: 100%; width: 100%;
z-index: 2;
} }
.canvas-qr { .canvas-qr {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 1;
} }
.canvas-qr-text { .canvas-qr-text {
@@ -563,6 +672,7 @@ canvas {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
z-index: 3;
} }
.dragLine:hover { .dragLine:hover {
@@ -588,17 +698,11 @@ canvas {
.canvas-progressbar { .canvas-progressbar {
position: absolute; position: absolute;
top: 0; top: 0;
//left: 50%;
//transform: translate(-50%, -50%);
width: 300px; width: 300px;
//height: 100px;
border-radius: 10px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
//background-color: rgba(255, 255, 255, 0.5);
//padding: 10px;
} }
.canvas-progressbar-text { .canvas-progressbar-text {
@@ -606,4 +710,41 @@ canvas {
font-weight: bold; font-weight: bold;
color: white; color: white;
} }
.floating-menu {
position: absolute;
background-color: rgb(255, 255, 255);
opacity: 0.8;
border-radius: 10px;
color: white;
width: 200px;
height: 50px;
padding: 10px;
bottom: 10px;
left: 50%;
transform: translate(-50%, 0%);
z-index: 4;
display: flex;
justify-content: center;
align-items: center;
transition: all 0.5s ease;
user-select: none;
}
.menu-btn {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.menu-btn:hover {
transform: scale(1.2);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
}
.menu-icon {
transition: transform 0.2s ease;
}
.menu-icon:hover {
transform: scale(1.1);
}
</style> </style>

View File

@@ -52,6 +52,7 @@
</div> </div>
<ADivider></ADivider> <ADivider></ADivider>
<AButton style="width: 100%;" size="large" shape="default" type="default" :loading="upscale.isProcessing" <AButton style="width: 100%;" size="large" shape="default" type="default" :loading="upscale.isProcessing"
:disabled="!upscale.input"
@click="startTask"> @click="startTask">
<template #icon> <template #icon>
<AAvatar shape="square" :size="25" :src="run"/> <AAvatar shape="square" :size="25" :src="run"/>
@@ -142,7 +143,6 @@ const imgCanvas = document.createElement("canvas");
* WebWorker 处理图片 * WebWorker 处理图片
*/ */
async function startTask() { async function startTask() {
console.log(upscale.input);
if (upscale.input === null) return; if (upscale.input === null) return;
upscale.isProcessing = true; upscale.isProcessing = true;
const start = Date.now(); const start = Date.now();
@@ -226,7 +226,7 @@ async function startTask() {
outputData.value = null; outputData.value = null;
imgCtx.putImageData(outImg, 0, 0); imgCtx.putImageData(outImg, 0, 0);
let type = "image/jpeg"; let type = "image/jpeg";
const quality = 0.92; const quality = 1.0;
if (upscale.hasAlpha) type = "image/png"; if (upscale.hasAlpha) type = "image/png";
imgCanvas.toBlob( imgCanvas.toBlob(

View File

@@ -39,7 +39,7 @@ import warn from '@/assets/svgs/warn.svg';
const upscale = useStore().upscale; const upscale = useStore().upscale;
const uploadDraggerRef = ref<HTMLDivElement | null>(null); const uploadDraggerRef = ref<HTMLDivElement | null>(null);
const containerRef = ref<HTMLDivElement | null>(null);

View File

@@ -19,9 +19,34 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import UploadImage from "@/views/Upscale/UploadImage.vue";
import CompareImage from "@/views/Upscale/CompareImage.vue"; import useStore from "@/store";
import ParameterSetting from "@/views/Upscale/ParameterSetting.vue";
const websocket = useStore().websocket;
const user = useStore().user;
const upscale = useStore().upscale;
const img = new Image();
const wsOptions = {
url: import.meta.env.VITE_FILE_SOCKET_URL + "?user_id=" + user.user.uid,
protocols: [user.user.access_token],
};
onMounted(() => {
websocket.initialize(wsOptions);
websocket.on("message", async (res: any) => {
if (res && res.code === 200) {
const {data} = res;
img.src = data;
await upscale.loadImg(img);
upscale.imageData = data;
}
});
});
onUnmounted(() => {
websocket.close(false);
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.upscale-container { .upscale-container {
@@ -30,13 +55,6 @@ import ParameterSetting from "@/views/Upscale/ParameterSetting.vue";
width: 100%; width: 100%;
height: 100%; height: 100%;
//.upscale-title {
// font-size: 16px;
// font-weight: bold;
// margin-left: 5px;
//}
.upscale-content { .upscale-content {
width: 100%; width: 100%;
height: 100%; height: 100%;

3
src/vite-env.d.ts vendored
View File

@@ -1,7 +1,6 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
declare interface ImportMetaEnv { declare interface ImportMetaEnv {
readonly VITE_APP_BASE_API: string; readonly VITE_APP_BASE_API: string;
readonly VITE_APP_TITLE: string;
readonly VITE_API_BASE_URL: string; readonly VITE_API_BASE_URL: string;
readonly VITE_NODE_ENV: string; readonly VITE_NODE_ENV: string;
readonly VITE_TITLE_NAME: string; readonly VITE_TITLE_NAME: string;
@@ -9,6 +8,8 @@ declare interface ImportMetaEnv {
readonly VITE_QR_SOCKET_URL: string; readonly VITE_QR_SOCKET_URL: string;
readonly VITE_MESSAGE_SOCKET_URL: string; readonly VITE_MESSAGE_SOCKET_URL: string;
readonly VITE_FINGERPRINT_KEY: string; readonly VITE_FINGERPRINT_KEY: string;
readonly VITE_FILE_SOCKET_URL: string;
readonly VITE_APP_WEB_URL: string;
} }
interface ImportMeta { interface ImportMeta {

View File

@@ -24,7 +24,6 @@ self.onmessage = async function (e: MessageEvent): Promise<void> {
let Model: tf.GraphModel; let Model: tf.GraphModel;
try { try {
Model = await tf.loadGraphModel(`indexeddb://${model_name}`); Model = await tf.loadGraphModel(`indexeddb://${model_name}`);
console.log("Model loaded successfully");
self.postMessage({info: "Model loaded from cache successfully"}); self.postMessage({info: "Model loaded from cache successfully"});
} catch (_error) { } catch (_error) {
self.postMessage({info: "Downloading model..."}); self.postMessage({info: "Downloading model..."});
@@ -221,7 +220,6 @@ self.onmessage = async function (e: MessageEvent): Promise<void> {
const factor = data?.factor || 4; const factor = data?.factor || 4;
const tile_size = data?.tile_size || 64; const tile_size = data?.tile_size || 64;
const min_lap = data?.min_lap || 12; const min_lap = data?.min_lap || 12;
const start = Date.now();
let output: any; let output: any;
try { try {
output = await enlargeImageWithFixedInput( output = await enlargeImageWithFixedInput(
@@ -237,8 +235,6 @@ self.onmessage = async function (e: MessageEvent): Promise<void> {
if (withPadding) { if (withPadding) {
output.cropToOriginalSize(width_ori * factor, height_ori * factor); output.cropToOriginalSize(width_ori * factor, height_ori * factor);
} }
const end = Date.now();
console.log("Time:", end - start);
await new Promise((resolve) => setTimeout(resolve, 10)); await new Promise((resolve) => setTimeout(resolve, 10));
self.postMessage({ self.postMessage({
progress: 100, progress: 100,