🚧 improve image uploading

This commit is contained in:
2025-01-14 17:34:55 +08:00
parent 9356c00815
commit e1ee33946d
6 changed files with 165 additions and 132 deletions

2
components.d.ts vendored
View File

@@ -8,7 +8,6 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
AAvatar: typeof import('ant-design-vue/es')['Avatar']
AAvatarGroup: typeof import('ant-design-vue/es')['AvatarGroup']
ABadge: typeof import('ant-design-vue/es')['Badge']
AButton: typeof import('ant-design-vue/es')['Button']
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']
Empty: typeof import('./src/components/MyUI/Empty/Empty.vue')['default']
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']
FloatButton: typeof import('./src/components/MyUI/FloatButton/FloatButton.vue')['default']
Folder: typeof import('./src/components/Folder/Folder.vue')['default']

View File

@@ -31,13 +31,13 @@
"@types/animejs": "^3.1.12",
"@types/crypto-js": "^4.2.2",
"@types/json-stringify-safe": "^5.0.3",
"@types/node": "^22.10.5",
"@types/node": "^22.10.6",
"@types/nprogress": "^0.2.3",
"@vladmandic/face-api": "^1.7.14",
"@vuepic/vue-datepicker": "^11.0.1",
"@vueuse/core": "^12.4.0",
"@vueuse/integrations": "^12.4.0",
"alova": "^3.2.7",
"alova": "^3.2.8",
"animejs": "^3.2.2",
"ant-design-vue": "^4.2.6",
"axios": "^1.7.9",
@@ -75,9 +75,9 @@
"@vitejs/plugin-vue": "^5.2.1",
"eslint-plugin-vue": "^9.32.0",
"globals": "^15.14.0",
"sass": "^1.83.1",
"sass": "^1.83.3",
"typescript": "^5.7.3",
"typescript-eslint": "^8.19.1",
"typescript-eslint": "^8.20.0",
"unplugin-vue-components": "^28.0.0",
"vite": "^6.0.7",
"vite-plugin-bundle-obfuscator": "1.4.0",

10
src/api/file/index.ts Normal file
View 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,
}
});
};

View File

@@ -2,7 +2,7 @@ import {service} from "@/utils/alova/service.ts";
import {uploadImageRequest} from "@/types/upscale";
export const uploadImage = (data: uploadImageRequest) => {
return service.Post('/api/auth/upscale/upload', {
return service.Post('/api/auth/upscale/phone/upload', {
image: data.image,
access_token: data.access_token,
user_id: data.user_id,

12
src/types/upload.d.ts vendored Normal file
View 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;
};
}

View File

@@ -1,7 +1,7 @@
<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>
<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>
@@ -11,41 +11,39 @@
</AFlex>
</template>
<div class="upload-container">
<Spin :spinning="predicting" indicator="magic-ring">
<AUploadDragger
v-model:fileList="fileList"
accept="image/*"
name="file"
:directory="false"
:multiple="true"
method="post"
:beforeUpload="beforeUpload"
:customRequest="customUploadRequest"
:progress="progress"
:maxCount="10"
list-type="picture"
>
<p class="ant-upload-drag-icon">
<inbox-outlined></inbox-outlined>
</p>
<p class="ant-upload-text">单击或拖动文件到此区域以上传</p>
<p class="ant-upload-hint">
支持单次或批量上传严禁上传非法图片违者将受到法律惩戒
</p>
</AUploadDragger>
</Spin>
<AUploadDragger
v-model:fileList="fileList"
:beforeUpload="beforeUpload"
:customRequest="customUploadRequest"
:directory="false"
:maxCount="10"
:multiple="true"
:disabled="predicting"
:progress="progress"
@remove="removeFile"
accept="image/*"
list-type="picture"
method="post"
name="file"
>
<p class="ant-upload-drag-icon">
<FileImageOutlined/>
</p>
<p v-show="!predicting" class="ant-upload-text">单击或拖动文件到此区域以上传</p>
<p v-show="!predicting" class="ant-upload-hint">
支持单次或批量上传严禁上传非法图片违者将受到法律惩戒
</p>
<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>
<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>
</template>
<script setup lang="ts">
<script lang="ts" setup>
import useStore from "@/store";
import {InboxOutlined} from '@ant-design/icons-vue';
import type {UploadProps} from 'ant-design-vue';
import {message} from "ant-design-vue";
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 {cocoSsdPredict} from "@/utils/tfjs/mobilenet.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 progressPercent = ref<number>(0);
const upload = useStore().upload;
const image: HTMLImageElement = document.createElement('img');
@@ -83,119 +82,131 @@ const progress: UploadProps['progress'] = {
*/
async function beforeUpload(file: File) {
predicting.value = true;
upload.clearPredictResult();
progressPercent.value = 0; // 初始化进度条
// 创建图片对象
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 检测
const nsfw: NSFWJS = await initNSFWJs();
await smoothUpdateProgress(10, 500); // 平滑更新进度条
const isNSFW: boolean = await predictNSFW(nsfw, image);
await smoothUpdateProgress(20, 500); // 平滑更新进度条
if (isNSFW) {
message.error(i18n.global.t('comment.illegalImage'));
predicting.value = false;
progressPercent.value = 0; // 重置进度条
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;
image.removeEventListener('webglcontextlost', () => void 0);
return true;
}
const {uploading, send: submitFile, abort} = useRequest(uploadFile, {
immediate: false,
debounce: 500,
});
/**
* 自定义上传请求
* @param file
*/
async function customUploadRequest(file: any) {
upload.clearPredictResult();
let percent = 1; // 初始化进度
const totalSteps = 5; // 总任务数,用于计算进度百分比
// 更新进度条函数
const updateProgress = (completedSteps: number) => {
const targetPercent = Math.min((completedSteps / totalSteps) * 100, 100); // 目标进度
if (percent < targetPercent) {
// 每次进度更新时,增加一个小增量
const increment = Math.min(1, targetPercent - percent); // 每次增量
percent += increment;
// 更新进度条
file.onProgress({percent});
// 控制进度条更新的速度
if (percent < targetPercent) {
setTimeout(() => updateProgress(completedSteps), 50); // 每50ms更新一次
}
}
};
let completedSteps = 0; // 已完成的步骤计数
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);
}
const formData = new FormData();
formData.append("file", file.file);
formData.append("result", JSON.stringify({
uid: file.file.uid,
fileName: file.file.name, // 添加文件名
fileType: file.file.type, // 添加文件类型
detectionResult: upload.predictResult,
}));
watch(
() => uploading.value,
(upload) => {
if (upload && upload.loaded && upload.total) {
file.onProgress({percent: Number(Math.round((upload.loaded / upload.total) * 100).toFixed(2))}, file);
}
},
);
submitFile(formData).then((response: any) => {
file.onSuccess(response.data, file);
}).catch(file.onError);
}
/**
* 取消上传
*/
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>
<style scoped lang="less">
<style lang="less" scoped>
</style>