Files
FamilyBook_harmonyos/entry/src/main/ets/components/Widgets.ets
T
laoyang 4a063114c5 v62:修 P0 登录被踢 / 关闭按钮统一右上角 / 观影三处 / 影院管理补齐 / 名册三处
用户 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`、四个页签、点头像不被踢)
2026-10-19 15:27:31 +08:00

516 lines
17 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 · 共享 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)
}
}
}