diff --git a/miniprogram/components/journal-detail-popup/index.less b/miniprogram/components/journal-detail-popup/index.less index b1b9ebe..6b2206b 100644 --- a/miniprogram/components/journal-detail-popup/index.less +++ b/miniprogram/components/journal-detail-popup/index.less @@ -131,17 +131,19 @@ height: 100%; .items { + gap: 8rpx; + display: flex; padding: 0 32rpx 128rpx 32rpx; - - .wrapper { - column-gap: 8rpx; - column-count: 3; - padding-bottom: 128rpx; + align-items: flex-start; + + .column { + flex: 1; + display: flex; + flex-direction: column; .item { overflow: hidden; background: var(--theme-bg-card); - break-inside: avoid; margin-bottom: 8rpx; &.thumbnail { diff --git a/miniprogram/components/journal-detail-popup/index.ts b/miniprogram/components/journal-detail-popup/index.ts index 3c9b261..713cbf6 100644 --- a/miniprogram/components/journal-detail-popup/index.ts +++ b/miniprogram/components/journal-detail-popup/index.ts @@ -1,7 +1,8 @@ // components/journal-detail-panel/index.ts import { Journal } from "../../types/Journal"; import config from "../../config/index"; -import { MediaAttachExt, MediaAttachType } from "../../types/Attachment"; +import Toolkit from "../../utils/Toolkit"; +import { ImageMetadata, MediaAttachExt, MediaAttachType } from "../../types/Attachment"; import { MediaItem, MediaItemType } from "../../types/UI"; import Time from "../../utils/Time"; @@ -61,7 +62,8 @@ Component({ if (!thumbItems) { return; } - const mediaItems: MediaItem[] = thumbItems.map((thumbItem) => { + const mediaItems: MediaItem[] = thumbItems.map((thumbItem, index) => { + const metadata = thumbItem.metadata as ImageMetadata; const ext = thumbItem.ext = JSON.parse(thumbItem.ext!.toString()) as MediaAttachExt; const thumbURL = `${config.url}/attachment/read/${thumbItem.mongoId}`; const sourceURL = `${config.url}/attachment/read/${ext.sourceMongoId}`; @@ -70,10 +72,21 @@ Component({ thumbURL, sourceURL, size: thumbItem.size || 0, - attachmentId: thumbItem.id + attachmentId: thumbItem.id, + width: metadata.width, + height: metadata.height, + originalIndex: index } as MediaItem; }); journal.mediaItems = mediaItems; + // 为每个 journal 添加分列后的 mediaItems + (journal as any).columnedMediaItems = Toolkit.splitItemsIntoColumns(mediaItems, 3, (item) => { + // 计算相对高度:如果有宽高信息,使用宽高比;否则假设为正方形 + if (item.width && item.height && 0 < item.width) { + return item.height / item.width; + } + return 1; // 默认正方形 + }); }) this.setData({ journals, @@ -119,14 +132,15 @@ Component({ }, /** 预览媒体 */ previewMedia(e: WechatMiniprogram.BaseEvent) { - const journals = this.properties.journals as Journal[]; + const journals = this.data.journals; if (!journals || journals.length === 0) { return; } const { itemIndex } = e.currentTarget.dataset; - const items = journals[this.data.currentJournalIndex].mediaItems!; + const journal = journals[this.data.currentJournalIndex]; + const items = journal.mediaItems!; const total = items.length; - + const startIndex = Math.max(0, itemIndex - 25); const endIndex = Math.min(total, startIndex + 50); const newCurrentIndex = itemIndex - startIndex; @@ -134,7 +148,7 @@ Component({ const sources = items.slice(startIndex, endIndex).map((item) => { return { url: item.sourceURL, - type: item.type === 0 ? "image" : "video" + type: item.type === MediaItemType.IMAGE ? "image" : "video" } }) as any; wx.previewMedia({ diff --git a/miniprogram/components/journal-detail-popup/index.wxml b/miniprogram/components/journal-detail-popup/index.wxml index 8499210..76df922 100644 --- a/miniprogram/components/journal-detail-popup/index.wxml +++ b/miniprogram/components/journal-detail-popup/index.wxml @@ -40,16 +40,16 @@ {{item.datetime}} {{item.idea}} - + - - + + diff --git a/miniprogram/pages/main/journal/index.less b/miniprogram/pages/main/journal/index.less index 4bf91c3..3c4fd65 100644 --- a/miniprogram/pages/main/journal/index.less +++ b/miniprogram/pages/main/journal/index.less @@ -78,37 +78,43 @@ } .items { - column-gap: .25rem; - column-count: 3; + gap: .25rem; + display: flex; padding-bottom: 2rem; + align-items: flex-start; - .item { - overflow: hidden; - background: var(--theme-bg-card); - break-inside: avoid; - margin-bottom: .25rem; + .column { + flex: 1; + display: flex; + flex-direction: column; - &.thumbnail { - width: 100%; - display: block; - } + .item { + overflow: hidden; + background: var(--theme-bg-card); + margin-bottom: .25rem; - &.video { - height: auto; - position: relative; + &.thumbnail { + width: 100%; + display: block; + } - &::after { - content: ""; - top: 50%; - left: 53%; - width: 0; - height: 0; - position: absolute; - transform: translate(-50%, -50%); - border-top: 16px solid transparent; - border-left: 24px solid var(--theme-video-play); - border-bottom: 16px solid transparent; - pointer-events: none; + &.video { + height: auto; + position: relative; + + &::after { + content: ""; + top: 50%; + left: 53%; + width: 0; + height: 0; + position: absolute; + transform: translate(-50%, -50%); + border-top: 16px solid transparent; + border-left: 24px solid var(--theme-video-play); + border-bottom: 16px solid transparent; + pointer-events: none; + } } } } diff --git a/miniprogram/pages/main/journal/index.ts b/miniprogram/pages/main/journal/index.ts index b45a030..dfce207 100644 --- a/miniprogram/pages/main/journal/index.ts +++ b/miniprogram/pages/main/journal/index.ts @@ -3,27 +3,11 @@ import Time from "../../../utils/Time"; import config from "../../../config/index" import Events from "../../../utils/Events"; -import { JournalPage, JournalPageType } from "../../../types/Journal"; -import { OrderType } from "../../../types/Model"; - -export type Journal = { - date: string; - location?: string; - lat?: number; - lng?: number; - idea?: string; - items: JournalItem[] -} - -export type JournalItem = { - type: JournalItemType; - mongoId: string; -} - -export enum JournalItemType { - IMAGE, - VIDEO -} +import Toolkit from "../../../utils/Toolkit"; +import { Journal, JournalPage, JournalPageType } from "../../../types/Journal"; +import { OrderType, QueryPageResult } from "../../../types/Model"; +import { ImageMetadata, MediaAttachExt } from "../../../types/Attachment"; +import { MediaItem, MediaItemType } from "../../../types/UI"; interface JournalData { page: JournalPage; @@ -174,30 +158,41 @@ Page({ }, data: this.data.page, success: async (resp: any) => { - const list = resp.data.data.list; + const pageResult = resp.data.data as QueryPageResult; + const list = pageResult.list; if (!list || list.length === 0) { this.setData({ isFinished: true }) return; } - const result = list.map((journal: any) => { - return { - date: Time.toPassedDateTime(journal.createdAt), - idea: journal.idea, - lat: journal.lat, - lng: journal.lng, - location: journal.location, - items: journal.items.filter((item: any) => item.attachType === "THUMB").map((item: any) => { - const ext = JSON.parse(item.ext); - return { - type: ext.isVideo ? JournalItemType.VIDEO : JournalItemType.IMAGE, - thumbUrl: `${config.url}/attachment/read/${item.mongoId}`, - mongoId: item.mongoId, - source: journal.items.find((source: any) => source.id === ext.sourceId) - } - }) - } + list.forEach(journal => { + const mediaItems = journal.items!.filter((item) => item.attachType === "THUMB").map((thumbItem, index) => { + const metadata = thumbItem.metadata as ImageMetadata; + const ext = thumbItem.ext = JSON.parse(thumbItem.ext!.toString()) as MediaAttachExt; + const thumbURL = `${config.url}/attachment/read/${thumbItem.mongoId}`; + const sourceURL = `${config.url}/attachment/read/${ext.sourceMongoId}`; + return { + type: ext.isVideo ? MediaItemType.VIDEO : MediaItemType.IMAGE, + thumbURL, + sourceURL, + size: thumbItem.size || 0, + attachmentId: thumbItem.id, + width: metadata.width, + height: metadata.height, + originalIndex: index + } as MediaItem; + }); + journal.date = Time.toDate(journal.createdAt); + journal.time = Time.toTime(journal.createdAt); + journal.datetime = Time.toPassedDateTime(journal.createdAt); + journal.mediaItems = mediaItems; + journal.columnedItems = Toolkit.splitItemsIntoColumns(mediaItems, 3, (item) => { + if (item.width && item.height && 0 < item.width) { + return item.height / item.width; + } + return 1; + }) }) this.setData({ page: { @@ -211,7 +206,7 @@ Page({ createdAt: OrderType.DESC } }, - list: this.data.list.concat(result), + list: this.data.list.concat(list), isFinished: list.length < this.data.page.size }); }, @@ -224,17 +219,18 @@ Page({ }, preview(e: WechatMiniprogram.BaseEvent) { const { journalIndex, itemIndex } = e.currentTarget.dataset; - const items = this.data.list[journalIndex].items; + const journal = this.data.list[journalIndex]; + const items = journal.mediaItems!; const total = items.length; - + const startIndex = Math.max(0, itemIndex - 25); const endIndex = Math.min(total, startIndex + 50); const newCurrentIndex = itemIndex - startIndex; - const sources = items.slice(startIndex, endIndex).map((item: any) => { + const sources = items.slice(startIndex, endIndex).map((item) => { return { - url: `${config.url}/attachment/read/${item.source.mongoId}`, - type: item.type === 0 ? "image" : "video" + url: item.sourceURL, + type: item.type === MediaItemType.IMAGE ? "image" : "video" } }) as any; wx.previewMedia({ diff --git a/miniprogram/pages/main/journal/index.wxml b/miniprogram/pages/main/journal/index.wxml index 46b472d..bcb42d9 100644 --- a/miniprogram/pages/main/journal/index.wxml +++ b/miniprogram/pages/main/journal/index.wxml @@ -37,17 +37,19 @@ {{journal.location}} - - - - + + + + + + 已回到最初的起点 diff --git a/miniprogram/types/Attachment.ts b/miniprogram/types/Attachment.ts index 8a9b50e..74839ba 100644 --- a/miniprogram/types/Attachment.ts +++ b/miniprogram/types/Attachment.ts @@ -15,6 +15,10 @@ export type Attachment = { /** 文件名 */ name: string; + mimeType?: string; + + metadata?: string | ImageMetadata; + /** 文件 MD5 */ md5: string; @@ -38,6 +42,16 @@ export enum MediaAttachType { THUMB = "THUMB" } +/** 图片附件元数据 */ +export type ImageMetadata = { + + /** 宽度 */ + width: number; + + /** 高度 */ + height: number; +} + /** 媒体附件扩展数据 */ export type MediaAttachExt = { @@ -52,4 +66,10 @@ export type MediaAttachExt = { /** true 为视频 */ isVideo: boolean; + + /** 原图宽度(像素) */ + width?: number; + + /** 原图高度(像素) */ + height?: number; } diff --git a/miniprogram/types/Journal.ts b/miniprogram/types/Journal.ts index 33f6a66..1d6f844 100644 --- a/miniprogram/types/Journal.ts +++ b/miniprogram/types/Journal.ts @@ -31,11 +31,14 @@ export type Journal = { /** 时间 */ time?: string; - /** 时间 */ + /** 详细时间 */ datetime?: string; /** 附件(照片、视频等) */ items?: Attachment[]; + + /** 分列后的 items,用于瀑布流展示 */ + columnedItems?: MediaItem[][]; /** 媒体项(由附件转) */ mediaItems?: MediaItem[]; diff --git a/miniprogram/types/UI.ts b/miniprogram/types/UI.ts index 12b6c4f..072907b 100644 --- a/miniprogram/types/UI.ts +++ b/miniprogram/types/UI.ts @@ -18,6 +18,15 @@ export type MediaItem = { /** 附件 ID */ attachmentId: number; + + /** 原图宽度 */ + width?: number; + + /** 原图高度 */ + height?: number; + + /** 原始索引(用于预览时定位) */ + originalIndex?: number; } /** 微信媒体项目 */ diff --git a/miniprogram/utils/Toolkit.ts b/miniprogram/utils/Toolkit.ts index 20a29e8..0aa0d6a 100644 --- a/miniprogram/utils/Toolkit.ts +++ b/miniprogram/utils/Toolkit.ts @@ -220,10 +220,58 @@ export default class Toolkit { public static symmetricDiff(arr1: any[], arr2: any[]) { const set1 = new Set(arr1); const set2 = new Set(arr2); - + return [ ...arr1.filter(item => !set2.has(item)), ...arr2.filter(item => !set1.has(item)) ]; } + + /** + * 将数组元素分配到指定数量的列中 + * + * - 如果提供 getHeight 函数:使用贪心算法,每次将元素放到当前高度最小的列(适合瀑布流布局) + * - 如果不提供 getHeight:使用轮询方式分配(第 1、4、7... 个到列 1,第 2、5、8... 个到列 2) + * + * @param items 原始数组 + * @param columnCount 列数,默认 3 + * @param getHeight 可选的高度获取函数,用于计算每个元素的高度 + * @returns 分列后的二维数组 + * + * @example + * // 简单轮询分配 + * splitItemsIntoColumns([1, 2, 3, 4, 5, 6], 3) + * // => [[1, 4], [2, 5], [3, 6]] + * + * @example + * // 基于高度的贪心分配 + * const items = [{ h: 100 }, { h: 200 }, { h: 150 }] + * splitItemsIntoColumns(items, 2, item => item.h) + * // => [[ { h: 100 }, { h: 150 }], [{ h: 200 }]] + */ + public static splitItemsIntoColumns( + items: T[], + columnCount = 3, + getHeight?: (item: T) => number + ): T[][] { + const columns: T[][] = Array.from({ length: columnCount }, () => []); + + if (!getHeight) { + // 降级为轮询分配 + items.forEach((item, index) => { + columns[index % columnCount].push(item); + }); + } else { + // 使用贪心算法:总是放到当前高度最小的列 + const columnHeights = Array(columnCount).fill(0); + items.forEach((item) => { + // 找到高度最小的列 + const minHeightIndex = columnHeights.indexOf(Math.min(...columnHeights)); + columns[minHeightIndex].push(item); + columnHeights[minHeightIndex] += getHeight(item); + }); + } + + return columns; + } }