✨ add image recognition classification
This commit is contained in:
1
src/assets/svgs/more.svg
Normal file
1
src/assets/svgs/more.svg
Normal 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 |
@@ -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);
|
||||
|
||||
91
src/components/MyUI/PhotoStack/PhotoStack.vue
Normal file
91
src/components/MyUI/PhotoStack/PhotoStack.vue
Normal 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>
|
||||
@@ -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">
|
||||
|
||||
|
||||
@@ -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: '事物相册'
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
18
src/store/modules/menuStore.ts
Normal file
18
src/store/modules/menuStore.ts
Normal 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']
|
||||
}
|
||||
}
|
||||
);
|
||||
80
src/store/modules/uploadStore.ts
Normal file
80
src/store/modules/uploadStore.ts
Normal 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: []
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -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
84
src/utils/tfjs/index.ts
Normal 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);
|
||||
}
|
||||
11
src/views/Album/LocationAlbum/Detail.vue
Normal file
11
src/views/Album/LocationAlbum/Detail.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
||||
11
src/views/Album/LocationAlbum/Index.vue
Normal file
11
src/views/Album/LocationAlbum/Index.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
||||
@@ -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>
|
||||
|
||||
9
src/views/Album/PeopleAlbum/Detail.vue
Normal file
9
src/views/Album/PeopleAlbum/Detail.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
||||
11
src/views/Album/PeopleAlbum/Index.vue
Normal file
11
src/views/Album/PeopleAlbum/Index.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
||||
@@ -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>
|
||||
|
||||
9
src/views/Album/Phoalbum/Detail.vue
Normal file
9
src/views/Album/Phoalbum/Detail.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
||||
9
src/views/Album/Phoalbum/Index.vue
Normal file
9
src/views/Album/Phoalbum/Index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
||||
@@ -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>
|
||||
|
||||
11
src/views/Album/ThingAlbum/Detail.vue
Normal file
11
src/views/Album/ThingAlbum/Detail.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
||||
11
src/views/Album/ThingAlbum/Index.vue
Normal file
11
src/views/Album/ThingAlbum/Index.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
74
src/views/Photograph/Upload/Upload.vue
Normal file
74
src/views/Photograph/Upload/Upload.vue
Normal 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>
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user