diff --git a/entry/src/main/ets/pages/trips/FbTripDetailPage.ets b/entry/src/main/ets/pages/trips/FbTripDetailPage.ets index ddcfc39..b011fb7 100644 --- a/entry/src/main/ets/pages/trips/FbTripDetailPage.ets +++ b/entry/src/main/ets/pages/trips/FbTripDetailPage.ets @@ -293,11 +293,16 @@ export struct FbTripDetailPage { /* ---- v76:照片查看器 / 视频播放器 ---- 🔴 用户 2026-10-22:「里面的照片要可以点击查看,视频要能够播放。」 ⚠️ 两个都是**全屏覆盖层**,所以用"非空即开着"的语义 - (`viewerKeys` 空 / `videoKey` 空 = 关着),不用额外的布尔开关 —— + (`viewerPhotos` 空 / `videoKey` 空 = 关着),不用额外的布尔开关 —— 少一个开关就少一处"关了浮层忘了复位"的漏洞。 */ - /** 查看器里的图片键列表(空 = 关着) */ - @Local viewerKeys: string[] = []; + /** 查看器里的媒体列表(v87:**图片+视频混排**,空 = 关着)。 + * 🔴 用户 2026-10-24:「图片和视频是不是可以都支持滑动…滑动到视频的时候, + * 点击就直接播放。」⇒ 列表不再是纯键数组,要带 `mime` 才分得清图片/视频。 */ + @Local viewerPhotos: FbTripNodePhoto[] = []; @Local viewerIndex: number = 0; + /** v87:Swiper 控制器 —— 底部「上一张/下一张」走 `showPrevious()/showNext()`, + * 与手势翻页同一条动画通道(直接改 index 是瞬跳,又要回到"生硬"的老毛病)。 */ + private viewerCtl: SwiperController = new SwiperController(); /** 正在播放的视频键(空 = 关着) */ @Local videoKey: string = ''; @Local videoName: string = ''; @@ -328,8 +333,8 @@ export struct FbTripDetailPage { this.videoName = ''; return true; } - if (this.viewerKeys.length > 0) { - this.viewerKeys = []; + if (this.viewerPhotos.length > 0) { + this.viewerPhotos = []; this.viewerIndex = 0; return true; } @@ -398,9 +403,9 @@ export struct FbTripDetailPage { /* v76:全屏查看器 / 播放器也要让底部悬浮栏让位(否则大图底下漏出一条悬浮栏)。 ⚠️ 同样是"一个 @Monitor 盯一个变量",两个分开写 —— 它们互斥,直接赋值即可。 */ - @Monitor('viewerKeys') + @Monitor('viewerPhotos') onViewerChanged(): void { - fbAppState().sheetCount = this.viewerKeys.length > 0 ? 1 : 0; + fbAppState().sheetCount = this.viewerPhotos.length > 0 ? 1 : 0; } @Monitor('videoKey') @@ -408,26 +413,19 @@ export struct FbTripDetailPage { fbAppState().sheetCount = this.videoKey !== '' ? 1 : 0; } - /** 打开某一组图片的大图查看器(从第几张开始) */ - private openViewer(keys: string[], at: number): void { - if (keys.length === 0) { - return; - } - this.viewerKeys = keys; - // ⚠️ 必须夹到 [0, n):点第 3 张时若列表刚被删短,直接赋会越界 - this.viewerIndex = at < 0 ? 0 : (at >= keys.length ? keys.length - 1 : at); - } - /** - * 查看器翻页。 - * ⚠️ 取模归一到 [0, n):直接加减会让 index 越界或变负数(相册里踩过)。 + * 打开某一段行程的大图查看器(从第 `at` 个开始)。 + * + * 🔴 v87:传入的是**图片+视频混排**的整列表(`mediaOf`),不再是纯图片 —— + * 左右滑动能一路划到视频,划到视频那页点一下就播。 */ - private viewerStep(delta: number): void { - const n: number = this.viewerKeys.length; - if (n === 0) { + private openViewer(photos: FbTripNodePhoto[], at: number): void { + if (photos.length === 0) { return; } - this.viewerIndex = ((this.viewerIndex + delta) % n + n) % n; + this.viewerPhotos = photos; + // ⚠️ 必须夹到 [0, n):点第 3 张时若列表刚被删短,直接赋会越界 + this.viewerIndex = at < 0 ? 0 : (at >= photos.length ? photos.length - 1 : at); } /** 打开一个视频(`key` 是裸 key,播放地址现拼,不带缓存) */ @@ -657,7 +655,7 @@ export struct FbTripDetailPage { /* 🔴 v76:照片查看器 / 视频播放器放在**最后一个位置**。 在 `Stack` 里"后写的盖在上面",所以它压住所有浮层 —— 用户点了照片 就该直接看到大图,而不是"点开又冒出一个浮层把它盖住"。 */ - if (this.viewerKeys.length > 0) { + if (this.viewerPhotos.length > 0) { this.photoViewer() } if (this.videoKey !== '') { @@ -1258,7 +1256,9 @@ export struct FbTripDetailPage { .margin({ right: 6, bottom: 6 }) .onClick((): void => { // 点第几张就从第几张开始看(不是每次都从第一张) - this.openViewer(this.imageKeysOf(n), this.imagesOf(n).indexOf(ph)); + // 🔴 v87:给的是**图片+视频混排**的整列表 —— 滑动能划到视频 + const all: FbTripNodePhoto[] = this.mediaOf(n); + this.openViewer(all, all.indexOf(ph)); }) }, (ph: FbTripNodePhoto): string => 'ph_' + ph.id.toString()) @@ -1375,32 +1375,76 @@ export struct FbTripDetailPage { .height('100%') .backgroundColor('#E6000000') .onClick((): void => { - this.viewerKeys = []; + this.viewerPhotos = []; this.viewerIndex = 0; }) - // ② 大图 + 左右滑翻页 - Stack({ alignContent: Alignment.Center }) { - Image(fbImageSrc(this.viewerKeys[this.viewerIndex], FB_TOKEN_STORE.get())) - .width('100%') - .height('100%') - .objectFit(ImageFit.Contain) - .gesture( - PanGesture({ direction: PanDirection.Horizontal }) - .onActionEnd((event: GestureEvent): void => { - const dx: number = event.offsetX; - const dy: number = event.offsetY; - // 位移太小 = 手抖;竖着划 = 用户想上下看,不该翻页 - if (Math.abs(dx) < 40 || Math.abs(dx) <= Math.abs(dy)) { - return; - } - // 左滑看下一张,右滑看上一张(与相册一致) - this.viewerStep(dx < 0 ? 1 : -1); - }) - ) + /* ② 媒体翻页(v87:Swiper) + 🔴 用户 2026-10-24:「图片和视频是不是可以都支持滑动…滑动到视频的时候, + 点击就直接播放。另外滑动的时候有没有个动画呀?现在整个一下就变化,特别生硬。」 + ⚠️ 原来是单张 `Image` + `PanGesture`,松手直接换 index ⇒ "整个一下就变化"。 + `Swiper` 的滑动**跟手**,松手(或 `changeIndex`)自带缓动动画, + 不用自己写位移插值;`.duration()` 调换页动画时长。 + ⚠️ 视频也是翻页项(`viewerPhotos` 本来就是混排):滑到视频那页,点一下 + 走 `openVideo()`,原 `videoPlayer` 全屏播放器(系统控件)原样叠上来。 + ⚠️ `loop(true)`:首尾相接,与旧版"取模循环翻页"行为一致(Swiper 自己处理回绕)。 */ + Swiper(this.viewerCtl) { + ForEach(this.viewerPhotos, (m: FbTripNodePhoto) => { + if (fbPhotoKind(m) === 'video') { + /* 视频页:没有服务端封面 ⇒ 深色底 + ▶ + 文件名 + 一句"点一下播放", + 比放一张坏图诚实。点它才播(用户:滑动到视频的时候,点击就直接播放)。 */ + Stack({ alignContent: Alignment.Center }) { + Column() + .width('100%') + .height('100%') + .backgroundColor('#2A2320') + Column({ space: 10 }) { + Text('▶') + .fontSize(44) + .fontColor('#FFFFFFCC') + Text(m.name !== '' ? m.name : '视频') + .fontSize(FbMetric.fontSmall) + .fontColor('#FFFFFF99') + .maxLines(2) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .textAlign(TextAlign.Center) + Text('点一下播放') + .fontSize(FbMetric.fontTiny) + .fontColor('#FFFFFF66') + } + .padding(20) + .alignItems(HorizontalAlign.Center) + } + .width('100%') + .height('100%') + .borderRadius(10) + .clip(true) + .onClick((): void => { + this.openVideo(m); + }) + } else { + Image(fbImageSrc(m.key, FB_TOKEN_STORE.get())) + .width('100%') + .height('100%') + .objectFit(ImageFit.Contain) + } + }, (m: FbTripNodePhoto): string => 'vw_' + m.id.toString()) } + .loop(true) + .duration(350) + .cachedCount(1) .width('100%') .height('76%') + .onChange((i: number): void => { + /* 手势与按钮翻页统一在这里落 index(动画结束才回调 ⇒ 底部计数不闪)。 */ + this.viewerIndex = i; + }) + .onAppear((): void => { + /* ⚠️ Swiper 挂载时只能从第 0 页开始 ⇒ 打开瞬间跳到用户点的那张。 + `useAnimation=false`:**开查看器**这一下不该放翻页动画(否则看起来 + 像"从第一张滑过去")。之后的手势/按钮翻页都走 Swiper 自带动画。 */ + this.viewerCtl.changeIndex(this.viewerIndex, false); + }) /* ③ 顶部关闭 + 底部翻页条 🔴🔴 v76 真机实测踩到:原来这两块各自写 `.align(Alignment.Bottom)` / @@ -1412,6 +1456,8 @@ export struct FbTripDetailPage { ⚠️ `hitTestBehavior(Transparent)` 必须有:这一层是**透明的**, 它只负责摆按钮的位置;中间大片空白要让点击穿下去给 ① 的遮罩 (否则"点背景关闭"失效)。Transparent = 自身与子节点响应、不阻挡兄弟。 + ⚠️ v87:‹ › 按钮走 `viewerCtl.showPrevious()/showNext()` —— 与手势同一条 + 动画通道;直接改 `viewerIndex` 是瞬跳(Swiper 也不认外部改 index)。 */ Column() { Row() { @@ -1427,7 +1473,7 @@ export struct FbTripDetailPage { .padding({ left: 18, right: 6, top: 8, bottom: 8 }) .alignItems(HorizontalAlign.End) .onClick((): void => { - this.viewerKeys = []; + this.viewerPhotos = []; this.viewerIndex = 0; }) } @@ -1441,9 +1487,9 @@ export struct FbTripDetailPage { .fontColor('#FFFFFFCC') .padding({ left: 12, right: 12, top: 8, bottom: 8 }) .onClick((): void => { - this.viewerStep(-1); + this.viewerCtl.showPrevious(); }) - Text((this.viewerIndex + 1).toString() + ' / ' + this.viewerKeys.length.toString()) + Text((this.viewerIndex + 1).toString() + ' / ' + this.viewerPhotos.length.toString()) .fontSize(FbMetric.fontTiny) .fontColor('#FFFFFF99') Text('下一张 ›') @@ -1451,7 +1497,7 @@ export struct FbTripDetailPage { .fontColor('#FFFFFFCC') .padding({ left: 12, right: 12, top: 8, bottom: 8 }) .onClick((): void => { - this.viewerStep(1); + this.viewerCtl.showNext(); }) } .width('100%') @@ -1681,12 +1727,12 @@ export struct FbTripDetailPage { return n.attachs.filter((a: FbTripNodePhoto): boolean => fbPhotoKind(a) === 'video' && a.key !== ''); } - /** 查看器要用的键列表(与 `imagesOf` 顺序严格一致 —— 索引要对得上) */ - private imageKeysOf(n: FbTripNode): string[] { - const out: string[] = []; - for (const a of this.imagesOf(n)) { - out.push(a.key); - } - return out; + /** + * 查看器要用的整份媒体列表(v87)。 + * 🔴 图片在前、视频在后 —— 与缩略图墙的铺排顺序严格一致,`indexOf(ph)` 才对得上。 + * 混排后左右滑动就能从图片一路划到视频(用户 2026-10-24 的原话)。 + */ + private mediaOf(n: FbTripNode): FbTripNodePhoto[] { + return this.imagesOf(n).concat(this.videosOf(n)); } } \ No newline at end of file