🚧 optimization & adjustments

This commit is contained in:
2025-03-03 01:00:18 +08:00
parent 1c3ee31c0b
commit 3f4bf14533
51 changed files with 1645 additions and 3441 deletions

View File

@@ -8,6 +8,7 @@ import {useMenuStore} from "@/store/modules/menuStore.ts";
import {useUploadStore} from "@/store/modules/uploadStore.ts";
import {useImageStore} from "@/store/modules/imageStore.ts";
import {useShareStore} from "@/store/modules/shareStore.ts";
import {useSearchStore} from "@/store/modules/searchStore.ts";
export default function useStore() {
return {
@@ -21,5 +22,6 @@ export default function useStore() {
upload: useUploadStore(),
image: useImageStore(),
share: useShareStore(),
search: useSearchStore(),
};
}

View File

@@ -37,6 +37,7 @@ export const useImageStore = defineStore(
"1": "我的分享",
"2": "我的收藏",
});
const albumIdList = ref<number[]>([]);
// 相册分享弹窗相关
const openAlbumShareDialog = ref<boolean>(false);
@@ -49,6 +50,10 @@ export const useImageStore = defineStore(
const faceList = ref<any[]>([]);
const faceListLoading = ref<boolean>(false);
// 图片编辑
const imageEditVisible = ref<boolean>(false);
/**
* 获取人脸列表
*/
@@ -118,6 +123,8 @@ export const useImageStore = defineStore(
faceListLoading,
openAlbumShareDialog,
JustifiedLayoutOptions,
albumIdList,
imageEditVisible,
countTotalImages,
openAlbumShareDialogHandler,
getAlbumList,
@@ -137,7 +144,8 @@ export const useImageStore = defineStore(
"homeTabMap",
"switchValue",
"faceSelectedKey",
"albumList"
"albumList",
"albumIdList"
],
}
}

View File

@@ -0,0 +1,60 @@
import {defineStore} from 'pinia';
import {ref} from "vue";
import time from "@/assets/svgs/time.svg";
import location from "@/assets/svgs/location-album.svg";
import people from "@/assets/svgs/people-album.svg";
import thing from "@/assets/svgs/thing-album.svg";
import image from "@/assets/svgs/image.svg";
export const useSearchStore = defineStore(
'search',
() => {
const options = reactive([
{
label: '时间',
value: 'time',
icon: time
}, {
label: '地点',
value: 'location',
icon: location
}, {
label: '人物',
value: 'person',
icon: people
},
{
label: '事物',
value: 'thing',
icon: thing
}, {
label: '图片',
value: 'picture',
icon: image
}
]);
const searchOption = ref<string>(options[0].value);
const searchValue = ref<string>('');
const getIconByValue = (value: string) => {
const option = options.find(option => option.value === value);
return option ? option.icon : undefined;
};
return {
searchOption,
options,
searchValue,
getIconByValue
};
},
{
// 开启数据持久化
persistedState: {
persist: true,
storage: localStorage,
key: 'search',
includePaths: ['searchOption', 'options']
}
}
);

View File

