v63+v64:影院管理补齐(v63)与观影「记一场」豆瓣/弹框选择(v64)
⚠️ 本提交含**两批**改动(同一批文件里交织,无法按文件拆开): · v63(影院管理与搜索/统计/年报拆成平级入口)—— 本会话之外的工作, 随本轮一并提交以免工作区长期留半成品; · v64(本会话,用户 2026-10-19 六条)。 v64 六条: ① 片名可经豆瓣自动获取简介与海报(与网页端同一份服务端输出: summary 全文 + 已落盘的 poster_key);简介**只填空白的**,不覆盖用户写过的。 联想 320ms 防抖(与网页端同值)。 ② 开始/结束改成**与日程同一个**日期时间选择控件 —— 新抽 components/FbDateTimeField.ets (行 + 弹层 + 一组换算函数),日程那边旧的自绘实现整段删掉改用它 (用户要的就是"两处一样")。 ③ 影院、同看人改成**弹框选择**: · FbCinemaPickSheet:顶部搜索框,匹配「地方 + 名称」,「不记录影院」恒在列表; · FbPersonPickSheet:顶部搜索框,拼音首字母**子序列**匹配(yyq 打成 yq 也能搜到), 多选走"暂存 + 确定"两段式(点一个不关窗)。 两排横向胶囊/头像全删(用户:"这种方式太难找了")。 ④ 允许上传**多张**图片:海报 1 张 + 观影照片可一次选 9 张、可多次追加、可逐张删。 ⚠️ 顺序是**先建记录再传图**(建记录失败就不会留孤儿图);图失败**不挡记录**。 ⑤ 删掉「地点」输入口(location 字段仍发,网页端与历史数据不受影响)。 ⑥ 观影页搜索框改明显:白底 + 实色描边 + 图标放大到 18 并染主题色 + 占位文字提到 muted/fontBody + 右侧「筛选 ›」暗示(用户:"根本看不清")。 FbMovieRepo 新增 doubanSuggest / doubanFetch / uploadPoster / uploadPhoto (multipart 手写,与头像上传同一形态,抽成共用的 uploadOne)。 验证:静态守卫 636/0 · 端上反向验证 87/0 · 接口 e2e 57/0 · 服务端反向验证 18/0 · 本地全量 e2e 1521/0 · hvigor 编译成功并已装机。 ⚠️ 真机验证脚本 _fb_v64_verify.py 已写好(六条全覆盖),因设备锁屏未能执行。
This commit is contained in:
@@ -44,6 +44,24 @@ import { FB_TOKEN_STORE } from '../data/FbTokenStore';
|
||||
import { FB_CAL_SETTINGS } from '../data/FbCalSettings';
|
||||
import { FbEventChip } from './FbEventChip';
|
||||
import { FbSheetRing } from './FbSheetRing';
|
||||
/* 🔴 v64(用户 2026-10-19):「观影的开始/结束时间应该跟**日程里面的是一样的**。」
|
||||
⇒ 「日期 + 时间」那一套控件抽成了共用组件(见该文件头),日程与观影都用它。
|
||||
⚠️ 本文件里原来的 `dateTimeRow` / `dateTimePickerSheet` / `cnDate` /
|
||||
`parseDate` / `parseTime` / `pad` 全部删掉,改调这里的 —— 两份实现必然漂移,
|
||||
用户这次点名的正是"两边不一致"。 */
|
||||
import {
|
||||
FbDateTimeRow,
|
||||
FbDateTimePicker,
|
||||
fbCnDate,
|
||||
fbPad2,
|
||||
fbToday,
|
||||
fbParseDate,
|
||||
fbParseTime,
|
||||
fbDateOf,
|
||||
fbTimeOf,
|
||||
fbIsoOf,
|
||||
fbAddHour
|
||||
} from './FbDateTimeField';
|
||||
|
||||
const DOMAIN: number = 0x0000;
|
||||
const TAG: string = 'FbEventEdit';
|
||||
@@ -178,10 +196,10 @@ export struct FbEventEditSheet {
|
||||
// ---- 编辑:回填 ----
|
||||
this.fTitle = e.title;
|
||||
this.fAllDay = e.all_day;
|
||||
this.fStartDate = this.dateOf(e.starts_at);
|
||||
this.fStartTime = this.timeOf(e.starts_at);
|
||||
this.fEndDate = e.ends_at !== '' ? this.dateOf(e.ends_at) : this.dateOf(e.starts_at);
|
||||
this.fEndTime = e.ends_at !== '' ? this.timeOf(e.ends_at) : this.addHour(e.starts_at);
|
||||
this.fStartDate = fbDateOf(e.starts_at, this.todayStr());
|
||||
this.fStartTime = fbTimeOf(e.starts_at, '09:00');
|
||||
this.fEndDate = e.ends_at !== '' ? fbDateOf(e.ends_at, this.todayStr()) : fbDateOf(e.starts_at, this.todayStr());
|
||||
this.fEndTime = e.ends_at !== '' ? fbTimeOf(e.ends_at, '10:00') : this.addHour(e.starts_at);
|
||||
this.fPlace = e.location;
|
||||
this.fNote = e.note;
|
||||
this.fRepeat = this.repeatKeyOf(e.rrule);
|
||||
@@ -190,15 +208,15 @@ export struct FbEventEditSheet {
|
||||
return;
|
||||
}
|
||||
// ---- 新建:预填 ----
|
||||
const d: string = this.presetDate !== '' ? this.presetDate : this.todayStr();
|
||||
const d: string = this.presetDate !== '' ? this.presetDate : fbToday();
|
||||
this.fStartDate = d;
|
||||
this.fEndDate = d;
|
||||
// 新建时把**开始时间**预填成"下一个整点"、结束 +1 小时 ——
|
||||
// 比固定 09:00 更符合"现在就要加一条"的场景(打开就能直接存)。
|
||||
const now: Date = new Date();
|
||||
const nh: number = Math.min(now.getHours() + 1, 22);
|
||||
this.fStartTime = this.pad(nh) + ':00';
|
||||
this.fEndTime = this.pad(nh + 1 > 23 ? 23 : nh + 1) + ':00';
|
||||
this.fStartTime = fbPad2(nh) + ':00';
|
||||
this.fEndTime = fbPad2(nh + 1 > 23 ? 23 : nh + 1) + ':00';
|
||||
this.fBookId = this.defaultBookId();
|
||||
}
|
||||
|
||||
@@ -207,29 +225,12 @@ export struct FbEventEditSheet {
|
||||
// ============================================================
|
||||
|
||||
private todayStr(): string {
|
||||
const n: Date = new Date();
|
||||
return this.pad(n.getFullYear()) + '-' + this.pad(n.getMonth() + 1) + '-' + this.pad(n.getDate());
|
||||
return fbToday();
|
||||
}
|
||||
|
||||
private pad(n: number): string {
|
||||
return n < 10 ? ('0' + n.toString()) : n.toString();
|
||||
}
|
||||
|
||||
/** 从 `YYYY-MM-DD HH:MM:SS` 取日期段 */
|
||||
private dateOf(dt: string): string {
|
||||
return dt.length >= 10 ? dt.substring(0, 10) : this.todayStr();
|
||||
}
|
||||
|
||||
/** 从 `YYYY-MM-DD HH:MM:SS` 取时间段的 `HH:mm` */
|
||||
private timeOf(dt: string): string {
|
||||
return dt.length >= 16 ? dt.substring(11, 16) : '09:00';
|
||||
}
|
||||
|
||||
/** 给一个时间的时分 +1 小时(当结束时间没填时的兜底) */
|
||||
/** 给一个时刻的时分 +1 小时(当结束时间没填时的兜底) */
|
||||
private addHour(dt: string): string {
|
||||
const hh: number = dt.length >= 16 ? parseInt(dt.substring(11, 13), 10) : 9;
|
||||
const h2: number = Math.min(hh + 1, 23);
|
||||
return this.pad(h2) + ':' + (dt.length >= 16 ? dt.substring(14, 16) : '00');
|
||||
return fbAddHour(fbTimeOf(dt, '09:00'));
|
||||
}
|
||||
|
||||
/** 默认落到的日历本:设置里选的(若还在可选列表里),否则第一本可写的 */
|
||||
@@ -577,51 +578,22 @@ export struct FbEventEditSheet {
|
||||
/**
|
||||
* **一个「日期 + 时间」控件**(照 SyncCalendar 的 `EventEditPage`)。
|
||||
*
|
||||
* 🔴 用户 2026-10-16:「你参考一下 SyncCalendar 的做法,实际上就是
|
||||
* **把日期和时间放在一个控件里面**。」
|
||||
* 它的做法(`dateTimePickerSheet`):**一个底部弹层里 `DatePicker` 与
|
||||
* `TimePicker` 并排**,选完点「确定」一次回填。
|
||||
* ⚠️ 我第一版做成了"点日期块就地展开 DatePicker、点时间块再展开 TimePicker"
|
||||
* —— 那是**两个控件、分两次选**,形态与它不同(也不如一次选完顺手)。
|
||||
*
|
||||
* 控件本身是**一整块可点的框**,显示 `10月16日 周五 20:00`;
|
||||
* 点它 → 打开弹层(里面日期/时间并排)→ 「确定」写回。
|
||||
* 🔴 v64:实现已抽到 `FbDateTimeField.ets::FbDateTimeRow`(**与观影共用**)——
|
||||
* 用户 2026-10-19:「观影的开始/结束时间应该跟日程里面的是一样的。」
|
||||
* 这里只负责"把本表单的值传进去 + 接住点击"。
|
||||
*/
|
||||
@Builder
|
||||
dateTimeRow(label: string, isStart: boolean) {
|
||||
Row({ space: 10 }) {
|
||||
Text(label)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.width(28)
|
||||
// 整块可点:日期与时间在**同一个控件**里,中间不再分两块
|
||||
Row() {
|
||||
Text(this.cnDate(isStart ? this.fStartDate : this.fEndDate))
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
if (!this.fAllDay) {
|
||||
Text(' ' + (isStart ? this.fStartTime : this.fEndTime))
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
}
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.height(42)
|
||||
.padding({ left: 12, right: 12 })
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.onClick((): void => {
|
||||
FbDateTimeRow({
|
||||
label: label,
|
||||
date: isStart ? this.fStartDate : this.fEndDate,
|
||||
time: isStart ? this.fStartTime : this.fEndTime,
|
||||
allDay: this.fAllDay,
|
||||
accent: this.accentColor(),
|
||||
onTap: (): void => {
|
||||
this.openPicker(isStart);
|
||||
})
|
||||
// 右侧小箭头:暗示"点开会选"(与 SyncCalendar 的行样式一致)
|
||||
Text('›')
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -632,19 +604,17 @@ export struct FbEventEditSheet {
|
||||
*/
|
||||
private openPicker(isEnd: boolean): void {
|
||||
this.pickIsEnd = isEnd;
|
||||
const d: Date = this.parseDate(isEnd ? this.fEndDate : this.fStartDate);
|
||||
this.pickDate = d;
|
||||
const tm: Date = this.parseTime(isEnd ? this.fEndTime : this.fStartTime);
|
||||
// ⚠️ 草稿值放在宿主,弹层组件 `aboutToAppear` 时从这里读初值 ——
|
||||
// 组件每次挂载都会重新读一次,所以"点开始 → 取消 → 点结束"拿到的是各自的值。
|
||||
this.pickDate = fbParseDate(isEnd ? this.fEndDate : this.fStartDate);
|
||||
const tm: Date = fbParseTime(isEnd ? this.fEndTime : this.fStartTime);
|
||||
this.pickHour = tm.getHours();
|
||||
this.pickMin = tm.getMinutes();
|
||||
this.pickOpen = true;
|
||||
}
|
||||
|
||||
/** 「确定」:把草稿写回;开始改了还会把结束顺延(避免"负时长") */
|
||||
private applyPicker(): void {
|
||||
const iso: string = this.pad(this.pickDate.getFullYear()) + '-'
|
||||
+ this.pad(this.pickDate.getMonth() + 1) + '-' + this.pad(this.pickDate.getDate());
|
||||
const hm: string = this.pad(this.pickHour) + ':' + this.pad(this.pickMin);
|
||||
private applyPicker(iso: string, hm: string): void {
|
||||
if (this.pickIsEnd) {
|
||||
this.fEndDate = iso;
|
||||
this.fEndTime = hm;
|
||||
@@ -663,105 +633,25 @@ export struct FbEventEditSheet {
|
||||
/**
|
||||
* 日期 + 时间选择弹层(**照 SyncCalendar 的 `dateTimePickerSheet`**)。
|
||||
*
|
||||
* 结构:标题 → `Row { DatePicker | TimePicker }`(**并排**)→ 「确定」。
|
||||
* ⚠️ 滚轮的 `textStyle`/`selectedTextStyle`/`disappearTextStyle` **要显式给色**:
|
||||
* 默认取系统主题色,深色模式下会和弹层底色撞色(SyncCalendar 注释里点明过)。
|
||||
* ⚠️ 全天日程没有"时刻"可言 ⇒ 只画 `DatePicker`。
|
||||
* 🔴 v64:实现已抽到 `FbDateTimeField.ets::FbDateTimePicker`(**与观影共用**)。
|
||||
* 结构(标题 → `Row { DatePicker | TimePicker }` 并排 → 确定)与原来完全一致,
|
||||
* 差别只是"这一份现在只有一处代码"。
|
||||
*/
|
||||
@Builder
|
||||
dateTimePickerSheet() {
|
||||
// 🔴 2026-10-19:与其它弹框统一(92% 宽 + 四角圆角 + 主题色环)。
|
||||
Column() {
|
||||
FbSheetRing({ radius: FbMetric.radius, ringColor: this.accentColor() }) {
|
||||
Column({ space: 14 }) {
|
||||
Text(this.pickIsEnd ? '选择结束时间' : '选择开始时间')
|
||||
.fontSize(FbMetric.fontTitle)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
.width('100%')
|
||||
.textAlign(TextAlign.Center)
|
||||
.padding({ top: 12 })
|
||||
|
||||
Row({ space: 6 }) {
|
||||
DatePicker({
|
||||
start: new Date('2000-01-01'),
|
||||
end: new Date('2035-12-31'),
|
||||
selected: this.pickDate
|
||||
})
|
||||
.onDateChange((v: Date): void => {
|
||||
this.pickDate = v;
|
||||
})
|
||||
.textStyle({ color: FbColors.faint, font: { size: 14 } })
|
||||
.selectedTextStyle({ color: FbColors.ink, font: { size: 16, weight: FontWeight.Medium } })
|
||||
.disappearTextStyle({ color: FbColors.faint, font: { size: 14 } })
|
||||
.layoutWeight(1)
|
||||
.height(210)
|
||||
|
||||
if (!this.fAllDay) {
|
||||
TimePicker({ selected: new Date(2000, 0, 1, this.pickHour, this.pickMin) })
|
||||
.useMilitaryTime(true)
|
||||
.onChange((v: TimePickerResult): void => {
|
||||
// ⚠️ `hour`/`minute` 是**数字**;这里存草稿数字,拼 `HH:MM` 时再补零
|
||||
this.pickHour = v.hour;
|
||||
this.pickMin = v.minute;
|
||||
})
|
||||
.textStyle({ color: FbColors.faint, font: { size: 14 } })
|
||||
.selectedTextStyle({ color: FbColors.ink, font: { size: 16, weight: FontWeight.Medium } })
|
||||
.disappearTextStyle({ color: FbColors.faint, font: { size: 14 } })
|
||||
.layoutWeight(1)
|
||||
.height(210)
|
||||
}
|
||||
FbDateTimePicker({
|
||||
title: this.pickIsEnd ? '选择结束时间' : '选择开始时间',
|
||||
date: fbIsoOf(this.pickDate),
|
||||
time: fbPad2(this.pickHour) + ':' + fbPad2(this.pickMin),
|
||||
allDay: this.fAllDay,
|
||||
accent: this.accentColor(),
|
||||
onConfirm: (d: string, t: string): void => {
|
||||
this.applyPicker(d, t);
|
||||
},
|
||||
onCancel: (): void => {
|
||||
this.pickOpen = false;
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
Button(this.fAllDay ? '确定日期' : '确定')
|
||||
.width('100%')
|
||||
.height(44)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.backgroundColor('#4A7C82')
|
||||
.fontColor(Color.White)
|
||||
.onClick((): void => {
|
||||
this.applyPicker();
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 18, right: 18, top: 8, bottom: 20 })
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
}
|
||||
.width('92%')
|
||||
}
|
||||
|
||||
/** `YYYY-MM-DD` → `10月16日 周五`(比裸日期好读,且和时刻并排也不长) */
|
||||
private cnDate(iso: string): string {
|
||||
if (iso.length < 10) {
|
||||
return iso;
|
||||
}
|
||||
const y: number = parseInt(iso.substring(0, 4), 10);
|
||||
const m: number = parseInt(iso.substring(5, 7), 10);
|
||||
const d: number = parseInt(iso.substring(8, 10), 10);
|
||||
const w: string[] = ['日', '一', '二', '三', '四', '五', '六'];
|
||||
return m.toString() + '月' + d.toString() + '日 周' + w[new Date(y, m - 1, d).getDay()];
|
||||
}
|
||||
|
||||
private parseDate(s: string): Date {
|
||||
if (s.length < 10) {
|
||||
return new Date();
|
||||
}
|
||||
const y: number = parseInt(s.substring(0, 4), 10);
|
||||
const m: number = parseInt(s.substring(5, 7), 10) - 1;
|
||||
const d: number = parseInt(s.substring(8, 10), 10);
|
||||
return new Date(y, m, d);
|
||||
}
|
||||
|
||||
private parseTime(s: string): Date {
|
||||
const out: Date = new Date();
|
||||
if (s.length >= 5) {
|
||||
out.setHours(parseInt(s.substring(0, 2), 10));
|
||||
out.setMinutes(parseInt(s.substring(3, 5), 10));
|
||||
}
|
||||
return out;
|
||||
})
|
||||
}
|
||||
|
||||
@Builder
|
||||
|
||||
Reference in New Issue
Block a user