-
搜索历史1
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -69,4 +171,56 @@ const handleInputBlur = () => {
margin-top: 10px;
}
}
+
+.header-search-params-container {
+ width: 230px;
+ height: 200px;
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ flex-wrap: wrap;
+ justify-content: flex-start;
+ gap: 25px;
+
+ .header-search-params-item {
+ width: 100px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ gap: 15px;
+
+ .photo-item-label {
+ font-size: 14px;
+ font-weight: bolder;
+ }
+ }
+}
+
+.header-search-time {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.header-search-picture {
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ justify-content: flex-start;
+ gap: 20px;
+
+ .avatar-uploader {
+ width: 100px;
+ height: 100px;
+ }
+
+ .header-search-picture-info {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: flex-start;
+ gap: 10px;
+ }
+}
diff --git a/src/router/modules/phone_upload.ts b/src/router/modules/phone_upload.ts
index eba2d21..e97a6a0 100644
--- a/src/router/modules/phone_upload.ts
+++ b/src/router/modules/phone_upload.ts
@@ -16,5 +16,15 @@ export default [
requiresAuth: false,
title: '手机上传'
}
+ },
+ {
+ path: '/main/image/phone/app',
+ name: 'imageApp',
+ component: () => import('@/views/Phone/CommonPhoneUpload/CommonPhoneUpload.vue'),
+ meta: {
+ requiresAuth: false,
+ title: '手机上传'
+ }
}
+
];
diff --git a/src/store/index.ts b/src/store/index.ts
index 14ac687..478b780 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -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(),
};
}
diff --git a/src/store/modules/imageStore.ts b/src/store/modules/imageStore.ts
index ddbeb26..9130f1f 100644
--- a/src/store/modules/imageStore.ts
+++ b/src/store/modules/imageStore.ts
@@ -37,6 +37,7 @@ export const useImageStore = defineStore(
"1": "我的分享",
"2": "我的收藏",
});
+ const albumIdList = ref
([]);
// 相册分享弹窗相关
const openAlbumShareDialog = ref(false);
@@ -49,6 +50,10 @@ export const useImageStore = defineStore(
const faceList = ref([]);
const faceListLoading = ref(false);
+
+ // 图片编辑
+ const imageEditVisible = ref(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"
],
}
}
diff --git a/src/store/modules/searchStore.ts b/src/store/modules/searchStore.ts
new file mode 100644
index 0000000..e7f02dd
--- /dev/null
+++ b/src/store/modules/searchStore.ts
@@ -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(options[0].value);
+ const searchValue = ref('');
+
+ 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']
+ }
+ }
+);
diff --git a/src/store/modules/shareStore.ts b/src/store/modules/shareStore.ts
index 126f069..7d7ce62 100644
--- a/src/store/modules/shareStore.ts
+++ b/src/store/modules/shareStore.ts
@@ -3,6 +3,8 @@ export const useShareStore = defineStore(
() => {
const sharePassword = reactive>({});
+ const openCommentDrawer = ref(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']
}
diff --git a/src/store/modules/uploadStore.ts b/src/store/modules/uploadStore.ts
index 09483ff..e201a9a 100644
--- a/src/store/modules/uploadStore.ts
+++ b/src/store/modules/uploadStore.ts
@@ -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({
+ 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([]);
- const albumSelected = ref(0);
+ const albumSelected = ref();
+
+ const predicting = ref(false);
+ const progressPercent = ref(0);
+ const progressStatus = ref('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([]);
/**
* 打开上传抽屉
@@ -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