🚧 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' {
|
||||
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']
|
||||
|
@@ -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
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";
|
||||
|
||||
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
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>
|
||||
<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>
|
||||
|
Reference in New Issue
Block a user