用户 2026-10-19 第二批八条(原话逐条对应): ═══ ⑨ P0:「现在还是不稳定,会退出,提示:这台设备已经被退出登录,请重新登录。」 真因**是我 v61 自己埋的**:「个人设置」页(`FbAccountPage`)里两处 `fbApi` 调用**取了 `token` 变量却没传进请求选项** ⇒ 没有 Authorization 头 ⇒ 服务端 401 ⇒ `FbHttp` 的全局回调 `unauthorizedCb` **清掉本地令牌 + 踢回登录页**。 而本页在 `aboutToAppear` 里就调它 ⇒ **点一下右上角头像,App 就退出登录**。 ⚠️ 我上一轮把同一现象归因成"残留旧令牌 / 设备会话被顶掉"(还去线上查了 `api_tokens` 表,一切正常)—— **那个结论是错的**,等于把 bug 归给了环境。 修法(两处调用 + **两道架构防线**,缺一不可): ① 修两处调用,把 token 传进去; ② `fbApi` 内部:**本地有令牌、调用方又没传时自动补上** (大部分接口本来就要鉴权,补上是"纠正";`/login`、`/ping` 带了也无害); ③ **401 只在"确实带了令牌"时才算被踢** —— 否则"没带令牌"也会触发全局清令牌, 那正是这次事故的机制。 ═══ ④「你这个按钮到底是在弹框的右上角关闭,还是在下面关闭……全放在右上角。」 选**右上角 ✕**(底部已被 编辑/保存/删除 占满,再塞关闭会堆三四个按钮,分不清主操作)。 新增 `components/FbSheetClose.ets`: · `FbSheetClose` —— 32vp 圆形 ✕(热区 40+,小图标在手机上点不中); · `FbSheetTitleBar` —— 左标题 + 右收尾(设置类给 `confirmText` 显示「完成」)。 9 个浮层全接;名册详情 / 日程详情 / 简要卡**底部不再有「关闭」**。 ⚠️ 浅色底走 `fbAlpha`,**不能拼 `+ '14'`** —— ArkUI 八位色是 `#AARRGGBB` (本项目曾因写 `#00000066` 导致遮罩**从来没显示过**)。 ═══ ①「观影……弹出的框左上角有一条灰色的线,不知道是什么。」 那是浮层顶部的**抽屉把手**(36×4 的 `line2` 浅灰短线)。本意是"可下拉关掉"的提示, 但 ① 它落在左上角、旁边没有文字;② 这个浮层的关闭本来就不靠下拉。 ⇒ 用户认不出它 = 它没起作用,**删掉**(不改成居中 —— 那只是把灰线挪个位置)。 ═══ ②「然后这个观影人的头像并没有带进来。」 同看人的 `FbAvatar` **漏传 `imageUrl`** —— 服务端 `Movie::attendees()` 本来就 `SELECT p.avatar`、端上 `parseAttendees` 也解析进了 `a.avatar`, 纯粹是**渲染时忘了用它** ⇒ 有头像的人也退化成色块。 ⚠️ 与名册详情 2026-10-15 那个是同一个 bug(当时是 `FbPersonSheet` 漏传)。 ═══ ③「点击这个图片呢,不能左右滑动看所有的这个图片。」 全屏查看器原来**只有底部「上一张/下一张」文字按钮**。现在整张大图左右滑翻页。 ⚠️ 用 `.gesture(PanGesture(Horizontal))` 而不是 `SwipeGesture`: `SwipeGesture` 要"快速划动"才识别,在有滚动容器的页面容易被先吃掉 (日程页实测:`gesture`/`priorityGesture` 都不触发)。`PanGesture` 看**累计位移**。 ⚠️ 判定要求 `|dx| >= 40 && |dx| > |dy|`,否则竖划会被误判成翻页。 ═══ ⑤「影院管理……增加影院呢、修改影院呢、高级搜索、统计、年度报告,这些全部都应该补充进去。」 端上原来**只有一个只读列表**(连"新增"都没有)。补成四个页签: · **影院**:新增 / 修改 / 删除(服务端 CRUD 早就有,端上一直没接)+ 删除二次确认; · **统计**:总览(看过/影院/人数/人次)+ 按影院/按年/按月的条形图; · **搜索**:片名 / 影院 / 起止日期(条件全空时不查,与网页 `hasCond` 同行为); · **年报**:按年切换(第一场/最后一场、最常去的影院、一起看最多的人、每月分布)。 ⚠️ **统计/年报必须联网**(`/movies/stats|report`):口径要与网页**完全一致** (同一份 `MovieStats`)。本地库算出来的是"已同步的那部分", 少同步一次就少一部 ⇒ 用户会发现"网页 124 部、App 121 部"。 ⇒ 断网时明说"需要联网",而不是给一个偏低的数字。 ⚠️ 影院写操作后**立刻回写本地库**(`refreshCinemas`),否则刚建的那家要等下次同步 才出现,看着像失败;并用 `INSERT OR REPLACE` 而非"清空再插" (中途失败会留下一个空表 = 用户的影院列表整个消失)。 ═══ ⑥「名册搜索,搜索后,下面有一个白色的框,什么内容都没有,删除。」 那是列表**末尾给悬浮栏留高度的占位块**(`height(FB_DOCK_RESERVE)`), 底色是 `FbColors.surface`(**白**)而列表容器是 `paper`(**米**)—— 列表内容一短(搜索只剩一两条)它就露出来,看着像"什么都没有的白框"。 ⇒ 改底色与列表同色。**不能删**:删了最后一张卡片会被悬浮栏压住。 ═══ ⑦「如果一个人没头像,在列表是有一个文字的头像的,那么点击后弹框,也应该有。」 `FbAvatar` 原来是 `if/else`:有 `imageUrl` 就**只画 Image**, 而图片**加载失败时 `Image` 什么都不显示**(只露出纯色底)⇒ 看着是个光板。 (`alt()` 只在**加载中**起作用,失败后是空白。) ⇒ 改成**叠加**:底色 + 文字头像**常驻**,图片有就叠在上面 (加载成功即盖住文字,失败则自动露出文字头像)。 ⚠️ 之前注释说"图层叠会错位"——那是因为当时想靠 Image 的 intrinsic size 撑尺寸; 现在两层都显式给 `dia`,尺寸来源唯一,不存在错位。 ═══ ⑧「现在名册的列表太空了,应该显示更多内容,例如工作单位、职务。」 列表卡片加一行「单位|职务」(独立成行,避免与标签/生日挤)。 ⚠️ 用 `persons.org` / `persons.title` 而**不是** `person_jobs`: 列表读的是**本地库**,而本地库只存 `persons` 那一行 (`person_jobs` 是一对多,端上没同步那张表)⇒ 服务端加了字段也到不了列表。 线上实测 801 人里 192 人有单位、48 人有职务,列表立刻有内容。 验证: · 编译 BUILD SUCCESSFUL · ArkTS 守卫 536/0(新增整组 groupV62,含 P0 两道防线) · 反向验证 51/0(新增 13 条注入,逐条确认能变红) · Schema 列守卫 12/0 · **真机 22/0**(6 组,含照片横滑 `1/5 → 2/5`、四个页签、点头像不被踢)
516 lines
17 KiB
Plaintext
516 lines
17 KiB
Plaintext
/**
|
||
* 家书 FamilyBook · 共享 UI 组件(鸿蒙端)
|
||
*
|
||
* 这些都是**无状态或轻状态**的展示件,对应 Web 端 `assets/css/app.css` 里
|
||
* 那几个反复出现的类:`.card` / `.page-head` / `.empty` / `.hint` / `.node-badge` …
|
||
*
|
||
* ⚠️ 全部用 @ComponentV2 + @Param(只读入参),不在子组件里改父组件状态。
|
||
* ⚠️ 所有 struct 加 `Fb` 前缀(arkts-no-decl-merging:同 hap 内顶层名不得重复)。
|
||
*/
|
||
|
||
import { FbColors, FbIcon, FbMetric, fbTint, FbTint } from '../common/Theme';
|
||
|
||
/**
|
||
* 圆点 / 色块(时间轴的点、日历的日程小点、看板的色标…)。
|
||
*
|
||
* ⚠️ 为什么不用 `Circle()`:它的 `.fill()` 是 **API 26.0.0** 才有的重载,
|
||
* 本工程 compatibleSdkVersion 是 6.1.1(24),编译器会告警
|
||
* 「This API is supported since SDK version 26.0.0」,真机上可能不生效。
|
||
* 改用「方块 + 圆角 = 圆」的写法:`Column().width().height().borderRadius(dia/2)`
|
||
* 只用通用属性,任何版本都成立,而且圆形效果完全等价。
|
||
*
|
||
* @Param dia 直径(vp)
|
||
* @Param fillC 填充色(ResourceColor:hex 字符串或 $r 语义色)
|
||
* @Param ringC 描边色(不传则不描边)
|
||
*/
|
||
@ComponentV2
|
||
export struct FbDot {
|
||
@Param dia: number = 8;
|
||
@Param fillC: ResourceColor = '#A84A28';
|
||
@Param ringC?: ResourceColor = undefined;
|
||
|
||
build() {
|
||
Stack({ alignContent: Alignment.Center }) {
|
||
// 底:填充
|
||
Column()
|
||
.width(this.dia)
|
||
.height(this.dia)
|
||
.borderRadius(this.dia / 2)
|
||
.backgroundColor(this.fillC)
|
||
// 描边(空心点用:内层挖一个背景色圆,等效于 1.4vp 环形)
|
||
if (this.ringC !== undefined) {
|
||
Column()
|
||
.width(this.dia - 2.8)
|
||
.height(this.dia - 2.8)
|
||
.borderRadius((this.dia - 2.8) / 2)
|
||
.backgroundColor(FbColors.surface)
|
||
}
|
||
}
|
||
.width(this.dia)
|
||
.height(this.dia)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 空心状态圈(任务卡的完成圈:未完成时空心、完成时实心 + 勾)。
|
||
*
|
||
* 同样避开 Circle().fill()/stroke(),用嵌套方块实现。
|
||
*/
|
||
@ComponentV2
|
||
export struct FbCheckCircle {
|
||
/** 直径 */
|
||
@Param dia: number = 16;
|
||
/** 是否已完成(实心 + 勾) */
|
||
@Param done: boolean = false;
|
||
/** 是否进行中(实心小点) */
|
||
@Param doing: boolean = false;
|
||
/** 主色 */
|
||
@Param color: string = '#A8574F';
|
||
|
||
build() {
|
||
Stack({ alignContent: Alignment.Center }) {
|
||
// 外圈:完成/进行中是实心主色,否则用浅灰实心(代表描边)
|
||
Column()
|
||
.width(this.dia)
|
||
.height(this.dia)
|
||
.borderRadius(this.dia / 2)
|
||
.backgroundColor(this.done || this.doing ? this.color : '#A8988A')
|
||
|
||
// 内层:未勾选时用卡片底「挖空」,形成空心环
|
||
if (!this.done && !this.doing) {
|
||
Column()
|
||
.width(this.dia - 2.8)
|
||
.height(this.dia - 2.8)
|
||
.borderRadius((this.dia - 2.8) / 2)
|
||
.backgroundColor(FbColors.surface)
|
||
}
|
||
|
||
// 已完成:打勾图标
|
||
if (this.done) {
|
||
Image($r('app.media.fb_ic_check'))
|
||
.width(this.dia * 0.66)
|
||
.height(this.dia * 0.66)
|
||
.renderMode(ImageRenderMode.Template)
|
||
.colorFilter(fbTint(FbTint.white))
|
||
} else if (this.doing) {
|
||
// 进行中:中心一个白点
|
||
Column()
|
||
.width(this.dia * 0.42)
|
||
.height(this.dia * 0.42)
|
||
.borderRadius(this.dia * 0.21)
|
||
.backgroundColor(Color.White)
|
||
}
|
||
}
|
||
.width(this.dia)
|
||
.height(this.dia)
|
||
}
|
||
}
|
||
|
||
/** 空态:一块居中的提示卡(Web 端 `.card.empty`) */
|
||
@ComponentV2
|
||
export struct FbEmptyBox {
|
||
/** 主标题 */
|
||
@Param title: string = '这里还是空的';
|
||
/** 副说明 */
|
||
@Param hint: string = '';
|
||
/** 可选的插图图标(默认圆点) */
|
||
@Param icon: Resource = $r('app.media.fb_ic_dot');
|
||
|
||
build() {
|
||
Column({ space: 8 }) {
|
||
Image(this.icon)
|
||
.width(34)
|
||
.height(34)
|
||
.opacity(0.35)
|
||
.renderMode(ImageRenderMode.Template)
|
||
.colorFilter(fbTint(FbTint.faint))
|
||
Text(this.title)
|
||
.fontSize(FbMetric.fontBody)
|
||
.fontWeight(FontWeight.Medium)
|
||
.fontColor(FbColors.ink)
|
||
if (this.hint.length > 0) {
|
||
Text(this.hint)
|
||
.fontSize(FbMetric.fontSmall)
|
||
.fontColor(FbColors.muted)
|
||
.textAlign(TextAlign.Center)
|
||
.lineHeight(20)
|
||
}
|
||
}
|
||
.width('100%')
|
||
.padding({ top: 40, bottom: 40, left: 24, right: 24 })
|
||
.alignItems(HorizontalAlign.Center)
|
||
.justifyContent(FlexAlign.Center)
|
||
}
|
||
}
|
||
|
||
/** 区块小标题(Web 端 `.cinema-h2`:一行标题 + 可选的右侧说明) */
|
||
@ComponentV2
|
||
export struct FbSectionTitle {
|
||
@Param title: string = '';
|
||
@Param extra: string = '';
|
||
|
||
build() {
|
||
Row() {
|
||
Text(this.title)
|
||
.fontSize(FbMetric.fontSmall)
|
||
.fontWeight(FontWeight.Medium)
|
||
.fontColor(FbColors.muted)
|
||
.letterSpacing(0.6)
|
||
if (this.extra.length > 0) {
|
||
Blank()
|
||
Text(this.extra)
|
||
.fontSize(FbMetric.fontTiny)
|
||
.fontColor(FbColors.faint)
|
||
}
|
||
}
|
||
.width('100%')
|
||
.padding({ left: 4, right: 4, top: 12, bottom: 8 })
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 头像(Web 端 `nav_avatar.php`:姓名首字 + 由名字哈希出的底色)。
|
||
*
|
||
* 端上不做「上传头像」—— 先把「首字 + 色块」这套做出来,
|
||
* 接接口后再叠真实图片。
|
||
*/
|
||
@ComponentV2
|
||
export struct FbAvatar {
|
||
/** 显示名(取首字;也是**没有图片时**的回落) */
|
||
@Param name: string = '';
|
||
/** 底色(由业务侧算好传进来) */
|
||
@Param color: string = '#A84A28';
|
||
/** 直径(vp) */
|
||
@Param dia: number = 36;
|
||
/** 字号(vp);不传则按直径推算 */
|
||
@Param fontSizeVp: number = 0;
|
||
/**
|
||
* 头像图 URL(**空 = 用色块字回落**)。
|
||
*
|
||
* 🔴 为什么要支持真图(用户 2026-10-13:"名册的头像怎么都没有?"):
|
||
* 之前这里只有色块字,看起来像"没有头像"。
|
||
* 实测名册 800 人里只有 19 人传过头像 ⇒ **两者都要有**:
|
||
* 有图的显示图、没图的回落到色块字(这是绝大多数)。
|
||
* ⚠️ URL 由调用方用 `fbImageUrl(key, token)` 拼好传进来 ——
|
||
* `Image()` 不支持自定义请求头,令牌只能走查询串(见 `fbImageUrl` 的说明)。
|
||
*/
|
||
@Param imageUrl: string = '';
|
||
|
||
/** 取名字的第一个字(含中文名的姓) */
|
||
private initial(): string {
|
||
const t: string = this.name.trim();
|
||
if (t.length === 0) {
|
||
return '?';
|
||
}
|
||
return t.substring(0, 1);
|
||
}
|
||
|
||
build() {
|
||
/* 🔴🔴 v62(用户 2026-10-19):「**如果一个人没头像,在列表是有一个文字的
|
||
头像的,那么点击后弹框,也应该有。**」
|
||
⇒ 列表有文字头像(因为 `imageUrl` 是空 ⇒ 走 Text 分支),
|
||
而详情/简要卡是**纯色块**。
|
||
真因:这里原来是 `if/else` —— 只要 `imageUrl` 非空就**只画 Image**,
|
||
而那张图**加载失败时 `Image` 什么都不显示**(只露出 `.backgroundColor(color)`
|
||
的纯色块)⇒ 看起来就是"一个光板,没有文字头像"
|
||
(`alt()` 只在**加载中**起作用,失败后是空白)。
|
||
⚠️ 而"有 imageUrl 的人"其实不多(实测 800 人里 19 人),
|
||
但**空 key 与坏 key 的观感差别很大**:前者有字,后者是光板。
|
||
⇒ 改成**叠加**:底色 + 文字头像**常驻**,图片有就叠在上面
|
||
(图片一旦加载成功就完全盖住文字,失败则自动露出文字头像)。
|
||
⚠️ 之前注释说"图层叠会错位"——那是因为当时想靠 Image 的 intrinsic size 撑尺寸。
|
||
现在**两层都显式给 `dia`**,尺寸来源唯一,不存在错位。
|
||
⚠️ 用 Stack 而不是 `backgroundImage`:`backgroundImage` 不接受网络地址,
|
||
而头像走的是 `media.php?k=...`(见 `fbImageUrl` 的说明)。 */
|
||
Stack({ alignContent: Alignment.Center }) {
|
||
// ① 底色 + 文字头像(**永远在**,是"图的兜底")
|
||
Text(this.initial())
|
||
.width(this.dia)
|
||
.height(this.dia)
|
||
.borderRadius(this.dia / 2)
|
||
.backgroundColor(this.color)
|
||
.fontColor(Color.White)
|
||
.fontSize(this.fontSizeVp > 0 ? this.fontSizeVp : Math.round(this.dia * 0.42))
|
||
.fontWeight(FontWeight.Medium)
|
||
.textAlign(TextAlign.Center)
|
||
|
||
// ② 真头像(有才画;加载成功即盖住①,失败则①露出来)
|
||
if (this.imageUrl !== '') {
|
||
Image(this.imageUrl)
|
||
.width(this.dia)
|
||
.height(this.dia)
|
||
.borderRadius(this.dia / 2)
|
||
.objectFit(ImageFit.Cover)
|
||
}
|
||
}
|
||
.width(this.dia)
|
||
.height(this.dia)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 「同看的人」这类人员标签(Web 端 `person_chip`:小头像 + 称呼)。
|
||
*
|
||
* ⚠️ 会出现在 <a> 内部的行内场景,所以整块不可点、纯展示。
|
||
*/
|
||
@ComponentV2
|
||
export struct FbPersonChip {
|
||
@Param name: string = '';
|
||
@Param color: string = '#A84A28';
|
||
/** 是否显示文字(只放头像时传 false) */
|
||
@Param withText: boolean = true;
|
||
|
||
build() {
|
||
Row({ space: 4 }) {
|
||
FbAvatar({ name: this.name, color: this.color, dia: 18, fontSizeVp: 10 })
|
||
if (this.withText) {
|
||
Text(this.name)
|
||
.fontSize(FbMetric.fontTiny)
|
||
.fontColor(FbColors.muted)
|
||
.maxLines(1)
|
||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||
}
|
||
}
|
||
.padding({ left: 4, right: 8, top: 2, bottom: 2 })
|
||
.borderRadius(FbMetric.radiusPill)
|
||
.backgroundColor(FbColors.inputBg)
|
||
}
|
||
}
|
||
|
||
/** 小徽标(Web 端 `.node-badge` / `.badge`:管理员、访客、标签…) */
|
||
@ComponentV2
|
||
export struct FbTag {
|
||
@Param text: string = '';
|
||
@Param tone: string = 'muted';
|
||
|
||
private fgColor(): string {
|
||
if (this.tone === 'brand') {
|
||
return '#A84A28';
|
||
}
|
||
if (this.tone === 'red') {
|
||
return '#C0533F';
|
||
}
|
||
if (this.tone === 'green') {
|
||
return '#4E7C3F';
|
||
}
|
||
if (this.tone === 'blue') {
|
||
return '#2F6FB0';
|
||
}
|
||
return '#7E6B5A';
|
||
}
|
||
|
||
private bgColor(): string {
|
||
if (this.tone === 'brand') {
|
||
return '#14A84A28';
|
||
}
|
||
if (this.tone === 'red') {
|
||
return '#14C0533F';
|
||
}
|
||
if (this.tone === 'green') {
|
||
return '#146F9E5C';
|
||
}
|
||
if (this.tone === 'blue') {
|
||
return '#142F6FB0';
|
||
}
|
||
return '#0F2C231D';
|
||
}
|
||
|
||
build() {
|
||
Text(this.text)
|
||
.fontSize(FbMetric.fontTiny)
|
||
.fontColor(this.fgColor())
|
||
.backgroundColor(this.bgColor())
|
||
.padding({ left: 7, right: 7, top: 2, bottom: 2 })
|
||
.borderRadius(FbMetric.radiusPill)
|
||
.maxLines(1)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 页头的「设置 / 更多」按钮(Web 端 `.btn.btn-icon`)。
|
||
*
|
||
* ⚠️ 必须是 `Button() { Image() }` 形式:`Button('label') { ... }` 同时给
|
||
* label 和子组件会报 10905202。
|
||
*/
|
||
@ComponentV2
|
||
export struct FbIconButton {
|
||
@Param icon: Resource = $r('app.media.fb_ic_gear');
|
||
/** 图标色:ResourceColor 同时接受 Resource($r 语义色)与 '#RRGGBB' 字符串 */
|
||
/**
|
||
* 图标颜色。**必须是 hex 字符串**(不是 `Resource`)——
|
||
* `colorFilter(fbTint(...))` 只吃 hex(颜色矩阵的入参)。
|
||
* ⚠️ 传 `FbColors.*`(Resource)会编译报错 `ResourceColor is not assignable to string`,
|
||
* 那是**故意的**:逼调用方用 `FbTint.*` 这组与 color.json 同值的常量。
|
||
*/
|
||
@Param tint: string = FbTint.muted;
|
||
/** 直径 */
|
||
@Param dia: number = 36;
|
||
@Event onTap: () => void = () => {
|
||
};
|
||
|
||
build() {
|
||
Button({ type: ButtonType.Circle }) {
|
||
Image(this.icon)
|
||
.width(18)
|
||
.height(18)
|
||
/* ⚠️ 本工程图标是 PNG ⇒ 必须 colorFilter(fillColor 只对 SVG 生效)。 */
|
||
.colorFilter(fbTint(this.tint))
|
||
}
|
||
.width(this.dia)
|
||
.height(this.dia)
|
||
.backgroundColor(FbColors.inputBg)
|
||
.onClick(() => {
|
||
this.onTap();
|
||
})
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 主操作按钮(Web 端 `.btn`:模块色底 + 白字)。
|
||
*
|
||
* ⚠️ 同样用 `Button() { Row() { … } }` 形式,label 与子组件二选一。
|
||
*/
|
||
@ComponentV2
|
||
export struct FbPrimaryButton {
|
||
@Param label: string = '';
|
||
/** 底色(一般传模块主题色的 hex 字符串,也可传 Resource 语义色) */
|
||
@Param bg: ResourceColor = '#A84A28';
|
||
/** 前置图标(不传则只显示文字) */
|
||
@Param icon?: Resource = undefined;
|
||
/** 是否撑满宽度 */
|
||
@Param block: boolean = false;
|
||
@Event onTap: () => void = () => {
|
||
};
|
||
|
||
build() {
|
||
Button({ type: ButtonType.ROUNDED_RECTANGLE }) {
|
||
Row({ space: 6 }) {
|
||
if (this.icon !== undefined) {
|
||
Image(this.icon)
|
||
.width(16)
|
||
.height(16)
|
||
.renderMode(ImageRenderMode.Template)
|
||
.colorFilter(fbTint(FbTint.white))
|
||
}
|
||
Text(this.label)
|
||
.fontSize(FbMetric.fontSmall)
|
||
.fontColor(Color.White)
|
||
.fontWeight(FontWeight.Medium)
|
||
}
|
||
.justifyContent(FlexAlign.Center)
|
||
}
|
||
.height(36)
|
||
.padding({ left: 14, right: 14 })
|
||
.backgroundColor(this.bg)
|
||
.borderRadius(FbMetric.radiusS)
|
||
.stateEffect(true)
|
||
.width(this.block ? '100%' : undefined)
|
||
.onClick(() => {
|
||
this.onTap();
|
||
})
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 一行「标签 + 值」的信息条(名册详情、观影信息、财务条目…都用它)。
|
||
*
|
||
* 对应 Web 端各页面里那种 `圆点 + 文字` 的行。
|
||
*/
|
||
@ComponentV2
|
||
export struct FbDotLine {
|
||
/** 圆点色(不传则不画点) */
|
||
@Param dotColor: string = '';
|
||
/** 左侧主文字 */
|
||
@Param label: string = '';
|
||
/** 右侧值 */
|
||
@Param tail: string = '';
|
||
/** 主文字淡化成次要色(用于「没有内容」的占位行) */
|
||
@Param muted: boolean = false;
|
||
|
||
build() {
|
||
Row({ space: 8 }) {
|
||
if (this.dotColor.length > 0) {
|
||
FbDot({ dia: 7, fillC: this.dotColor })
|
||
}
|
||
Text(this.label)
|
||
.fontSize(FbMetric.fontSmall)
|
||
.fontColor(this.muted ? FbColors.faint : FbColors.ink)
|
||
.layoutWeight(1)
|
||
.maxLines(2)
|
||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||
if (this.tail.length > 0) {
|
||
Text(this.tail)
|
||
.fontSize(FbMetric.fontTiny)
|
||
.fontColor(FbColors.faint)
|
||
.maxLines(1)
|
||
}
|
||
}
|
||
.width('100%')
|
||
.alignItems(VerticalAlign.Center)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 一条「N 格数字」统计条 —— 🔴🔴 **已删除(v54b,2026-10-17)**。
|
||
*
|
||
* 原来这里有个 `FbStatTriple`(三格:日程 / 待办 / 逾期),内部用
|
||
* `@Builder cell(label, value, alarm)` 画每一格。
|
||
*
|
||
* 用户 2026-10-17 报「概述卡……**日程、在做、待办的数量是错误的**」,
|
||
* 真机实测:接口与端上缓存都是 `{1,3,2,0}`(正确),界面却是 `0/0/0`。
|
||
* 根因:SDK 规则 `r/quick-rules/05-state.md` →
|
||
* > **@Builder 参数传递**:「**两个及以上参数不触发刷新**」
|
||
* ⇒ `cell()` 那份子树只按**初始值**渲染一次(那时还是默认的 0),
|
||
* 数据回来、`build()` 重跑了,但**这个多值 @Builder 的子树不重建**。
|
||
*
|
||
* ⇒ **删除而不是修补**,理由:
|
||
* ① 它当时**已无人引用**(首页改成四格 + `FbStatCell` 之后);
|
||
* ② 留着它,以后有人"顺手用一下"就会原样复发(本项目已踩过 6 次这个坑);
|
||
* ③ 它的三格形态也不再匹配需求(现在要四格)。
|
||
* ⇒ 新的写法用 **`components/FbStatCell.ets`**(`@ComponentV2` + `@Param`,
|
||
* **@Param 参与观察**),需要几格就摆几个。**不要再恢复 `@Builder` 版本。**
|
||
*/
|
||
|
||
/**
|
||
* 进度条(读书进度、旅程完成度…)。
|
||
*
|
||
* ⚠️ 用 Progress 的 Linear 类型;`ProgressType` 枚举是 Linear / Ring / Eclipse…
|
||
*/
|
||
@ComponentV2
|
||
export struct FbProgressLine {
|
||
/** 0–100 */
|
||
@Param percent: number = 0;
|
||
@Param color: string = '#A84A28';
|
||
|
||
build() {
|
||
Progress({ value: this.percent, total: 100, type: ProgressType.Linear })
|
||
.width('100%')
|
||
.height(4)
|
||
.color(this.color)
|
||
.backgroundColor(FbColors.inputBg)
|
||
}
|
||
}
|
||
|
||
/** 评分(观影 /·/10 分、读书 5 星)—— 用文字而非图形,避免臆造评分组件 */
|
||
@ComponentV2
|
||
export struct FbRating {
|
||
/** 分值 */
|
||
@Param score: number = 0;
|
||
/** 满分(10 或 5) */
|
||
@Param max: number = 10;
|
||
|
||
build() {
|
||
Row({ space: 2 }) {
|
||
Text('★')
|
||
.fontSize(FbMetric.fontTiny)
|
||
.fontColor('#C08A2E')
|
||
Text(this.max === 5 ? this.score.toFixed(1) : this.score.toFixed(1))
|
||
.fontSize(FbMetric.fontTiny)
|
||
.fontWeight(FontWeight.Medium)
|
||
.fontColor('#C08A2E')
|
||
Text('/' + this.max.toString())
|
||
.fontSize(FbMetric.fontTiny)
|
||
.fontColor(FbColors.faint)
|
||
}
|
||
}
|
||
} |