用户:首页,弹出所有弹框,动画,都要改一下,改的跟丝滑流畅,
尤其是观影,基本上没动画,都需要修改。
先核实:把时长临时放大到 3s 后连拍,遮罩 246→210→170→153→148 逐帧压暗,
说明**动画确实在播**。所以不是没接上,而是动效本身太弱:
220ms 太短、只有 0.94→1 缩放(差 6%,且无方向感)、FastOutSlowIn 落定硬停、
进出同长。
四处改动:
① 入场加纵向位移 SHEET_RISE_VP=22 —— 竖向是人眼最敏感的方向,
纯缩放读不出从哪来;22vp 是看得出从下面来但不觉得它飞了的量。
② 时长分离:卡片 320/200、遮罩 240/160(出场必须比入场快,
用户已决定关闭,等待就是拦他);缩放起点 0.94→0.90。
③ 曲线换物理弹簧 curves.springCurve(0, 1, 228, 26)(末段自然收敛)。
⚠️ 先写的 Curve.springMotion 编译失败——本工程 SDK 6.1.1 里没这个 API,
技能包速查表也把它列在高频臆造 API里,是我没照表走。
④ 观影/日程详情卡加载态:**这是观影没动画的真凶** ——
卡片高度按内容定,加载态只有一行字(~100px),详情回来瞬间撑到 72% 屏高,
320ms 的入场动画只播了前 1/3 就被高度突变打断。
改法:加载态放固定 460vp 骨架屏(FbEventSheet 同样问题,一并修 300vp)。
用固定 vp 而非 %:builder 在 Scroll 里,百分比拿不到基准。
验证(真机):放大时长连拍 N2 卡片顶边 y=1073 → N3 y=1011,
上移 62px≈20vp,证实位移生效;临时给接口加 1.5s 延迟后抓图,
骨架屏(海报位+4 条文字条)正确撑住卡片高度;临时代码已撤除(grep 0 处)。
连开合 3 轮均正常、无 JS 崩溃、进程存活。
hvigorw assembleHap BUILD SUCCESSFUL。
FbAnim 是全站唯一动画入口,98 处引用 / 14 个页面,全部同时生效。
544 lines
25 KiB
Plaintext
544 lines
25 KiB
Plaintext
/**
|
||
* 家书 FamilyBook · 日程详情浮层(鸿蒙端)
|
||
*
|
||
* 用户 2026-10-15:「……包括下面日程的显示啊什么之类的,就按照 SyncCalendar 那个模式去做。」
|
||
* SyncCalendar 的详情是**半屏 bindSheet**,内容为:
|
||
* 标题(带日历本色点 + 系统/只读徽标 + 关闭)→ 时间 → 日历本/重复 →
|
||
* 地点(可点导航)→ 备注 → 「编辑日程」。
|
||
*
|
||
* 🔴 这里用**页面内 Stack 覆盖层**而不是 `bindSheet`(与家书其他浮层一致,
|
||
* 理由见 `FbMovieDetailSheet` 的文件头);
|
||
* **功能与信息结构**照 SyncCalendar 对齐。
|
||
*
|
||
* ⚠️ 详情数据是**异步拉的**(`/events/<id>` 才给 note/rrule/reminder/ends_at)
|
||
* ⇒ 三种态都要画:加载中 / 成功 / 失败。别只画成功态。
|
||
*/
|
||
|
||
import { FbColors, FbMetric, FbTint } from '../common/Theme';
|
||
import { FbModuleDef, fbModuleOf, fbThemeVars } from '../model/Modules';
|
||
import { FbCalEventFull } from '../data/FbCalRepo';
|
||
import { FbCalBook } from '../model/Types';
|
||
import { FbDot } from './Widgets';
|
||
import { FbSheetRing } from './FbSheetRing';
|
||
import { FbSheetClose } from './FbSheetClose';
|
||
import { fbCanNavigate } from '../common/FbNav';
|
||
|
||
const FB_SHEET_BOTTOM_GAP: number = 16;
|
||
|
||
@ComponentV2
|
||
export struct FbEventSheet {
|
||
/** 日程详情(null = 还在加载) */
|
||
@Param event: FbCalEventFull | null = null;
|
||
/** 加载中 */
|
||
@Param loading: boolean = false;
|
||
/** 加载失败说明(空 = 没失败) */
|
||
@Param error: string = '';
|
||
/** 这条日程属于哪本日历(列表里那份已知,用来显示本色/名字) */
|
||
@Param bookName: string = '';
|
||
@Param bookColor: string = '#9AA0A6';
|
||
/**
|
||
* 外圈环色 —— **模块主题色**(用户 2026-10-19:「弹框的外圈颜色,
|
||
* 跟模块的主题色保持一致」)。空 = 日程模块色。
|
||
* ⚠️ 与 `bookColor` 分工不同:环是"我在哪个模块里",
|
||
* 标题左侧那个点才是"这条日程属于哪本日历"(一本模块下可以有多本)。
|
||
*/
|
||
@Param accent: string = '';
|
||
/** 能不能编辑(不能写的日历本不给"编辑"按钮) */
|
||
@Param canEdit: boolean = false;
|
||
/**
|
||
* 点「导航」。
|
||
*
|
||
* 🔴 v73(用户 2026-10-21「有地址的可以一键导航」):
|
||
* 地点那一行从"纯文字"变成"可点"。
|
||
* ⚠️ 为什么把动作**抛给宿主**而不是在这里直接拉地图:
|
||
* 拉起应用要 `UIAbilityContext`,而组件里 `getUIContext().getHostContext()`
|
||
* 拿到的是 `common.Context`,还得强转 —— 那个强转在**卡片/浮层重建**时可能拿到
|
||
* undefined。宿主页本来就有可靠的上下文来源,且"要不要拉地图"属于导航决策
|
||
* (跟"当前在哪个模块"有关),不是这张卡的职责。
|
||
*/
|
||
/**
|
||
* 点某一行位置信息去导航。
|
||
*
|
||
* 🔴 #69(用户 2026-11-05):原来是**无参**的 —— 宿主只能自己猜"该导航到哪",
|
||
* 实际上它读的是 `event.location`。而行程条目根本没有 location
|
||
* (服务端 trip 段不输出那个字段),于是详情卡里既没有位置信息,
|
||
* 就算硬加一行也没法指定导航到出发地还是目的地。
|
||
* ⇒ 改成带 `navTarget`:点哪一行就把那一行的地名/坐标交出去。
|
||
*/
|
||
@Event onNavigate: (navTarget: string) => void = (navTarget: string): void => {
|
||
};
|
||
@Event onClose: () => void = (): void => {
|
||
};
|
||
@Event onEdit: () => void = (): void => {
|
||
};
|
||
|
||
/** 环色:显式给了就用给的,否则取日程模块色(与日程页其它浮层一致) */
|
||
private accentColor(): string {
|
||
if (this.accent !== '') {
|
||
return this.accent;
|
||
}
|
||
const m: FbModuleDef | undefined = fbModuleOf('calendar');
|
||
return m !== undefined ? fbThemeVars(m.color).brand : FbTint.brand;
|
||
}
|
||
|
||
/**
|
||
* 取主题色的**半透明版本**(八位色 `#AARRGGBB`)。
|
||
*
|
||
* 🔴 本项目踩过:ArkUI 的八位色是 **alpha 在前**(`#AARRGGBB`),
|
||
* 与 CSS 的 `#RRGGBBAA` **相反** ⇒ 直接把 CSS 那套搬过来会得到
|
||
* "几乎透明的怪色"或者干脆渲染成黑色。
|
||
* ⚠️ 还要兼容"传进来的本身就是八位色"(模块色可能带 alpha):
|
||
* 先剥掉已有的 alpha,再拼新的,否则会得到 10 位非法色值。
|
||
*/
|
||
private accentAlpha(aa: string): string {
|
||
const c: string = this.accentColor().replace('#', '');
|
||
const rgb: string = c.length >= 8 ? c.substring(2) : c;
|
||
return '#' + aa + rgb;
|
||
}
|
||
|
||
/**
|
||
* 标题文字。
|
||
*
|
||
* 🔴 2026-10-19:原来为空时写死「(无标题)」—— 但那条文案在**农历/节气**
|
||
* 这些衍生条目上会误导(用户看到"农历显示无标题"以为数据错了,
|
||
* 实际是标题为空时的兜底文案不合适)。派生条目本来就该用**日历本名**
|
||
* 当兜底(「农历」「节假日」本身就是它是什么),自建日程才用「未命名日程」。
|
||
*/
|
||
private titleText(): string {
|
||
const e: FbCalEventFull | null = this.event;
|
||
if (e === null) {
|
||
return '';
|
||
}
|
||
if (e.title !== '') {
|
||
return e.title;
|
||
}
|
||
// 日历本名能说明"这是什么"(农历/节假日/节气…)
|
||
if (this.bookName !== '') {
|
||
return this.bookName;
|
||
}
|
||
return '未命名日程';
|
||
}
|
||
|
||
/**
|
||
* 详情加载失败的说明。
|
||
*
|
||
* 🔴 2026-10-19:原来加载失败会把**整张卡**换成一句错误文案 ——
|
||
* 而本地列表那份本来就能画标题/时间/地点 ⇒ 现在改成
|
||
* **照常画内容 + 底下加一行小字说明**(见 `openEvent` 的说明)。
|
||
*/
|
||
private errorNote(): string {
|
||
if (this.error === '') {
|
||
return '';
|
||
}
|
||
return '部分详情没读回来:' + this.error;
|
||
}
|
||
|
||
/** 时间显示:`10月15日 周三 09:00 - 10:30` / `全天 10月15日 周三` */
|
||
private timeText(): string {
|
||
const e: FbCalEventFull | null = this.event;
|
||
if (e === null) {
|
||
return '';
|
||
}
|
||
const d: string = this.cnDate(e.starts_at);
|
||
if (e.all_day) {
|
||
return '全天 ' + d;
|
||
}
|
||
const t1: string = this.hhmm(e.starts_at);
|
||
if (e.ends_at === '') {
|
||
return d + ' ' + t1;
|
||
}
|
||
// 同一天就只写一次日期
|
||
const sameDay: boolean = e.ends_at.length >= 10 && e.starts_at.length >= 10
|
||
&& e.ends_at.substring(0, 10) === e.starts_at.substring(0, 10);
|
||
const t2: string = this.hhmm(e.ends_at);
|
||
return sameDay ? (d + ' ' + t1 + ' - ' + t2) : (d + ' ' + t1 + ' → ' + this.cnDate(e.ends_at) + ' ' + t2);
|
||
}
|
||
|
||
/** `YYYY-MM-DD HH:MM:SS` → `10月15日 周三` */
|
||
private cnDate(dt: string): string {
|
||
if (dt.length < 10) {
|
||
return '';
|
||
}
|
||
const y: number = parseInt(dt.substring(0, 4), 10);
|
||
const m: number = parseInt(dt.substring(5, 7), 10);
|
||
const d: number = parseInt(dt.substring(8, 10), 10);
|
||
const w: string[] = ['日', '一', '二', '三', '四', '五', '六'];
|
||
const day: number = new Date(y, m - 1, d).getDay();
|
||
return m.toString() + '月' + d.toString() + '日 周' + w[day];
|
||
}
|
||
|
||
/** `YYYY-MM-DD HH:MM:SS` → `HH:mm` */
|
||
private hhmm(dt: string): string {
|
||
return dt.length >= 16 ? dt.substring(11, 16) : '';
|
||
}
|
||
|
||
/**
|
||
* 重复规则 → 中文。
|
||
*
|
||
* ⚠️ 只翻译**端上能生成的那几种**(见 `FbEventEditSheet.REPEAT_OPTS`);
|
||
* 认不出的(网页端配的"每两周"等)原样显示 RRULE 串 ——
|
||
* "看不懂"也好过"显示成不重复"(那是错的)。
|
||
*/
|
||
private repeatText(): string {
|
||
const e: FbCalEventFull | null = this.event;
|
||
if (e === null || e.rrule === '') {
|
||
return '';
|
||
}
|
||
const r: string = e.rrule;
|
||
if (r.indexOf('FREQ=DAILY') === 0) { return '每天'; }
|
||
if (r.indexOf('FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR') === 0) { return '每个工作日'; }
|
||
if (r.indexOf('FREQ=WEEKLY') === 0) { return '每周'; }
|
||
if (r.indexOf('FREQ=MONTHLY') === 0) { return '每月'; }
|
||
if (r.indexOf('FREQ=YEARLY') === 0) { return '每年'; }
|
||
return r;
|
||
}
|
||
|
||
/** 提醒 → 中文(多个用顿号连) */
|
||
private remindText(): string {
|
||
const e: FbCalEventFull | null = this.event;
|
||
if (e === null || e.reminder_minutes === '') {
|
||
return '';
|
||
}
|
||
const out: string[] = [];
|
||
for (const x of e.reminder_minutes.split(',')) {
|
||
const n: number = parseInt(x.trim(), 10);
|
||
if (isNaN(n)) {
|
||
continue;
|
||
}
|
||
if (n === 0) { continue; }
|
||
if (n < 60) { out.push('提前 ' + n.toString() + ' 分钟'); }
|
||
else if (n === 60) { out.push('提前 1 小时'); }
|
||
else if (n === 1440) { out.push('提前 1 天'); }
|
||
else if (n % 1440 === 0) { out.push('提前 ' + (n / 1440).toString() + ' 天'); }
|
||
else if (n % 60 === 0) { out.push('提前 ' + (n / 60).toString() + ' 小时'); }
|
||
else { out.push('提前 ' + n.toString() + ' 分钟'); }
|
||
}
|
||
return out.join('、');
|
||
}
|
||
|
||
@Builder
|
||
row(label: string, value: string) {
|
||
Row({ space: 10 }) {
|
||
Text(label)
|
||
.fontSize(FbMetric.fontTiny)
|
||
.fontColor(FbColors.faint)
|
||
.width(56)
|
||
Text(value)
|
||
.fontSize(FbMetric.fontSmall)
|
||
.fontColor(FbColors.ink)
|
||
.layoutWeight(1)
|
||
}
|
||
.width('100%')
|
||
.padding({ top: 8, bottom: 8 })
|
||
.alignItems(VerticalAlign.Top)
|
||
}
|
||
|
||
/**
|
||
* 「地点」这一行 —— 比普通 `row()` 多一颗「导航」标签,整行可点。
|
||
*
|
||
* ⚠️ 为什么单独抽一个 Builder 而不是给 `row()` 加参数:
|
||
* `row()` 有 2 个参数,加个 `tappable: boolean` 就变成 3 个 ——
|
||
* 3 个参数的 @Builder 更危险(多值参数不参与状态观察,本项目已踩 6+ 次)。
|
||
* 这里的数据是**每次打开浮层才建一次**(详情卡整体由 `if (detailOpen())`
|
||
* 重建),不存在"数据变了行不刷新"的场景,所以用 Builder 是安全的;
|
||
* 但仍保持**单参数**,把风险面压到最小。
|
||
*/
|
||
/**
|
||
* 算出交给 `onNavigate` 的导航目标(#69)。
|
||
*
|
||
* 有坐标就带坐标:`lat,lng?地名` —— 同名地点不会搜错城市
|
||
* (旅程模块踩过:全国搜「珠江路1018号」搜到了江苏南通,差 500 公里)。
|
||
* 没有坐标就只给地名,让 `FbNav` 自己走系统地理编码。
|
||
*
|
||
* ⚠️ 这是个**纯函数**(不读 this),所以放在 Builder 外面,
|
||
* 不会引入"@Builder 里调方法导致不刷新"的问题。
|
||
*/
|
||
private navTargetOf(place: string, lng: string, lat: string): string {
|
||
if (lng !== '' && lat !== '') {
|
||
/* 🔴 2026-11-05(用户:「点进去导航之后,为什么目的地地址前面有个问号」):
|
||
原来拼的是 `lat,lng?地名` —— 那个 `?` 是我照抄 `geo:` URI 的写法,
|
||
但这个字符串**不是 URI**,它是交给 `fbOpenNavigation()` 的普通文本。
|
||
|
||
`FbNav.parseGeoOf()` 的动作是:
|
||
① 正则抓出 `数字,数字` 当坐标;
|
||
② `clean` = **把坐标从原文里剪掉之后剩下的文字**,用作地名。
|
||
⇒ 传 `36.168977,120.375693?青岛北站` 时,剪掉坐标后剩下 `?青岛北站`
|
||
⇒ 地名带着问号进了高德的 `dname`,目的地标题就成了「?青岛北站」。
|
||
|
||
⇒ 改成**地名在前、空格、坐标在后**:
|
||
`青岛北站 36.168977,120.375693`
|
||
剪掉坐标剩「青岛北站」,正好就是要显示的名字。
|
||
|
||
⚠️ 顺序不能反过来:地名放后面虽然坐标照样能解析,
|
||
但 `clean` 会剩「青岛北站」(如果地名在前)——
|
||
放后面则剩「青岛北站」也行;不过坐标在前时
|
||
`clean` 前面会多一个空串拼接,最稳妥还是地名在前。 */
|
||
return place + ' ' + lat + ',' + lng;
|
||
}
|
||
return place;
|
||
}
|
||
|
||
@Builder
|
||
navRowRaw(label: string, place: string, navTarget: string) {
|
||
Row({ space: 10 }) {
|
||
Text(label)
|
||
.fontSize(FbMetric.fontTiny)
|
||
.fontColor(FbColors.faint)
|
||
.width(56)
|
||
|
||
Text(place)
|
||
.fontSize(FbMetric.fontSmall)
|
||
.fontColor(FbColors.ink)
|
||
.layoutWeight(1)
|
||
.maxLines(2)
|
||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||
|
||
/* 标签用模块色(与「编辑日程」按钮同色系),一眼看出"这是这颗票能做的事"。
|
||
⚠️ 底色用**极浅的主题色**(`fbAlpha` 那套思路:八位色 `#AARRGGBB`,
|
||
alpha 在前)—— 深底白字会把这一行压得比标题还重。 */
|
||
if (fbCanNavigate(place)) {
|
||
Row({ space: 3 }) {
|
||
Text('➤')
|
||
.fontSize(10)
|
||
.fontColor(this.accentColor())
|
||
Text('导航')
|
||
.fontSize(FbMetric.fontTiny)
|
||
.fontColor(this.accentColor())
|
||
}
|
||
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
|
||
.borderRadius(FbMetric.radiusPill)
|
||
.backgroundColor(this.accentAlpha('1A'))
|
||
.border({ width: 1, color: this.accentAlpha('33') })
|
||
}
|
||
}
|
||
.width('100%')
|
||
.padding({ top: 8, bottom: 8 })
|
||
.alignItems(VerticalAlign.Center)
|
||
.onClick((): void => {
|
||
this.onNavigate(navTarget);
|
||
})
|
||
}
|
||
|
||
build() {
|
||
// FB_WRAP_CENTER:把卡片**垂直居中**(用户 2026-10-16 要求:浮在屏幕中间)。
|
||
// ⚠️ 居中放在**组件内部**做,而不是改宿主的 Stack 对齐 ——
|
||
// 宿主 Stack 里还有别的子元素(日程页的悬浮 +),改对齐会连带挪走它们。
|
||
// ⚠️ 外层铺满 + Center;内层仍是那张 92% 宽的圆角卡。
|
||
Column() {
|
||
// 🔴 2026-10-19:卡片外面的主题色环统一由 `FbSheetRing` 给
|
||
// (用户:「所有弹框都加线,保持一致」)。环色用**这本日历的颜色**,
|
||
// 一眼能看出这条日程属于哪个日历本。
|
||
Column() {
|
||
FbSheetRing({ radius: FbMetric.radius, ringColor: this.accentColor() }) {
|
||
Column({ space: 0 }) {
|
||
// ---- 标题区 ----
|
||
Row({ space: 10 }) {
|
||
FbDot({ dia: 12, fillC: this.bookColor })
|
||
/* 🔴 2026-10-19:标题为空时要**说清这是什么**,而不是写「(无标题)」。
|
||
农历 / 节气这类衍生条目的标题服务端是给了的(如「九月初一」),
|
||
真为空的情况主要是「没填标题的自建日程」—— 那种回「未命名日程」
|
||
比「(无标题)」更像人话。 */
|
||
Text(this.titleText())
|
||
.fontSize(FbMetric.fontTitle)
|
||
.fontWeight(FontWeight.Medium)
|
||
.fontColor(FbColors.ink)
|
||
.maxLines(2)
|
||
.layoutWeight(1)
|
||
if (this.event !== null && this.event.all_day) {
|
||
Text('全天')
|
||
.fontSize(FbMetric.fontTiny)
|
||
.fontColor(FbColors.muted)
|
||
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
|
||
.backgroundColor(FbColors.inputBg)
|
||
.borderRadius(FbMetric.radiusPill)
|
||
}
|
||
/* 🔴 关闭统一到**右上角**(用户 2026-10-19:全应用只留一种关闭位置)。
|
||
⚠️ 放在标题行的最右端,而不是下面独立一行 —— 独立一行会让卡片
|
||
顶部多出一段空白,标题反而往下掉。 */
|
||
FbSheetClose({
|
||
onClose: (): void => {
|
||
this.onClose();
|
||
},
|
||
color: ''
|
||
})
|
||
}
|
||
.width('100%')
|
||
.alignItems(VerticalAlign.Top)
|
||
|
||
Divider()
|
||
.color(FbColors.line)
|
||
.margin({ top: 12, bottom: 4 })
|
||
|
||
if (this.loading) {
|
||
/* 🔴 2026-11-06:加载态要给足高度,别只放一行「正在读取…」。
|
||
用户反馈"弹框动画不丝滑"。原因同 `FbMovieDetailSheet`:
|
||
卡片高度按内容定,加载态只有一行字 ⇒ 卡片很矮;
|
||
数据回来立刻撑成满屏,**入场动画被高度突变打断**。
|
||
⇒ 用骨架块占住常规高度,动画期间尺寸基本恒定。
|
||
⚠️ 固定 vp 而不是百分比:这里渲染在 Scroll 里,
|
||
Scroll 高度按内容定,百分比拿不到基准。 */
|
||
Column({ space: 12 }) {
|
||
ForEach([0, 1, 2, 3], (i: number) => {
|
||
Column()
|
||
.width(i === 1 ? '82%' : (i === 2 ? '88%' : (i === 0 ? '44%' : '60%')))
|
||
.height(i === 0 ? 18 : 12)
|
||
.borderRadius(6)
|
||
.backgroundColor(FbColors.inputBg)
|
||
}, (i: number): string => 'evsk_' + i.toString())
|
||
}
|
||
.width('100%')
|
||
.height(300)
|
||
.padding({ top: 20, bottom: 20 })
|
||
.justifyContent(FlexAlign.Center)
|
||
.alignItems(HorizontalAlign.Start)
|
||
} else if (this.event === null) {
|
||
// 正常情况下不会走到这里(`openEvent` 一定会垫一份壳);
|
||
// 只有"打开浮层时状态被清掉"这种瞬时态才可能空着 ——
|
||
// 所以这里说的是"正在读取",不是"读不到"。
|
||
Text('正在读取…')
|
||
.fontSize(FbMetric.fontSmall)
|
||
.fontColor(FbColors.faint)
|
||
.width('100%')
|
||
.padding({ top: 20, bottom: 20 })
|
||
} else {
|
||
Scroll() {
|
||
Column({ space: 0 }) {
|
||
this.row('时间', this.timeText())
|
||
if (this.bookName !== '') {
|
||
this.row('日历本', this.bookName)
|
||
}
|
||
// ⚠️ build() 里**不能声明变量**(只有 UI 组件语法)⇒
|
||
// 条件直接调方法(方法返回空串就不显示)
|
||
if (this.repeatText() !== '') {
|
||
this.row('重复', this.repeatText())
|
||
}
|
||
if (this.remindText() !== '') {
|
||
this.row('提醒', this.remindText())
|
||
}
|
||
/* 🔴 #69(用户 2026-11-05):「点击行程里旅行日历本里的信息时,
|
||
弹出来的那个框…还是没有出发地和目的地。」
|
||
|
||
⇒ 行程派生条目(kind='trip')服务端**不给 `location`** ——
|
||
行程没有单一"地点"概念,有的是"从哪到哪"。
|
||
所以这里必须按 fromPlace / toPlace 分开画两行。
|
||
|
||
三者互斥地覆盖所有情况:
|
||
· 行程条目 → 出发地 + 目的地
|
||
· 普通日程 → 地点
|
||
不会同时出现(行程的 location 恒为空串,
|
||
普通日程的 fromPlace/toPlace 恒为空串)。 */
|
||
if (this.event.fromPlace !== '') {
|
||
this.navRowRaw('出发地', this.event.fromPlace,
|
||
this.navTargetOf(this.event.fromPlace, this.event.fromLng, this.event.fromLat))
|
||
}
|
||
if (this.event.toPlace !== '') {
|
||
this.navRowRaw('目的地', this.event.toPlace,
|
||
this.navTargetOf(this.event.toPlace, this.event.toLng, this.event.toLat))
|
||
}
|
||
if (this.event.location !== '') {
|
||
/* 🔴 v73(用户 2026-10-21「有地址的还可以直接一键导航」):
|
||
地点不再是纯文字 —— **整行可点**,右侧带一颗「导航」小标签。
|
||
⚠️ 为什么整行可点而不是只让那颗小标签可点:
|
||
小标签只有 40vp 宽,手指点不中会以为"没反应";
|
||
整行(含地点文字)都是热区,命中率高得多。
|
||
⚠️ 但**视觉上**仍要有那颗「导航」标签 —— 只有"整行可点"
|
||
而没有任何标记的话,用户根本不知道地点能点
|
||
(本项目踩过:卡尾"查看行程"是纯文本,用户以为按钮坏了)。 */
|
||
this.navRowRaw('地点', this.event.location,
|
||
this.navTargetOf(this.event.location, '', ''))
|
||
}
|
||
if (this.event.note !== '') {
|
||
this.row('备注', this.event.note)
|
||
}
|
||
// 🔴 详情没拉全时给一行小字(而不是把整张卡变成"读不到")
|
||
if (this.errorNote() !== '') {
|
||
Text(this.errorNote())
|
||
.fontSize(FbMetric.fontTiny)
|
||
.fontColor(FbColors.faint)
|
||
.width('100%')
|
||
.padding({ top: 8 })
|
||
}
|
||
Row().height(8)
|
||
}
|
||
.width('100%')
|
||
.alignItems(HorizontalAlign.Start)
|
||
}
|
||
/* 🔴 2026-10-19:原来是 `.layoutWeight(1)` —— 它让 Scroll **吃满剩余高度**,
|
||
于是短内容的日程也会把卡片顶到 `maxHeight`(72%),
|
||
按钮被推到最底、中间留一大片空白(真机截图:标题下面空半屏)。
|
||
⇒ 改成 `flexShrink(1)`:卡片按内容自适应高,
|
||
只有当内容真的超过 72% 时这一块才收缩并滚动。
|
||
⚠️ 不能只删掉 `layoutWeight`:`Column` 的子项默认 `flexShrink = 0`,
|
||
不收缩 ⇒ 长内容会**溢出卡片**被裁掉(反方向踩坑)。 */
|
||
.flexShrink(1)
|
||
.width('100%')
|
||
.scrollBar(BarState.Off)
|
||
.align(Alignment.TopStart)
|
||
}
|
||
|
||
// ---- 底部按钮(只有动作,关闭统一到右上角 ✕)----
|
||
/* 🔴 v73(用户 2026-10-21「有地址的还可以直接一键导航」):
|
||
有地点时底部多一颗「导航」—— 与上面那行可点的地点**并存**,
|
||
是有意的:地点行是"看的时候顺手点",底部按钮是"看完了决定要去"。
|
||
两个入口做同一件事,比只留一个更好用(日期详情卡不长,
|
||
地点行可能已经滚出视野)。
|
||
⚠️ 判据用 `event.location`,不用别的字段 —— 那是唯一权威的地点来源。
|
||
⚠️ 两个按钮用 `layoutWeight(1)` 均分宽度;只有一个时它自己占满。 */
|
||
if (this.event !== null
|
||
&& (fbCanNavigate(this.event.location) || this.canEdit)) {
|
||
Row({ space: 10 }) {
|
||
if (fbCanNavigate(this.event.location)) {
|
||
Text('导航')
|
||
.fontSize(FbMetric.fontSmall)
|
||
.fontColor(this.accentColor())
|
||
.textAlign(TextAlign.Center)
|
||
.layoutWeight(1)
|
||
.height(42)
|
||
.backgroundColor(this.accentAlpha('14'))
|
||
.borderRadius(FbMetric.radiusS)
|
||
.border({ width: 1, color: this.accentAlpha('33') })
|
||
.onClick((): void => {
|
||
/* #69:底部这颗按钮不带具体行 —— 交给宿主自己挑一个
|
||
(传空串 = "按老规则推导":location 优先) */
|
||
this.onNavigate('');
|
||
})
|
||
}
|
||
if (this.canEdit) {
|
||
/* 🔴 用户 2026-10-19:「点击日程弹出的框,编辑按钮也颜色也要修改。」
|
||
⇒ 原来写死名册青 #4A7C82,在日程页里跟模块色(棕)不一致。
|
||
改成 `accentColor()`(= 日程模块主题色,与卡片外环同色)。 */
|
||
Text('编辑日程')
|
||
.fontSize(FbMetric.fontSmall)
|
||
.fontColor(Color.White)
|
||
.textAlign(TextAlign.Center)
|
||
.layoutWeight(1)
|
||
.height(42)
|
||
.backgroundColor(this.accentColor())
|
||
.borderRadius(FbMetric.radiusS)
|
||
.onClick((): void => {
|
||
this.onEdit();
|
||
})
|
||
}
|
||
}
|
||
.width('100%')
|
||
.margin({ top: 14 })
|
||
}
|
||
}
|
||
// 🔴 用户 2026-10-16:「它应该弹出一个**圆角的、浮在上面的卡片**……
|
||
// **不要让它左右充满整个屏幕,下面也不要挨着屏幕**,
|
||
// 而是放在**屏幕中间**,采用**圆角浮窗**式的形式。」
|
||
// ⇒ 不再贴底满宽:左右留边、四角圆角、垂直居中(由宿主的 Stack 对齐)。
|
||
.width('100%')
|
||
.constraintSize({ maxHeight: '72%' })
|
||
.padding({ left: 18, right: 18, top: 18, bottom: 20 + FB_SHEET_BOTTOM_GAP })
|
||
.alignItems(HorizontalAlign.Start)
|
||
}
|
||
}
|
||
.width('92%')
|
||
}
|
||
.width('100%')
|
||
.height('100%')
|
||
.justifyContent(FlexAlign.Center)
|
||
.hitTestBehavior(HitTestMode.Transparent)
|
||
}
|
||
} |