Files
FamilyBook_harmonyos/entry/src/main/ets/components/FbEventSheet.ets
T
laoyang 0f22aef689 弹框动画丝滑化:入场加位移+弹簧曲线,修加载态打断动画 (#82)
用户:首页,弹出所有弹框,动画,都要改一下,改的跟丝滑流畅,
尤其是观影,基本上没动画,都需要修改。

先核实:把时长临时放大到 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 个页面,全部同时生效。
2026-11-06 10:18:22 +08:00

544 lines
25 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 家书 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)
}
}