add image recognition classification

This commit is contained in:
2025-01-06 17:42:27 +08:00
parent 6854e41b82
commit 90a68221fe
59 changed files with 19194 additions and 111 deletions

1
src/assets/svgs/more.svg Normal file
View File

@@ -0,0 +1 @@
<svg t="1735790957970" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7635" width="200" height="200"><path d="M512 0c282.331429 0 512 229.668571 512 512s-229.668571 512-512 512-512-229.668571-512-512 229.668571-512 512-512z m256 438.857143a73.142857 73.142857 0 1 0-0.073143 146.212571A73.142857 73.142857 0 0 0 768 438.857143zM512 438.857143a73.142857 73.142857 0 1 0 0 146.285714 73.142857 73.142857 0 0 0 0-146.285714zM256 438.857143a73.142857 73.142857 0 1 0 0 146.285714 73.142857 73.142857 0 0 0 0-146.285714z" fill="#536076" p-id="7636"></path></svg>

After

Width:  |  Height:  |  Size: 603 B

View File

@@ -1,5 +1,9 @@
<template>
<div class="check-card" :class="{ 'selected': isSelected }" @click="handleClick" :style="cardStyle">
<div
class="check-card"
:class="{ 'selected': isSelected && props.showSelectedEffect }"
@click="handleClick"
:style="cardStyle">
<div class="hover-circle" @click.stop="toggleSelection"
v-if="showHoverCircle"
:style="{ width: iconSize + 'px', height: iconSize + 'px' }">
@@ -35,6 +39,7 @@ interface Props {
backgroundColor?: string;
showHoverCircle?: boolean; // 控制是否显示悬停圆环
iconSize?: number; // 控制图标大小
showSelectedEffect?: boolean; // 控制是否显示选中效果
}
const props = withDefaults(defineProps<Props>(), {
@@ -45,6 +50,7 @@ const props = withDefaults(defineProps<Props>(), {
backgroundColor: '#e5eeff',
showHoverCircle: true, // 默认显示悬停圆环
iconSize: 24, // 默认图标大小
showSelectedEffect: true, // 默认显示选中效果
});
const emits = defineEmits(['update:modelValue']);
@@ -90,6 +96,7 @@ function toggleSelection() {
transition: border-color 0.3s, background-color 0.3s;
overflow: visible; /* Ensure the icon is not cut off */
}
.check-card.selected {
border: 1px solid rgba(125, 167, 255, 0.68);
box-shadow: 0 0 2px rgba(77, 167, 255, 0.89);

View File

@@ -0,0 +1,91 @@
<template>
<div class="stack rotated">
<img :src="imageSrc" alt="">
</div>
</template>
<script setup lang="ts">
import { defineProps } from 'vue';
const props = defineProps<{
src?: string;
defaultSrc: string;
}>();
const imageSrc = computed(() => props.src || props.defaultSrc);
</script>
<style scoped lang="scss">
.stack {
position: relative;
z-index: 10;
float: left;
}
.stack img {
width: 200px;
height: 180px;
vertical-align: bottom;
border: 5px #fff solid;
border-radius: 10px;
box-sizing: border-box;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
}
.stack:before,
.stack:after {
content: "";
border-radius: 10px;
width: 100%;
height: 100%;
position: absolute;
border: 10px solid #fff;
left: 0;
box-sizing: border-box;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
transition: 0.3s all ease-out;
}
.stack:before {
top: 4px;
z-index: -10;
}
.stack:after {
top: 8px;
z-index: -20;
}
.stack.rotated:before {
transform-origin: bottom left;
transform: rotate(2deg);
}
.stack.rotated:after {
transform-origin: bottom left;
transform: rotate(4deg);
}
.stack.twisted:before {
transform: rotate(4deg);
}
.stack.twisted:after {
transform: rotate(-4deg);
}
.stack.rotated-left:before {
transform-origin: bottom left;
transform: rotate(-3deg);
}
.stack.rotated-left:after {
transform-origin: bottom left;
transform: rotate(-6deg);
}
.stack:hover:before,
.stack:hover:after {
transform: rotate(0deg);
}
</style>

View File

@@ -2,7 +2,7 @@
<div>
<div class="sidebar">
<AMenu
:selectedKeys="[route.path.split('/').slice(-2).join('/')]"
:selectedKeys="[menu.currentMenu]"
:selectable="true"
:multiple="false"
mode="vertical"
@@ -96,10 +96,12 @@ import recyclingbin from '@/assets/svgs/recyclingbin.svg';
import Folder from "@/components/Folder/Folder.vue";
import ai from '@/assets/svgs/ai.svg';
import share from '@/assets/svgs/share.svg';
import useStore from "@/store";
const {t} = useI18n();
const router = useRouter();
const route = useRoute();
const menu = useStore().menu;
/**
* handle click event of menu item
@@ -107,6 +109,7 @@ const route = useRoute();
*/
function handleClick({key}) {
router.push(`/main/${key}`);
menu.currentMenu = key;
}
const menuCSSStyle: any = reactive({
@@ -135,6 +138,17 @@ function scrollToSelectedMenuItem() {
onMounted(() => {
scrollToSelectedMenuItem();
});
// watch(
// () => route.path,
// (newPath) => {
// if (!newPath.includes(menu.currentMenu)) {
// router.push(`/main/${menu.currentMenu}`);
// }
// scrollToSelectedMenuItem();
// }
// );
</script>
<style scoped lang="scss" src="./index.scss">

View File

@@ -1,43 +1,115 @@
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 LocationAlbum from "@/views/Album/LocationAlbum/LocationAlbum.vue";
import ThingAlbum from "@/views/Album/ThingAlbum/ThingAlbum.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";
export default [
{
path: '/main/album/albums',
name: 'albums',
component: Phoalbum,
meta: {
requiresAuth: true,
title: '相册'
},
component: PhoalbumIndex,
redirect: '/main/album/albums',
children: [
{
path: '/main/album/albums',
name: 'album',
component: Phoalbum,
meta: {
requiresAuth: true,
title: '相册'
},
},
{
path: '/main/album/albums/:id',
name: 'albumList',
component: PhoalbumDetail,
meta: {
requiresAuth: true,
title: '相册'
},
}
]
},
{
path: '/main/album/people',
name: 'people',
component: PeopleAlbum,
meta: {
requiresAuth: true,
title: '人物相册'
},
component: PeopleAlbumIndex,
redirect: '/main/album/people',
children: [
{
path: '/main/album/people',
name: 'people',
component: PeopleAlbum,
meta: {
requiresAuth: true,
title: '人物相册'
},
},
{
path: '/main/album/people/:id',
name: 'peopleList',
component: PeopleAlbumDetail,
meta: {
requiresAuth: true,
title: '人物相册'
},
}
]
},
{
path: '/main/album/location',
name: 'location',
component: LocationAlbum,
meta: {
requiresAuth: true,
title: '地点相册'
},
component: LocationAlbumIndex,
redirect: '/main/album/location',
children: [
{
path: '/main/album/location',
name: 'location',
component: LocationAlbum,
meta: {
requiresAuth: true,
title: '地点相册'
},
},
{
path: '/main/album/location/:id',
name: 'locationList',
component: LocationAlbumDetail,
meta: {
requiresAuth: true,
title: '地点相册'
},
}
]
},
{
path: '/main/album/thing',
name: 'thing',
component: ThingAlbum,
meta: {
requiresAuth: true,
title: '事物相册'
},
component: ThingAlbumIndex,
redirect: '/main/album/thing',
children: [
{
path: '/main/album/thing',
name: 'thing',
component: ThingAlbum,
meta: {
requiresAuth: true,
title: '事物相册'
},
},
{
path: '/main/album/thing/:id',
name: 'thingList',
component: ThingAlbumDetail,
meta: {
requiresAuth: true,
title: '事物相册'
},
}
]
},
];

View File

@@ -4,6 +4,8 @@ import {langStore} from "@/store/modules/langStore.ts";
import {useCommentStore} from "@/store/modules/commentStore.ts";
import {useWebSocketStore} from "@/store/modules/websocketStore.ts";
import {useUpscaleStore} from "@/store/modules/upscaleStore.ts";
import {useMenuStore} from "@/store/modules/menuStore.ts";
import {useUploadStore} from "@/store/modules/uploadStore.ts";
export default function useStore() {
return {
@@ -13,5 +15,7 @@ export default function useStore() {
comment: useCommentStore(),
websocket: useWebSocketStore(),
upscale: useUpscaleStore(),
menu: useMenuStore(),
upload: useUploadStore(),
};
}

View File

@@ -0,0 +1,18 @@
export const useMenuStore = defineStore(
'menu',
() => {
const currentMenu = ref<string>('photo/all');
return {
currentMenu,
};
},
{
// 开启数据持久化
persistedState: {
persist: true,
storage: localStorage,
key: 'menu',
includePaths: ['currentMenu']
}
}
);

View File

@@ -0,0 +1,80 @@
import {initNSFWJs, predictNSFW} from "@/utils/nsfw/nsfw.ts";
import i18n from "@/locales";
import {NSFWJS} from "nsfwjs";
import {message} from "ant-design-vue";
import {loadCocoSsd, loadMobileNet} from "@/utils/tfjs";
export const useUploadStore = defineStore(
'upload',
() => {
const openUploadDrawer = ref<boolean>(false);
const image: HTMLImageElement = document.createElement('img');
/**
* 图片上传前的校验
* @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);
return 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);
}
return {
openUploadDrawer,
beforeUpload,
customUploadRequest
};
},
{
// 开启数据持久化
persistedState: {
persist: false,
storage: localStorage,
key: 'upload',
includePaths: []
}
}
);

View File

@@ -101,6 +101,13 @@ export const useUpscaleStore = defineStore(
const msg = ref<string>("");
const progressBar = ref<number>(0);
const status = ref<string>('loading');
const dragging = ref<boolean>(false);
const linePosition = ref(0);
const draggingLine = ref(false);
/**
* 图片上传前的校验
* @param file
@@ -118,10 +125,11 @@ export const useUpscaleStore = defineStore(
uploading.value = false;
return false;
}
await clear();
fileData.value = urlData;
await loadImg(image);
uploading.value = false;
await clear();
return true;
}
@@ -142,6 +150,12 @@ export const useUpscaleStore = defineStore(
msg.value = "";
progressBar.value = 0;
isProcessing.value = false;
dragging.value = false;
linePosition.value = 0;
draggingLine.value = false;
input.value = null;
inputAlpha.value = null;
wasmModule.value = null;
}
/**
@@ -203,6 +217,9 @@ export const useUpscaleStore = defineStore(
msg,
progressBar,
status,
dragging,
linePosition,
draggingLine,
loadImg,
beforeUpload,
customUploadRequest,

84
src/utils/tfjs/index.ts Normal file
View File

@@ -0,0 +1,84 @@
import * as tf from '@tensorflow/tfjs';
import * as mobilenet from '@tensorflow-models/mobilenet';
import * as cocoSsd from '@tensorflow-models/coco-ssd';
// 确保 TensorFlow.js 已准备好并设置后端
async function initializeTensorFlow(backend = "webgl") {
await tf.ready();
if (!(await tf.setBackend(backend))) {
console.error(`${backend} is not supported in your browser.`);
return false;
}
return true;
}
// 加载 MobileNet 模型的工具函数
export async function loadMobileNet(image) {
const modelName = "mobilenet-model";
const modelUrl = '/tfjs/mobilenet/mobilenet-v3-tfjs-large-100-224-feature-vector-v1/model.json';
// 初始化 TensorFlow.js
if (!(await initializeTensorFlow())) {
return;
}
let model;
try {
// 尝试从 IndexedDB 加载模型
model = await mobilenet.load({
version: 2,
alpha: 1.0,
modelUrl: `indexeddb://${modelName}`,
});
console.log("MobileNet model loaded from IndexedDB successfully");
} catch (_error) {
console.log("Downloading MobileNet model...");
// 如果 IndexedDB 中没有模型则从 URL 加载并保存到 IndexedDB
model = await mobilenet.load({
version: 2,
alpha: 1.0,
modelUrl: modelUrl,
});
const Model = await tf.loadGraphModel(modelUrl);
await Model.save(`indexeddb://${modelName}`);
console.log("MobileNet model downloaded and saved to IndexedDB");
}
// 使用模型进行分类
return await model.classify(image, 3);
}
// 加载 COCO SSD 模型的工具函数
export async function loadCocoSsd(image) {
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");
} catch (_error) {
console.log("Downloading COCO SSD model...");
// 如果 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");
}
// 使用模型进行检测
return await model.detect(image);
}

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped lang="less">
</style>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<router-view></router-view>
</template>
<style scoped lang="less">
</style>

View File

@@ -1,11 +1,122 @@
<script setup lang="ts">
</script>
<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();
<style scoped lang="scss" src="./index.scss">
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>

View File

@@ -0,0 +1,9 @@
<template>
</template>
<script setup lang="ts">
</script>
<style scoped lang="less">
</style>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<router-view></router-view>
</template>
<style scoped lang="less">
</style>

View File

@@ -1,11 +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;
<template>
.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>
.people-album-button {
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: bold;
color: #333;
<style scoped lang="scss" src="./index.scss">
.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>

View File

@@ -0,0 +1,9 @@
<template>
</template>
<script setup lang="ts">
</script>
<style scoped lang="less">
</style>

View File

@@ -0,0 +1,9 @@
<template>
<router-view></router-view>
</template>
<script setup lang="ts">
</script>
<style scoped lang="less">
</style>

View File

@@ -1,11 +1,180 @@
<script setup lang="ts">
</script>
<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";
<style scoped lang="scss" src="./index.scss">
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>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped lang="less">
</style>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<router-view></router-view>
</template>
<style scoped lang="less">
</style>

View File

@@ -1,11 +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;
<template>
.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>
.thing-album-button {
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: bold;
color: #333;
}
}
<style scoped lang="scss" src="./index.scss">
.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>

View File

@@ -1,7 +1,7 @@
<template>
<div class="all-photo">
<div class="photo-header">
<AButton type="primary" shape="round" size="middle">
<AButton type="primary" shape="round" size="middle" @click="upload.openUploadDrawer = true">
<template #icon>
<PlusOutlined/>
</template>
@@ -83,11 +83,13 @@
:breakpoints="breakpoints">
<template #default="{ item, url, index }">
<CheckCard :key="index"
class="photo-item"
margin="0"
border-radius="0"
v-model="selected"
:showHoverCircle="true"
:iconSize="20"
:showSelectedEffect="true"
:value="url">
<AImage :src="url"
:alt="item.title"
@@ -113,6 +115,7 @@
</ATabPane>
</ATabs>
</div>
<Upload/>
</div>
</template>
@@ -121,9 +124,12 @@ 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 useStore from "@/store";
const selected = ref<(string | number)[]>([]);
const switchValue = ref<boolean>(false);
const upload = useStore().upload;
const breakpoints = reactive({
breakpoints: {
1200: {
@@ -162,6 +168,11 @@ function loadImages() {
tag: '全部',
date: '2022-01-01',
});
images.value.push({
title: `image-${i}`,
link: '',
src: `/test/${i}.png`,
});
}
}
@@ -273,4 +284,10 @@ onBeforeMount(() => { // 组件已完成响应式状态设置但未创建DOM
transform: translateY(0);
opacity: 1;
}
.photo-item:hover {
transition: all 0.3s ease-in-out, transform 0.3s ease-in-out;
//transform: scale(0.99);
box-shadow: 0 0 10px 0 rgba(77, 167, 255, 0.89);
}
</style>

View File

@@ -15,23 +15,125 @@
</AButton>
</div>
<div class="photo-list">
<CheckCard style="margin-top: 20px;" v-for="item in items" :key="item.id" :value="item.id" v-model="selectedItems">
{{ item.name }}
</CheckCard>
<div>Selected Items: {{ selectedItems }}</div>
<div style="width:100%;height:100%;" v-if="images.length !== 0">
<span style="margin-left: 10px;font-size: 13px">2024年12月27日 星期日</span>
<AImagePreviewGroup>
<Waterfall :list="images"
:backgroundColor="`transparent`"
:width="400"
:gutter="15"
align="left"
:lazyload="true"
:animationDelay="300"
:animationDuration="1000"
:animationCancel="false"
:hasAroundGutter="true"
rowKey="id"
:imgSelector="'src'"
:loadProps="loadProps"
:breakpoints="breakpoints">
<template #default="{ item, url, index }">
<CheckCard :key="index"
margin="0"
border-radius="0"
v-model="selected"
:showHoverCircle="true"
:iconSize="20"
:value="url">
<AImage :src="url"
:alt="item.title"
:key="index"
:previewMask="false"
loading="lazy"/>
</CheckCard>
</template>
</Waterfall>
</AImagePreviewGroup>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const items = ref([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' },
]);
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';
const selectedItems = ref<(string | number)[]>([]);
const selected = ref<(string | number)[]>([]);
const breakpoints = reactive({
breakpoints: {
1200: {
// 当屏幕宽度小于等于1200
rowPerView: 4,
},
800: {
// 当屏幕宽度小于等于800
rowPerView: 3,
},
500: {
// 当屏幕宽度小于等于500
rowPerView: 2,
},
},
});
const loadProps = reactive({
loading,
error,
ratioCalculator: (_width: number, _height: number) => {
// 我设置了最小宽高比
const minRatio = 3 / 4;
const maxRatio = 4 / 3;
return Math.random() > 0.5 ? minRatio : maxRatio;
},
});
const images = ref<any[]>([]);
function loadImages() {
for (let i = 1; i < 10; i++) {
images.value.push({
title: `image-${i}`,
link: '',
src: `https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.0.5/${i}.jpg`,
tag: '全部',
date: '2022-01-01',
});
}
}
onBeforeMount(() => { // 组件已完成响应式状态设置但未创建DOM节点
loadImages();
});
</script>
<style scoped lang="scss" src="./index.scss">
<style scoped lang="scss">
.recent-upload {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
width: 100%;
height: 100%;
.photo-header {
width: 100%;
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 10px;
border-bottom: 1px solid #e2e2e2;
}
.photo-list {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
width: 100%;
height: calc(100% - 65px);
margin-top: 10px;
}
}
</style>

View File

@@ -1,19 +0,0 @@
.recent-upload {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
width: 100%;
height: 100%;
.photo-header {
width: 100%;
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 10px;
border-bottom: 1px solid #e2e2e2;
}
}

View File

@@ -0,0 +1,74 @@
<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>

View File

@@ -110,11 +110,8 @@ import deleted from '@/assets/svgs/deleted.svg';
import getImageSizeWithUnit from "@/utils/imageUtils/getImageSizeWithUnit.ts";
const canvasContainer = ref<HTMLDivElement | null>(null);
const dragging = ref<boolean>(false);
const linePosition = ref(0);
const dpr = window.devicePixelRatio || 1;
const canvas = ref<HTMLCanvasElement | null>(null);
const draggingLine = ref(false);
const imgX = ref(0);
const imgY = ref(0);
const imgScale = ref(1);
@@ -197,8 +194,8 @@ function deletedImage() {
store.isProcessing = false;
store.progressBar = 0;
store.msg = "";
draggingLine.value = false;
dragging.value = false;
store.draggingLine = false;
store.dragging = false;
imgX.value = 0;
imgY.value = 0;
imgScale.value = 1;
@@ -217,11 +214,11 @@ function startDragging(event: any) {
if (canvas.value) {
const rect = canvas.value.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
if (Math.abs(mouseX - linePosition.value / dpr) < 12) {
if (Math.abs(mouseX - store.linePosition / dpr) < 12) {
startDraggingLine(event);
return;
}
dragging.value = true;
store.dragging = true;
}
}
@@ -229,11 +226,11 @@ function startDragging(event: any) {
* 停止拖动
*/
function stopDragging() {
if (draggingLine.value) {
if (store.draggingLine) {
stopDraggingLine();
return;
}
dragging.value = false;
store.dragging = false;
}
/**
@@ -241,12 +238,12 @@ function stopDragging() {
* @param event
*/
function dragImage(event: any) {
if (dragging.value) {
if (store.dragging) {
imgX.value += event.movementX * dpr;
imgY.value += event.movementY * dpr;
drawImage();
}
if (draggingLine.value) {
if (store.draggingLine) {
updateLinePosition(event);
drawImage();
}
@@ -290,9 +287,9 @@ function touchStart(event: any) {
touchStartImgY.value = imgY.value;
if (event.touches.length == 1) {
if (
Math.abs(event.touches[0].clientX - linePosition.value / dpr) < 12
Math.abs(event.touches[0].clientX - store.linePosition / dpr) < 12
) {
draggingLine.value = true;
store.draggingLine = true;
return;
}
touchStartX.value = event.touches[0].clientX * dpr;
@@ -332,7 +329,7 @@ function touchMove(event: any) {
touchStartY.value +
touchStartImgY.value -
imgY.value;
if (draggingLine.value) {
if (store.draggingLine) {
updateLinePosition(event.touches[0]);
drawImage();
return;
@@ -402,7 +399,7 @@ function touchEnd(event: any) {
return;
}
touching.value = false;
draggingLine.value = false;
store.draggingLine = false;
touchStartImgX.value = 0;
touchStartImgY.value = 0;
touchStartX.value = 0;
@@ -416,14 +413,14 @@ function touchEnd(event: any) {
*/
function startDraggingLine(event: any) {
event.preventDefault();
draggingLine.value = true;
store.draggingLine = true;
}
/**
* 停止拖动线
*/
function stopDraggingLine() {
draggingLine.value = false;
store.draggingLine = false;
}
/**
@@ -454,17 +451,17 @@ function drawImage_() {
ctx.drawImage(
processedImg.value,
((processedImg.value.width / img.value.width) *
(linePosition.value - imgX.value)) /
(store.linePosition - imgX.value)) /
imgScale.value,
0,
processedImg.value.width -
((processedImg.value.width / img.value.width) *
(linePosition.value - imgX.value)) /
(store.linePosition - imgX.value)) /
imgScale.value,
processedImg.value.height,
linePosition.value,
store.linePosition,
imgY.value,
imgX.value + img.value.width * imgScale.value - linePosition.value,
imgX.value + img.value.width * imgScale.value - store.linePosition,
img.value.height * imgScale.value
);
}
@@ -488,7 +485,7 @@ function updateLinePosition(event: any) {
newPosition = Math.max(0, Math.min(newPosition, containerRect.width - lineWidth));
// 更新线的位置
linePosition.value = newPosition * dpr;
store.linePosition = newPosition * dpr;
const line: any = dragLine.value;
line.style.left = Math.floor(newPosition) + "px";
}
@@ -500,7 +497,7 @@ function updateLinePosition(event: any) {
*/
function dragLineFn(event: any) {
event.preventDefault();
if (draggingLine.value) {
if (store.draggingLine) {
requestAnimationFrame(() => {
updateLinePosition(event);
drawImage();
@@ -522,8 +519,8 @@ function updateCanvasSize() {
// canvas.value.height) * container.offsetHeight * dpr - (img.value.height * imgScale.value) / 2;
imgX.value = (container.offsetWidth * dpr - img.value.width * imgScale.value) / 2;
imgY.value = (container.offsetHeight * dpr - img.value.height * imgScale.value) / 2;
linePosition.value = (linePosition.value / canvas.value.width) * container.offsetWidth * dpr;
// dragLine.value.style.left = linePosition.value / dpr + "px";
store.linePosition = (store.linePosition / canvas.value.width) * container.offsetWidth * dpr;
// dragLine.value.style.left = store.linePosition / dpr + "px";
}
if (canvas.value) {
canvas.value.width = container.offsetWidth * dpr;