v76 旅程:信息铺得开 + 每段行程各自成卡 + 照片可点开 / 视频可播

用户 2026-10-22(四条毛病,同一个根因:信息往一行里挤):
「那个城市……如果一行显不下的话,应该显示在下边一行,把所有的城市全列出来。
  就跟那个人员一样,你人员特别多的时候,一行显示不下,就直接往下放就行了。」
「点进详情之后……现在人员显示不下的话,你直接都放在右边,都溢出到这个块外面去了……
  应该直接顺着往下铺就行了,所以它长一点无所谓。」
「实际行程它应该也是一段一段的,不要把它整个实际行程放在一块,
  就是每一个行程都是一个一个块。然后里面的照片要可以点击查看,视频要能够播放。」

根因不是"忘了加滚动",而是 **`Row` 在 ArkUI 里不换行**:
塞不下的子节点会被压缩、或直接横向溢出父容器。四处都改成
`Flex({ wrap: FlexWrap.Wrap })`。

- `Types.ets`:`FbTripItem` 新增 `cities: string[]`(整串只能截断);
  `FbTripNodePhoto` 新增 `mime` / `size`
- `FbParse.parseTrips` 新增 `cityListOf()`:优先用服务端 `cities` 数组,
  只给整串时按 `·` 拆回来("显示不全"不再从别的入口漏进来)
- 列表卡:城市从「📍 一行长串 + 省略号」改成一颗颗 chip,日期单独占一行
- 详情页:城市 / 参与的人 / 每段的参与人 全部改 `Flex wrap`
  (实测「黑龙江」那趟 5 个人自动排成 2 行,不再溢出卡片)
- **每段行程各自一张卡**:组头退成章节标题(不带卡片底),
  `nodeCard` 自己带白底 + 圆角 + 左侧阶段色条(取 `n.stage`)
- 照片:点缩略图开全屏查看器(`PanGesture` 左右滑、上一张/下一张、关闭)
- 视频:按 `mime` 前缀认出视频(`is_image` 只有两态),缩略块 + ▶ 角标,
  点开用系统 `Video` 播放(`media.php` 支持 Range,可拖进度条)

🔴 真机抓到的布局 bug:`Stack` 子节点上的 `.align(Alignment.Bottom/TopEnd)`
  **完全无效** —— 关闭按钮和翻页条双双掉到屏幕正中。改成铺满的 Column +
  `layoutWeight(1)` 撑开,并给控制层 `hitTestBehavior(Transparent)`
  保证"点背景关闭"仍穿透。

验证:真机 33/0(含"城市 chip 数 == 统计里个城市"、"相邻行程卡有实际间隙"、
"点视频块起播放器且无放不出来");守卫 953/0;反向验证 181/0
(其中一条注入暴露了我新写断言的假绿,已改成结构化顺序判据)。
This commit is contained in:
2026-10-22 09:02:16 +08:00
parent 8c92195558
commit 79e942d0e4
6 changed files with 484 additions and 53 deletions
+39 -1
View File
@@ -271,7 +271,7 @@ export function parseTrips(json: string): FbTripItem[] {
const rows: RawTrip[] = asArray<RawTrip>(json);
const out: FbTripItem[] = [];
for (const r of rows) {
const cities: string[] = arr<string>(r.cities);
const cities: string[] = cityListOf(r);
const dayCount: number = daySpan(s(r.starts_at), s(r.ends_at));
const it: FbTripItem = {
id: n(r.id),
@@ -285,6 +285,7 @@ export function parseTrips(json: string): FbTripItem[] {
⚠️ `cover_key` 存**原始键**,拼地址交给 `fbMediaUrl()`(见 FbTripItem 的说明)。 */
coverKey: s(r.cover_key),
city: s(r.city) !== '' ? s(r.city) : cities.join(' · '),
cities: cities,
summary: s(r.summary),
attendeeCount: n(r.attendee_count),
};
@@ -293,6 +294,43 @@ export function parseTrips(json: string): FbTripItem[] {
return out;
}
/**
* 城市的**列表**(卡片上一颗颗 chip 换行用)。
*
* 🔴 v76(用户 2026-10-22):「如果一行显不下的话,应该显示在下边一行,
* 把所有的城市全列出来。」
* ⇒ 端上必须要数组,不能只有一个 `city` 整串 —— 整串只能截断。
*
* ⚠️ 服务端 `Trip::peek()` 同时给 `cities`(数组,`Trip::cities()` 的原值)
* 与 `city`(`implode(' · ', $cities)`)。**优先用数组**;
* 万一将来某条接口只给整串(老版本 / 单条接口),按同一个分隔符拆回来,
* 这样"显示不全"这个毛病不会再从别的入口漏进来。
*/
function cityListOf(r: RawTrip): string[] {
const arr0: string[] = arr<string>(r.cities);
const out: string[] = [];
for (const c of arr0) {
const t: string = c.trim();
if (t !== '' && out.indexOf(t) < 0) {
out.push(t);
}
}
if (out.length > 0) {
return out;
}
const whole: string = s(r.city);
if (whole === '') {
return out;
}
for (const p of whole.split('·')) {
const t: string = p.trim();
if (t !== '' && out.indexOf(t) < 0) {
out.push(t);
}
}
return out;
}
/** 纪念日 → FbAnniversaryItem */
export function parseDays(json: string): FbAnniversaryItem[] {
const rows: RawDay[] = asArray<RawDay>(json);
+6 -1
View File
@@ -405,7 +405,12 @@ function attachsOf(o: Record<string, Object>, k: string): FbTripNodePhoto[] {
id: numOf(a, 'id'),
key: key,
name: strOf(a, 'name'),
isImage: numOf(a, 'is_image') === 1
isImage: numOf(a, 'is_image') === 1,
/* 🔴 v76:`mime` / `size` 是「照片可点开、视频能播放」的前提。
⚠️ 服务端 `Attachment::list()` 是 `SELECT *`,这两列本来就有;
以前端上不取,所以只能靠 `is_image` 二选一(视频 vs 图片分不开)。 */
mime: strOf(a, 'mime'),
size: numOf(a, 'size')
});
}
return out;