🚧 improve image uploading
This commit is contained in:
2
components.d.ts
vendored
2
components.d.ts
vendored
@@ -8,7 +8,6 @@ export {}
|
|||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
AAvatar: typeof import('ant-design-vue/es')['Avatar']
|
AAvatar: typeof import('ant-design-vue/es')['Avatar']
|
||||||
AAvatarGroup: typeof import('ant-design-vue/es')['AvatarGroup']
|
|
||||||
ABadge: typeof import('ant-design-vue/es')['Badge']
|
ABadge: typeof import('ant-design-vue/es')['Badge']
|
||||||
AButton: typeof import('ant-design-vue/es')['Button']
|
AButton: typeof import('ant-design-vue/es')['Button']
|
||||||
ACard: typeof import('ant-design-vue/es')['Card']
|
ACard: typeof import('ant-design-vue/es')['Card']
|
||||||
@@ -89,6 +88,7 @@ declare module 'vue' {
|
|||||||
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']
|
EyeOutlined: typeof import('@ant-design/icons-vue')['EyeOutlined']
|
||||||
|
FileImageOutlined: typeof import('@ant-design/icons-vue')['FileImageOutlined']
|
||||||
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']
|
||||||
|
@@ -31,13 +31,13 @@
|
|||||||
"@types/animejs": "^3.1.12",
|
"@types/animejs": "^3.1.12",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/json-stringify-safe": "^5.0.3",
|
"@types/json-stringify-safe": "^5.0.3",
|
||||||
"@types/node": "^22.10.5",
|
"@types/node": "^22.10.6",
|
||||||
"@types/nprogress": "^0.2.3",
|
"@types/nprogress": "^0.2.3",
|
||||||
"@vladmandic/face-api": "^1.7.14",
|
"@vladmandic/face-api": "^1.7.14",
|
||||||
"@vuepic/vue-datepicker": "^11.0.1",
|
"@vuepic/vue-datepicker": "^11.0.1",
|
||||||
"@vueuse/core": "^12.4.0",
|
"@vueuse/core": "^12.4.0",
|
||||||
"@vueuse/integrations": "^12.4.0",
|
"@vueuse/integrations": "^12.4.0",
|
||||||
"alova": "^3.2.7",
|
"alova": "^3.2.8",
|
||||||
"animejs": "^3.2.2",
|
"animejs": "^3.2.2",
|
||||||
"ant-design-vue": "^4.2.6",
|
"ant-design-vue": "^4.2.6",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
@@ -75,9 +75,9 @@
|
|||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"eslint-plugin-vue": "^9.32.0",
|
"eslint-plugin-vue": "^9.32.0",
|
||||||
"globals": "^15.14.0",
|
"globals": "^15.14.0",
|
||||||
"sass": "^1.83.1",
|
"sass": "^1.83.3",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"typescript-eslint": "^8.19.1",
|
"typescript-eslint": "^8.20.0",
|
||||||
"unplugin-vue-components": "^28.0.0",
|
"unplugin-vue-components": "^28.0.0",
|
||||||
"vite": "^6.0.7",
|
"vite": "^6.0.7",
|
||||||
"vite-plugin-bundle-obfuscator": "1.4.0",
|
"vite-plugin-bundle-obfuscator": "1.4.0",
|
||||||
|
10
src/api/file/index.ts
Normal file
10
src/api/file/index.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import {service} from "@/utils/alova/service.ts";
|
||||||
|
|
||||||
|
export const uploadFile = (formData) => {
|
||||||
|
return service.Post('/api/auth/file/uploads', formData, {
|
||||||
|
meta: {
|
||||||
|
ignoreToken: false,
|
||||||
|
signature: false,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -2,7 +2,7 @@ import {service} from "@/utils/alova/service.ts";
|
|||||||
import {uploadImageRequest} from "@/types/upscale";
|
import {uploadImageRequest} from "@/types/upscale";
|
||||||
|
|
||||||
export const uploadImage = (data: uploadImageRequest) => {
|
export const uploadImage = (data: uploadImageRequest) => {
|
||||||
return service.Post('/api/auth/upscale/upload', {
|
return service.Post('/api/auth/upscale/phone/upload', {
|
||||||
image: data.image,
|
image: data.image,
|
||||||
access_token: data.access_token,
|
access_token: data.access_token,
|
||||||
user_id: data.user_id,
|
user_id: data.user_id,
|
||||||
|
12
src/types/upload.d.ts
vendored
Normal file
12
src/types/upload.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export interface UploadedFile {
|
||||||
|
id: string; // 唯一标识符
|
||||||
|
fileName: string; // 文件名
|
||||||
|
file: File | Blob; // 图片数据
|
||||||
|
type: string; // 文件类型
|
||||||
|
detectionResults: {
|
||||||
|
isAnime: boolean;
|
||||||
|
hasFace: boolean;
|
||||||
|
objectArray: string[] | unknown[];
|
||||||
|
landscape: string | null;
|
||||||
|
};
|
||||||
|
}
|
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<ADrawer v-model:open="upload.openUploadDrawer" width="40%" placement="right" title="上传照片">
|
<ADrawer v-model:open="upload.openUploadDrawer" placement="right" title="上传照片" width="40%" @close="cancelUpload">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<AFlex :vertical="false" justify="center" align="center" gap="large">
|
<AFlex :vertical="false" align="center" gap="large" justify="center">
|
||||||
<ASelect size="middle" style="width: 150px">
|
<ASelect size="middle" style="width: 150px">
|
||||||
|
|
||||||
</ASelect>
|
</ASelect>
|
||||||
@@ -11,41 +11,39 @@
|
|||||||
</AFlex>
|
</AFlex>
|
||||||
</template>
|
</template>
|
||||||
<div class="upload-container">
|
<div class="upload-container">
|
||||||
<Spin :spinning="predicting" indicator="magic-ring">
|
<AUploadDragger
|
||||||
<AUploadDragger
|
v-model:fileList="fileList"
|
||||||
v-model:fileList="fileList"
|
:beforeUpload="beforeUpload"
|
||||||
accept="image/*"
|
:customRequest="customUploadRequest"
|
||||||
name="file"
|
:directory="false"
|
||||||
:directory="false"
|
:maxCount="10"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
method="post"
|
:disabled="predicting"
|
||||||
:beforeUpload="beforeUpload"
|
:progress="progress"
|
||||||
:customRequest="customUploadRequest"
|
@remove="removeFile"
|
||||||
:progress="progress"
|
accept="image/*"
|
||||||
:maxCount="10"
|
list-type="picture"
|
||||||
list-type="picture"
|
method="post"
|
||||||
>
|
name="file"
|
||||||
<p class="ant-upload-drag-icon">
|
>
|
||||||
<inbox-outlined></inbox-outlined>
|
<p class="ant-upload-drag-icon">
|
||||||
</p>
|
<FileImageOutlined/>
|
||||||
<p class="ant-upload-text">单击或拖动文件到此区域以上传</p>
|
</p>
|
||||||
<p class="ant-upload-hint">
|
<p v-show="!predicting" class="ant-upload-text">单击或拖动文件到此区域以上传</p>
|
||||||
支持单次或批量上传,严禁上传非法图片,违者将受到法律惩戒。
|
<p v-show="!predicting" class="ant-upload-hint">
|
||||||
</p>
|
支持单次或批量上传,严禁上传非法图片,违者将受到法律惩戒。
|
||||||
</AUploadDragger>
|
</p>
|
||||||
</Spin>
|
<p v-show="predicting" class="ant-upload-hint">
|
||||||
|
AI 正在识别图片,请稍候...
|
||||||
|
</p>
|
||||||
|
<AProgress :stroke-color="{'0%': '#108ee9','100%': '#87d068',}" :percent="progressPercent" status="active"
|
||||||
|
:show-info="true" size="small" type="line" v-show="predicting" style="width: 80%"/>
|
||||||
|
</AUploadDragger>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
|
||||||
<AFlex :vertical="false" justify="end" align="center" gap="large">
|
|
||||||
<AButton type="default" size="middle" style="width: 100px">取消</AButton>
|
|
||||||
<AButton type="primary" size="middle" style="width: 100px">上传</AButton>
|
|
||||||
</AFlex>
|
|
||||||
</template>
|
|
||||||
</ADrawer>
|
</ADrawer>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
import {InboxOutlined} from '@ant-design/icons-vue';
|
|
||||||
import type {UploadProps} from 'ant-design-vue';
|
import type {UploadProps} from 'ant-design-vue';
|
||||||
import {message} from "ant-design-vue";
|
import {message} from "ant-design-vue";
|
||||||
import {initNSFWJs, predictNSFW} from "@/utils/nsfw/nsfw.ts";
|
import {initNSFWJs, predictNSFW} from "@/utils/nsfw/nsfw.ts";
|
||||||
@@ -57,10 +55,11 @@ import {animePredictImagePro} from "@/utils/tfjs/anime_classifier_pro.ts";
|
|||||||
import {fnDetectFace} from "@/utils/tfjs/face_extraction.ts";
|
import {fnDetectFace} from "@/utils/tfjs/face_extraction.ts";
|
||||||
import {cocoSsdPredict} from "@/utils/tfjs/mobilenet.ts";
|
import {cocoSsdPredict} from "@/utils/tfjs/mobilenet.ts";
|
||||||
import {predictLandscape} from "@/utils/tfjs/landscape_recognition.ts";
|
import {predictLandscape} from "@/utils/tfjs/landscape_recognition.ts";
|
||||||
import Spin from "@/components/MyUI/Spin/Spin.vue";
|
import {useRequest} from 'alova/client';
|
||||||
|
import {uploadFile} from "@/api/file";
|
||||||
|
|
||||||
const predicting = ref<boolean>(false);
|
const predicting = ref<boolean>(false);
|
||||||
|
const progressPercent = ref<number>(0);
|
||||||
|
|
||||||
const upload = useStore().upload;
|
const upload = useStore().upload;
|
||||||
const image: HTMLImageElement = document.createElement('img');
|
const image: HTMLImageElement = document.createElement('img');
|
||||||
@@ -83,119 +82,131 @@ const progress: UploadProps['progress'] = {
|
|||||||
*/
|
*/
|
||||||
async function beforeUpload(file: File) {
|
async function beforeUpload(file: File) {
|
||||||
predicting.value = true;
|
predicting.value = true;
|
||||||
|
upload.clearPredictResult();
|
||||||
|
progressPercent.value = 0; // 初始化进度条
|
||||||
|
|
||||||
|
// 创建图片对象
|
||||||
image.src = URL.createObjectURL(file);
|
image.src = URL.createObjectURL(file);
|
||||||
|
|
||||||
|
image.addEventListener('webglcontextlost', (_event) => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新进度条函数,逐步增加
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// 图片 NSFW 检测
|
// 图片 NSFW 检测
|
||||||
const nsfw: NSFWJS = await initNSFWJs();
|
const nsfw: NSFWJS = await initNSFWJs();
|
||||||
|
await smoothUpdateProgress(10, 500); // 平滑更新进度条
|
||||||
|
|
||||||
const isNSFW: boolean = await predictNSFW(nsfw, image);
|
const isNSFW: boolean = await predictNSFW(nsfw, image);
|
||||||
|
await smoothUpdateProgress(20, 500); // 平滑更新进度条
|
||||||
|
|
||||||
if (isNSFW) {
|
if (isNSFW) {
|
||||||
message.error(i18n.global.t('comment.illegalImage'));
|
message.error(i18n.global.t('comment.illegalImage'));
|
||||||
predicting.value = false;
|
predicting.value = false;
|
||||||
|
progressPercent.value = 0; // 重置进度条
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Step 1: 动漫预测
|
||||||
|
const prediction1 = await animePredictImage(image);
|
||||||
|
await smoothUpdateProgress(40, 500); // 平滑更新进度条
|
||||||
|
|
||||||
|
const prediction2 = await animePredictImagePro(image);
|
||||||
|
await smoothUpdateProgress(60, 500); // 平滑更新进度条
|
||||||
|
|
||||||
|
upload.predictResult.isAnime = prediction1 === 'Anime' && (prediction2 === 'Furry' || prediction2 === 'Anime');
|
||||||
|
|
||||||
|
// Step 2: 人脸检测
|
||||||
|
const faceImageData = await fnDetectFace(image);
|
||||||
|
await smoothUpdateProgress(80, 500); // 平滑更新进度条
|
||||||
|
|
||||||
|
upload.predictResult.hasFace = !!faceImageData;
|
||||||
|
|
||||||
|
// Step 3: 目标识别
|
||||||
|
const cocoResults = await cocoSsdPredict(image);
|
||||||
|
await smoothUpdateProgress(90, 500); // 平滑更新进度条
|
||||||
|
|
||||||
|
if (cocoResults.length > 0) {
|
||||||
|
const classSet = new Set(cocoResults.map(result => result.class));
|
||||||
|
upload.predictResult.objectArray = Array.from(classSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: 风景识别
|
||||||
|
upload.predictResult.landscape = await predictLandscape(image);
|
||||||
|
await smoothUpdateProgress(100, 500); // 平滑更新进度条
|
||||||
|
|
||||||
|
// 完成
|
||||||
predicting.value = false;
|
predicting.value = false;
|
||||||
|
image.removeEventListener('webglcontextlost', () => void 0);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {uploading, send: submitFile, abort} = useRequest(uploadFile, {
|
||||||
|
immediate: false,
|
||||||
|
debounce: 500,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自定义上传请求
|
* 自定义上传请求
|
||||||
* @param file
|
* @param file
|
||||||
*/
|
*/
|
||||||
async function customUploadRequest(file: any) {
|
async function customUploadRequest(file: any) {
|
||||||
upload.clearPredictResult();
|
|
||||||
let percent = 1; // 初始化进度
|
|
||||||
const totalSteps = 5; // 总任务数,用于计算进度百分比
|
|
||||||
|
|
||||||
// 更新进度条函数
|
const formData = new FormData();
|
||||||
const updateProgress = (completedSteps: number) => {
|
formData.append("file", file.file);
|
||||||
const targetPercent = Math.min((completedSteps / totalSteps) * 100, 100); // 目标进度
|
formData.append("result", JSON.stringify({
|
||||||
if (percent < targetPercent) {
|
uid: file.file.uid,
|
||||||
// 每次进度更新时,增加一个小增量
|
fileName: file.file.name, // 添加文件名
|
||||||
const increment = Math.min(1, targetPercent - percent); // 每次增量
|
fileType: file.file.type, // 添加文件类型
|
||||||
percent += increment;
|
detectionResult: upload.predictResult,
|
||||||
|
}));
|
||||||
// 更新进度条
|
watch(
|
||||||
file.onProgress({percent});
|
() => uploading.value,
|
||||||
|
(upload) => {
|
||||||
// 控制进度条更新的速度
|
if (upload && upload.loaded && upload.total) {
|
||||||
if (percent < targetPercent) {
|
file.onProgress({percent: Number(Math.round((upload.loaded / upload.total) * 100).toFixed(2))}, file);
|
||||||
setTimeout(() => updateProgress(completedSteps), 50); // 每50ms更新一次
|
}
|
||||||
}
|
},
|
||||||
}
|
);
|
||||||
};
|
submitFile(formData).then((response: any) => {
|
||||||
|
file.onSuccess(response.data, file);
|
||||||
let completedSteps = 0; // 已完成的步骤计数
|
}).catch(file.onError);
|
||||||
|
|
||||||
try {
|
|
||||||
// Step 1: 动漫预测
|
|
||||||
const prediction1 = await animePredictImage(image);
|
|
||||||
completedSteps++;
|
|
||||||
updateProgress(completedSteps);
|
|
||||||
|
|
||||||
const prediction2 = await animePredictImagePro(image);
|
|
||||||
completedSteps++;
|
|
||||||
updateProgress(completedSteps);
|
|
||||||
|
|
||||||
if (prediction1 === 'Anime' && (prediction2 === 'Furry' || prediction2 === 'Anime')) {
|
|
||||||
upload.predictResult.isAnime = true;
|
|
||||||
|
|
||||||
// 任务提前完成,直接设置进度为 100%
|
|
||||||
percent = 100;
|
|
||||||
file.onProgress({percent});
|
|
||||||
setTimeout(() => {
|
|
||||||
file.onSuccess();
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: 人脸检测
|
|
||||||
const faceImageData = await fnDetectFace(image);
|
|
||||||
completedSteps++;
|
|
||||||
updateProgress(completedSteps);
|
|
||||||
|
|
||||||
if (faceImageData) {
|
|
||||||
upload.predictResult.hasFace = true;
|
|
||||||
|
|
||||||
// 任务提前完成,直接设置进度为 100%
|
|
||||||
percent = 100;
|
|
||||||
file.onProgress({percent});
|
|
||||||
setTimeout(() => {
|
|
||||||
file.onSuccess();
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: 目标识别
|
|
||||||
const cocoResults = await cocoSsdPredict(image);
|
|
||||||
completedSteps++;
|
|
||||||
updateProgress(completedSteps);
|
|
||||||
|
|
||||||
if (cocoResults.length > 0) {
|
|
||||||
const classSet = new Set(cocoResults.map(result => result.class));
|
|
||||||
upload.predictResult.objectArray = Array.from(classSet);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 4: 风景识别
|
|
||||||
upload.predictResult.landscape = await predictLandscape(image);
|
|
||||||
completedSteps++;
|
|
||||||
updateProgress(completedSteps);
|
|
||||||
|
|
||||||
// 任务完成,确保进度条到达 100%
|
|
||||||
percent = 100;
|
|
||||||
file.onProgress({percent});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
file.onSuccess();
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
// 出现错误,直接设置进度为 100%,并调用错误回调
|
|
||||||
percent = 100;
|
|
||||||
file.onProgress({percent});
|
|
||||||
file.onError(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消上传
|
||||||
|
*/
|
||||||
|
function cancelUpload() {
|
||||||
|
abort();
|
||||||
|
fileList.value = [];
|
||||||
|
upload.clearPredictResult();
|
||||||
|
predicting.value = false;
|
||||||
|
progressPercent.value = 0; // 重置进度条
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除文件
|
||||||
|
* @param file
|
||||||
|
*/
|
||||||
|
function removeFile(file: any) {
|
||||||
|
fileList.value = fileList.value.filter((item: any) => item.uid !== file.uid);
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="less">
|
<style lang="less" scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
Reference in New Issue
Block a user