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'
# 页面 title 前缀
VITE_APP_TITLE=开发环境
# 网站域名
VITE_APP_WEB_URL='http://localhost:5173'
# 网络请求公用地址
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_FILE_SOCKET_URL='ws://127.0.0.1:80/api/ws/file'
VITE_FINGERPRINT_KEY='idm0jdoau38lwourb4pbjk4dxkat0kcx'

View File

@@ -3,8 +3,8 @@ VITE_NODE_ENV='production'
# 生产环境
VITE_APP_BASE_API='/sys'
# 页面 title 前缀
VITE_APP_TITLE=生产环境
# 网站域名
VITE_APP_WEB_URL='http://localhost:5173'
# 网络请求公用地址
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_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'

15
components.d.ts vendored
View File

@@ -27,21 +27,30 @@ declare module 'vue' {
AInput: typeof import('ant-design-vue/es')['Input']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
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']
AMenu: typeof import('ant-design-vue/es')['Menu']
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
AMenuItemGroup: typeof import('ant-design-vue/es')['MenuItemGroup']
AModal: typeof import('ant-design-vue/es')['Modal']
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']
AProgress: typeof import('ant-design-vue/es')['Progress']
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']
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
ASpace: typeof import('ant-design-vue/es')['Space']
ASpin: typeof import('ant-design-vue/es')['Spin']
ATabPane: typeof import('ant-design-vue/es')['TabPane']
ATabs: typeof import('ant-design-vue/es')['Tabs']
ATag: typeof import('ant-design-vue/es')['Tag']
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']
@@ -55,6 +64,7 @@ declare module 'vue' {
Carousel: typeof import('./src/components/MyUI/Carousel/Carousel.vue')['default']
Cascader: typeof import('./src/components/MyUI/Cascader/Cascader.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']
Col: typeof import('./src/components/MyUI/Grid/Col.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']
Ellipsis: typeof import('./src/components/MyUI/Ellipsis/Ellipsis.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']
FloatButton: typeof import('./src/components/MyUI/FloatButton/FloatButton.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']
PeopleAlbum: typeof import('./src/views/Album/PeopleAlbum/PeopleAlbum.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']
Popover: typeof import('./src/components/MyUI/Popover/Popover.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']
Segmented: typeof import('./src/components/MyUI/Segmented/Segmented.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']
Slider: typeof import('./src/components/MyUI/Slider/Slider.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']
Video: typeof import('./src/components/MyUI/Video/Video.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']
}
}

View File

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

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

View File

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

View File

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

View File

@@ -21,16 +21,16 @@ export default [
name: 'upscale',
component: () => import('@/views/Upscale/index.vue'),
meta: {
requiresAuth: false,
requiresAuth: true,
title: '图像修复'
}
},
},
{
path: '/main/photo/share',
name: 'share',
component: () => import('@/views/ImageShare/ImageShare.vue'),
meta: {
requiresAuth: false,
requiresAuth: true,
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 mainRouter from "./modules/main_router.ts";
import i18n from "@/locales";
import phone_upload from "@/router/modules/phone_upload.ts";
const routes: Array<RouteRecordRaw> = [
...login,
...notFound,
...landing,
...mainRouter,
...phone_upload,
{
path: '/:pathMatch(.*)',
redirect: '/404',

View File

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

View File

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

View File

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

View File

@@ -6,8 +6,7 @@ export const localforageStorageAdapter = {
await localforage.setItem(key, value);
},
async get(key: string) {
const res: any = await localforage.getItem(key);
return res ? JSON.parse(res) : null;
return await localforage.getItem(key);
},
async remove(key: any) {
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>
<CommentReply/>
</template>
<script setup lang="ts">
</script>

View File

@@ -11,20 +11,24 @@
@touchmove="touchMove"
@touchend="touchEnd"
>
<div v-if="store.isProcessing" class="canvas-progressbar">
<!-- 进度条 -->
<div class="canvas-progressbar">
<span class="canvas-progressbar-text">
{{ store.msg }}
</span>
<AProgress
v-if="store.isProcessing"
:stroke-color="{
'0%': '#108ee9',
'100%': '#87d068',
}"
'100%': '#87d068',}"
:percent="store.progressBar"
:showInfo="false"
status="active"
/>
</div>
<canvas ref="canvas" v-if="store.imageData || store.processedImg"></canvas>
<!-- 图片 -->
<canvas ref="canvas"></canvas>
<!-- 拖动条 -->
<div
class="dragLine"
v-if="store.isDone"
@@ -40,20 +44,58 @@
</svg>
</div>
</div>
<div class="canvas-qr" v-if="!store.isDone && !store.imageData">
<AQrcode :bordered="false" color="rgba(126, 126, 135, 0.48)" :size="200"
:value="'https://git.landaiqing.cn'"
<!-- 二维码 -->
<div class="canvas-qr">
<AQrcode :bordered="false" color="rgba(126, 126, 135, 0.48)"
:size="qrcodeSize"
:value="generateQrCodeUrl()"
:icon="phone"
:iconSize="40"
:iconSize="iconSize"
/>
<span class="canvas-qr-text">手机扫码上传</span>
</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>
</template>
<script setup lang="ts">
import useStore from "@/store";
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 dragging = ref<boolean>(false);
@@ -76,9 +118,68 @@ const touchStartDistance = ref(0);
const imgScaleStart = ref(1);
const store = useStore().upscale;
const user = useStore().user;
const img = 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
@@ -409,6 +510,7 @@ function updateCanvasSize() {
*/
function handleResize() {
updateCanvasSize();
updateQrcodeSize();
}
/**
@@ -464,6 +566,7 @@ onBeforeUnmount(() => {
width: 100%;
height: 100%;
position: relative;
z-index: 0;
}
.bg {
@@ -536,13 +639,19 @@ onBeforeUnmount(() => {
canvas {
height: 100%;
width: 100%;
z-index: 2;
}
.canvas-qr {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1;
}
.canvas-qr-text {
@@ -563,6 +672,7 @@ canvas {
justify-content: center;
align-items: center;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
z-index: 3;
}
.dragLine:hover {
@@ -588,17 +698,11 @@ canvas {
.canvas-progressbar {
position: absolute;
top: 0;
//left: 50%;
//transform: translate(-50%, -50%);
width: 300px;
//height: 100px;
border-radius: 10px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
//background-color: rgba(255, 255, 255, 0.5);
//padding: 10px;
}
.canvas-progressbar-text {
@@ -606,4 +710,41 @@ canvas {
font-weight: bold;
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>

View File

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

View File

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

View File

@@ -19,9 +19,34 @@
</div>
</template>
<script setup lang="ts">
import UploadImage from "@/views/Upscale/UploadImage.vue";
import CompareImage from "@/views/Upscale/CompareImage.vue";
import ParameterSetting from "@/views/Upscale/ParameterSetting.vue";
import useStore from "@/store";
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>
<style scoped lang="scss">
.upscale-container {
@@ -30,13 +55,6 @@ import ParameterSetting from "@/views/Upscale/ParameterSetting.vue";
width: 100%;
height: 100%;
//.upscale-title {
// font-size: 16px;
// font-weight: bold;
// margin-left: 5px;
//}
.upscale-content {
width: 100%;
height: 100%;

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

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

View File

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