Feat: Add Settings Modal
This commit is contained in:
parent
240fdedec7
commit
1ea027e4f1
98
src/App.vue
98
src/App.vue
|
|
@ -1,8 +1,10 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, nextTick } from "vue";
|
import { ref, computed, onMounted } from "vue";
|
||||||
import { snapdom } from "@zumer/snapdom";
|
import { snapdom } from "@zumer/snapdom";
|
||||||
import { Camera, Palette, Aperture, Zap, User, Lightbulb, HeartHandshake, Users2, MapPin, WandSparkles } from "lucide-vue-next";
|
import { Camera, Palette, Aperture, Zap, User, Lightbulb, Users2, MapPin, WandSparkles, Settings, BadgeDollarSign } from "lucide-vue-next";
|
||||||
import { IconQQ, IconWeChat, IconXHS } from "./components/common/icons";
|
import { IconQQ, IconWeChat, IconXHS } from "./components/common/icons";
|
||||||
|
import SettingsModal from "./components/biz/settings-modal.vue";
|
||||||
|
import { usePhotoColumns } from "./composables/use-photo-columns";
|
||||||
|
|
||||||
interface MediaItem {
|
interface MediaItem {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -18,13 +20,30 @@ interface MediaItem {
|
||||||
|
|
||||||
const photos = ref<MediaItem[]>([]);
|
const photos = ref<MediaItem[]>([]);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const columns = ref<MediaItem[][]>([[], [], []]);
|
|
||||||
const posterRef = ref<HTMLElement>();
|
const posterRef = ref<HTMLElement>();
|
||||||
const isCapturing = ref(false);
|
const isCapturing = ref(false);
|
||||||
|
const showSettings = ref(false);
|
||||||
|
const photoSize = ref(30);
|
||||||
|
const hiddenPhotos = ref<number[]>([]);
|
||||||
|
const columnCount = ref(4);
|
||||||
|
const showPrice = ref(true);
|
||||||
|
|
||||||
|
// 使用 hooks 计算宫格图片
|
||||||
|
const columns = usePhotoColumns(photos, columnCount);
|
||||||
|
|
||||||
|
// 计算 grid 类名
|
||||||
|
const gridColsClass = computed(() => {
|
||||||
|
const colsMap: Record<number, string> = {
|
||||||
|
3: 'grid-cols-3',
|
||||||
|
4: 'grid-cols-4',
|
||||||
|
5: 'grid-cols-5',
|
||||||
|
};
|
||||||
|
return colsMap[columnCount.value] || 'grid-cols-4';
|
||||||
|
});
|
||||||
|
|
||||||
const fetchPhotos = async () => {
|
const fetchPhotos = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("https://paul.ren/api/media?cate=9&starred=1&size=30");
|
const response = await fetch(`https://paul.ren/api/media?cate=9&starred=1&size=${photoSize.value}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
// 处理图片 URL
|
// 处理图片 URL
|
||||||
photos.value = data.data.map((photo: MediaItem) => {
|
photos.value = data.data.map((photo: MediaItem) => {
|
||||||
|
|
@ -39,22 +58,34 @@ const fetchPhotos = async () => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
arrangePhotos();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch photos:", error);
|
console.error("Failed to fetch photos:", error);
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const arrangePhotos = () => {
|
const handlePhotoToggle = (id: number, hidden: boolean) => {
|
||||||
columns.value = [[], [], []];
|
if (hidden) {
|
||||||
|
hiddenPhotos.value.push(id);
|
||||||
|
} else {
|
||||||
|
hiddenPhotos.value = hiddenPhotos.value.filter((photoId) => photoId !== id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
photos.value.forEach((photo, index) => {
|
const handleSizeConfirm = (newSize: number) => {
|
||||||
const columnIndex = index % 3;
|
if (photoSize.value !== newSize) {
|
||||||
columns.value[columnIndex].push(photo);
|
photoSize.value = newSize;
|
||||||
});
|
loading.value = true;
|
||||||
|
fetchPhotos();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleColumnCountChange = (newCount: number) => {
|
||||||
|
columnCount.value = newCount;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShowPriceChange = (newValue: boolean) => {
|
||||||
|
showPrice.value = newValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDate = (dateStr: string) => {
|
const formatDate = (dateStr: string) => {
|
||||||
|
|
@ -69,7 +100,7 @@ const formatDate = (dateStr: string) => {
|
||||||
const getImageUrl = (url: string) => {
|
const getImageUrl = (url: string) => {
|
||||||
// 在开发环境下使用代理
|
// 在开发环境下使用代理
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
return url.replace("https://legacy.paul.ren", "/proxy-images");
|
return url.replace("https://legacy.paul.ren", "https://photo.paul.xin");
|
||||||
}
|
}
|
||||||
|
|
||||||
return url.replace("https://legacy.paul.ren", "/");
|
return url.replace("https://legacy.paul.ren", "/");
|
||||||
|
|
@ -107,6 +138,13 @@ onMounted(() => {
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-gradient-to-br from-gray-100 to-gray-200 min-[769px]:px-4 min-[769px]:py-8">
|
<div class="min-h-screen bg-gradient-to-br from-gray-100 to-gray-200 min-[769px]:px-4 min-[769px]:py-8">
|
||||||
<div class="fixed top-4 right-4 z-50 flex gap-2">
|
<div class="fixed top-4 right-4 z-50 flex gap-2">
|
||||||
|
<button
|
||||||
|
class="cursor-pointer bg-white/90 backdrop-blur-sm text-gray-700 px-4 py-2 rounded-full shadow-lg hover:bg-white transition-all duration-300 flex items-center gap-2 hover:scale-105"
|
||||||
|
@click="showSettings = true"
|
||||||
|
>
|
||||||
|
<Settings :size="20" />
|
||||||
|
<span>设置</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
class="cursor-pointer bg-white/90 backdrop-blur-sm text-gray-700 px-4 py-2 rounded-full shadow-lg hover:bg-white transition-all duration-300 flex items-center gap-2 hover:scale-105"
|
class="cursor-pointer bg-white/90 backdrop-blur-sm text-gray-700 px-4 py-2 rounded-full shadow-lg hover:bg-white transition-all duration-300 flex items-center gap-2 hover:scale-105"
|
||||||
:disabled="isCapturing"
|
:disabled="isCapturing"
|
||||||
|
|
@ -160,10 +198,6 @@ onMounted(() => {
|
||||||
<Users2 class="opacity-60" />
|
<Users2 class="opacity-60" />
|
||||||
<span class="flex-1">可动作指导,建议自带动作</span>
|
<span class="flex-1">可动作指导,建议自带动作</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex items-center gap-2">
|
|
||||||
<HeartHandshake class="opacity-60" />
|
|
||||||
<span class="flex-1">审题材、妆造互勉</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
<li class="flex items-center gap-2">
|
<li class="flex items-center gap-2">
|
||||||
|
|
@ -187,7 +221,11 @@ onMounted(() => {
|
||||||
<div class="text-gray-200 text-xl leading-[1.8]">
|
<div class="text-gray-200 text-xl leading-[1.8]">
|
||||||
<p class="flex items-center gap-2">
|
<p class="flex items-center gap-2">
|
||||||
<WandSparkles class="shrink-0 opacity-60" />
|
<WandSparkles class="shrink-0 opacity-60" />
|
||||||
<span class="flex-1">互勉角色: 原神、崩坏:星穹铁道、绝区零、鸣潮、蔚蓝档案、碧蓝航线等</span>
|
<span class="flex-1">互勉角色: 原神、崩铁、绝区零、鸣潮、蔚蓝档案、碧蓝航线等</span>
|
||||||
|
</p>
|
||||||
|
<p v-if="showPrice" class="flex items-center gap-2">
|
||||||
|
<BadgeDollarSign class="opacity-60" />
|
||||||
|
<span class="flex-1">接单价格: 场照单人 10 元 1 张,双人 15 元 1 张,组合有优惠,详情可咨询</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -197,15 +235,19 @@ onMounted(() => {
|
||||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-yellow-400"></div>
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-yellow-400"></div>
|
||||||
</main>
|
</main>
|
||||||
<main v-else>
|
<main v-else>
|
||||||
<section class="grid grid-cols-3 gap-2 mb-8">
|
<section :class="['grid', 'gap-2', 'mb-8', gridColsClass]">
|
||||||
<div v-for="(column, colIndex) in columns" :key="colIndex" class="space-y-2">
|
<div v-for="(column, colIndex) in columns" :key="colIndex" class="space-y-2">
|
||||||
<div v-for="photo in column" :key="photo.id" class="relative rounded-lg overflow-hidden group bg-gray-900">
|
<template v-for="photo in column" :key="photo.id">
|
||||||
|
<div
|
||||||
|
v-if="!hiddenPhotos.includes(photo.id)"
|
||||||
|
class="relative rounded-lg overflow-hidden group bg-gray-900">
|
||||||
<img :src="photo.url" :alt="photo.title" class="w-full h-auto block rounded-lg" :width="photo.width" :height="photo.height" />
|
<img :src="photo.url" :alt="photo.title" class="w-full h-auto block rounded-lg" :width="photo.width" :height="photo.height" />
|
||||||
<div class="absolute bg-gradient-to-t from-black to-transparent bottom-0 left-0 right-0 p-4 pt-8 text-white transition-opacity duration-300 opacity-0 group-hover:opacity-100">
|
<div class="absolute bg-gradient-to-t from-black to-transparent bottom-0 left-0 right-0 p-4 pt-8 text-white transition-opacity duration-300 opacity-0 group-hover:opacity-100">
|
||||||
<h3 class="font-semibold text-white mb-1 text-lg">{{ photo.title }}</h3>
|
<h3 class="font-semibold text-white mb-1 text-lg">{{ photo.title }}</h3>
|
||||||
<p class="text-xs opacity-60">{{ formatDate(photo.take_time) }}</p>
|
<p class="text-xs opacity-60">{{ formatDate(photo.take_time) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="grid gap-4 grid-cols-3 text-gray-200 text-lg">
|
<section class="grid gap-4 grid-cols-3 text-gray-200 text-lg">
|
||||||
|
|
@ -226,5 +268,19 @@ onMounted(() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SettingsModal
|
||||||
|
:show="showSettings"
|
||||||
|
:initial-size="photoSize"
|
||||||
|
:initial-column-count="columnCount"
|
||||||
|
:initial-show-price="showPrice"
|
||||||
|
:hidden-photos="hiddenPhotos"
|
||||||
|
:photos="photos"
|
||||||
|
@close="showSettings = false"
|
||||||
|
@update:size="handleSizeConfirm"
|
||||||
|
@update:column-count="handleColumnCountChange"
|
||||||
|
@update:show-price="handleShowPriceChange"
|
||||||
|
@toggle-photo="handlePhotoToggle"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,200 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch } from "vue";
|
||||||
|
import BaseDrawer from "../ui/drawer.vue";
|
||||||
|
import { Check } from "lucide-vue-next";
|
||||||
|
import { usePhotoColumns } from "../../composables/use-photo-columns";
|
||||||
|
|
||||||
|
interface MediaItem {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
url: string;
|
||||||
|
thumb_url: string;
|
||||||
|
meta: string;
|
||||||
|
take_time: string;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
show: boolean;
|
||||||
|
initialSize: number;
|
||||||
|
initialColumnCount: number;
|
||||||
|
initialShowPrice: boolean;
|
||||||
|
hiddenPhotos: number[];
|
||||||
|
photos: MediaItem[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "close"): void;
|
||||||
|
(e: "update:size", value: number): void;
|
||||||
|
(e: "update:column-count", value: number): void;
|
||||||
|
(e: "update:show-price", value: boolean): void;
|
||||||
|
(e: "toggle-photo", id: number, hidden: boolean): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const tempSize = ref(props.initialSize);
|
||||||
|
const tempColumnCount = ref(props.initialColumnCount);
|
||||||
|
const tempShowPrice = ref(props.initialShowPrice);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.initialSize,
|
||||||
|
(value) => {
|
||||||
|
tempSize.value = value;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.initialColumnCount,
|
||||||
|
(value) => {
|
||||||
|
tempColumnCount.value = value;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.initialShowPrice,
|
||||||
|
(value) => {
|
||||||
|
tempShowPrice.value = value;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 使用 hooks 计算宫格图片
|
||||||
|
const photosRef = computed(() => props.photos);
|
||||||
|
const columns = usePhotoColumns(photosRef, tempColumnCount);
|
||||||
|
|
||||||
|
// 计算 grid 类名
|
||||||
|
const gridColsClass = computed(() => {
|
||||||
|
const colsMap: Record<number, string> = {
|
||||||
|
3: 'grid-cols-3',
|
||||||
|
4: 'grid-cols-4',
|
||||||
|
5: 'grid-cols-5',
|
||||||
|
};
|
||||||
|
return colsMap[tempColumnCount.value] || 'grid-cols-4';
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit("close");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSizeCommit = () => {
|
||||||
|
emit("update:size", tempSize.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleColumnCountCommit = () => {
|
||||||
|
emit("update:column-count", tempColumnCount.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShowPriceChange = () => {
|
||||||
|
emit("update:show-price", tempShowPrice.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPhotoHidden = (id: number) => props.hiddenPhotos.includes(id);
|
||||||
|
|
||||||
|
const togglePhoto = (id: number) => {
|
||||||
|
emit("toggle-photo", id, !isPhotoHidden(id));
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BaseDrawer :show="show" title="桌面预览设置" @close="handleClose">
|
||||||
|
<div class="space-y-8">
|
||||||
|
<section class="space-y-3">
|
||||||
|
<header class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-gray-400">显示数量</p>
|
||||||
|
<p class="text-base font-medium text-gray-900">最大可见照片</p>
|
||||||
|
</div>
|
||||||
|
<span class="text-xl font-semibold text-gray-900">{{ tempSize }} 张</span>
|
||||||
|
</header>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
v-model="tempSize"
|
||||||
|
min="10"
|
||||||
|
max="50"
|
||||||
|
step="1"
|
||||||
|
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-yellow-400"
|
||||||
|
@change="handleSizeCommit"
|
||||||
|
/>
|
||||||
|
<div class="flex justify-between text-xs text-gray-400">
|
||||||
|
<span>10 张</span>
|
||||||
|
<span>50 张</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="space-y-3">
|
||||||
|
<header class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-gray-400">布局设置</p>
|
||||||
|
<p class="text-base font-medium text-gray-900">照片列数</p>
|
||||||
|
</div>
|
||||||
|
<span class="text-xl font-semibold text-gray-900">{{ tempColumnCount }} 列</span>
|
||||||
|
</header>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
v-model="tempColumnCount"
|
||||||
|
min="3"
|
||||||
|
max="5"
|
||||||
|
step="1"
|
||||||
|
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-yellow-400"
|
||||||
|
@change="handleColumnCountCommit"
|
||||||
|
/>
|
||||||
|
<div class="flex justify-between text-xs text-gray-400">
|
||||||
|
<span>3 列</span>
|
||||||
|
<span>5 列</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="space-y-3">
|
||||||
|
<header class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-gray-400">内容显示</p>
|
||||||
|
<p class="text-base font-medium text-gray-900">显示接单价格</p>
|
||||||
|
</div>
|
||||||
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
v-model="tempShowPrice"
|
||||||
|
@change="handleShowPriceChange"
|
||||||
|
class="sr-only peer"
|
||||||
|
/>
|
||||||
|
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-yellow-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-yellow-400"></div>
|
||||||
|
</label>
|
||||||
|
</header>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="space-y-3">
|
||||||
|
<header>
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-gray-400">内容</p>
|
||||||
|
<p class="text-base font-medium text-gray-900">照片显示控制</p>
|
||||||
|
</header>
|
||||||
|
<div :class="['grid', 'gap-2', gridColsClass]">
|
||||||
|
<div class="space-y-2" v-for="(column, index) in columns" :key="index">
|
||||||
|
<button
|
||||||
|
v-for="photo in column"
|
||||||
|
:key="photo.id"
|
||||||
|
@click="togglePhoto(photo.id)"
|
||||||
|
class="relative text-xs rounded-xl transition-colors"
|
||||||
|
:class="[
|
||||||
|
'w-full p-1 cursor-pointer',
|
||||||
|
isPhotoHidden(photo.id) ? 'bg-gray-50 text-gray-400 opacity-60' : 'bg-yellow-300 text-gray-700',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="!isPhotoHidden(photo.id)"
|
||||||
|
class="absolute top-2 left-2 bg-yellow-400 rounded-full p-1"
|
||||||
|
>
|
||||||
|
<Check class="size-3" />
|
||||||
|
</span>
|
||||||
|
<img :src="photo.url" :alt="photo.title" class="w-full h-auto block rounded-lg" :width="photo.width" :height="photo.height" />
|
||||||
|
<!-- <span class="mt-1 px-2 block truncate">{{ photo.title }}</span> -->
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</BaseDrawer>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
<!-- eslint-disable vue/multi-word-component-names -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, onUnmounted } from "vue";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
show: boolean;
|
||||||
|
title?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "close"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
emit("close");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener("keydown", handleEscape);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener("keydown", handleEscape);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<Transition name="drawer">
|
||||||
|
<div v-if="show" class="fixed inset-0 z-50 flex">
|
||||||
|
<div class="drawer-panel w-full max-w-md h-full bg-white shadow-xl border-r border-gray-200 flex flex-col">
|
||||||
|
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-100">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm uppercase tracking-wide text-gray-400">设置</p>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900">{{ title }}</h3>
|
||||||
|
</div>
|
||||||
|
<button @click="emit('close')" class="p-2 text-gray-400 hover:text-gray-600 rounded-full transition-colors" aria-label="关闭设置抽屉">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 overflow-y-auto p-5">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="drawer-overlay flex-1" @click="emit('close')" />
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.drawer-enter-active,
|
||||||
|
.drawer-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-enter-from,
|
||||||
|
.drawer-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-enter-active .drawer-panel,
|
||||||
|
.drawer-leave-active .drawer-panel {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-enter-from .drawer-panel,
|
||||||
|
.drawer-leave-to .drawer-panel {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-enter-active .drawer-overlay,
|
||||||
|
.drawer-leave-active .drawer-overlay {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-enter-from .drawer-overlay,
|
||||||
|
.drawer-leave-to .drawer-overlay {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { computed, type Ref } from "vue";
|
||||||
|
|
||||||
|
interface MediaItem {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
url: string;
|
||||||
|
thumb_url: string;
|
||||||
|
meta: string;
|
||||||
|
take_time: string;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算宫格图片对象的 hooks
|
||||||
|
* @param photos 照片数组
|
||||||
|
* @param gridCount 宫格数量,可以是数字或 Ref<number>,默认为 4
|
||||||
|
* @returns 返回一个 computed,包含按宫格数分配的图片数组
|
||||||
|
*/
|
||||||
|
export function usePhotoColumns(
|
||||||
|
photos: Ref<MediaItem[]>,
|
||||||
|
gridCount: number | Ref<number> = 4
|
||||||
|
) {
|
||||||
|
const columns = computed<MediaItem[][]>(() => {
|
||||||
|
const count = typeof gridCount === 'number' ? gridCount : gridCount.value;
|
||||||
|
const result: MediaItem[][] = Array.from({ length: count }, () => []);
|
||||||
|
|
||||||
|
photos.value.forEach((photo, index) => {
|
||||||
|
const columnIndex = index % count;
|
||||||
|
result[columnIndex].push(photo);
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
return columns;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue