✨ complete model integration
This commit is contained in:
@@ -5,8 +5,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
src?: string;
|
||||
defaultSrc: string;
|
||||
|
@@ -39,7 +39,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
accept: '*', // 默认支持所有类型
|
||||
multiple: false,
|
||||
maxCount: undefined,
|
||||
tip: 'Upload',
|
||||
tip: 'ImageUpload',
|
||||
fit: 'contain',
|
||||
draggable: true,
|
||||
disabled: false,
|
||||
|
242
src/constant/coco_ssd_label_category.ts
Normal file
242
src/constant/coco_ssd_label_category.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
// 定义类别名称常量
|
||||
export const CATEGORIES = {
|
||||
ANIMALS: {en: 'animals', zh: '动物'},
|
||||
VEHICLES: {en: 'vehicles', zh: '交通工具'},
|
||||
FURNITURE: {en: 'furniture', zh: '家具'},
|
||||
FOOD: {en: 'food', zh: '食物'},
|
||||
SPORTS: {en: 'sports', zh: '运动'},
|
||||
CONTAINERS: {en: 'containers', zh: '容器'},
|
||||
ELECTRONICS: {en: 'electronics', zh: '电子产品'},
|
||||
EVERYDAY_ITEMS: {en: 'everyday_items', zh: '日常物品'},
|
||||
HOUSEHOLD: {en: 'household', zh: '家居用品'},
|
||||
HUMAN: {en: 'human', zh: '人类'},
|
||||
} as const;
|
||||
|
||||
// 为每个标签提供中文名称的映射
|
||||
export const LABELS = {
|
||||
|
||||
// Human 人类
|
||||
'person': {en: 'person', zh: '人'},
|
||||
|
||||
// Vehicles 交通工具
|
||||
'bicycle': {en: 'bicycle', zh: '自行车'},
|
||||
'car': {en: 'car', zh: '汽车'},
|
||||
'motorcycle': {en: 'motorcycle', zh: '摩托车'},
|
||||
'airplane': {en: 'airplane', zh: '飞机'},
|
||||
'bus': {en: 'bus', zh: '公共汽车'},
|
||||
'train': {en: 'train', zh: '火车'},
|
||||
'truck': {en: 'truck', zh: '卡车'},
|
||||
'boat': {en: 'boat', zh: '船'},
|
||||
'traffic_light': {en: 'traffic_light', zh: '交通信号灯'},
|
||||
'fire_hydrant': {en: 'fire_hydrant', zh: '消防栓'},
|
||||
'stop_sign': {en: 'stop_sign', zh: '停车标志'},
|
||||
'parking_meter': {en: 'parking_meter', zh: '停车计时器'},
|
||||
|
||||
// Furniture 家具
|
||||
'bench': {en: 'bench', zh: '长椅'},
|
||||
'chair': {en: 'chair', zh: '椅子'},
|
||||
'couch': {en: 'couch', zh: '沙发'},
|
||||
'potted_plant': {en: 'potted_plant', zh: '盆栽'},
|
||||
'bed': {en: 'bed', zh: '床'},
|
||||
'dining_table': {en: 'dining_table', zh: '餐桌'},
|
||||
'toilet': {en: 'toilet', zh: '厕所'},
|
||||
|
||||
// Food 食物
|
||||
'banana': {en: 'banana', zh: '香蕉'},
|
||||
'apple': {en: 'apple', zh: '苹果'},
|
||||
'sandwich': {en: 'sandwich', zh: '三明治'},
|
||||
'orange': {en: 'orange', zh: '橙子'},
|
||||
'broccoli': {en: 'broccoli', zh: '西兰花'},
|
||||
'carrot': {en: 'carrot', zh: '胡萝卜'},
|
||||
'hot_dog': {en: 'hot_dog', zh: '热狗'},
|
||||
'pizza': {en: 'pizza', zh: '披萨'},
|
||||
'donut': {en: 'donut', zh: '甜甜圈'},
|
||||
'cake': {en: 'cake', zh: '蛋糕'},
|
||||
|
||||
// Sports 运动
|
||||
'frisbee': {en: 'frisbee', zh: '飞盘'},
|
||||
'skis': {en: 'skis', zh: '滑雪板'},
|
||||
'snowboard': {en: 'snowboard', zh: '滑雪板'},
|
||||
'sports_ball': {en: 'sports_ball', zh: '运动球'},
|
||||
'kite': {en: 'kite', zh: '风筝'},
|
||||
'baseball_bat': {en: 'baseball_bat', zh: '棒球棒'},
|
||||
'baseball_glove': {en: 'baseball_glove', zh: '棒球手套'},
|
||||
'skateboard': {en: 'skateboard', zh: '滑板'},
|
||||
'surfboard': {en: 'surfboard', zh: '冲浪板'},
|
||||
'tennis_racket': {en: 'tennis_racket', zh: '网球拍'},
|
||||
|
||||
// Containers 容器
|
||||
'bottle': {en: 'bottle', zh: '瓶子'},
|
||||
'wine_glass': {en: 'wine_glass', zh: '酒杯'},
|
||||
'cup': {en: 'cup', zh: '杯子'},
|
||||
'fork': {en: 'fork', zh: '叉子'},
|
||||
'knife': {en: 'knife', zh: '刀'},
|
||||
'spoon': {en: 'spoon', zh: '勺子'},
|
||||
'bowl': {en: 'bowl', zh: '碗'},
|
||||
|
||||
// Electronics 电子产品
|
||||
'tv': {en: 'tv', zh: '电视'},
|
||||
'laptop': {en: 'laptop', zh: '笔记本电脑'},
|
||||
'mouse': {en: 'mouse', zh: '鼠标'},
|
||||
'remote': {en: 'remote', zh: '遥控器'},
|
||||
'keyboard': {en: 'keyboard', zh: '键盘'},
|
||||
'cell_phone': {en: 'cell_phone', zh: '手机'},
|
||||
'microwave': {en: 'microwave', zh: '微波炉'},
|
||||
'oven': {en: 'oven', zh: '烤箱'},
|
||||
'toaster': {en: 'toaster', zh: '烤面包机'},
|
||||
'sink': {en: 'sink', zh: '水槽'},
|
||||
'refrigerator': {en: 'refrigerator', zh: '冰箱'},
|
||||
|
||||
// Everyday Items 日常物品
|
||||
'backpack': {en: 'backpack', zh: '背包'},
|
||||
'umbrella': {en: 'umbrella', zh: '雨伞'},
|
||||
'handbag': {en: 'handbag', zh: '手袋'},
|
||||
'tie': {en: 'tie', zh: '领带'},
|
||||
'suitcase': {en: 'suitcase', zh: '行李箱'},
|
||||
'scissors': {en: 'scissors', zh: '剪刀'},
|
||||
'teddy_bear': {en: 'teddy_bear', zh: '泰迪熊'},
|
||||
'hair_dryer': {en: 'hair_dryer', zh: '吹风机'},
|
||||
'toothbrush': {en: 'toothbrush', zh: '牙刷'},
|
||||
|
||||
// Household 家居用品
|
||||
'book': {en: 'book', zh: '书'},
|
||||
'clock': {en: 'clock', zh: '时钟'},
|
||||
'vase': {en: 'vase', zh: '花瓶'},
|
||||
|
||||
// Animals 动物
|
||||
'cat': {en: 'cat', zh: '猫'},
|
||||
'dog': {en: 'dog', zh: '狗'},
|
||||
'horse': {en: 'horse', zh: '马'},
|
||||
'sheep': {en: 'sheep', zh: '羊'},
|
||||
'cow': {en: 'cow', zh: '牛'},
|
||||
'elephant': {en: 'elephant', zh: '大象'},
|
||||
'bear': {en: 'bear', zh: '熊'},
|
||||
'zebra': {en: 'zebra', zh: '斑马'},
|
||||
'giraffe': {en: 'giraffe', zh: '长颈鹿'},
|
||||
'bird': {en: 'bird', zh: '鸟'}
|
||||
};
|
||||
|
||||
|
||||
// 创建标签到类别的映射(使用 Map 实现高效查找)
|
||||
export const LABEL_TO_CATEGORY = new Map<string, { en: string, zh: string }>([
|
||||
// Human 人类
|
||||
['person', CATEGORIES.HUMAN],
|
||||
// Vehicles 交通工具
|
||||
['bicycle', CATEGORIES.VEHICLES],
|
||||
['car', CATEGORIES.VEHICLES],
|
||||
['motorcycle', CATEGORIES.VEHICLES],
|
||||
['airplane', CATEGORIES.VEHICLES],
|
||||
['bus', CATEGORIES.VEHICLES],
|
||||
['train', CATEGORIES.VEHICLES],
|
||||
['truck', CATEGORIES.VEHICLES],
|
||||
['boat', CATEGORIES.VEHICLES],
|
||||
['traffic_light', CATEGORIES.VEHICLES],
|
||||
['fire_hydrant', CATEGORIES.VEHICLES],
|
||||
['stop_sign', CATEGORIES.VEHICLES],
|
||||
['parking_meter', CATEGORIES.VEHICLES],
|
||||
|
||||
// Furniture 家具
|
||||
['bench', CATEGORIES.FURNITURE],
|
||||
['chair', CATEGORIES.FURNITURE],
|
||||
['couch', CATEGORIES.FURNITURE],
|
||||
['potted_plant', CATEGORIES.FURNITURE],
|
||||
['bed', CATEGORIES.FURNITURE],
|
||||
['dining_table', CATEGORIES.FURNITURE],
|
||||
['toilet', CATEGORIES.FURNITURE],
|
||||
|
||||
// Food 食物
|
||||
['banana', CATEGORIES.FOOD],
|
||||
['apple', CATEGORIES.FOOD],
|
||||
['sandwich', CATEGORIES.FOOD],
|
||||
['orange', CATEGORIES.FOOD],
|
||||
['broccoli', CATEGORIES.FOOD],
|
||||
['carrot', CATEGORIES.FOOD],
|
||||
['hot_dog', CATEGORIES.FOOD],
|
||||
['pizza', CATEGORIES.FOOD],
|
||||
['donut', CATEGORIES.FOOD],
|
||||
['cake', CATEGORIES.FOOD],
|
||||
|
||||
// Sports 运动
|
||||
['frisbee', CATEGORIES.SPORTS],
|
||||
['skis', CATEGORIES.SPORTS],
|
||||
['snowboard', CATEGORIES.SPORTS],
|
||||
['sports_ball', CATEGORIES.SPORTS],
|
||||
['kite', CATEGORIES.SPORTS],
|
||||
['baseball_bat', CATEGORIES.SPORTS],
|
||||
['baseball_glove', CATEGORIES.SPORTS],
|
||||
['skateboard', CATEGORIES.SPORTS],
|
||||
['surfboard', CATEGORIES.SPORTS],
|
||||
['tennis_racket', CATEGORIES.SPORTS],
|
||||
|
||||
// Containers 容器
|
||||
['bottle', CATEGORIES.CONTAINERS],
|
||||
['wine_glass', CATEGORIES.CONTAINERS],
|
||||
['cup', CATEGORIES.CONTAINERS],
|
||||
['fork', CATEGORIES.CONTAINERS],
|
||||
['knife', CATEGORIES.CONTAINERS],
|
||||
['spoon', CATEGORIES.CONTAINERS],
|
||||
['bowl', CATEGORIES.CONTAINERS],
|
||||
|
||||
// Electronics 电子产品
|
||||
['tv', CATEGORIES.ELECTRONICS],
|
||||
['laptop', CATEGORIES.ELECTRONICS],
|
||||
['mouse', CATEGORIES.ELECTRONICS],
|
||||
['remote', CATEGORIES.ELECTRONICS],
|
||||
['keyboard', CATEGORIES.ELECTRONICS],
|
||||
['cell_phone', CATEGORIES.ELECTRONICS],
|
||||
['microwave', CATEGORIES.ELECTRONICS],
|
||||
['oven', CATEGORIES.ELECTRONICS],
|
||||
['toaster', CATEGORIES.ELECTRONICS],
|
||||
['sink', CATEGORIES.ELECTRONICS],
|
||||
['refrigerator', CATEGORIES.ELECTRONICS],
|
||||
|
||||
// Everyday Items 日常物品
|
||||
['backpack', CATEGORIES.EVERYDAY_ITEMS],
|
||||
['umbrella', CATEGORIES.EVERYDAY_ITEMS],
|
||||
['handbag', CATEGORIES.EVERYDAY_ITEMS],
|
||||
['tie', CATEGORIES.EVERYDAY_ITEMS],
|
||||
['suitcase', CATEGORIES.EVERYDAY_ITEMS],
|
||||
['scissors', CATEGORIES.EVERYDAY_ITEMS],
|
||||
['teddy_bear', CATEGORIES.EVERYDAY_ITEMS],
|
||||
['hair_dryer', CATEGORIES.EVERYDAY_ITEMS],
|
||||
['toothbrush', CATEGORIES.EVERYDAY_ITEMS],
|
||||
|
||||
// Household 家居用品
|
||||
['book', CATEGORIES.HOUSEHOLD],
|
||||
['clock', CATEGORIES.HOUSEHOLD],
|
||||
['vase', CATEGORIES.HOUSEHOLD],
|
||||
|
||||
// Animals 动物
|
||||
['cat', CATEGORIES.ANIMALS],
|
||||
['dog', CATEGORIES.ANIMALS],
|
||||
['horse', CATEGORIES.ANIMALS],
|
||||
['sheep', CATEGORIES.ANIMALS],
|
||||
['cow', CATEGORIES.ANIMALS],
|
||||
['elephant', CATEGORIES.ANIMALS],
|
||||
['bear', CATEGORIES.ANIMALS],
|
||||
['zebra', CATEGORIES.ANIMALS],
|
||||
['giraffe', CATEGORIES.ANIMALS],
|
||||
['bird', CATEGORIES.ANIMALS]
|
||||
]);
|
||||
|
||||
// 获取标签所属大类的函数,支持英文和中文返回
|
||||
export function getCategoryByLabel(label: string, lang: 'en' | 'zh' = 'en'): string | undefined {
|
||||
const category = LABEL_TO_CATEGORY.get(label);
|
||||
return category ? category[lang] : undefined;
|
||||
}
|
||||
|
||||
// 获取标签所属大类的函数,支持英文和中文返回
|
||||
export function getCategoryName(label: string, lang: 'en' | 'zh' = 'en'): string | undefined {
|
||||
const category = CATEGORIES[label];
|
||||
return category ? category[lang] : undefined;
|
||||
}
|
||||
|
||||
// 获取标签的小分类名称
|
||||
export function getLabelName(label: string, lang: 'en' | 'zh' = 'en'): string | undefined {
|
||||
const labelInfo = LABELS[label];
|
||||
return labelInfo ? labelInfo[lang] : undefined;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
// console.log(getLabelName('person')); // 输出: 'person' (英文)
|
||||
// console.log(getLabelName('person', 'zh')); // 输出: '人' (中文)
|
@@ -1,20 +1,20 @@
|
||||
import PhoalbumIndex from "@/views/Album/Phoalbum/Index.vue";
|
||||
import PeopleAlbumIndex from "@/views/Album/PeopleAlbum/Index.vue";
|
||||
import LocationAlbum from "@/views/Album/LocationAlbum/LocationAlbum.vue";
|
||||
import LocationAlbumIndex from "@/views/Album/LocationAlbum/Index.vue";
|
||||
import ThingAlbum from "@/views/Album/ThingAlbum/ThingAlbum.vue";
|
||||
import ThingAlbumIndex from "@/views/Album/ThingAlbum/Index.vue";
|
||||
import Phoalbum from "@/views/Album/Phoalbum/Phoalbum.vue";
|
||||
import PeopleAlbum from "@/views/Album/PeopleAlbum/PeopleAlbum.vue";
|
||||
import PhoalbumDetail from "@/views/Album/Phoalbum/Detail.vue";
|
||||
import PeopleAlbumDetail from "@/views/Album/PeopleAlbum/Detail.vue";
|
||||
import LocationAlbumDetail from "@/views/Album/LocationAlbum/Detail.vue";
|
||||
import ThingAlbumDetail from "@/views/Album/ThingAlbum/Detail.vue";
|
||||
import PhoalbumPhoalbum from "@/views/Album/Phoalbum/Phoalbum.vue";
|
||||
import PeopleAlbumPeopleAlbum from "@/views/Album/PeopleAlbum/PeopleAlbum.vue";
|
||||
import LocationAlbum from "@/views/Album/LocationAlbum/LocationAlbumList.vue";
|
||||
import LocationAlbumIndex from "@/views/Album/LocationAlbum/LocationAlbum.vue";
|
||||
import ThingAlbum from "@/views/Album/ThingAlbum/ThingAlbumList.vue";
|
||||
import ThingAlbumThingAlbum from "@/views/Album/ThingAlbum/ThingAlbum.vue";
|
||||
import Phoalbum from "@/views/Album/Phoalbum/PhoalbumList.vue";
|
||||
import PeopleAlbum from "@/views/Album/PeopleAlbum/PeopleAlbumList.vue";
|
||||
import PhoalbumDetail from "@/views/Album/Phoalbum/PhoalbumDetail.vue";
|
||||
import PeopleAlbumDetail from "@/views/Album/PeopleAlbum/PeopleAlbumDetail.vue";
|
||||
import LocationAlbumDetail from "@/views/Album/LocationAlbum/LocationAlbumDetail.vue";
|
||||
import ThingAlbumDetail from "@/views/Album/ThingAlbum/ThingAlbumDetail.vue";
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/main/album/albums',
|
||||
component: PhoalbumIndex,
|
||||
component: PhoalbumPhoalbum,
|
||||
redirect: '/main/album/albums',
|
||||
children: [
|
||||
{
|
||||
@@ -39,7 +39,7 @@ export default [
|
||||
},
|
||||
{
|
||||
path: '/main/album/people',
|
||||
component: PeopleAlbumIndex,
|
||||
component: PeopleAlbumPeopleAlbum,
|
||||
redirect: '/main/album/people',
|
||||
children: [
|
||||
{
|
||||
@@ -89,7 +89,7 @@ export default [
|
||||
},
|
||||
{
|
||||
path: '/main/album/thing',
|
||||
component: ThingAlbumIndex,
|
||||
component: ThingAlbumThingAlbum,
|
||||
redirect: '/main/album/thing',
|
||||
children: [
|
||||
{
|
||||
|
@@ -1,80 +1,45 @@
|
||||
import {initNSFWJs, predictNSFW} from "@/utils/nsfw/nsfw.ts";
|
||||
import i18n from "@/locales";
|
||||
interface UploadPredictResult {
|
||||
isAnime: boolean;
|
||||
hasFace: boolean;
|
||||
objectArray: string[] | unknown[];
|
||||
landscape: 'building' | 'forest' | 'glacier' | 'mountain' | 'sea' | 'street' | 'none' | undefined;
|
||||
}
|
||||
|
||||
import {NSFWJS} from "nsfwjs";
|
||||
import {message} from "ant-design-vue";
|
||||
|
||||
// import {loadCocoSsd, loadMobileNet} from "@/utils/tfjs/tfjs.ts";
|
||||
import {loadModel, predictImage} from "@/utils/tfjs/anime_classifier.ts";
|
||||
|
||||
export const useUploadStore = defineStore(
|
||||
'upload',
|
||||
() => {
|
||||
const openUploadDrawer = ref<boolean>(false);
|
||||
const image: HTMLImageElement = document.createElement('img');
|
||||
|
||||
const predictResult = reactive<UploadPredictResult>({
|
||||
isAnime: false,
|
||||
hasFace: false,
|
||||
objectArray: [],
|
||||
landscape: undefined as 'building' | 'forest' | 'glacier' | 'mountain' | 'sea' | 'street' | 'none' | undefined,
|
||||
});
|
||||
|
||||
/**
|
||||
* 图片上传前的校验
|
||||
* @param file
|
||||
* 打开上传抽屉
|
||||
*/
|
||||
async function beforeUpload(file: File) {
|
||||
image.src = URL.createObjectURL(file);
|
||||
// 图片 NSFW 检测
|
||||
const nsfw: NSFWJS = await initNSFWJs();
|
||||
const isNSFW: boolean = await predictNSFW(nsfw, image);
|
||||
if (isNSFW) {
|
||||
message.error(i18n.global.t('comment.illegalImage'));
|
||||
return false;
|
||||
}
|
||||
// const predictions = await loadMobileNet(image);
|
||||
// console.log(predictions);
|
||||
//
|
||||
// const prediction = await loadCocoSsd(image);
|
||||
// console.log(prediction);
|
||||
|
||||
const model = await loadModel('/tfjs/anime_classifier/model.json');
|
||||
|
||||
// 进行预测
|
||||
const output = await predictImage(model, image);
|
||||
console.log(output);
|
||||
|
||||
// console.log('Predicted Class:', predictedClass);
|
||||
return true;
|
||||
async function openUploadDrawerFn() {
|
||||
openUploadDrawer.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义上传请求
|
||||
* @param file
|
||||
* 清除预测结果
|
||||
*/
|
||||
async function customUploadRequest(file: any) {
|
||||
|
||||
const progress = {percent: 1};
|
||||
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
if (progress.percent < 100) {
|
||||
progress.percent++;
|
||||
file.onProgress(progress);
|
||||
} else {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
}, 100);
|
||||
file.onSuccess(true);
|
||||
|
||||
// file.onSuccess = () => {
|
||||
// message.success(i18n.global.t('comment.uploadSuccess'));
|
||||
// };
|
||||
// file.onError = () => {
|
||||
// message.error(i18n.global.t('comment.uploadError'));
|
||||
// };
|
||||
// return Promise.resolve(file);
|
||||
|
||||
function clearPredictResult() {
|
||||
predictResult.isAnime = false;
|
||||
predictResult.hasFace = false;
|
||||
predictResult.objectArray = [];
|
||||
predictResult.landscape = undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
openUploadDrawer,
|
||||
beforeUpload,
|
||||
customUploadRequest
|
||||
predictResult,
|
||||
openUploadDrawerFn,
|
||||
clearPredictResult
|
||||
};
|
||||
},
|
||||
{
|
||||
|
@@ -5,8 +5,8 @@ import i18n from "@/locales";
|
||||
import {NSFWJS} from "nsfwjs";
|
||||
import localForage from "localforage";
|
||||
import {message} from "ant-design-vue";
|
||||
import Img from "@/workers/image.ts";
|
||||
import Module from "@/workers/imghelper.ts";
|
||||
import Img from "@/workers/upscale/image.ts";
|
||||
import Module from "@/workers/upscale/imghelper.ts";
|
||||
|
||||
|
||||
export const useUpscaleStore = defineStore(
|
||||
|
19
src/utils/imageUtils/imageElementToUint8Array.ts
Normal file
19
src/utils/imageUtils/imageElementToUint8Array.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as tf from '@tensorflow/tfjs';
|
||||
|
||||
export async function imageToUint8Array(imageElement) {
|
||||
// 创建一个 TensorFlow.js 图像张量
|
||||
const tensor = tf.browser.fromPixels(imageElement, 3).toFloat();
|
||||
|
||||
// 获取图像的宽度和高度
|
||||
const width = imageElement.width;
|
||||
const height = imageElement.height;
|
||||
|
||||
// 将张量转为 Uint8Array(RGB 格式,值范围从 0 到 255)
|
||||
const uint8Array = await tensor.data();
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
uint8Array
|
||||
};
|
||||
}
|
10
src/utils/imageUtils/imgToBase64.ts
Normal file
10
src/utils/imageUtils/imgToBase64.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export function imageToBase64(img) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.drawImage(img, 0, 0);
|
||||
}
|
||||
return canvas.toDataURL('image/png'); // 或者 'image/jpeg'
|
||||
}
|
@@ -2,21 +2,23 @@ import * as nsfwjs from "nsfwjs";
|
||||
import {NSFWJS} from "nsfwjs";
|
||||
import * as tf from "@tensorflow/tfjs";
|
||||
|
||||
/**
|
||||
* Initializes the NSFWJS model and returns it.
|
||||
*/
|
||||
let isInit: boolean = false;
|
||||
|
||||
const initNSFWJs = async (): Promise<NSFWJS> => {
|
||||
tf.enableProdMode();
|
||||
if (!isInit) {
|
||||
const initialLoad: nsfwjs.NSFWJS = await nsfwjs.load("/nsfw/mobilenet_v2_mid/", {
|
||||
size: 224,
|
||||
type: "graph"
|
||||
});
|
||||
await initialLoad.model.save("indexeddb://nsfwjs-model");
|
||||
isInit = true;
|
||||
let nsfwModelCache: NSFWJS | null = null; // 缓存模型实例
|
||||
// 如果模型已经加载,则直接返回缓存
|
||||
try {
|
||||
// 首先尝试从 IndexedDB 加载模型
|
||||
nsfwModelCache = await nsfwjs.load("indexeddb://nsfwjs-model", {size: 224, type: "graph"});
|
||||
console.log("NSFWJS 模型成功从 IndexedDB 加载");
|
||||
} catch (_error) {
|
||||
console.warn("IndexedDB 中未找到模型,正在从网络加载...");
|
||||
// 如果 IndexedDB 加载失败,从 URL 加载模型并保存到 IndexedDB
|
||||
nsfwModelCache = await nsfwjs.load("/nsfw/mobilenet_v2_mid/", {size: 224, type: "graph"});
|
||||
await nsfwModelCache.model.save("indexeddb://nsfwjs-model");
|
||||
console.log("NSFWJS 模型已从网络加载并保存到 IndexedDB");
|
||||
}
|
||||
return await nsfwjs.load("indexeddb://nsfwjs-model", {size: 224, type: "graph"});
|
||||
|
||||
return nsfwModelCache;
|
||||
};
|
||||
/**
|
||||
* Predicts the NSFW score of an image using the NSFWJS model.
|
||||
|
16
src/utils/tfjs/BBT_face_similarity.ts
Normal file
16
src/utils/tfjs/BBT_face_similarity.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import * as faceapi from '@vladmandic/face-api/dist/face-api.esm-nobundle.js';
|
||||
|
||||
export async function loadModel() {
|
||||
const modelsPath = `/tfjs/face_api/model`;
|
||||
// 面部识别模型
|
||||
await faceapi.nets.faceRecognitionNet.load(modelsPath);
|
||||
}
|
||||
|
||||
export async function faceSimilarity(img1: HTMLImageElement, img2: HTMLImageElement) {
|
||||
const descriptor1 = await faceapi.computeFaceDescriptor(img1);
|
||||
const descriptor2 = await faceapi.computeFaceDescriptor(img2);
|
||||
if (descriptor1 instanceof Float32Array && descriptor2 instanceof Float32Array) {
|
||||
return faceapi.euclideanDistance(descriptor1, descriptor2).toFixed(2);
|
||||
}
|
||||
return -1;
|
||||
}
|
@@ -1,23 +1,68 @@
|
||||
import * as tf from '@tensorflow/tfjs';
|
||||
import '@tensorflow/tfjs-backend-webgl';
|
||||
|
||||
// 封装处理图像和推理的工具函数
|
||||
export async function loadModel(modelPath) {
|
||||
const model = await tf.loadGraphModel(modelPath);
|
||||
console.log('Model Loaded');
|
||||
async function loadModelFromIndexedDBOrUrl(modelName: string, modelUrl: string) {
|
||||
let model: tf.GraphModel;
|
||||
tf.setBackend('webgl');
|
||||
try {
|
||||
// 尝试从 IndexedDB 加载模型
|
||||
model = await tf.loadGraphModel(`indexeddb://${modelName}-model`);
|
||||
console.log("模型成功从 IndexedDB 加载");
|
||||
} catch (_error) {
|
||||
console.log("从 URL 下载模型...");
|
||||
// 如果 IndexedDB 中没有模型,则从 URL 加载并保存到 IndexedDB
|
||||
model = await tf.loadGraphModel(modelUrl);
|
||||
await model.save(`indexeddb://${modelName}-model`);
|
||||
console.log("模型已从 URL 下载并保存到 IndexedDB");
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
// 封装处理图像和推理的工具函数
|
||||
export async function loadAnimeClassifierModel() {
|
||||
const modelName = 'anime_classifier';
|
||||
const modelUrl = '/tfjs/anime_classifier/model.json';
|
||||
return await loadModelFromIndexedDBOrUrl(modelName, modelUrl);
|
||||
}
|
||||
|
||||
// 处理图片并进行推理
|
||||
export async function predictImage(model, imageElement) {
|
||||
export async function animePredictImage(imageElement) {
|
||||
|
||||
const model: tf.GraphModel = await loadAnimeClassifierModel();
|
||||
// 将图片转换为张量
|
||||
const tensor = tf.browser.fromPixels(imageElement).toFloat();
|
||||
const tensor = tf.browser.fromPixels(imageElement, 3).toFloat();
|
||||
const resized = tf.image.resizeBilinear(tensor, [224, 224]); // 调整图片大小为模型输入大小
|
||||
const input = resized.expandDims(0); // 增加批次维度
|
||||
|
||||
// 进行推理
|
||||
const prediction = model.predict(input);
|
||||
const prediction: any = model.predict(input);
|
||||
|
||||
// 获取预测结果并返回
|
||||
const resultArray = await prediction.array();
|
||||
return resultArray[0]; // 返回第一项的预测结果
|
||||
const result = resultArray[0]; // 获取预测结果数组
|
||||
return result.indexOf(1) === 0 ? 'Anime' : 'Neutral';
|
||||
}
|
||||
|
||||
// export async function animePredictImage(width: number, height: number, uint8Array: Uint8Array) {
|
||||
// const model: tf.GraphModel = await loadModel();
|
||||
//
|
||||
// // 将 Uint8Array 转换为 Tensor
|
||||
// const tensor = tf.tensor3d(uint8Array, [height, width, 3], 'int32').toFloat();
|
||||
//
|
||||
// // 调整图片大小为模型输入大小
|
||||
// const resized = tf.image.resizeBilinear(tensor, [224, 224]);
|
||||
//
|
||||
// // 增加批次维度
|
||||
// const input = resized.expandDims(0);
|
||||
//
|
||||
// // 进行推理
|
||||
// const prediction: any = model.predict(input);
|
||||
//
|
||||
// // 获取预测结果并返回
|
||||
// const resultArray = await prediction.array();
|
||||
// const result = resultArray[0]; // 获取预测结果数组
|
||||
// return result.indexOf(1) === 0 ? 'Anime' : 'Neutral';
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
65
src/utils/tfjs/anime_classifier_pro.ts
Normal file
65
src/utils/tfjs/anime_classifier_pro.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import * as tf from '@tensorflow/tfjs';
|
||||
import '@tensorflow/tfjs-backend-webgl';
|
||||
|
||||
async function loadModelFromIndexedDBOrUrl(modelName: string, modelUrl: string) {
|
||||
let model: tf.LayersModel;
|
||||
tf.setBackend('webgl');
|
||||
try {
|
||||
// 尝试从 IndexedDB 加载模型
|
||||
model = await tf.loadLayersModel(`indexeddb://${modelName}-model`);
|
||||
console.log("模型成功从 IndexedDB 加载");
|
||||
} catch (_error) {
|
||||
console.log("从 URL 下载模型...");
|
||||
// 如果 IndexedDB 中没有模型,则从 URL 加载并保存到 IndexedDB
|
||||
model = await tf.loadLayersModel(modelUrl);
|
||||
await model.save(`indexeddb://${modelName}-model`);
|
||||
console.log("模型已从 URL 下载并保存到 IndexedDB");
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
// 封装处理图像和推理的工具函数
|
||||
export async function loadAnimeClassifierProModel() {
|
||||
const modelName = 'anime_classifier2';
|
||||
const modelUrl = '/tfjs/anime_classifier2/model.json';
|
||||
return await loadModelFromIndexedDBOrUrl(modelName, modelUrl);
|
||||
}
|
||||
|
||||
// 处理图片并进行推理
|
||||
export async function animePredictImagePro(imageElement) {
|
||||
|
||||
const model: any = await loadAnimeClassifierProModel();
|
||||
// 将图片转换为张量
|
||||
const tensor = tf.browser.fromPixels(imageElement).toFloat();
|
||||
const imageResized = tf.image.resizeBilinear(tensor, [224, 224]);
|
||||
const imageReshaped = imageResized.reshape([1, 224, 224, 3]);
|
||||
const imageNormalized = imageReshaped.div(255);
|
||||
|
||||
// 进行推理
|
||||
const prediction: any = model.predict(imageNormalized);
|
||||
|
||||
|
||||
const predictedClass = tf.argMax(prediction, 1).dataSync()[0];
|
||||
// const predictedClassConfidence = await prediction.dataSync()[predictedClass].toFixed(2);
|
||||
// console.log(`预测结果: ${predictedClassName}(${predictedClassConfidence})`);
|
||||
return ['Anime', 'Furry', 'Neutral'][predictedClass];
|
||||
}
|
||||
|
||||
// export async function animePredictImagePro(width: number, height: number, uint8Array: Uint8Array) {
|
||||
//
|
||||
// const model: any = await loadModel();
|
||||
// // 将图片转换为张量
|
||||
// const tensor = tf.tensor3d(uint8Array, [height, width, 3], 'int32').toFloat();
|
||||
// const imageResized = tf.image.resizeBilinear(tensor, [224, 224]);
|
||||
// const imageReshaped = imageResized.reshape([1, 224, 224, 3]);
|
||||
// const imageNormalized = imageReshaped.div(255);
|
||||
//
|
||||
// // 进行推理
|
||||
// const prediction: any = model.predict(imageNormalized);
|
||||
//
|
||||
//
|
||||
// const predictedClass = tf.argMax(prediction, 1).dataSync()[0];
|
||||
// // const predictedClassConfidence = await prediction.dataSync()[predictedClass].toFixed(2);
|
||||
// // console.log(`预测结果: ${predictedClassName}(${predictedClassConfidence})`);
|
||||
// return ['Anime', 'Furry', 'Neutral'][predictedClass];
|
||||
// }
|
42
src/utils/tfjs/face_detection.ts
Normal file
42
src/utils/tfjs/face_detection.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import '@mediapipe/face_detection';
|
||||
import '@tensorflow/tfjs-core';
|
||||
// Register WebGL backend.
|
||||
import '@tensorflow/tfjs-backend-webgl';
|
||||
import * as faceDetection from '@tensorflow-models/face-detection';
|
||||
import * as faceLandmarksDetection from '@tensorflow-models/face-landmarks-detection';
|
||||
import '@mediapipe/face_mesh';
|
||||
|
||||
/**
|
||||
* 检测人脸
|
||||
* @param image
|
||||
*/
|
||||
export async function detectFaces(image: HTMLImageElement) {
|
||||
const model = faceDetection.SupportedModels.MediaPipeFaceDetector;
|
||||
const detectorConfig: any = {
|
||||
runtime: 'tfjs',
|
||||
maxFaces: 1,
|
||||
modelType: 'short', //'short'|'full'
|
||||
};
|
||||
const detector = await faceDetection.createDetector(model, detectorConfig);
|
||||
const estimationConfig = {flipHorizontal: false};
|
||||
const faces = await detector.estimateFaces(image, estimationConfig);
|
||||
return faces;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测人脸特征点
|
||||
* @param image
|
||||
*/
|
||||
export async function detectionFaceLandmarks(image: HTMLImageElement) {
|
||||
const model = faceLandmarksDetection.SupportedModels.MediaPipeFaceMesh;
|
||||
const detectorConfig: any = {
|
||||
runtime: 'tfjs',
|
||||
maxFaces: 1,
|
||||
refineLandmarks: false,
|
||||
};
|
||||
const detector = await faceLandmarksDetection.createDetector(model, detectorConfig);
|
||||
const estimationConfig = {flipHorizontal: false};
|
||||
const faces = await detector.estimateFaces(image, estimationConfig);
|
||||
return faces;
|
||||
|
||||
}
|
28
src/utils/tfjs/face_extraction.ts
Normal file
28
src/utils/tfjs/face_extraction.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import * as faceapi from '@vladmandic/face-api/dist/face-api.esm-nobundle.js';
|
||||
import '@tensorflow/tfjs-backend-webgl';
|
||||
import * as tf from '@tensorflow/tfjs';
|
||||
|
||||
export async function loadFaceExtractorModel() {
|
||||
tf.setBackend('webgl'); // set webgl backend
|
||||
// 模型文件访问路径
|
||||
const modelsPath = `/tfjs/face_api/model/ssd_mobilenetv1_model-weights_manifest.json`;
|
||||
// 模型参数-ssdMobilenetv1
|
||||
await faceapi.nets.ssdMobilenetv1.load(modelsPath);
|
||||
return new faceapi.SsdMobilenetv1Options({
|
||||
minConfidence: 0.5, // 0 ~ 1
|
||||
maxResults: 50, // 0 ~ 100
|
||||
});
|
||||
}
|
||||
|
||||
export async function fnDetectFace(img: HTMLImageElement) {
|
||||
const options = await loadFaceExtractorModel();
|
||||
const detections = await faceapi.detectSingleFace(
|
||||
img,
|
||||
options,
|
||||
);
|
||||
if (!detections) {
|
||||
return null;
|
||||
}
|
||||
const faceImages = await faceapi.extractFaces(img, [detections]);
|
||||
return faceImages[0].toDataURL('image/png');
|
||||
}
|
54
src/utils/tfjs/landscape_recognition.ts
Normal file
54
src/utils/tfjs/landscape_recognition.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import * as tf from '@tensorflow/tfjs';
|
||||
import '@tensorflow/tfjs-backend-webgl';
|
||||
|
||||
export async function loadLandscapeRecognitionModel() {
|
||||
const modelName = 'landscape_recognition';
|
||||
const modelUrl = '/tfjs/landscape_recognition/model.json';
|
||||
let model: tf.LayersModel;
|
||||
tf.setBackend('webgl');
|
||||
try {
|
||||
// 尝试从 IndexedDB 加载模型
|
||||
model = await tf.loadLayersModel(`indexeddb://${modelName}-model`);
|
||||
console.log("模型成功从 IndexedDB 加载");
|
||||
} catch (_error) {
|
||||
console.log("从 URL 下载模型...");
|
||||
// 如果 IndexedDB 中没有模型,则从 URL 加载模型
|
||||
model = await tf.loadLayersModel(modelUrl);
|
||||
await model.save(`indexeddb://${modelName}-model`);
|
||||
console.log("模型已从 URL 下载并保存到 IndexedDB");
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
export const predictLandscape = async (imgElement) => {
|
||||
if (!imgElement) return;
|
||||
const model = await loadLandscapeRecognitionModel();
|
||||
const img = tf.cast(tf.browser.fromPixels(imgElement), 'float32').resizeBilinear([150, 150]);
|
||||
|
||||
const offset = tf.scalar(127.5);
|
||||
const normalized = img.sub(offset).div(offset);
|
||||
const batched = normalized.reshape([1, 150, 150, 3]);
|
||||
|
||||
const results: any = model.predict(batched);
|
||||
return getCategory(results.dataSync().indexOf(results.max().dataSync()[0]));
|
||||
};
|
||||
|
||||
const getCategory = (index: number) => {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return "building";
|
||||
case 1:
|
||||
return "forest";
|
||||
case 2:
|
||||
return "glacier";
|
||||
case 3:
|
||||
return "mountain";
|
||||
case 4:
|
||||
return "sea";
|
||||
case 5:
|
||||
return "street";
|
||||
default:
|
||||
return "none";
|
||||
}
|
||||
};
|
@@ -1,6 +1,7 @@
|
||||
import * as tf from '@tensorflow/tfjs';
|
||||
import * as mobilenet from '@tensorflow-models/mobilenet';
|
||||
import * as cocoSsd from '@tensorflow-models/coco-ssd';
|
||||
import '@tensorflow/tfjs-backend-webgl';
|
||||
|
||||
// 确保 TensorFlow.js 已准备好并设置后端
|
||||
async function initializeTensorFlow(backend = "webgl") {
|
||||
@@ -49,36 +50,43 @@ export async function loadMobileNet(image) {
|
||||
return await model.classify(image, 3);
|
||||
}
|
||||
|
||||
// 加载 COCO SSD 模型的工具函数
|
||||
export async function loadCocoSsd(image) {
|
||||
// 工具函数:加载或缓存模型
|
||||
export async function loadCocoSsdModel() {
|
||||
const modelName = "cocoSsd-model";
|
||||
const modelUrl = '/tfjs/mobilenet/ssd-mobilenet-v2-tfjs-default-v1/model.json';
|
||||
|
||||
// 初始化 TensorFlow.js
|
||||
if (!(await initializeTensorFlow())) {
|
||||
return;
|
||||
}
|
||||
|
||||
let model;
|
||||
|
||||
try {
|
||||
// 尝试从 IndexedDB 加载模型
|
||||
model = await cocoSsd.load({
|
||||
base: 'mobilenet_v2',
|
||||
modelUrl: `indexeddb://${modelName}`,
|
||||
});
|
||||
console.log("COCO SSD model loaded from IndexedDB successfully");
|
||||
console.log(`${modelName} loaded from IndexedDB successfully`);
|
||||
} catch (_error) {
|
||||
console.log("Downloading COCO SSD model...");
|
||||
console.log(`Downloading ${modelName}...`);
|
||||
// 如果 IndexedDB 中没有模型则从 URL 加载并保存到 IndexedDB
|
||||
model = await cocoSsd.load({
|
||||
base: 'mobilenet_v2',
|
||||
modelUrl: modelUrl,
|
||||
});
|
||||
const Model = await tf.loadGraphModel(modelUrl);
|
||||
await Model.save(`indexeddb://${modelName}`);
|
||||
console.log("COCO SSD model downloaded and saved to IndexedDB");
|
||||
const graphModel = await tf.loadGraphModel(modelUrl);
|
||||
await graphModel.save(`indexeddb://${modelName}`);
|
||||
console.log(`${modelName} downloaded and saved to IndexedDB`);
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
// 加载 COCO SSD 模型的工具函数
|
||||
// 使用提取的加载模型工具函数
|
||||
export async function cocoSsdPredict(image) {
|
||||
// 初始化 TensorFlow.js
|
||||
tf.setBackend('webgl');
|
||||
if (!(await initializeTensorFlow())) {
|
||||
return [];
|
||||
}
|
||||
// 加载模型
|
||||
const model = await loadCocoSsdModel();
|
||||
// 使用模型进行检测
|
||||
return await model.detect(image);
|
||||
}
|
||||
|
@@ -1,11 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
@@ -1,122 +1,11 @@
|
||||
<template>
|
||||
<div class="location-album">
|
||||
<div class="location-album-header">
|
||||
<AButton type="link" size="large" class="location-album-button">地点</AButton>
|
||||
<span class="location-album-count">你一共在2个地点留下足迹</span>
|
||||
</div>
|
||||
<div class="location-album-content">
|
||||
<div class="location-album-container" @click="handleClick">
|
||||
<img class="background-image" src="/test/5.png" alt=""/>
|
||||
<div class="overlay">
|
||||
<span>乌鲁木齐市</span>
|
||||
<span class="location-album-overlay-count">---</span>
|
||||
<span class="location-album-overlay-count">16张照片</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
function handleClick() {
|
||||
router.push({ path: route.path + '/1' });
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.location-album {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.location-album-header {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid #e2e2e2;
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
|
||||
.location-album-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
}
|
||||
<style scoped lang="less">
|
||||
|
||||
.location-album-count {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.location-album-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding-top: 20px;
|
||||
gap: 20px;
|
||||
|
||||
.location-album-container {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
background-color: #f5f5f5;
|
||||
|
||||
.background-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.2); /* 黑色半透明 */
|
||||
backdrop-filter: blur(2px); /* 背景虚化 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.3s ease, backdrop-filter 0.3s ease;
|
||||
gap: 0;
|
||||
|
||||
.location-album-overlay-count {
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1); /* 黑色半透明 */
|
||||
backdrop-filter: blur(0px); /* 背景虚化 */
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.location-album-container:hover {
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
122
src/views/Album/LocationAlbum/LocationAlbumList.vue
Normal file
122
src/views/Album/LocationAlbum/LocationAlbumList.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="location-album">
|
||||
<div class="location-album-header">
|
||||
<AButton type="link" size="large" class="location-album-button">地点</AButton>
|
||||
<span class="location-album-count">你一共在2个地点留下足迹</span>
|
||||
</div>
|
||||
<div class="location-album-content">
|
||||
<div class="location-album-container" @click="handleClick">
|
||||
<img class="background-image" src="/test/5.png" alt=""/>
|
||||
<div class="overlay">
|
||||
<span>乌鲁木齐市</span>
|
||||
<span class="location-album-overlay-count">---</span>
|
||||
<span class="location-album-overlay-count">16张照片</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
function handleClick() {
|
||||
router.push({ path: route.path + '/1' });
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.location-album {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.location-album-header {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid #e2e2e2;
|
||||
|
||||
.location-album-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.location-album-count {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.location-album-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding-top: 20px;
|
||||
gap: 20px;
|
||||
|
||||
.location-album-container {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
background-color: #f5f5f5;
|
||||
|
||||
.background-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.2); /* 黑色半透明 */
|
||||
backdrop-filter: blur(2px); /* 背景虚化 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.3s ease, backdrop-filter 0.3s ease;
|
||||
gap: 0;
|
||||
|
||||
.location-album-overlay-count {
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1); /* 黑色半透明 */
|
||||
backdrop-filter: blur(0px); /* 背景虚化 */
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.location-album-container:hover {
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,11 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
@@ -1,170 +1,11 @@
|
||||
<template>
|
||||
<div class="people-album">
|
||||
<div class="people-album-header">
|
||||
<ADropdown>
|
||||
<AButton type="text" size="large" class="people-album-button">
|
||||
人物
|
||||
<DownOutlined class="people-album-icon"/>
|
||||
</AButton>
|
||||
<template #overlay>
|
||||
<AMenu>
|
||||
<AMenuItem>人 物</AMenuItem>
|
||||
<AMenuItem>已隐藏</AMenuItem>
|
||||
</AMenu>
|
||||
</template>
|
||||
|
||||
</ADropdown>
|
||||
</div>
|
||||
<div class="people-album-content">
|
||||
<div class="people-album-item" @mouseover="showButton = true" @mouseleave="showButton = false">
|
||||
<div class="people-album-item-avatar">
|
||||
<AAvatar :size="86" shape="circle" src="/test/4.png"/>
|
||||
</div>
|
||||
<div class="people-album-item-name">
|
||||
<AButton @click="showAddNameInput" class="people-album-add-name" v-show="showButton && !showInput" type="link"
|
||||
size="small">
|
||||
添加名字
|
||||
</AButton>
|
||||
<AInput v-show="showInput" @blur="hideAddNameInput" size="small" class="people-album-add-input">
|
||||
<template #suffix>
|
||||
<AButton type="link" size="small">完成</AButton>
|
||||
</template>
|
||||
</AInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="people-album-item" @mouseover="showButton = true" @mouseleave="showButton = false">
|
||||
<div class="people-album-item-avatar">
|
||||
<AAvatar :size="86" shape="circle" src="/test/4.png"/>
|
||||
</div>
|
||||
<div class="people-album-item-name">
|
||||
<AButton @click="showAddNameInput" class="people-album-add-name" v-show="showButton && !showInput" type="link"
|
||||
size="small">
|
||||
添加名字
|
||||
</AButton>
|
||||
<AInput v-show="showInput" @blur="hideAddNameInput" size="small" class="people-album-add-input">
|
||||
<template #suffix>
|
||||
<AButton type="link" size="small">完成</AButton>
|
||||
</template>
|
||||
</AInput>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
const showButton = ref(false);
|
||||
const showInput = ref(false);
|
||||
|
||||
function showAddNameInput() {
|
||||
showInput.value = true;
|
||||
showButton.value = false;
|
||||
}
|
||||
|
||||
function hideAddNameInput() {
|
||||
showInput.value = false;
|
||||
showButton.value = false;
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.people-album {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.people-album-header {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid #e2e2e2;
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
|
||||
.people-album-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
<style scoped lang="less">
|
||||
|
||||
.people-album-icon {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.people-album-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
padding-top: 20px;
|
||||
padding-left: 20px;
|
||||
gap: 20px;
|
||||
|
||||
.people-album-item {
|
||||
width: 130px;
|
||||
height: 160px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s ease-in-out;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
.people-album-item-avatar {
|
||||
width: 100%;
|
||||
height: 75%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.people-album-item-name {
|
||||
width: 100%;
|
||||
height: 25%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
.people-album-add-input {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.people-album-add-name {
|
||||
color: rgba(126, 126, 135, 0.99);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.people-album-add-name:hover {
|
||||
color: #0e87cc;
|
||||
}
|
||||
}
|
||||
|
||||
.people-album-item:hover {
|
||||
background-color: rgba(248, 248, 248, 0.74);
|
||||
opacity: 1;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
170
src/views/Album/PeopleAlbum/PeopleAlbumList.vue
Normal file
170
src/views/Album/PeopleAlbum/PeopleAlbumList.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<div class="people-album">
|
||||
<div class="people-album-header">
|
||||
<ADropdown>
|
||||
<AButton type="text" size="large" class="people-album-button">
|
||||
人物
|
||||
<DownOutlined class="people-album-icon"/>
|
||||
</AButton>
|
||||
<template #overlay>
|
||||
<AMenu>
|
||||
<AMenuItem>人 物</AMenuItem>
|
||||
<AMenuItem>已隐藏</AMenuItem>
|
||||
</AMenu>
|
||||
</template>
|
||||
|
||||
</ADropdown>
|
||||
</div>
|
||||
<div class="people-album-content">
|
||||
<div class="people-album-item" @mouseover="showButton = true" @mouseleave="showButton = false">
|
||||
<div class="people-album-item-avatar">
|
||||
<AAvatar :size="86" shape="circle" src="/test/4.png"/>
|
||||
</div>
|
||||
<div class="people-album-item-name">
|
||||
<AButton @click="showAddNameInput" class="people-album-add-name" v-show="showButton && !showInput" type="link"
|
||||
size="small">
|
||||
添加名字
|
||||
</AButton>
|
||||
<AInput v-show="showInput" @blur="hideAddNameInput" size="small" class="people-album-add-input">
|
||||
<template #suffix>
|
||||
<AButton type="link" size="small">完成</AButton>
|
||||
</template>
|
||||
</AInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="people-album-item" @mouseover="showButton = true" @mouseleave="showButton = false">
|
||||
<div class="people-album-item-avatar">
|
||||
<AAvatar :size="86" shape="circle" src="/test/4.png"/>
|
||||
</div>
|
||||
<div class="people-album-item-name">
|
||||
<AButton @click="showAddNameInput" class="people-album-add-name" v-show="showButton && !showInput" type="link"
|
||||
size="small">
|
||||
添加名字
|
||||
</AButton>
|
||||
<AInput v-show="showInput" @blur="hideAddNameInput" size="small" class="people-album-add-input">
|
||||
<template #suffix>
|
||||
<AButton type="link" size="small">完成</AButton>
|
||||
</template>
|
||||
</AInput>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
const showButton = ref(false);
|
||||
const showInput = ref(false);
|
||||
|
||||
function showAddNameInput() {
|
||||
showInput.value = true;
|
||||
showButton.value = false;
|
||||
}
|
||||
|
||||
function hideAddNameInput() {
|
||||
showInput.value = false;
|
||||
showButton.value = false;
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.people-album {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.people-album-header {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid #e2e2e2;
|
||||
|
||||
.people-album-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
|
||||
.people-album-icon {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.people-album-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
padding-top: 20px;
|
||||
padding-left: 20px;
|
||||
gap: 20px;
|
||||
|
||||
.people-album-item {
|
||||
width: 130px;
|
||||
height: 160px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s ease-in-out;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
.people-album-item-avatar {
|
||||
width: 100%;
|
||||
height: 75%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.people-album-item-name {
|
||||
width: 100%;
|
||||
height: 25%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
.people-album-add-input {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.people-album-add-name {
|
||||
color: rgba(126, 126, 135, 0.99);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.people-album-add-name:hover {
|
||||
color: #0e87cc;
|
||||
}
|
||||
}
|
||||
|
||||
.people-album-item:hover {
|
||||
background-color: rgba(248, 248, 248, 0.74);
|
||||
opacity: 1;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,9 +0,0 @@
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
@@ -1,180 +1,9 @@
|
||||
<template>
|
||||
<div class="phoalbum">
|
||||
<div class="phoalbum-header">
|
||||
<AButton type="primary" shape="round" size="middle">
|
||||
<template #icon>
|
||||
<PlusSquareOutlined/>
|
||||
</template>
|
||||
创建相册
|
||||
</AButton>
|
||||
<ADropdown>
|
||||
<AButton type="default" shape="round" size="middle">
|
||||
<template #icon>
|
||||
<OrderedListOutlined/>
|
||||
</template>
|
||||
排序
|
||||
</AButton>
|
||||
<template #overlay>
|
||||
<AMenu>
|
||||
<AMenuItem key="1">按时间排序</AMenuItem>
|
||||
<AMenuItem key="2">按名称排序</AMenuItem>
|
||||
</AMenu>
|
||||
</template>
|
||||
</ADropdown>
|
||||
<AInput class="phoalbum-search" placeholder="搜索相册">
|
||||
<template #suffix>
|
||||
<AButton size="small" type="text" shape="circle" @click.prevent>
|
||||
<template #icon>
|
||||
<SearchOutlined/>
|
||||
</template>
|
||||
</AButton>
|
||||
</template>
|
||||
</AInput>
|
||||
</div>
|
||||
<div class="phoalbum-content">
|
||||
<ATabs size="small" :tabBarGutter="50" type="line" tabPosition="top" :tabBarStyle="{position:'unset'}"
|
||||
style="width: 100%;">
|
||||
<template #rightExtra>
|
||||
<span style="color: #999; font-size: 12px;">已全部加载,共 0 个相册</span>
|
||||
</template>
|
||||
<ATabPane key="1" tab="全部相册">
|
||||
<div class="phoalbum-item-container">
|
||||
<div class="phoalbum-item" @mouseover="isHovered = true" @mouseleave="isHovered = false">
|
||||
<PhotoStack :src="'/test/1.png'" default-src=""/>
|
||||
<div class="phoalbum-item-info">
|
||||
<span class="phoalbum-item-name">我的相册</span>
|
||||
<span class="phoalbum-item-date">2022-01-01</span>
|
||||
</div>
|
||||
<div class="phoalbum-item-operation" :class="{ 'fade-in': isHovered, 'fade-out': !isHovered }">
|
||||
<ADropdown trigger="click">
|
||||
<AButton type="text" shape="circle" size="small">
|
||||
<template #icon>
|
||||
<AAvatar shape="circle" size="small" :src="more"/>
|
||||
</template>
|
||||
</AButton>
|
||||
<template #overlay>
|
||||
<AMenu>
|
||||
<AMenuItem key="1">重命名相册</AMenuItem>
|
||||
<AMenuItem key="2">分享相册</AMenuItem>
|
||||
<AMenuItem key="3">删除相册</AMenuItem>
|
||||
<AMenuItem key="4">下载相册</AMenuItem>
|
||||
</AMenu>
|
||||
</template>
|
||||
</ADropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ATabPane>
|
||||
<ATabPane key="2" tab="我的相册">
|
||||
|
||||
</ATabPane>
|
||||
<ATabPane key="3" tab="收藏相册">
|
||||
</ATabPane>
|
||||
</ATabs>
|
||||
</div>
|
||||
</div>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import more from "@/assets/svgs/more.svg";
|
||||
|
||||
const isHovered = ref<boolean>(false);
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.phoalbum {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
<style scoped lang="less">
|
||||
|
||||
.phoalbum-header {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid #e2e2e2;
|
||||
|
||||
.phoalbum-search {
|
||||
width: 300px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.phoalbum-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
height: calc(100% - 65px);
|
||||
|
||||
.phoalbum-item-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
.phoalbum-item {
|
||||
width: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
gap: 15px;
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
.phoalbum-item-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
|
||||
.phoalbum-item-name {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.phoalbum-item-date {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.phoalbum-item-operation {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 10;
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
opacity: 1; /* 显示时透明度为1 */
|
||||
transform: scale(1); /* 显示时缩放为1 */
|
||||
z-index: 10; /* 显示时z-index为10 */
|
||||
}
|
||||
|
||||
.fade-out {
|
||||
opacity: 0; /* 隐藏时透明度为0 */
|
||||
transform: scale(0); /* 隐藏时缩放为0 */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
180
src/views/Album/Phoalbum/PhoalbumList.vue
Normal file
180
src/views/Album/Phoalbum/PhoalbumList.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<div class="phoalbum">
|
||||
<div class="phoalbum-header">
|
||||
<AButton type="primary" shape="round" size="middle">
|
||||
<template #icon>
|
||||
<PlusSquareOutlined/>
|
||||
</template>
|
||||
创建相册
|
||||
</AButton>
|
||||
<ADropdown>
|
||||
<AButton type="default" shape="round" size="middle">
|
||||
<template #icon>
|
||||
<OrderedListOutlined/>
|
||||
</template>
|
||||
排序
|
||||
</AButton>
|
||||
<template #overlay>
|
||||
<AMenu>
|
||||
<AMenuItem key="1">按时间排序</AMenuItem>
|
||||
<AMenuItem key="2">按名称排序</AMenuItem>
|
||||
</AMenu>
|
||||
</template>
|
||||
</ADropdown>
|
||||
<AInput class="phoalbum-search" placeholder="搜索相册">
|
||||
<template #suffix>
|
||||
<AButton size="small" type="text" shape="circle" @click.prevent>
|
||||
<template #icon>
|
||||
<SearchOutlined/>
|
||||
</template>
|
||||
</AButton>
|
||||
</template>
|
||||
</AInput>
|
||||
</div>
|
||||
<div class="phoalbum-content">
|
||||
<ATabs size="small" :tabBarGutter="50" type="line" tabPosition="top" :tabBarStyle="{position:'unset'}"
|
||||
style="width: 100%;">
|
||||
<template #rightExtra>
|
||||
<span style="color: #999; font-size: 12px;">已全部加载,共 0 个相册</span>
|
||||
</template>
|
||||
<ATabPane key="1" tab="全部相册">
|
||||
<div class="phoalbum-item-container">
|
||||
<div class="phoalbum-item" @mouseover="isHovered = true" @mouseleave="isHovered = false">
|
||||
<PhotoStack :src="'/test/1.png'" default-src=""/>
|
||||
<div class="phoalbum-item-info">
|
||||
<span class="phoalbum-item-name">我的相册</span>
|
||||
<span class="phoalbum-item-date">2022-01-01</span>
|
||||
</div>
|
||||
<div class="phoalbum-item-operation" :class="{ 'fade-in': isHovered, 'fade-out': !isHovered }">
|
||||
<ADropdown trigger="click">
|
||||
<AButton type="text" shape="circle" size="small">
|
||||
<template #icon>
|
||||
<AAvatar shape="circle" size="small" :src="more"/>
|
||||
</template>
|
||||
</AButton>
|
||||
<template #overlay>
|
||||
<AMenu>
|
||||
<AMenuItem key="1">重命名相册</AMenuItem>
|
||||
<AMenuItem key="2">分享相册</AMenuItem>
|
||||
<AMenuItem key="3">删除相册</AMenuItem>
|
||||
<AMenuItem key="4">下载相册</AMenuItem>
|
||||
</AMenu>
|
||||
</template>
|
||||
</ADropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ATabPane>
|
||||
<ATabPane key="2" tab="我的相册">
|
||||
|
||||
</ATabPane>
|
||||
<ATabPane key="3" tab="收藏相册">
|
||||
</ATabPane>
|
||||
</ATabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import more from "@/assets/svgs/more.svg";
|
||||
|
||||
const isHovered = ref<boolean>(false);
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.phoalbum {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.phoalbum-header {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid #e2e2e2;
|
||||
|
||||
.phoalbum-search {
|
||||
width: 300px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.phoalbum-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
height: calc(100% - 65px);
|
||||
|
||||
.phoalbum-item-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
.phoalbum-item {
|
||||
width: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
gap: 15px;
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
.phoalbum-item-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
|
||||
.phoalbum-item-name {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.phoalbum-item-date {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.phoalbum-item-operation {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 10;
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
opacity: 1; /* 显示时透明度为1 */
|
||||
transform: scale(1); /* 显示时缩放为1 */
|
||||
z-index: 10; /* 显示时z-index为10 */
|
||||
}
|
||||
|
||||
.fade-out {
|
||||
opacity: 0; /* 隐藏时透明度为0 */
|
||||
transform: scale(0); /* 隐藏时缩放为0 */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,11 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
@@ -1,120 +1,11 @@
|
||||
<template>
|
||||
<div class="thing-album">
|
||||
<div class="thing-album-header">
|
||||
|
||||
<AButton type="link" size="large" class="thing-album-button">事物</AButton>
|
||||
|
||||
</div>
|
||||
<div class="thing-album-content">
|
||||
<span class="thing-album-title">动物</span>
|
||||
<div class="thing-album-container">
|
||||
<img class="background-image" src="/test/7.png" alt=""/>
|
||||
<div class="overlay">
|
||||
<span>猫</span>
|
||||
<span class="thing-album-overlay-count">---</span>
|
||||
<span class="thing-album-overlay-count">16张照片</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.thing-album {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.thing-album-header {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid #e2e2e2;
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
|
||||
.thing-album-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
<style scoped lang="less">
|
||||
|
||||
.thing-album-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding-top: 20px;
|
||||
padding-left: 25px;
|
||||
gap: 20px;
|
||||
|
||||
.thing-album-title {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.thing-album-container {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
background-color: #f5f5f5;
|
||||
|
||||
.background-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.2); /* 黑色半透明 */
|
||||
backdrop-filter: blur(2px); /* 背景虚化 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.3s ease, backdrop-filter 0.3s ease;
|
||||
gap: 0;
|
||||
|
||||
.thing-album-overlay-count {
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1); /* 黑色半透明 */
|
||||
backdrop-filter: blur(0px); /* 背景虚化 */
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.thing-album-container:hover {
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
120
src/views/Album/ThingAlbum/ThingAlbumList.vue
Normal file
120
src/views/Album/ThingAlbum/ThingAlbumList.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div class="thing-album">
|
||||
<div class="thing-album-header">
|
||||
|
||||
<AButton type="link" size="large" class="thing-album-button">事物</AButton>
|
||||
|
||||
</div>
|
||||
<div class="thing-album-content">
|
||||
<span class="thing-album-title">动物</span>
|
||||
<div class="thing-album-container">
|
||||
<img class="background-image" src="/test/7.png" alt=""/>
|
||||
<div class="overlay">
|
||||
<span>猫</span>
|
||||
<span class="thing-album-overlay-count">---</span>
|
||||
<span class="thing-album-overlay-count">16张照片</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.thing-album {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.thing-album-header {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid #e2e2e2;
|
||||
|
||||
.thing-album-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.thing-album-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding-top: 20px;
|
||||
padding-left: 25px;
|
||||
gap: 20px;
|
||||
|
||||
.thing-album-title {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.thing-album-container {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
background-color: #f5f5f5;
|
||||
|
||||
.background-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.2); /* 黑色半透明 */
|
||||
backdrop-filter: blur(2px); /* 背景虚化 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.3s ease, backdrop-filter 0.3s ease;
|
||||
gap: 0;
|
||||
|
||||
.thing-album-overlay-count {
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1); /* 黑色半透明 */
|
||||
backdrop-filter: blur(0px); /* 背景虚化 */
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.thing-album-container:hover {
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="all-photo">
|
||||
<div class="photo-header">
|
||||
<AButton type="primary" shape="round" size="middle" @click="upload.openUploadDrawer = true">
|
||||
<AButton type="primary" shape="round" size="middle" @click="upload.openUploadDrawerFn()">
|
||||
<template #icon>
|
||||
<PlusOutlined/>
|
||||
</template>
|
||||
@@ -115,7 +115,7 @@
|
||||
</ATabPane>
|
||||
</ATabs>
|
||||
</div>
|
||||
<Upload/>
|
||||
<ImageUpload/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -124,7 +124,7 @@ import {Waterfall} from 'vue-waterfall-plugin-next';
|
||||
import 'vue-waterfall-plugin-next/dist/style.css';
|
||||
import loading from '@/assets/gif/loading.gif';
|
||||
import error from '@/assets/svgs/no-image.svg';
|
||||
import Upload from "@/views/Photograph/Upload/Upload.vue";
|
||||
import ImageUpload from "@/views/Photograph/ImageUpload/ImageUpload.vue";
|
||||
import useStore from "@/store";
|
||||
|
||||
const selected = ref<(string | number)[]>([]);
|
||||
|
201
src/views/Photograph/ImageUpload/ImageUpload.vue
Normal file
201
src/views/Photograph/ImageUpload/ImageUpload.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<ADrawer v-model:open="upload.openUploadDrawer" width="40%" placement="right" title="上传照片">
|
||||
<template #extra>
|
||||
<AFlex :vertical="false" justify="center" align="center" gap="large">
|
||||
<ASelect size="middle" style="width: 150px">
|
||||
|
||||
</ASelect>
|
||||
<ASelect size="middle" style="width: 150px">
|
||||
|
||||
</ASelect>
|
||||
</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>
|
||||
</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">
|
||||
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";
|
||||
import i18n from "@/locales";
|
||||
|
||||
import {NSFWJS} from "nsfwjs";
|
||||
import {animePredictImage} from "@/utils/tfjs/anime_classifier.ts";
|
||||
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";
|
||||
|
||||
const predicting = ref<boolean>(false);
|
||||
|
||||
|
||||
const upload = useStore().upload;
|
||||
const image: HTMLImageElement = document.createElement('img');
|
||||
|
||||
const fileList = ref([]);
|
||||
|
||||
const progress: UploadProps['progress'] = {
|
||||
strokeColor: {
|
||||
'0%': '#108ee9',
|
||||
'100%': '#87d068',
|
||||
},
|
||||
strokeWidth: 3,
|
||||
format: (percent: any) => `${parseFloat(percent.toFixed(2))}%`,
|
||||
class: 'progress-bar',
|
||||
};
|
||||
|
||||
/**
|
||||
* 图片上传前的校验
|
||||
* @param file
|
||||
*/
|
||||
async function beforeUpload(file: File) {
|
||||
predicting.value = true;
|
||||
image.src = URL.createObjectURL(file);
|
||||
// 图片 NSFW 检测
|
||||
const nsfw: NSFWJS = await initNSFWJs();
|
||||
const isNSFW: boolean = await predictNSFW(nsfw, image);
|
||||
if (isNSFW) {
|
||||
message.error(i18n.global.t('comment.illegalImage'));
|
||||
predicting.value = false;
|
||||
return false;
|
||||
}
|
||||
predicting.value = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义上传请求
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
@@ -1,74 +0,0 @@
|
||||
<template>
|
||||
<ADrawer v-model:open="upload.openUploadDrawer" width="40%" placement="right" title="上传照片">
|
||||
<template #extra>
|
||||
<AFlex :vertical="false" justify="center" align="center" gap="large">
|
||||
<ASelect size="middle" style="width: 150px">
|
||||
|
||||
</ASelect>
|
||||
<ASelect size="middle" style="width: 150px">
|
||||
|
||||
</ASelect>
|
||||
</AFlex>
|
||||
</template>
|
||||
<div>
|
||||
<AUploadDragger
|
||||
v-model:fileList="fileList"
|
||||
accept="image/*"
|
||||
name="file"
|
||||
:directory="false"
|
||||
:multiple="true"
|
||||
@drop="handleDrop"
|
||||
:beforeUpload="upload.beforeUpload"
|
||||
:customRequest="upload.customUploadRequest"
|
||||
:progress="progress"
|
||||
>
|
||||
<p class="ant-upload-drag-icon">
|
||||
<inbox-outlined></inbox-outlined>
|
||||
</p>
|
||||
<p class="ant-upload-text">Click or drag file to this area to upload</p>
|
||||
<p class="ant-upload-hint">
|
||||
Support for a single or bulk upload. Strictly prohibit from uploading company data or other
|
||||
band files
|
||||
</p>
|
||||
</AUploadDragger>
|
||||
</div>
|
||||
</ADrawer>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import useStore from "@/store";
|
||||
import {InboxOutlined} from '@ant-design/icons-vue';
|
||||
import type {UploadProps} from 'ant-design-vue';
|
||||
|
||||
const upload = useStore().upload;
|
||||
|
||||
|
||||
const fileList = ref([]);
|
||||
// const handleChange = (info: UploadChangeParam) => {
|
||||
// const status = info.file.status;
|
||||
// if (status !== 'uploading') {
|
||||
// console.log(info.file, info.fileList);
|
||||
// }
|
||||
// if (status === 'done') {
|
||||
// message.success(`${info.file.name} file uploaded successfully.`);
|
||||
// } else if (status === 'error') {
|
||||
// message.error(`${info.file.name} file upload failed.`);
|
||||
// }
|
||||
// };
|
||||
|
||||
function handleDrop(e: DragEvent) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
const progress: UploadProps['progress'] = {
|
||||
strokeColor: {
|
||||
'0%': '#108ee9',
|
||||
'100%': '#87d068',
|
||||
},
|
||||
strokeWidth: 3,
|
||||
format: (percent: any) => `${parseFloat(percent.toFixed(2))}%`,
|
||||
class: 'progress-bar',
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
@@ -64,7 +64,7 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {message} from "ant-design-vue";
|
||||
import Img from "@/workers/image.ts";
|
||||
import Img from "@/workers/upscale/image.ts";
|
||||
import useStore from "@/store";
|
||||
import run from '@/assets/svgs/run.svg';
|
||||
|
||||
@@ -82,7 +82,7 @@ async function startTask() {
|
||||
if (upscale.input === null) return;
|
||||
upscale.isProcessing = true;
|
||||
const start = Date.now();
|
||||
const worker = new Worker(new URL("@/workers/upscale.worker.ts", import.meta.url), {
|
||||
const worker = new Worker(new URL("@/workers/upscale/upscale.worker.ts", import.meta.url), {
|
||||
type: "module",
|
||||
});
|
||||
worker.onmessage = (e: MessageEvent<any>) => {
|
||||
|
11
src/workers/tfjs/tfjs.worker.ts
Normal file
11
src/workers/tfjs/tfjs.worker.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
self.onmessage = async function (_e: MessageEvent): Promise<void> {
|
||||
// const {data} = e;
|
||||
// const {width, height, uint8Array} = data;
|
||||
|
||||
// const prediction1 = await animePredictImage(width, height, uint8Array);
|
||||
// const prediction2 = await animePredictImagePro(width, height, uint8Array);
|
||||
// if (prediction1 === 'Anime' && (prediction2 === 'Furry' || prediction2 === 'Anime')) {
|
||||
// self.postMessage({isAnime: true});
|
||||
// return;
|
||||
// }
|
||||
};
|
@@ -1,3 +1,4 @@
|
||||
/* @vite-ignore */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// This file is a modified version of the original imghelper.ts file from Emscripten.
|
@@ -1,5 +1,5 @@
|
||||
import * as tf from "@tensorflow/tfjs";
|
||||
import Image from "./image";
|
||||
import Image from "./image.ts";
|
||||
|
||||
export default async function upscale(
|
||||
image: Image,
|
Reference in New Issue
Block a user