@@ -3,6 +3,8 @@ export const useShareStore = defineStore(
() => {
const sharePassword = reactive<Record<string, string>>({});
const openCommentDrawer = ref<boolean>(false);
// 获取密码保持相同API
const getPassword = (key: string): string | undefined => {
return sharePassword[key];
@@ -12,17 +14,23 @@ export const useShareStore = defineStore(
const addPassword = (key: string, password: string) => {
sharePassword[key] = password;
};
function setOpenCommentDrawer(value: boolean): void {
openCommentDrawer.value = value;
};
return {
sharePassword,
getPassword,
addPassword
addPassword,
openCommentDrawer,
setOpenCommentDrawer
};
},
{
// 开启数据持久化
persistedState: {
persist: true,
storage: localStorage,
storage: sessionStorage,
key: 'share',
includePaths: ['sharePassword']
}

View File

@@ -1,5 +1,19 @@
// import localforage from 'localforage';
import imageCompression from "browser-image-compression";
import {message, type UploadProps} from "ant-design-vue";
import {initNSFWJs, predictNSFW} from "@/utils/tfjs/nsfw.ts";
import i18n from "@/locales";
import {NSFWJS} from "nsfwjs";
import {animePredictImagePro} from "@/utils/tfjs/anime_classifier_pro.ts";
import {cocoSsdPredict} from "@/utils/tfjs/mobilenet.ts";
import {predictLandscape} from "@/utils/tfjs/landscape_recognition.ts";
import {getCategoryByLabel} from "@/constant/coco_ssd_label_category.ts";
import exifr from "exifr";
import isScreenshot from "@/utils/imageUtils/isScreenshot.ts";
import {ready, scan} from 'qr-scanner-wechat';
interface UploadPredictResult {
isAnime: boolean;
tagName: string | null;
@@ -13,6 +27,7 @@ interface UploadPredictResult {
thumb_w: number | null;
thumb_h: number | null;
thumb_size: number | null;
hasQrcode: boolean;
}
export const useUploadStore = defineStore(
@@ -33,11 +48,45 @@ export const useUploadStore = defineStore(
thumb_w: null,
thumb_h: null,
thumb_size: null,
hasQrcode: false,
});
const uploadSetting = reactive<any>({
nsfw_detection: true,
anime_detection: true,
landscape_detection: true,
screenshot_detection: true,
gps_detection: true,
target_detection: false,
qrcode_detection: true,
face_detection: true,
});
const storageSelected = ref<any[]>([]);
const albumSelected = ref<number>(0);
const albumSelected = ref<number>();
const predicting = ref<boolean>(false);
const progressPercent = ref<number>(0);
const progressStatus = ref<string>('active');
const image = new Image();
// 压缩图片配置
const options = reactive({
maxSizeMB: 0.2,
maxWidthOrHeight: 750,
maxIteration: 2,
useWebWorker: true,
});
const progress: UploadProps['progress'] = {
strokeColor: {
'0%': '#108ee9',
'100%': '#87d068',
},
strokeWidth: 3,
format: (percent: any) => `${parseFloat(percent.toFixed(2))}%`,
class: 'progress-bar',
};
const fileList = ref<any[]>([]);
/**
* 打开上传抽屉
@@ -62,8 +111,208 @@ export const useUploadStore = defineStore(
predictResult.thumb_w = null;
predictResult.thumb_h = null;
predictResult.thumb_size = null;
predictResult.hasQrcode = false;
}
/**
* 图片上传前的校验
* @param file
* @param fileList
*/
async function beforeUpload(file: File, fileList: File[]) {
predicting.value = true;
clearPredictResult();
progressPercent.value = 0; // 初始化进度条
progressStatus.value = 'active'; // 开始状态
// 压缩图片
const compressedFile = await imageCompression(file, options);
// 创建图片对象
image.src = URL.createObjectURL(compressedFile);
// 获取图片宽高
image.onload = async () => {
const {width, height} = image;
predictResult.width = width;
predictResult.height = height;
// 二维码检测
if (uploadSetting.qrcode_detection) {
await ready();
const result = await scan(image);
if (result.text) {
predictResult.hasQrcode = true;
}
}
};
// 更新进度条函数,逐步增加
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);
});
};
try {
// 动态计算启用的检测步骤及进度分配
const enabledSteps = [
uploadSetting.nsfw_detection,
uploadSetting.gps_detection,
uploadSetting.screenshot_detection,
uploadSetting.anime_detection,
uploadSetting.target_detection || uploadSetting.landscape_detection
].filter(Boolean).length;
const stepIncrement = enabledSteps > 0 ? Math.floor(100 / enabledSteps) : 100;
let currentProgress = 0;
// NSFW 检测
if (uploadSetting.nsfw_detection) {
const nsfw: NSFWJS = await initNSFWJs();
await smoothUpdateProgress(currentProgress + stepIncrement, 500);
currentProgress += stepIncrement;
const isNSFW: boolean = await predictNSFW(nsfw, image);
if (isNSFW) {
message.error(i18n.global.t('comment.illegalImage'));
progressStatus.value = 'exception';
fileList.pop();
predicting.value = false;
return false;
}
}
// GPS 数据提取
if (uploadSetting.gps_detection) {
const gpsData = await extractGPSExifData(file);
if (gpsData) {
predictResult.longitude = gpsData.longitude;
predictResult.latitude = gpsData.latitude;
}
await smoothUpdateProgress(currentProgress + stepIncrement, 500);
currentProgress += stepIncrement;
}
// 截图检测
if (uploadSetting.screenshot_detection) {
predictResult.isScreenshot = await isScreenshot(file);
await smoothUpdateProgress(currentProgress + stepIncrement, 500);
currentProgress += stepIncrement;
}
// 动漫检测
if (uploadSetting.anime_detection) {
const prediction = await animePredictImagePro(image);
if (prediction === 'Furry' || prediction === 'Anime') {
predictResult.isAnime = true;
predicting.value = false;
progressPercent.value = 100;
return true;
}
await smoothUpdateProgress(currentProgress + stepIncrement, 500);
currentProgress += stepIncrement;
}
// 目标检测
if (uploadSetting.target_detection) {
const cocoResults: any = await cocoSsdPredict(image);
if (cocoResults.length > 0) {
// 取置信度最高的结果
// 如果只有一个结果,直接取第一个
if (cocoResults.length === 1) {
predictResult.topCategory = getCategoryByLabel(cocoResults[0].class);
predictResult.tagName = cocoResults[0].class;
} else {
// 多个结果时,按 score 排序,取置信度最高的结果
const sortedResults = cocoResults.sort((a, b) => b.score - a.score);
predictResult.topCategory = getCategoryByLabel(sortedResults[0].class);
predictResult.tagName = sortedResults[0].class;
}
}
await smoothUpdateProgress(currentProgress + stepIncrement, 500);
currentProgress += stepIncrement;
}
if (uploadSetting.landscape_detection) {
const landscape = await predictLandscape(image);
predictResult.landscape = landscape as 'building' | 'forest' | 'glacier' | 'mountain' | 'sea' | 'street' | null;
await smoothUpdateProgress(currentProgress + stepIncrement, 500);
}
// 确保进度条到100%
if (currentProgress < 100) {
await smoothUpdateProgress(100, 500);
}
predicting.value = false;
return true;
} catch (error) {
console.error('识别过程中发生错误:', error);
predicting.value = false;
progressPercent.value = 0; // 重置进度条
return false;
}
}
/**
* 提取 EXIF 数据
* @param {File} file - 图片文件
* @returns {Promise<Object|null>} - 返回所有 EXIF 数据或 null如果格式不支持或提取失败
*/
async function extractGPSExifData(file): Promise<{ latitude: any; longitude: any; } | null> {
const supportedFormats = ['image/jpeg', 'image/tiff', 'image/iiq', 'image/heif', 'image/heic', 'image/avif', 'image/png'];
// 判断文件格式是否支持
if (!supportedFormats.includes(file.type)) {
return null;
}
const options: any = {
ifd0: false,
exif: false,
gps: ['GPSLatitudeRef', 'GPSLatitude', 0x0003, 0x0004],
interop: false,
ifd1: false // thumbnail
};
// 提取GPS EXIF 数据
const gpsData = await exifr.parse(file, options);
if (!gpsData) {
return null;
}
const {latitude, longitude} = gpsData;
if (latitude && longitude) {
return {latitude, longitude};
}
return null;
}
/**
* 拒绝文件回调
* @param fileList
*/
function rejectFile(fileList: any) {
fileList.value.pop();
}
/**
* 删除文件
* @param file
*/
function removeFile(file: any) {
fileList.value = fileList.value.filter((item: any) => item.uid !== file.uid);
}
onMounted(async () => {
await ready();
});
return {
openUploadDrawer,
@@ -71,7 +320,17 @@ export const useUploadStore = defineStore(
storageSelected,
openUploadDrawerFn,
clearPredictResult,
beforeUpload,
albumSelected,
options,
predicting,
progressPercent,
progressStatus,
uploadSetting,
progress,
fileList,
rejectFile,
removeFile,
};
},
{
@@ -80,7 +339,7 @@ export const useUploadStore = defineStore(
persist: true,
storage: localStorage,
key: 'upload',
includePaths: ["storageSelected", "albumSelected"]
includePaths: ["storageSelected", "albumSelected", "uploadSetting"]
}
}
);