Files
FamilyBook_harmonyos/entry/src/main/ets/components/Widgets.ets
T

486 lines
14 KiB
Plaintext
Raw Normal View History

/**
* 家书 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 } 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)
.fillColor(Color.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)
.fillColor(FbColors.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;
/** 取名字的第一个字(含中文名的姓) */
private initial(): string {
const t: string = this.name.trim();
if (t.length === 0) {
return '?';
}
return t.substring(0, 1);
}
build() {
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)
}
}
/**
* 「同看的人」这类人员标签(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' 字符串 */
@Param tint: ResourceColor = FbColors.muted;
/** 直径 */
@Param dia: number = 36;
@Event onTap: () => void = () => {
};
build() {
Button({ type: ButtonType.Circle }) {
Image(this.icon)
.width(18)
.height(18)
.renderMode(ImageRenderMode.Template)
.fillColor(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)
.fillColor(Color.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)
}
}
/** 一条「三格数字」统计条(首页近七天:日程 / 待办 / 逾期) */
@ComponentV2
export struct FbStatTriple {
@Param labelA: string = '';
@Param valueA: string = '';
@Param labelB: string = '';
@Param valueB: string = '';
@Param labelC: string = '';
@Param valueC: string = '';
/** C 项是否用警示红(逾期数) */
@Param cAlarm: boolean = false;
@Builder
cell(label: string, value: string, alarm: boolean) {
Column({ space: 2 }) {
Text(value)
.fontSize(20)
.fontWeight(FontWeight.Medium)
.fontColor(alarm ? FbColors.red : FbColors.ink)
Text(label)
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.muted)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}
build() {
Row() {
this.cell(this.labelA, this.valueA, false)
this.cell(this.labelB, this.valueB, false)
this.cell(this.labelC, this.valueC, this.cAlarm)
}
.width('100%')
.padding({ top: 10, bottom: 10 })
}
}
/**
* 进度条(读书进度、旅程完成度…)。
*
* ⚠️ 用 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)
}
}
}