diff --git a/src/App.vue b/src/App.vue index 0b80be4..ad53123 100644 --- a/src/App.vue +++ b/src/App.vue @@ -4,7 +4,6 @@ import { snapdom } from "@zumer/snapdom"; import { Camera, Palette, Aperture, Zap, User, Lightbulb, Users2, MapPin, WandSparkles, Settings, BadgeDollarSign } from "lucide-vue-next"; import { IconQQ, IconTikTok, IconWeChat, IconXHS } from "./components/common/icons"; import SettingsModal from "./components/biz/settings-modal.vue"; -import { usePhotoColumns } from "./composables/use-photo-columns"; interface MediaItem { id: number; @@ -19,6 +18,8 @@ interface MediaItem { } const photos = ref([]); +const photoOrder = ref([]); +const columns = ref([]); const loading = ref(true); const posterRef = ref(); const isCapturing = ref(false); @@ -28,8 +29,35 @@ const hiddenPhotos = ref([]); const columnCount = ref(4); const showPrice = ref(true); -// 使用 hooks 计算宫格图片 -const columns = usePhotoColumns(photos, columnCount); +let defaultColumnsCache: MediaItem[][] = []; + +const cloneColumns = (columnsValue: MediaItem[][]) => + columnsValue.map((column) => column.slice()); + +const buildColumns = (list: MediaItem[], count: number) => { + const result: MediaItem[][] = Array.from({ length: count }, () => []); + list.forEach((photo, index) => { + const columnIndex = index % count; + result[columnIndex].push(photo); + }); + return result; +}; + +const flattenColumns = (columnsValue: MediaItem[][]) => { + const result: MediaItem[] = []; + const maxRows = Math.max(0, ...columnsValue.map((column) => column.length)); + + for (let row = 0; row < maxRows; row += 1) { + for (let col = 0; col < columnsValue.length; col += 1) { + const photo = columnsValue[col][row]; + if (photo) result.push(photo); + } + } + + return result; +}; + +const getInitialColumns = () => cloneColumns(defaultColumnsCache); // 计算 grid 类名 const gridColsClass = computed(() => { @@ -46,7 +74,7 @@ const fetchPhotos = async () => { const response = await fetch(`https://paul.ren/api/media?cate=9&starred=1&size=${photoSize.value}`); const data = await response.json(); // 处理图片 URL - photos.value = data.data.map((photo: MediaItem) => { + const nextPhotos = data.data.map((photo: MediaItem) => { const parsedMeta = JSON.parse(photo.meta); return { @@ -57,6 +85,11 @@ const fetchPhotos = async () => { height: parsedMeta.COMPUTED.Height, }; }); + photos.value = nextPhotos; + photoOrder.value = nextPhotos.slice(); + const nextColumns = buildColumns(nextPhotos, columnCount.value); + columns.value = cloneColumns(nextColumns); + defaultColumnsCache = cloneColumns(nextColumns); loading.value = false; } catch (error) { console.error("Failed to fetch photos:", error); @@ -72,6 +105,12 @@ const handlePhotoToggle = (id: number, hidden: boolean) => { } }; +const handleColumnsUpdate = (nextColumns: MediaItem[][]) => { + columns.value = cloneColumns(nextColumns); + photoOrder.value = flattenColumns(nextColumns); + photos.value = photoOrder.value.slice(); +}; + const handleSizeConfirm = (newSize: number) => { if (photoSize.value !== newSize) { photoSize.value = newSize; @@ -82,6 +121,7 @@ const handleSizeConfirm = (newSize: number) => { const handleColumnCountChange = (newCount: number) => { columnCount.value = newCount; + columns.value = buildColumns(photoOrder.value, newCount); }; const handleShowPriceChange = (newValue: boolean) => { @@ -278,12 +318,14 @@ onMounted(() => { :initial-column-count="columnCount" :initial-show-price="showPrice" :hidden-photos="hiddenPhotos" - :photos="photos" + :columns="columns" + :initial-columns="getInitialColumns()" @close="showSettings = false" @update:size="handleSizeConfirm" @update:column-count="handleColumnCountChange" @update:show-price="handleShowPriceChange" @toggle-photo="handlePhotoToggle" + @update:columns="handleColumnsUpdate" /> diff --git a/src/components/biz/settings-modal.vue b/src/components/biz/settings-modal.vue index d7a732f..d28ddb0 100644 --- a/src/components/biz/settings-modal.vue +++ b/src/components/biz/settings-modal.vue @@ -2,7 +2,6 @@ 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; @@ -22,7 +21,8 @@ const props = defineProps<{ initialColumnCount: number; initialShowPrice: boolean; hiddenPhotos: number[]; - photos: MediaItem[]; + columns: MediaItem[][]; + initialColumns: MediaItem[][]; }>(); const emit = defineEmits<{ @@ -31,11 +31,14 @@ const emit = defineEmits<{ (e: "update:column-count", value: number): void; (e: "update:show-price", value: boolean): void; (e: "toggle-photo", id: number, hidden: boolean): void; + (e: "update:columns", value: MediaItem[][]): void; }>(); const tempSize = ref(props.initialSize); const tempColumnCount = ref(props.initialColumnCount); const tempShowPrice = ref(props.initialShowPrice); +const tempColumns = ref([]); +const draggingPhotoId = ref(null); watch( () => props.initialSize, @@ -58,9 +61,16 @@ watch( } ); -// 使用 hooks 计算宫格图片 -const photosRef = computed(() => props.photos); -const columns = usePhotoColumns(photosRef, tempColumnCount); +const cloneColumns = (columnsValue: MediaItem[][]) => + columnsValue.map((column) => column.slice()); + +watch( + () => props.columns, + (value) => { + tempColumns.value = cloneColumns(value); + }, + { immediate: true } +); // 计算 grid 类名 const gridColsClass = computed(() => { @@ -93,6 +103,61 @@ const isPhotoHidden = (id: number) => props.hiddenPhotos.includes(id); const togglePhoto = (id: number) => { emit("toggle-photo", id, !isPhotoHidden(id)); }; + +const emitColumns = (nextColumns: MediaItem[][]) => { + tempColumns.value = cloneColumns(nextColumns); + emit("update:columns", cloneColumns(nextColumns)); +}; + +const handleDragStart = (photoId: number, event: DragEvent) => { + draggingPhotoId.value = photoId; + event.dataTransfer?.setData("text/plain", String(photoId)); + event.dataTransfer?.setDragImage?.((event.target as HTMLElement) ?? new Image(), 10, 10); +}; + +const handleDragEnd = () => { + draggingPhotoId.value = null; +}; + +const takeDraggedPhoto = (columnsValue: MediaItem[][]) => { + let draggedPhoto: MediaItem | undefined; + + columnsValue.forEach((column) => { + const index = column.findIndex((photo) => photo.id === draggingPhotoId.value); + if (index !== -1) { + draggedPhoto = column.splice(index, 1)[0]; + } + }); + + return draggedPhoto; +}; + +const movePhotoToColumn = (targetColumnIndex: number) => { + if (draggingPhotoId.value === null) return; + const columnsSnapshot = cloneColumns(tempColumns.value); + const draggedPhoto = takeDraggedPhoto(columnsSnapshot); + if (!draggedPhoto) return; + columnsSnapshot[targetColumnIndex]?.push(draggedPhoto); + emitColumns(columnsSnapshot); +}; + +const movePhotoBeforeTarget = (targetColumnIndex: number, targetIndex: number) => { + if (draggingPhotoId.value === null) return; + const columnsSnapshot = cloneColumns(tempColumns.value); + const draggedPhoto = takeDraggedPhoto(columnsSnapshot); + if (!draggedPhoto) return; + const targetColumn = columnsSnapshot[targetColumnIndex]; + if (!targetColumn) return; + + const insertIndex = Math.min(Math.max(targetIndex, 0), targetColumn.length); + targetColumn.splice(insertIndex, 0, draggedPhoto); + emitColumns(columnsSnapshot); +}; + +const resetPhotoOrder = () => { + if (!props.initialColumns.length) return; + emitColumns(cloneColumns(props.initialColumns)); +};