观影详情卡:对齐、去「写评价」、补编辑入口与完整编辑 (#80)
用户:评价与评论和上面的观影照片对齐吗?下面的写评价不要了,三个字。
另外一个,是不是得有个地方放编辑按钮啊?观影其实可以编辑……后面会补几张照片。
但编辑这个功能应该还是有的。
① 对齐:FbSectionTitle 自带 padding left:4,简介/观影照片是裸 Text 无内边距,
实测评价与评论起点 111 vs 观影照片 99(差 4vp)。用 margin left:-4 抵消。
不改用裸 Text —— 那样会丢掉全站统一的字号/字重/字距/胶囊按钮口径。
② 去「写评价」:FbMovieReviewSheet 加 hideFormTitle;无评价时整行不画,
但有评价时保留那一行(右边的修改/删除/收起按钮在那)。
③ 编辑入口 + 完整编辑(用户选定:按钮放卡片最底部、做完整编辑):
- 服务端 PATCH /api/v1/movies/<id> 本来就有(movieData 支持缺省回退原值),
这轮只补端上缺的那层;
- FbMovieRepo 新增 updateMovie() → PATCH,成功后重拉列表回写本地库;
- FbMovieNewSheet 加 editingId/editingDetail,aboutToAppear 走 prefill 回填,
submit 按模式分派 POST/PATCH;标题/按钮文案随模式切换;
- 宿主(首页 + 电影页)详情卡 onEdit → 关卡 + 同一张表单编辑;
- 编辑时不发 poster_key(发空串会清掉已有海报);
海报区显示「当前海报 / 不选新图就保持不变」,避免用户以为图丢了白传一次。
两个真坑:
1) if/else if 条件里不能调方法:curPoster() 探针打出来是对的,
界面却一直走 else —— ArkUI V2 只对字段/参数建依赖。改读 @Local 字段。
2) Row { Text().width(100%); Row {...} } 会内层挤出:posterField 原来就是这个结构,
标签吃满整行 ⇒ 内层塌成 0 高、完全不渲染(症状是图/字凭空消失)。
按全局表单规范改两排。
验证(真机 5NC0225B27004718):评价与评论 x 起点 98 vs 观影照片 99 已对齐;
「写评价」不再出现;编辑按钮在 y=1987;点开标题为「编辑观影记录」,
片名/时间(10月9日 16:20)/简介全部回填;海报区三行文案齐全,
缩略图区域色数 13567(真图)。hvigorw assembleHap BUILD SUCCESSFUL。
This commit is contained in:
@@ -42,10 +42,12 @@
|
||||
|
||||
import { FbColors, FbMetric, fbAlpha } from '../common/Theme';
|
||||
import { FbModuleDef, fbModuleOf, fbThemeVars } from '../model/Modules';
|
||||
import { FbCinemaItem, FbDoubanFetch, FbDoubanSuggest, FbMovieDraft, FbMovieRepo } from '../data/FbMovieRepo';
|
||||
import { FbCinemaItem, FbDoubanFetch, FbDoubanSuggest, FbMovieAttendeeRow, FbMovieDetail, FbMovieDraft,
|
||||
FbMovieRepo } from '../data/FbMovieRepo';
|
||||
import { FbRosterPerson, FbPersonRepo } from '../data/FbPersonRepo';
|
||||
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
|
||||
import { fbApiBase, fbImageUrl } from '../data/FbHttp';
|
||||
import { fbImageSrc } from '../data/FbAttachQueue';
|
||||
import { FbSheetRing } from './FbSheetRing';
|
||||
import { FbSheetClose, FbSheetTitleBar } from './FbSheetClose';
|
||||
import { FbCinemaPickSheet } from './FbCinemaPickSheet';
|
||||
@@ -88,6 +90,24 @@ export struct FbMovieNewSheet {
|
||||
@Param accent: string = '';
|
||||
/** 登录者 id(**默认把"我"选进同看人**,与网页端 v38 同口径;0 = 不知道) */
|
||||
@Param meId: number = 0;
|
||||
/**
|
||||
* 编辑模式:要修改的记录 id(0 = 新建)。
|
||||
*
|
||||
* 🔴 用户 2026-11-06:「观影其实是可以编辑的……后面会补几张照片。
|
||||
* 但编辑这个功能应该还是有的。」
|
||||
*
|
||||
* ⇒ 表单改成双用途:新建(`editingId = 0`)与编辑(传具体 id)。
|
||||
* 同一套字段、同一套 UI,只是**保存时走不同的接口**:
|
||||
* · 新建 → `POST /api/v1/movies`(`createMovie`)
|
||||
* · 编辑 → `PATCH /api/v1/movies/<id>`(`updateMovie`)
|
||||
*
|
||||
* ⚠️ 为什么不另写一个 `FbMovieEditSheet`:这张表单有 300+ 行
|
||||
* (豆瓣联想、地址联想、影院/人选择器、海报与照片多选、日期时间控件),
|
||||
* 抄一份必然"改一处漏一处"(本项目已多次踩过)。
|
||||
*/
|
||||
@Param editingId: number = 0;
|
||||
/** 编辑时用来回填的原始详情(`FbMovieDetail`;新建时为 null) */
|
||||
@Param editingDetail: FbMovieDetail | null = null;
|
||||
|
||||
// ---- 表单字段 ----
|
||||
@Local title: string = '';
|
||||
@@ -101,6 +121,14 @@ export struct FbMovieNewSheet {
|
||||
@Local note: string = '';
|
||||
/** 豆瓣抓到的海报键(保存时随记录提交;端上另选的海报走单独上传) */
|
||||
@Local doubanPosterKey: string = '';
|
||||
/**
|
||||
* 编辑模式下**这条记录已有的**海报键(只用于展示,保存时不提交)。
|
||||
*
|
||||
* ⚠️ 与 `doubanPosterKey` 分开存:那个的语义是"这次要写进库的键",
|
||||
* 而这个只是"库里现在是什么",混用会导致"打开编辑框、什么都没动、
|
||||
* 保存后海报变成了另一张"。
|
||||
*/
|
||||
@Local existingPosterKey: string = '';
|
||||
/** 豆瓣抓到的元数据 JSON(存进库,详情页另有展示) */
|
||||
@Local doubanMeta: string = '';
|
||||
|
||||
@@ -135,6 +163,13 @@ export struct FbMovieNewSheet {
|
||||
private repo: FbMovieRepo = FbMovieRepo.inst();
|
||||
|
||||
aboutToAppear(): void {
|
||||
/* 🔴 编辑模式:先用原始详情把字段填满,**不要**再走下面那套"今天+下个整点"
|
||||
的默认值 —— 那会把用户原来填的时间冲掉,看起来像"打开就变了个日期"。 */
|
||||
if (this.editingId > 0 && this.editingDetail !== null) {
|
||||
this.prefill(this.editingDetail);
|
||||
this.loadRoster();
|
||||
return;
|
||||
}
|
||||
// 默认给"今天 + 下一个整点":记一场电影通常就是"刚刚看的",
|
||||
// 给个空的让用户从头选日期是最烦的。
|
||||
const now: Date = new Date();
|
||||
@@ -152,6 +187,59 @@ export struct FbMovieNewSheet {
|
||||
this.loadRoster();
|
||||
}
|
||||
|
||||
/**
|
||||
* 用已有详情回填表单(编辑模式)。
|
||||
*
|
||||
* ⚠️ `startsAt` / `endsAt` 是 `YYYY-MM-DD HH:MM:SS`(服务端格式),
|
||||
* 而表单存的是"日期 + 时分"两段 ⇒ 必须拆开,不能整串塞给日期控件。
|
||||
* ⚠️ 同看人 `attendees` 是对象数组,这里只要 id。
|
||||
* ⚠️ 海报:`editingDetail.posterKey` 是**已有**的海报,不要把键塞进
|
||||
* `doubanPosterKey`(那个字段的语义是"这次从豆瓣抓来、随保存一起提交")。
|
||||
* 已有海报在服务端本来就在,不传 `poster_key` 就不会动它。
|
||||
*/
|
||||
private prefill(d: FbMovieDetail): void {
|
||||
this.title = d.title;
|
||||
const st: string = d.startsAt;
|
||||
this.startDate = st.length >= 10 ? st.substring(0, 10) : st;
|
||||
this.startTime = st.length >= 16 ? st.substring(11, 16) : '';
|
||||
const en: string = d.endsAt;
|
||||
this.endDate = en.length >= 10 ? en.substring(0, 10) : this.startDate;
|
||||
this.endTime = en.length >= 16 ? en.substring(11, 16) : '';
|
||||
this.cinemaId = this.cinemaIdOf(d.cinemaName);
|
||||
this.info = d.info;
|
||||
this.note = d.note;
|
||||
/* 已有海报:只展示,不提交(见 existingPosterKey 的说明)。
|
||||
⚠️ 别塞进 doubanPosterKey —— 那个会被 PATCH 带上去,
|
||||
虽然值相同不出错,但语义就乱了,后人容易改错。 */
|
||||
this.existingPosterKey = d.posterKey;
|
||||
const ids: number[] = [];
|
||||
/* ⚠️ 必须显式标类型(`arkts-no-any-unknown`):
|
||||
直接 `for (const a of d.attendees)` 会让 `a` 被推断成 any 而编译失败。 */
|
||||
const arr: FbMovieAttendeeRow[] = d.attendees;
|
||||
for (const a of arr) {
|
||||
ids.push(a.id);
|
||||
}
|
||||
this.picked = ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* 影院名 → 影院 id。
|
||||
*
|
||||
* ⚠️ 详情接口只给 `cinemaName`(不给 id)—— 查一下本地库那份影院列表。
|
||||
* 找不到(影院被删/改名)就返回 0,表单显示"未选影院",不会硬塞一个错的。
|
||||
*/
|
||||
private cinemaIdOf(name: string): number {
|
||||
if (name === '') {
|
||||
return 0;
|
||||
}
|
||||
for (const c of this.cinemas) {
|
||||
if (c.name === name) {
|
||||
return c.id;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
this.clearSuggestTimer();
|
||||
}
|
||||
@@ -412,12 +500,28 @@ export struct FbMovieNewSheet {
|
||||
this.bad = false;
|
||||
this.note2 = '正在保存…';
|
||||
|
||||
const id: number = await this.repo.createMovie(d, token);
|
||||
if (id <= 0) {
|
||||
this.saving = false;
|
||||
this.bad = true;
|
||||
this.note2 = this.repo.lastError !== '' ? this.repo.lastError : '保存失败';
|
||||
return;
|
||||
/* 🔴 2026-11-06:新建与编辑走**不同的接口**。
|
||||
· 新建 → `createMovie`(POST),返回新 id,后面传图要用它;
|
||||
· 编辑 → `updateMovie`(PATCH),id 就是 `editingId`。
|
||||
⚠️ 两者都不发 `poster_key`(编辑时发空串会清掉已有海报,
|
||||
新建时的键由 `doubanPosterKey` 那条老路单独处理)。 */
|
||||
let id: number = this.editingId;
|
||||
if (this.editingId > 0) {
|
||||
const ok: boolean = await this.repo.updateMovie(this.editingId, d, token);
|
||||
if (!ok) {
|
||||
this.saving = false;
|
||||
this.bad = true;
|
||||
this.note2 = this.repo.lastError !== '' ? this.repo.lastError : '保存失败';
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
id = await this.repo.createMovie(d, token);
|
||||
if (id <= 0) {
|
||||
this.saving = false;
|
||||
this.bad = true;
|
||||
this.note2 = this.repo.lastError !== '' ? this.repo.lastError : '保存失败';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 图片:记录已经建好了,逐个传(失败不挡记录)----
|
||||
@@ -473,8 +577,11 @@ export struct FbMovieNewSheet {
|
||||
Column({ space: 0 }) {
|
||||
// ---- 标题栏(统一版式:左标题 + 右 ✕)----
|
||||
FbSheetTitleBar({
|
||||
title: '记一场电影',
|
||||
subtitle: '片名和时间必填,其余可以留空',
|
||||
/* 编辑模式换个标题,用户一眼知道自己在改而不是新建 */
|
||||
title: this.editingId > 0 ? '编辑观影记录' : '记一场电影',
|
||||
subtitle: this.editingId > 0
|
||||
? '改完点下面保存;照片可以再补几张'
|
||||
: '片名和时间必填,其余可以留空',
|
||||
accent: this.accentColor(),
|
||||
onClose: (): void => {
|
||||
this.onClose();
|
||||
@@ -555,7 +662,7 @@ export struct FbMovieNewSheet {
|
||||
|
||||
// ---- 底部:保存(关闭统一在右上角 ✕)----
|
||||
Row({ space: 10 }) {
|
||||
Text(this.saving ? '保存中…' : '记下这一场')
|
||||
Text(this.saving ? '保存中…' : (this.editingId > 0 ? '保存修改' : '记下这一场'))
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(Color.White)
|
||||
.textAlign(TextAlign.Center)
|
||||
@@ -875,72 +982,108 @@ export struct FbMovieNewSheet {
|
||||
*/
|
||||
@Builder
|
||||
posterField() {
|
||||
Column({ space: 6 }) {
|
||||
Row({ space: 8 }) {
|
||||
/* GLOBAL-FORM-SPEC:标签独占第一排(主题色 Bold,必填红无星号) */
|
||||
Text('海报')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor(this.accentColor())
|
||||
.width('100%')
|
||||
/* 🔴 2026-11-06 结构性修正。
|
||||
真机症状:「海报」标签在、下面那句说明也在,**中间那张缩略图、
|
||||
「当前海报」、上传按钮全都凭空消失**。
|
||||
|
||||
Row({ space: 8 }) {
|
||||
if (this.posterUri !== '') {
|
||||
// 端上自己选的(本地文件,直接可显示)
|
||||
Image(this.posterUri)
|
||||
.width(42)
|
||||
.height(60)
|
||||
.borderRadius(4)
|
||||
.objectFit(ImageFit.Cover)
|
||||
Text('已选 1 张')
|
||||
原因:原来外层是
|
||||
`Row { Text('海报').width('100%'); Row { 预览 … 按钮 } }`
|
||||
—— 标签 `width('100%')` 吃满整行,把内层 `Row` 挤到行外;
|
||||
外层 `Row` 又没给高度 ⇒ 内层内容塌成 0 高、根本不渲染。
|
||||
|
||||
⇒ 按《全局表单规范》改成**上下两排**:标签独占第一排、控件第二排整宽。
|
||||
其它字段(片名/时间/影院/同看的人)早就是这个版式,此处是漏改。 */
|
||||
Column({ space: 6 }) {
|
||||
/* GLOBAL-FORM-SPEC:标签独占第一排(主题色 Bold,必填红无星号) */
|
||||
Text('海报')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor(this.accentColor())
|
||||
.width('100%')
|
||||
|
||||
/* 第二排:预览图 + 说明 + 上传按钮(整宽,固定 60vp 高) */
|
||||
Row({ space: 8 }) {
|
||||
if (this.posterUri !== '') {
|
||||
// 端上自己选的(本地文件,直接可显示)
|
||||
Image(this.posterUri)
|
||||
.width(42)
|
||||
.height(60)
|
||||
.borderRadius(4)
|
||||
.objectFit(ImageFit.Cover)
|
||||
Text('新选了 1 张封面')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.ink)
|
||||
.layoutWeight(1)
|
||||
} else if (this.doubanPosterKey !== '') {
|
||||
// 豆瓣抓来的(走 media.php 代理,补域名 + 令牌)
|
||||
Image(fbImageUrl(this.doubanPosterKey, this.token()))
|
||||
.width(42)
|
||||
.height(60)
|
||||
.borderRadius(4)
|
||||
.objectFit(ImageFit.Cover)
|
||||
Column({ space: 2 }) {
|
||||
Text('豆瓣海报')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.ink)
|
||||
.layoutWeight(1)
|
||||
} else if (this.doubanPosterKey !== '') {
|
||||
// 豆瓣抓来的(走 media.php 代理,补域名 + 令牌)
|
||||
Image(fbImageUrl(this.doubanPosterKey, this.token()))
|
||||
.width(42)
|
||||
.height(60)
|
||||
.borderRadius(4)
|
||||
.objectFit(ImageFit.Cover)
|
||||
Column({ space: 2 }) {
|
||||
Text('豆瓣海报')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.ink)
|
||||
Text('保存后生效')
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
} else {
|
||||
Text('还没有海报')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.muted)
|
||||
.layoutWeight(1)
|
||||
Text('保存后生效')
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
} else if (this.existingPosterKey !== '') {
|
||||
/* 编辑模式:这条记录**本来就有**海报。
|
||||
⚠️ 只是**展示**,保存时不提交(见 `updateMovie` 的注释:
|
||||
不发 `poster_key`,服务端就不动它)。
|
||||
|
||||
Text(this.posterUri !== '' || this.doubanPosterKey !== '' ? '换一张' : '上传')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.accentColor())
|
||||
.onClick((): void => {
|
||||
this.pickPoster();
|
||||
})
|
||||
🔴🔴 条件**必须读 `@Local existingPosterKey`,不能写方法调用**:
|
||||
ArkUI V2 的 `if/else if` 只对字段/参数建立依赖,条件里调方法
|
||||
不会被观察 —— 首次 build 读到空串就永久走 `else`,之后有值也不重算。
|
||||
真机症状:探针打出 `posters/963d`(正确),界面却一直说"还没有海报"。
|
||||
本项目同一个坑:`ForEach(this.typeOptions(), ...)` 追踪不到
|
||||
(见 `pitfalls.md`「ArkUI 三个反复踩的坑」第 2 条)。 */
|
||||
Image(fbImageSrc(this.existingPosterKey, this.token()))
|
||||
.width(42)
|
||||
.height(60)
|
||||
.borderRadius(4)
|
||||
.objectFit(ImageFit.Cover)
|
||||
.alt($r('app.media.fb_ic_movies'))
|
||||
Column({ space: 2 }) {
|
||||
Text('当前海报')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.ink)
|
||||
Text('不选新图就保持不变')
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
} else {
|
||||
Text('还没有海报')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.muted)
|
||||
.layoutWeight(1)
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.height(60)
|
||||
.padding({ left: 12, right: 12 })
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
/* 上传/换图。条件同样只读字段,不调方法。 */
|
||||
Text((this.posterUri !== '' || this.doubanPosterKey !== '' || this.existingPosterKey !== '')
|
||||
? '换一张' : '上传')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.accentColor())
|
||||
.onClick((): void => {
|
||||
this.pickPoster();
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.height(60)
|
||||
.padding({ left: 12, right: 12 })
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
Text('海报可以点「自动获取」由豆瓣带回来,也可以自己传一张(竖版效果最好)')
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
.padding({ left: 56 })
|
||||
.width('100%')
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
Reference in New Issue
Block a user