v90 新增观影卡与任务卡(2x2/2x4):卡片按类型分流推数据,key 带 version 前缀
This commit is contained in:
@@ -53,7 +53,7 @@ import { preferences } from '@kit.ArkData';
|
|||||||
import { formBindingData, formProvider } from '@kit.FormKit';
|
import { formBindingData, formProvider } from '@kit.FormKit';
|
||||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||||
import { BusinessError } from '@kit.BasicServicesKit';
|
import { BusinessError } from '@kit.BasicServicesKit';
|
||||||
import { FbCardData, fbCardBuild } from './FbCardData';
|
import { fbCardDataByKind } from './FbCardMediaData';
|
||||||
|
|
||||||
const DOMAIN: number = 0x0000;
|
const DOMAIN: number = 0x0000;
|
||||||
const TAG: string = 'FbCardForms';
|
const TAG: string = 'FbCardForms';
|
||||||
@@ -61,6 +61,10 @@ const TAG: string = 'FbCardForms';
|
|||||||
const STORE_NAME: string = 'fb_card_forms';
|
const STORE_NAME: string = 'fb_card_forms';
|
||||||
const K_IDS: string = 'form_ids';
|
const K_IDS: string = 'form_ids';
|
||||||
const K_OFF: string = 'day_offsets';
|
const K_OFF: string = 'day_offsets';
|
||||||
|
/** 🔴 v90:formId → 卡片类型(`calendar` / `movies` / `tasks`)。
|
||||||
|
* ⚠️ 旧卡(v90 之前登记的日程卡)没有这一项 ⇒ 读不到一律按 `calendar` 处理,
|
||||||
|
* 绝不能让升级把桌面上已有的日程卡弄瞎。 */
|
||||||
|
const K_KIND: string = 'form_kinds';
|
||||||
|
|
||||||
/** 翻页最多到前后一年(防外部输入把日期算成 Invalid Date) */
|
/** 翻页最多到前后一年(防外部输入把日期算成 Invalid Date) */
|
||||||
const MAX_OFFSET: number = 366;
|
const MAX_OFFSET: number = 366;
|
||||||
@@ -181,6 +185,52 @@ class FbCardFormsImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- 卡片类型(v90:formId → calendar / movies / tasks)----
|
||||||
|
结构与偏移量同构(键值对都落在一份字符串里),值是类型名而非数字。 */
|
||||||
|
|
||||||
|
private parseKinds(raw: string): Map<string, string> {
|
||||||
|
const map: Map<string, string> = new Map<string, string>();
|
||||||
|
for (const item of raw.split(SEP_ITEM)) {
|
||||||
|
if (item === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const at: number = item.indexOf(SEP_KV);
|
||||||
|
if (at <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
map.set(item.substring(0, at), item.substring(at + 1));
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private joinKinds(map: Map<string, string>): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
map.forEach((kind: string, id: string): void => {
|
||||||
|
parts.push(id + SEP_KV + kind);
|
||||||
|
});
|
||||||
|
return parts.join(SEP_ITEM);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 读某张卡的类型(读不到按 `calendar` —— 兼容 v90 之前登记的旧日程卡) */
|
||||||
|
async kindOf(ctx: common.Context, formId: string): Promise<string> {
|
||||||
|
const map: Map<string, string> = this.parseKinds(await this.readStr(ctx, K_KIND));
|
||||||
|
const v: string | undefined = map.get(formId);
|
||||||
|
return v === undefined || v === '' ? 'calendar' : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setKind(ctx: common.Context, formId: string, kind: string): Promise<void> {
|
||||||
|
const map: Map<string, string> = this.parseKinds(await this.readStr(ctx, K_KIND));
|
||||||
|
map.set(formId, kind);
|
||||||
|
await this.writeStr(ctx, K_KIND, this.joinKinds(map));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async clearKind(ctx: common.Context, formId: string): Promise<void> {
|
||||||
|
const map: Map<string, string> = this.parseKinds(await this.readStr(ctx, K_KIND));
|
||||||
|
if (map.delete(formId)) {
|
||||||
|
await this.writeStr(ctx, K_KIND, this.joinKinds(map));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- 卡片注册表 ---- */
|
/* ---- 卡片注册表 ---- */
|
||||||
|
|
||||||
private async ids(ctx: common.Context): Promise<string[]> {
|
private async ids(ctx: common.Context): Promise<string[]> {
|
||||||
@@ -198,7 +248,7 @@ class FbCardFormsImpl {
|
|||||||
await this.writeStr(ctx, K_IDS, list.join(SEP_ITEM));
|
await this.writeStr(ctx, K_IDS, list.join(SEP_ITEM));
|
||||||
}
|
}
|
||||||
|
|
||||||
async register(ctx: common.Context, formId: string): Promise<void> {
|
async register(ctx: common.Context, formId: string, kind: string): Promise<void> {
|
||||||
if (formId === '') {
|
if (formId === '') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -207,17 +257,21 @@ class FbCardFormsImpl {
|
|||||||
list.push(formId);
|
list.push(formId);
|
||||||
await this.saveIds(ctx, list);
|
await this.saveIds(ctx, list);
|
||||||
}
|
}
|
||||||
|
// 类型只增不改:同一张卡不会换类型(卡片类型由 form_config 决定,随 formId 固定)
|
||||||
|
await this.setKind(ctx, formId, kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
async unregister(ctx: common.Context, formId: string): Promise<void> {
|
async unregister(ctx: common.Context, formId: string): Promise<void> {
|
||||||
const list: string[] = (await this.ids(ctx)).filter((id: string): boolean => id !== formId);
|
const list: string[] = (await this.ids(ctx)).filter((id: string): boolean => id !== formId);
|
||||||
await this.saveIds(ctx, list);
|
await this.saveIds(ctx, list);
|
||||||
|
await this.clearKind(ctx, formId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用内日程变化后刷新**所有**已添加的卡。
|
* 应用内数据变化后刷新**所有**已添加的卡。
|
||||||
*
|
*
|
||||||
* ⚠️ 按偏移量分组构建:两张卡可能翻在不同日期,逐张构建会重复打同一个接口。
|
* ⚠️ 按类型 + 偏移量分组构建:两张卡可能类型不同 / 翻在不同日期,
|
||||||
|
* 逐张构建会重复打同一个接口。
|
||||||
* ⚠️ 推失败的 formId 直接**注销**(卡片多半已被用户删掉),否则每刷一次都白推一遍。
|
* ⚠️ 推失败的 formId 直接**注销**(卡片多半已被用户删掉),否则每刷一次都白推一遍。
|
||||||
*/
|
*/
|
||||||
async pushAll(ctx: common.Context): Promise<void> {
|
async pushAll(ctx: common.Context): Promise<void> {
|
||||||
@@ -233,16 +287,20 @@ class FbCardFormsImpl {
|
|||||||
hilog.warn(DOMAIN, TAG, 'pushAll:注册表为空(桌面没有卡片,或跨进程没读到)');
|
hilog.warn(DOMAIN, TAG, 'pushAll:注册表为空(桌面没有卡片,或跨进程没读到)');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const cache: Map<number, formBindingData.FormBindingData> =
|
const cache: Map<string, formBindingData.FormBindingData> =
|
||||||
new Map<number, formBindingData.FormBindingData>();
|
new Map<string, formBindingData.FormBindingData>();
|
||||||
const stale: string[] = [];
|
const stale: string[] = [];
|
||||||
for (const formId of list) {
|
for (const formId of list) {
|
||||||
|
/* 🔴 v90:每张卡按自己的类型构建(日程卡才吃偏移量;
|
||||||
|
观影/任务卡没有"看哪一天"的概念,偏移量传 0 即可)。 */
|
||||||
|
const kind: string = await this.kindOf(ctx, formId);
|
||||||
const off: number = await this.getOffset(ctx, formId);
|
const off: number = await this.getOffset(ctx, formId);
|
||||||
let binding: formBindingData.FormBindingData | undefined = cache.get(off);
|
const ck: string = kind + '#' + off.toString();
|
||||||
|
let binding: formBindingData.FormBindingData | undefined = cache.get(ck);
|
||||||
if (binding === undefined) {
|
if (binding === undefined) {
|
||||||
const data: FbCardData = await fbCardBuild(ctx, off);
|
const data: Object = await fbCardDataByKind(ctx, kind, off);
|
||||||
binding = formBindingData.createFormBindingData(data);
|
binding = formBindingData.createFormBindingData(data);
|
||||||
cache.set(off, binding);
|
cache.set(ck, binding);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await formProvider.updateForm(formId, binding);
|
await formProvider.updateForm(formId, binding);
|
||||||
@@ -252,7 +310,7 @@ class FbCardFormsImpl {
|
|||||||
stale.push(formId);
|
stale.push(formId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hilog.info(DOMAIN, TAG, 'pushAll:已推 %{public}d 张卡(%{public}d 种偏移)',
|
hilog.info(DOMAIN, TAG, 'pushAll:已推 %{public}d 张卡(%{public}d 种组合)',
|
||||||
list.length, cache.size);
|
list.length, cache.size);
|
||||||
if (stale.length > 0) {
|
if (stale.length > 0) {
|
||||||
const keep: string[] = list.filter((id: string): boolean => stale.indexOf(id) < 0);
|
const keep: string[] = list.filter((id: string): boolean => stale.indexOf(id) < 0);
|
||||||
|
|||||||
@@ -0,0 +1,366 @@
|
|||||||
|
/**
|
||||||
|
* 家书 FamilyBook · **观影 / 任务**桌面卡片数据(鸿蒙端)—— v90
|
||||||
|
*
|
||||||
|
* 用户 2026-10-24:「在做几个服务卡片,包括观影、任务,你看着做吧。」
|
||||||
|
* ⇒ 照日程卡片(`FbCardData`)的既有范式:
|
||||||
|
* · FormExtensionAbility 回调只活约 10 秒 ⇒ 永远不抛错、立刻给壳;
|
||||||
|
* · `FormBindingData` 只认标量 ⇒ 列表一律 `JSON.stringify`;
|
||||||
|
* · 卡片是后台刷新 ⇒ `authProbe: true` 且显式带 token(401 不踢登录);
|
||||||
|
* · 🔴 v89 教训:卡片 ForEach 的 key **必须**带 `version`(`Date.now()`)前缀,
|
||||||
|
* 否则下次推送数据换了一批、key 却一样 ⇒ ArkUI 直接复用旧组件不重绘。
|
||||||
|
*
|
||||||
|
* 与日程卡的一个差异:这两类卡**没有翻页**(观影/任务没有"看哪一天"的概念),
|
||||||
|
* `dayOffset` 对它们无意义 ⇒ 构建时不吃偏移,`FbFormAbility` 按类型分流即可。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { common } from '@kit.AbilityKit';
|
||||||
|
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||||
|
import { FB_SERVER_STORE } from './FbServerStore';
|
||||||
|
import { FB_TOKEN_STORE } from './FbTokenStore';
|
||||||
|
import { fbApi, FbApiResult, FbApiOptions } from './FbHttp';
|
||||||
|
import { fbCardBuild, FbCardData } from './FbCardData';
|
||||||
|
import { parseTaskList } from './FbParse';
|
||||||
|
import { FbTaskItem } from '../model/Types';
|
||||||
|
|
||||||
|
const DOMAIN: number = 0x0000;
|
||||||
|
const TAG: string = 'FbCardMedia';
|
||||||
|
|
||||||
|
/** 两位补零(与 FbCardData 同口径,卡片时间戳用) */
|
||||||
|
function pad2(v: number): string {
|
||||||
|
return v < 10 ? '0' + v.toString() : v.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前时刻 `HH:mm` */
|
||||||
|
function clockText(): string {
|
||||||
|
const d: Date = new Date();
|
||||||
|
return pad2(d.getHours()) + ':' + pad2(d.getMinutes());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `str | null | undefined` → `str`(服务端行里 null 很常见) */
|
||||||
|
function strOf(v: string | null | undefined): string {
|
||||||
|
return typeof v === 'string' ? v : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `number | null | undefined` → `number` */
|
||||||
|
function numOf(v: number | null | undefined): number {
|
||||||
|
return typeof v === 'number' && !isNaN(v) ? v : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 卡片环境预加载 + 取令牌(三个动作一起做,失败给空串) */
|
||||||
|
async function cardToken(ctx: common.Context): Promise<string> {
|
||||||
|
try {
|
||||||
|
await FB_SERVER_STORE.init(ctx);
|
||||||
|
await FB_TOKEN_STORE.init(ctx);
|
||||||
|
} catch (err) {
|
||||||
|
hilog.warn(DOMAIN, TAG, '卡片环境预加载失败:%{public}s', JSON.stringify(err));
|
||||||
|
}
|
||||||
|
return FB_TOKEN_STORE.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ══════════════════════════════════════════════════════════════
|
||||||
|
观影卡
|
||||||
|
══════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/** 观影卡上的一行(最近观影) */
|
||||||
|
export class FbMovieCardRow {
|
||||||
|
key: string = '';
|
||||||
|
title: string = '';
|
||||||
|
/** `10月24日`(解析不出就空着,卡片不画日期) */
|
||||||
|
dateText: string = '';
|
||||||
|
cinema: string = '';
|
||||||
|
/** 家庭评分 `★ 4.5`(没评分为空串) */
|
||||||
|
rating: string = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 观影卡整体数据(字段名 = 卡片页 `@LocalStorageProp` 键名,两侧改名必须同步) */
|
||||||
|
export class FbMovieCardData {
|
||||||
|
/** `ok` / `login` / `noserver` / `offline` / `loading` */
|
||||||
|
state: string = 'loading';
|
||||||
|
hint: string = '';
|
||||||
|
/** `15:40 更新` */
|
||||||
|
updatedAt: string = '';
|
||||||
|
/** 今年看过的场数(2×2 的大数字) */
|
||||||
|
yearCount: number = 0;
|
||||||
|
/** 本次拉到的总场数(服务端分页最多 200 条,够用) */
|
||||||
|
totalCount: number = 0;
|
||||||
|
/** 最近观影(按日期倒序,最多 4 条) */
|
||||||
|
recentJson: string = '[]';
|
||||||
|
/** 🔴 v89 教训:ForEach key 前缀源 */
|
||||||
|
version: string = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 服务端观影列表的原始行(只取卡片要用的几个字段) */
|
||||||
|
interface FbRawMovie {
|
||||||
|
id?: number | null;
|
||||||
|
title?: string | null;
|
||||||
|
starts_at?: string | null;
|
||||||
|
cinema_name?: string | null;
|
||||||
|
review_count?: number | null;
|
||||||
|
review_avg?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `starts_at` → `10月24日`(解析不出给空串) */
|
||||||
|
function movieDateText(startsAt: string): string {
|
||||||
|
// `YYYY-MM-DD HH:MM:SS`
|
||||||
|
if (startsAt.length < 10) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const mm: string = startsAt.substring(5, 7);
|
||||||
|
const dd: string = startsAt.substring(8, 10);
|
||||||
|
const m: number = parseInt(mm, 10);
|
||||||
|
const d: number = parseInt(dd, 10);
|
||||||
|
if (isNaN(m) || isNaN(d) || m < 1 || m > 12 || d < 1 || d > 31) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return m.toString() + '月' + d.toString() + '日';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 一条原始行 → 卡片行 */
|
||||||
|
function movieRowOf(m: FbRawMovie, idx: number): FbMovieCardRow {
|
||||||
|
const r: FbMovieCardRow = new FbMovieCardRow();
|
||||||
|
const id: number = numOf(m.id);
|
||||||
|
r.key = 'm' + idx.toString() + '_' + id.toString();
|
||||||
|
r.title = strOf(m.title) !== '' ? strOf(m.title) : '未命名观影';
|
||||||
|
r.dateText = movieDateText(strOf(m.starts_at));
|
||||||
|
r.cinema = strOf(m.cinema_name);
|
||||||
|
const avg: number = numOf(m.review_avg);
|
||||||
|
r.rating = avg > 0 ? '★ ' + avg.toFixed(1) : '';
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组装观影卡数据(异步,**永远不抛错**)。
|
||||||
|
*
|
||||||
|
* ⚠️ 直接拉 `/api/v1/movies`(与同步同一条接口,per_page=200 一次拿全):
|
||||||
|
* 卡片进程不能也不该走应用里的本地库(FbDb 是应用进程的单例)。
|
||||||
|
*/
|
||||||
|
export async function fbMovieCardBuild(ctx: common.Context): Promise<FbMovieCardData> {
|
||||||
|
const out: FbMovieCardData = new FbMovieCardData();
|
||||||
|
out.updatedAt = clockText();
|
||||||
|
out.version = Date.now().toString();
|
||||||
|
|
||||||
|
const token: string = await cardToken(ctx);
|
||||||
|
if (FB_SERVER_STORE.get() === '') {
|
||||||
|
out.state = 'noserver';
|
||||||
|
out.hint = '先在「家书」里填写服务器地址';
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
if (token === '') {
|
||||||
|
out.state = 'login';
|
||||||
|
out.hint = '先在「家书」里登录';
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
const query: Record<string, string> = {};
|
||||||
|
query['per_page'] = '200';
|
||||||
|
query['page'] = '1';
|
||||||
|
const opts: FbApiOptions = { method: 'GET', query: query, token: token, authProbe: true };
|
||||||
|
const r: FbApiResult = await fbApi('/api/v1/movies', opts);
|
||||||
|
if (!r.ok) {
|
||||||
|
out.state = 'offline';
|
||||||
|
out.hint = r.status === 401 ? '登录已失效,请重新登录'
|
||||||
|
: (r.error !== '' ? r.error : '连不上服务器');
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析(解析失败按"暂时没数据"处理,不抛)
|
||||||
|
let arr: FbRawMovie[] = [];
|
||||||
|
try {
|
||||||
|
arr = JSON.parse(r.data) as FbRawMovie[];
|
||||||
|
} catch (err) {
|
||||||
|
hilog.warn(DOMAIN, TAG, '观影卡数据解析失败');
|
||||||
|
}
|
||||||
|
if (!Array.isArray(arr)) {
|
||||||
|
arr = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按观看日期倒序(服务端顺序不保证,卡片要"最近几部"就必须自己排;
|
||||||
|
// ISO 日期字符串可直接按字典序比较)
|
||||||
|
arr.sort((a: FbRawMovie, b: FbRawMovie): number => {
|
||||||
|
const sa: string = strOf(a.starts_at);
|
||||||
|
const sb: string = strOf(b.starts_at);
|
||||||
|
if (sa === sb) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return sa > sb ? -1 : 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const nowYear: string = (new Date()).getFullYear().toString();
|
||||||
|
let yearCount: number = 0;
|
||||||
|
for (const m of arr) {
|
||||||
|
if (strOf(m.starts_at).substring(0, 4) === nowYear) {
|
||||||
|
yearCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.yearCount = yearCount;
|
||||||
|
out.totalCount = arr.length;
|
||||||
|
|
||||||
|
const rows: FbMovieCardRow[] = [];
|
||||||
|
for (let i = 0; i < arr.length && rows.length < 4; i++) {
|
||||||
|
rows.push(movieRowOf(arr[i], i));
|
||||||
|
}
|
||||||
|
out.recentJson = JSON.stringify(rows);
|
||||||
|
out.state = 'ok';
|
||||||
|
out.hint = '';
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ══════════════════════════════════════════════════════════════
|
||||||
|
任务卡
|
||||||
|
══════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/** 任务卡上的一行 */
|
||||||
|
export class FbTaskCardRow {
|
||||||
|
key: string = '';
|
||||||
|
title: string = '';
|
||||||
|
/** 截止时间文案(服务端给的显示文本,空 = 没有期限) */
|
||||||
|
dueText: string = '';
|
||||||
|
/** `todo` / `doing`(done 的不进卡片) */
|
||||||
|
status: string = '';
|
||||||
|
/** 看板主题色(左侧色条) */
|
||||||
|
boardColor: string = '#A8574F';
|
||||||
|
overdue: boolean = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 任务卡整体数据 */
|
||||||
|
export class FbTaskCardData {
|
||||||
|
state: string = 'loading';
|
||||||
|
hint: string = '';
|
||||||
|
updatedAt: string = '';
|
||||||
|
/** 未完成任务数(todo + doing) */
|
||||||
|
openCount: number = 0;
|
||||||
|
/** 其中逾期数(标题行给红色提示) */
|
||||||
|
overdueCount: number = 0;
|
||||||
|
/** 优先展示的几条(逾期 > 进行中 > 普通,同组按截止时间升序),最多 4 条 */
|
||||||
|
rowsJson: string = '[]';
|
||||||
|
version: string = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 卡片展示优先级:逾期 0 / 进行中 1 / 普通 2 */
|
||||||
|
function taskRank(t: FbTaskItem): number {
|
||||||
|
if (t.overdue) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return t.status === 'doing' ? 1 : 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 一条任务 → 卡片行 */
|
||||||
|
function taskRowOf(t: FbTaskItem): FbTaskCardRow {
|
||||||
|
const r: FbTaskCardRow = new FbTaskCardRow();
|
||||||
|
r.key = 't' + t.id.toString() + '_' + t.status;
|
||||||
|
r.title = t.title !== '' ? t.title : '未命名任务';
|
||||||
|
r.dueText = t.dueText;
|
||||||
|
r.status = t.status;
|
||||||
|
r.boardColor = t.boardColor !== '' ? t.boardColor : '#A8574F';
|
||||||
|
r.overdue = t.overdue;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组装任务卡数据(异步,**永远不抛错**)。
|
||||||
|
*
|
||||||
|
* ⚠️ `/api/v1/tasks` 默认视图就是"未完成任务"(已完成走 `?view=done`);
|
||||||
|
* 这里仍按 status 过滤一道,服务端口径将来变了也不会把 done 显示成待办。
|
||||||
|
*/
|
||||||
|
export async function fbTaskCardBuild(ctx: common.Context): Promise<FbTaskCardData> {
|
||||||
|
const out: FbTaskCardData = new FbTaskCardData();
|
||||||
|
out.updatedAt = clockText();
|
||||||
|
out.version = Date.now().toString();
|
||||||
|
|
||||||
|
const token: string = await cardToken(ctx);
|
||||||
|
if (FB_SERVER_STORE.get() === '') {
|
||||||
|
out.state = 'noserver';
|
||||||
|
out.hint = '先在「家书」里填写服务器地址';
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
if (token === '') {
|
||||||
|
out.state = 'login';
|
||||||
|
out.hint = '先在「家书」里登录';
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
const opts: FbApiOptions = { method: 'GET', token: token, authProbe: true };
|
||||||
|
const r: FbApiResult = await fbApi('/api/v1/tasks', opts);
|
||||||
|
if (!r.ok) {
|
||||||
|
out.state = 'offline';
|
||||||
|
out.hint = r.status === 401 ? '登录已失效,请重新登录'
|
||||||
|
: (r.error !== '' ? r.error : '连不上服务器');
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
let items: FbTaskItem[] = [];
|
||||||
|
try {
|
||||||
|
items = parseTaskList(r.data);
|
||||||
|
} catch (err) {
|
||||||
|
hilog.warn(DOMAIN, TAG, '任务卡数据解析失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const open: FbTaskItem[] = [];
|
||||||
|
for (const t of items) {
|
||||||
|
if (t.status !== 'done') {
|
||||||
|
open.push(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.openCount = open.length;
|
||||||
|
|
||||||
|
let overdueCount: number = 0;
|
||||||
|
for (const t of open) {
|
||||||
|
if (t.overdue) {
|
||||||
|
overdueCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.overdueCount = overdueCount;
|
||||||
|
|
||||||
|
// 逾期优先,其次进行中,再普通;同组按截止时间升序(没有期限的排最后)
|
||||||
|
open.sort((a: FbTaskItem, b: FbTaskItem): number => {
|
||||||
|
const ra: number = taskRank(a);
|
||||||
|
const rb: number = taskRank(b);
|
||||||
|
if (ra !== rb) {
|
||||||
|
return ra - rb;
|
||||||
|
}
|
||||||
|
if (a.dueAt === b.dueAt) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (a.dueAt === '') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (b.dueAt === '') {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return a.dueAt < b.dueAt ? -1 : 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const rows: FbTaskCardRow[] = [];
|
||||||
|
for (let i = 0; i < open.length && rows.length < 4; i++) {
|
||||||
|
rows.push(taskRowOf(open[i]));
|
||||||
|
}
|
||||||
|
out.rowsJson = JSON.stringify(rows);
|
||||||
|
out.state = 'ok';
|
||||||
|
out.hint = '';
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ══════════════════════════════════════════════════════════════
|
||||||
|
按卡片类型分流
|
||||||
|
══════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按卡片类型构建数据 —— `FbFormAbility` 与 `FbCardForms.pushAll` 共用这一个入口。
|
||||||
|
*
|
||||||
|
* @param kind `calendar` / `movies` / `tasks`(认不出按日程处理 ——
|
||||||
|
* 兼容升级前已登记的旧卡片,它们的 kind 没落盘,只能默认日程)
|
||||||
|
* @param dayOffset 仅日程卡用(翻页);其余类型忽略
|
||||||
|
*/
|
||||||
|
export async function fbCardDataByKind(ctx: common.Context, kind: string,
|
||||||
|
dayOffset: number): Promise<Object> {
|
||||||
|
if (kind === 'movies') {
|
||||||
|
const m: FbMovieCardData = await fbMovieCardBuild(ctx);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
if (kind === 'tasks') {
|
||||||
|
const t: FbTaskCardData = await fbTaskCardBuild(ctx);
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
const d: FbCardData = await fbCardBuild(ctx, dayOffset);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
@@ -20,29 +20,43 @@
|
|||||||
* 系统定时刷新**不能**把用户翻走的日期拽回今天。
|
* 系统定时刷新**不能**把用户翻走的日期拽回今天。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FormExtensionAbility, formBindingData, formProvider } from '@kit.FormKit';
|
import { FormExtensionAbility, formBindingData, formProvider, formInfo } from '@kit.FormKit';
|
||||||
import { Want } from '@kit.AbilityKit';
|
import { Want } from '@kit.AbilityKit';
|
||||||
import { BusinessError } from '@kit.BasicServicesKit';
|
import { BusinessError } from '@kit.BasicServicesKit';
|
||||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||||
import { FbCardData, fbCardBuild } from '../data/FbCardData';
|
import { fbCardDataByKind } from '../data/FbCardMediaData';
|
||||||
import { FB_CARD_FORMS } from '../data/FbCardForms';
|
import { FB_CARD_FORMS } from '../data/FbCardForms';
|
||||||
import { FB_BG_SYNC } from '../common/FbBackgroundSync';
|
import { FB_BG_SYNC } from '../common/FbBackgroundSync';
|
||||||
|
|
||||||
const DOMAIN: number = 0x0000;
|
const DOMAIN: number = 0x0000;
|
||||||
const TAG: string = 'FbForm';
|
const TAG: string = 'FbForm';
|
||||||
|
|
||||||
|
/** 卡片名 → 数据类型(v90 起不止日程卡)。
|
||||||
|
* ⚠️ 名字来自 `form_config.json` 的 `name` 字段(Want 里的 `form_name`),
|
||||||
|
* 两边改名必须同步 —— 改一边的表现是"新卡永远拿日程数据",编译不报错。 */
|
||||||
|
function kindOfFormName(name: string): string {
|
||||||
|
if (name.indexOf('FbMovieCard') === 0) {
|
||||||
|
return 'movies';
|
||||||
|
}
|
||||||
|
if (name.indexOf('FbTaskCard') === 0) {
|
||||||
|
return 'tasks';
|
||||||
|
}
|
||||||
|
return 'calendar';
|
||||||
|
}
|
||||||
|
|
||||||
export default class FbFormAbility extends FormExtensionAbility {
|
export default class FbFormAbility extends FormExtensionAbility {
|
||||||
/**
|
/**
|
||||||
* 重新算一遍并推给指定卡片。
|
* 重新算一遍并推给指定卡片。
|
||||||
*
|
*
|
||||||
* ⚠️ 四块卡(2×2 / 2×4 / 4×4 / 6×4)共用这份数据:它们只是**排版不同**,
|
* ⚠️ v90 起按类型分流:日程卡吃 `dayOffset`(翻页);
|
||||||
* 数字与条目必须是同一个 —— 多算一遍既慢又可能口径不一致。
|
* 观影/任务卡没有"看哪一天"的概念,`dayOffset` 固定传 0。
|
||||||
*/
|
*/
|
||||||
private push(formId: string, dayOffset: number): void {
|
private push(formId: string, kind: string, dayOffset: number): void {
|
||||||
fbCardBuild(this.context, dayOffset).then((data: FbCardData): void => {
|
fbCardDataByKind(this.context, kind, dayOffset).then((data: Object): void => {
|
||||||
const binding: formBindingData.FormBindingData = formBindingData.createFormBindingData(data);
|
const binding: formBindingData.FormBindingData = formBindingData.createFormBindingData(data);
|
||||||
formProvider.updateForm(formId, binding).then((): void => {
|
formProvider.updateForm(formId, binding).then((): void => {
|
||||||
hilog.info(DOMAIN, TAG, '卡片 %{public}s 已更新(偏移 %{public}d)', formId, data.dayOffset);
|
hilog.info(DOMAIN, TAG, '卡片 %{public}s(%{public}s) 已更新(偏移 %{public}d)',
|
||||||
|
formId, kind, dayOffset);
|
||||||
}).catch((err: BusinessError): void => {
|
}).catch((err: BusinessError): void => {
|
||||||
// 卡片可能已经被用户删了 —— 打一条日志就够,不要重试
|
// 卡片可能已经被用户删了 —— 打一条日志就够,不要重试
|
||||||
hilog.warn(DOMAIN, TAG, '卡片 %{public}s 更新失败:%{public}s', formId, err.message);
|
hilog.warn(DOMAIN, TAG, '卡片 %{public}s 更新失败:%{public}s', formId, err.message);
|
||||||
@@ -61,21 +75,31 @@ export default class FbFormAbility extends FormExtensionAbility {
|
|||||||
return typeof raw === 'string' ? raw : '';
|
return typeof raw === 'string' ? raw : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 从 Want 里取卡片名(`ohos.extra.param.key.form_name`,即 form_config 的 name) */
|
||||||
|
private formNameOf(want: Want): string {
|
||||||
|
if (want.parameters === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const raw: Object | undefined = want.parameters[formInfo.FormParam.NAME_KEY];
|
||||||
|
return typeof raw === 'string' ? raw : '';
|
||||||
|
}
|
||||||
|
|
||||||
onAddForm(want: Want): formBindingData.FormBindingData {
|
onAddForm(want: Want): formBindingData.FormBindingData {
|
||||||
const formId: string = this.formIdOf(want);
|
const formId: string = this.formIdOf(want);
|
||||||
hilog.info(DOMAIN, TAG, '添加卡片:%{public}s', formId);
|
const kind: string = kindOfFormName(this.formNameOf(want));
|
||||||
|
hilog.info(DOMAIN, TAG, '添加卡片:%{public}s(%{public}s)', formId, kind);
|
||||||
if (formId !== '') {
|
if (formId !== '') {
|
||||||
// 异步:登记 + 补真实数据(不 await:见文件头约束 ①)
|
// 异步:登记 + 补真实数据(不 await:见文件头约束 ①)
|
||||||
FB_CARD_FORMS.register(this.context, formId).then((): void => {
|
FB_CARD_FORMS.register(this.context, formId, kind).then((): void => {
|
||||||
this.push(formId, 0);
|
this.push(formId, kind, 0);
|
||||||
}).catch((err: BusinessError): void => {
|
}).catch((err: BusinessError): void => {
|
||||||
hilog.warn(DOMAIN, TAG, '卡片登记失败:%{public}s', err.message);
|
hilog.warn(DOMAIN, TAG, '卡片登记失败:%{public}s', err.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// 先给一份"正在取"的壳 —— 卡片上会显示空态,而不是白屏
|
// 先给一份"正在取"的壳 —— 卡片上会显示空态,而不是白屏
|
||||||
const shell: FbCardData = new FbCardData();
|
const shell: Record<string, string> = {};
|
||||||
shell.state = 'loading';
|
shell['state'] = 'loading';
|
||||||
shell.hint = '正在取数据…';
|
shell['hint'] = '正在取数据…';
|
||||||
return formBindingData.createFormBindingData(shell);
|
return formBindingData.createFormBindingData(shell);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,11 +124,21 @@ export default class FbFormAbility extends FormExtensionAbility {
|
|||||||
⚠️ 节流:系统刷新可能漂移或重入,`runThrottledByCard` 内部用时间戳挡了一道,
|
⚠️ 节流:系统刷新可能漂移或重入,`runThrottledByCard` 内部用时间戳挡了一道,
|
||||||
并且**先写时间戳再跑**,避免卡片进程与主进程并发拉同一批数据。 */
|
并且**先写时间戳再跑**,避免卡片进程与主进程并发拉同一批数据。 */
|
||||||
// ⚠️ 必须按**这张卡自己的偏移**重建,否则定时刷新会把用户翻走的日期拽回今天
|
// ⚠️ 必须按**这张卡自己的偏移**重建,否则定时刷新会把用户翻走的日期拽回今天
|
||||||
FB_CARD_FORMS.getOffset(this.context, formId).then((off: number): void => {
|
// 🔴 v90:偏移量只对日程卡有意义;观影/任务卡按类型直接推。
|
||||||
this.push(formId, off);
|
FB_CARD_FORMS.kindOf(this.context, formId).then((kind: string): void => {
|
||||||
|
if (kind !== 'calendar') {
|
||||||
|
this.push(formId, kind, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FB_CARD_FORMS.getOffset(this.context, formId).then((off: number): void => {
|
||||||
|
this.push(formId, kind, off);
|
||||||
|
}).catch((err: BusinessError): void => {
|
||||||
|
hilog.warn(DOMAIN, TAG, '读卡片偏移失败:%{public}s', err.message);
|
||||||
|
this.push(formId, kind, 0);
|
||||||
|
});
|
||||||
}).catch((err: BusinessError): void => {
|
}).catch((err: BusinessError): void => {
|
||||||
hilog.warn(DOMAIN, TAG, '读卡片偏移失败:%{public}s', err.message);
|
hilog.warn(DOMAIN, TAG, '读卡片类型失败(按日程处理):%{public}s', err.message);
|
||||||
this.push(formId, 0);
|
this.push(formId, 'calendar', 0);
|
||||||
});
|
});
|
||||||
/* ⚠️ 这一句必须放在**推送之后**(原因见上面的 ①:卡片刷新只有几秒的窗口,
|
/* ⚠️ 这一句必须放在**推送之后**(原因见上面的 ①:卡片刷新只有几秒的窗口,
|
||||||
不能被 12 个模块的同步挤掉)。两条都是"发起即返回",不会互相阻塞。 */
|
不能被 12 个模块的同步挤掉)。两条都是"发起即返回",不会互相阻塞。 */
|
||||||
@@ -116,26 +150,35 @@ export default class FbFormAbility extends FormExtensionAbility {
|
|||||||
onFormEvent(formId: string, message: string): void {
|
onFormEvent(formId: string, message: string): void {
|
||||||
/* 卡片上的按钮都走 `action: 'message'`,`message` 是 JSON 字符串。
|
/* 卡片上的按钮都走 `action: 'message'`,`message` 是 JSON 字符串。
|
||||||
⚠️ 不解析 JSON:不同版本系统给的形态可能是 `{params:{...}}` 或直接 `{...}`,
|
⚠️ 不解析 JSON:不同版本系统给的形态可能是 `{params:{...}}` 或直接 `{...}`,
|
||||||
子串匹配更稳(与 SyncCalendar 同一个取舍)。 */
|
子串匹配更稳(与 SyncCalendar 同一个取舍)。
|
||||||
|
⚠️ v90:只有日程卡有 ‹ / › / 今 三个按钮;观影/任务卡不发 message 事件,
|
||||||
|
收到也直接忽略(不把 message 撞进偏移量里)。 */
|
||||||
hilog.info(DOMAIN, TAG, '卡片事件:%{public}s %{public}s', formId, message);
|
hilog.info(DOMAIN, TAG, '卡片事件:%{public}s %{public}s', formId, message);
|
||||||
FB_CARD_FORMS.getOffset(this.context, formId).then((cur: number): void => {
|
FB_CARD_FORMS.kindOf(this.context, formId).then((kind: string): void => {
|
||||||
let next: number = cur;
|
if (kind !== 'calendar') {
|
||||||
if (message.indexOf('prev') >= 0) {
|
return;
|
||||||
next = cur - 1;
|
|
||||||
} else if (message.indexOf('next') >= 0) {
|
|
||||||
next = cur + 1;
|
|
||||||
} else if (message.indexOf('today') >= 0) {
|
|
||||||
next = 0;
|
|
||||||
}
|
}
|
||||||
FB_CARD_FORMS.setOffset(this.context, formId, next).then((off: number): void => {
|
FB_CARD_FORMS.getOffset(this.context, formId).then((cur: number): void => {
|
||||||
this.push(formId, off);
|
let next: number = cur;
|
||||||
|
if (message.indexOf('prev') >= 0) {
|
||||||
|
next = cur - 1;
|
||||||
|
} else if (message.indexOf('next') >= 0) {
|
||||||
|
next = cur + 1;
|
||||||
|
} else if (message.indexOf('today') >= 0) {
|
||||||
|
next = 0;
|
||||||
|
}
|
||||||
|
FB_CARD_FORMS.setOffset(this.context, formId, next).then((off: number): void => {
|
||||||
|
this.push(formId, kind, off);
|
||||||
|
}).catch((err: BusinessError): void => {
|
||||||
|
hilog.warn(DOMAIN, TAG, '卡片翻页失败:%{public}s', err.message);
|
||||||
|
this.push(formId, kind, cur);
|
||||||
|
});
|
||||||
}).catch((err: BusinessError): void => {
|
}).catch((err: BusinessError): void => {
|
||||||
hilog.warn(DOMAIN, TAG, '卡片翻页失败:%{public}s', err.message);
|
hilog.warn(DOMAIN, TAG, '读卡片偏移失败(按今天处理):%{public}s', err.message);
|
||||||
this.push(formId, cur);
|
this.push(formId, kind, 0);
|
||||||
});
|
});
|
||||||
}).catch((err: BusinessError): void => {
|
}).catch((err: BusinessError): void => {
|
||||||
hilog.warn(DOMAIN, TAG, '读卡片偏移失败(按今天处理):%{public}s', err.message);
|
hilog.warn(DOMAIN, TAG, '读卡片类型失败:%{public}s', err.message);
|
||||||
this.push(formId, 0);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
/**
|
||||||
|
* 家书 FamilyBook · 2×2 服务卡片(观影 · 今年概览)
|
||||||
|
*
|
||||||
|
* 用户 2026-10-24:「在做几个服务卡片,包括观影、任务,你看着做吧。」
|
||||||
|
* ⇒ 排版照日程 2×2(FbCard2x2)的骨架:抬头 + 分隔线 + 大数字。
|
||||||
|
* 大数字 = **今年**看过的场数(比总数更有"今年看了多少"的当下感);
|
||||||
|
* 底部一行给最近一部电影,让卡上有具体内容而不只是一个数。
|
||||||
|
*
|
||||||
|
* ⚠️ 动态卡片不能自己发网络请求:只画 `fbMovieCardBuild` 推过来的字段。
|
||||||
|
* ⚠️ 整卡可点 → 打开应用并落到「观影」模块(fbModule: 'movies')。
|
||||||
|
*/
|
||||||
|
|
||||||
|
let storageMv2 = new LocalStorage();
|
||||||
|
|
||||||
|
/** 最近观影一行(`recentJson` 的元素,只用到这几个字段) */
|
||||||
|
class FbMovieRow2 {
|
||||||
|
title: string = '';
|
||||||
|
dateText: string = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entry(storageMv2)
|
||||||
|
@Component
|
||||||
|
struct FbMovieCard2x2 {
|
||||||
|
@LocalStorageProp('state') state: string = 'loading';
|
||||||
|
@LocalStorageProp('hint') hint: string = '';
|
||||||
|
@LocalStorageProp('updatedAt') updatedAt: string = '';
|
||||||
|
@LocalStorageProp('yearCount') yearCount: number = 0;
|
||||||
|
@LocalStorageProp('recentJson') recentJson: string = '[]';
|
||||||
|
/* 🔴 v89 教训:ForEach key 要带 version 前缀(本卡列表最多一条,也照规矩来) */
|
||||||
|
@LocalStorageProp('version') version: string = '';
|
||||||
|
|
||||||
|
/** 最近一部(取不到给空) */
|
||||||
|
private latest(): FbMovieRow2 {
|
||||||
|
try {
|
||||||
|
const all: FbMovieRow2[] = JSON.parse(this.recentJson) as FbMovieRow2[];
|
||||||
|
if (all.length > 0) {
|
||||||
|
return all[0];
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// 解析失败按"没有最近一部"处理
|
||||||
|
}
|
||||||
|
const none: FbMovieRow2 = new FbMovieRow2();
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
|
||||||
|
private openApp(): void {
|
||||||
|
postCardAction(this, {
|
||||||
|
action: 'router',
|
||||||
|
abilityName: 'EntryAbility',
|
||||||
|
params: { fbModule: 'movies' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Column({ space: 6 }) {
|
||||||
|
// ---- 抬头:模块名 + 更新时间 ----
|
||||||
|
Row({ space: 6 }) {
|
||||||
|
Text('观影')
|
||||||
|
.fontSize(15)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.fontColor('#2C231D')
|
||||||
|
.maxLines(1)
|
||||||
|
.layoutWeight(1)
|
||||||
|
Text(this.updatedAt)
|
||||||
|
.fontSize(9)
|
||||||
|
.fontColor('#A8988A')
|
||||||
|
.maxLines(1)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
.strokeWidth(0.5)
|
||||||
|
.color('#EFE6D8')
|
||||||
|
|
||||||
|
// ---- 今年场数(占满剩余空间,垂直居中)----
|
||||||
|
Row({ space: 8 }) {
|
||||||
|
Text(`${this.yearCount}`)
|
||||||
|
.fontSize(40)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.fontColor('#7A4E8C')
|
||||||
|
Column({ space: 2 }) {
|
||||||
|
Text('今年观影')
|
||||||
|
.fontSize(12)
|
||||||
|
.fontColor('#2C231D')
|
||||||
|
.maxLines(1)
|
||||||
|
Text(this.hint !== '' ? this.hint : '点击查看')
|
||||||
|
.fontSize(10)
|
||||||
|
.fontColor(this.hint !== '' ? '#C0533F' : '#A8988A')
|
||||||
|
.maxLines(1)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
}
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
.layoutWeight(1)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.layoutWeight(1)
|
||||||
|
.alignItems(VerticalAlign.Center)
|
||||||
|
|
||||||
|
// ---- 最近一部(有数据才画)----
|
||||||
|
if (this.latest().title !== '') {
|
||||||
|
Row({ space: 6 }) {
|
||||||
|
Column()
|
||||||
|
.width(3)
|
||||||
|
.height(26)
|
||||||
|
.borderRadius(2)
|
||||||
|
.backgroundColor('#7A4E8C')
|
||||||
|
Text('《' + this.latest().title + '》')
|
||||||
|
.fontSize(11)
|
||||||
|
.fontColor('#5A4A3D')
|
||||||
|
.maxLines(1)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
.layoutWeight(1)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.padding(14)
|
||||||
|
.backgroundColor('#FFFFFF')
|
||||||
|
.borderRadius(16)
|
||||||
|
// ⭐ 整卡可点
|
||||||
|
.onClick((): void => this.openApp())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
/**
|
||||||
|
* 家书 FamilyBook · 2×4 服务卡片(观影 · 左概览 / 右最近)
|
||||||
|
*
|
||||||
|
* 用户 2026-10-24:「在做几个服务卡片,包括观影、任务,你看着做吧。」
|
||||||
|
* ⇒ 骨架照日程 2×4(FbCard2x4):左半 = 2×2 那套;右半 = 最近观影(最多三条)。
|
||||||
|
*
|
||||||
|
* ⚠️ 数据与 2×2 同一份(fbMovieCardBuild 算一次),条数口径一致 —— 卡片里不自己算。
|
||||||
|
*/
|
||||||
|
|
||||||
|
let storageMv4 = new LocalStorage();
|
||||||
|
|
||||||
|
/** 右半「最近观影」一行 */
|
||||||
|
class FbMovieRow4 {
|
||||||
|
key: string = '';
|
||||||
|
title: string = '';
|
||||||
|
dateText: string = '';
|
||||||
|
cinema: string = '';
|
||||||
|
rating: string = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entry(storageMv4)
|
||||||
|
@Component
|
||||||
|
struct FbMovieCard2x4 {
|
||||||
|
@LocalStorageProp('state') state: string = 'loading';
|
||||||
|
@LocalStorageProp('hint') hint: string = '';
|
||||||
|
@LocalStorageProp('updatedAt') updatedAt: string = '';
|
||||||
|
@LocalStorageProp('yearCount') yearCount: number = 0;
|
||||||
|
@LocalStorageProp('totalCount') totalCount: number = 0;
|
||||||
|
@LocalStorageProp('recentJson') recentJson: string = '[]';
|
||||||
|
/* 🔴 v89 教训:ForEach key 必须带 version 前缀,否则下次推送不重绘 */
|
||||||
|
@LocalStorageProp('version') version: string = '';
|
||||||
|
|
||||||
|
/** 右半最多三条(数据侧已裁到 4,这里再兜底截断) */
|
||||||
|
private recent(): FbMovieRow4[] {
|
||||||
|
try {
|
||||||
|
const all: FbMovieRow4[] = JSON.parse(this.recentJson) as FbMovieRow4[];
|
||||||
|
return all.slice(0, 3);
|
||||||
|
} catch (err) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private openApp(): void {
|
||||||
|
postCardAction(this, {
|
||||||
|
action: 'router',
|
||||||
|
abilityName: 'EntryAbility',
|
||||||
|
params: { fbModule: 'movies' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 右半单条:左色条 + 片名 + 日期/影院(+评分) */
|
||||||
|
@Builder
|
||||||
|
recentRow(item: FbMovieRow4) {
|
||||||
|
Row({ space: 8 }) {
|
||||||
|
Column()
|
||||||
|
.width(3)
|
||||||
|
.height(38)
|
||||||
|
.borderRadius(2)
|
||||||
|
.backgroundColor('#7A4E8C')
|
||||||
|
Column({ space: 2 }) {
|
||||||
|
Text(item.title)
|
||||||
|
.fontSize(13)
|
||||||
|
.fontColor('#2C231D')
|
||||||
|
.maxLines(1)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
Row({ space: 4 }) {
|
||||||
|
if (item.dateText !== '') {
|
||||||
|
Text(item.dateText)
|
||||||
|
.fontSize(9)
|
||||||
|
.fontColor('#A8988A')
|
||||||
|
.maxLines(1)
|
||||||
|
}
|
||||||
|
if (item.cinema !== '') {
|
||||||
|
Text(item.cinema)
|
||||||
|
.fontSize(9)
|
||||||
|
.fontColor('#A8988A')
|
||||||
|
.maxLines(1)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
.layoutWeight(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
}
|
||||||
|
.layoutWeight(1)
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
if (item.rating !== '') {
|
||||||
|
Text(item.rating)
|
||||||
|
.fontSize(10)
|
||||||
|
.fontColor('#C08A3E')
|
||||||
|
.maxLines(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alignItems(VerticalAlign.Center)
|
||||||
|
.width('100%')
|
||||||
|
.padding({ left: 8, right: 8, top: 5, bottom: 5 })
|
||||||
|
.borderRadius(8)
|
||||||
|
.backgroundColor('#F6F2F7')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 右半空态 */
|
||||||
|
@Builder
|
||||||
|
emptyHint() {
|
||||||
|
Column() {
|
||||||
|
Text(this.hint !== '' ? this.hint : '还没有观影记录')
|
||||||
|
.fontSize(12)
|
||||||
|
.fontColor(this.hint !== '' ? '#C0533F' : '#A8988A')
|
||||||
|
.maxLines(3)
|
||||||
|
.textAlign(TextAlign.Center)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.layoutWeight(1)
|
||||||
|
.justifyContent(FlexAlign.Center)
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Row({ space: 10 }) {
|
||||||
|
// ===== 左半:与 2×2 一致 =====
|
||||||
|
Column({ space: 6 }) {
|
||||||
|
Row({ space: 6 }) {
|
||||||
|
Text('观影')
|
||||||
|
.fontSize(15)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.fontColor('#2C231D')
|
||||||
|
.maxLines(1)
|
||||||
|
.layoutWeight(1)
|
||||||
|
Text(this.updatedAt)
|
||||||
|
.fontSize(9)
|
||||||
|
.fontColor('#A8988A')
|
||||||
|
.maxLines(1)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
.strokeWidth(0.5)
|
||||||
|
.color('#EFE6D8')
|
||||||
|
|
||||||
|
Row({ space: 6 }) {
|
||||||
|
Text(`${this.yearCount}`)
|
||||||
|
.fontSize(38)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.fontColor('#7A4E8C')
|
||||||
|
Column({ space: 2 }) {
|
||||||
|
Text('今年观影')
|
||||||
|
.fontSize(11)
|
||||||
|
.fontColor('#2C231D')
|
||||||
|
.maxLines(1)
|
||||||
|
Text(this.hint !== '' ? this.hint
|
||||||
|
: ('共 ' + this.totalCount.toString() + ' 条 · 点击查看'))
|
||||||
|
.fontSize(9)
|
||||||
|
.fontColor(this.hint !== '' ? '#C0533F' : '#A8988A')
|
||||||
|
.maxLines(1)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
}
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.layoutWeight(1)
|
||||||
|
.alignItems(VerticalAlign.Center)
|
||||||
|
}
|
||||||
|
.layoutWeight(1)
|
||||||
|
.height('100%')
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
.vertical(true)
|
||||||
|
.height('92%')
|
||||||
|
.strokeWidth(0.5)
|
||||||
|
.color('#EFE6D8')
|
||||||
|
|
||||||
|
// ===== 右半:最近观影(不滚动)=====
|
||||||
|
Column() {
|
||||||
|
if (this.recent().length === 0) {
|
||||||
|
this.emptyHint()
|
||||||
|
} else {
|
||||||
|
Column({ space: 6 }) {
|
||||||
|
ForEach(this.recent(), (item: FbMovieRow4) => {
|
||||||
|
this.recentRow(item)
|
||||||
|
}, (item: FbMovieRow4) => `mv_${this.version}_${item.key}`)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.layoutWeight(1)
|
||||||
|
.justifyContent(FlexAlign.Center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.layoutWeight(1)
|
||||||
|
.height('100%')
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.padding(12)
|
||||||
|
.backgroundColor('#FFFFFF')
|
||||||
|
.borderRadius(16)
|
||||||
|
// ⭐ 整卡可点
|
||||||
|
.onClick((): void => this.openApp())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/**
|
||||||
|
* 家书 FamilyBook · 2×2 服务卡片(任务 · 待办概览)
|
||||||
|
*
|
||||||
|
* 用户 2026-10-24:「在做几个服务卡片,包括观影、任务,你看着做吧。」
|
||||||
|
* ⇒ 骨架照日程 2×2;大数字 = 未完成任务数(todo + doing);
|
||||||
|
* 有逾期时副行变红给「逾期 N 条」,让"该处理了"一眼可见。
|
||||||
|
*
|
||||||
|
* ⚠️ 动态卡片不能自己发网络请求:只画 `fbTaskCardBuild` 推过来的字段。
|
||||||
|
* ⚠️ 整卡可点 → 打开应用并落到「任务」模块(fbModule: 'tasks')。
|
||||||
|
*/
|
||||||
|
|
||||||
|
let storageTk2 = new LocalStorage();
|
||||||
|
|
||||||
|
@Entry(storageTk2)
|
||||||
|
@Component
|
||||||
|
struct FbTaskCard2x2 {
|
||||||
|
@LocalStorageProp('state') state: string = 'loading';
|
||||||
|
@LocalStorageProp('hint') hint: string = '';
|
||||||
|
@LocalStorageProp('updatedAt') updatedAt: string = '';
|
||||||
|
@LocalStorageProp('openCount') openCount: number = 0;
|
||||||
|
@LocalStorageProp('overdueCount') overdueCount: number = 0;
|
||||||
|
|
||||||
|
private openApp(): void {
|
||||||
|
postCardAction(this, {
|
||||||
|
action: 'router',
|
||||||
|
abilityName: 'EntryAbility',
|
||||||
|
params: { fbModule: 'tasks' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Column({ space: 6 }) {
|
||||||
|
// ---- 抬头:模块名 + 更新时间 ----
|
||||||
|
Row({ space: 6 }) {
|
||||||
|
Text('任务')
|
||||||
|
.fontSize(15)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.fontColor('#2C231D')
|
||||||
|
.maxLines(1)
|
||||||
|
.layoutWeight(1)
|
||||||
|
Text(this.updatedAt)
|
||||||
|
.fontSize(9)
|
||||||
|
.fontColor('#A8988A')
|
||||||
|
.maxLines(1)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
.strokeWidth(0.5)
|
||||||
|
.color('#EFE6D8')
|
||||||
|
|
||||||
|
// ---- 待办数 ----
|
||||||
|
Row({ space: 8 }) {
|
||||||
|
Text(`${this.openCount}`)
|
||||||
|
.fontSize(40)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.fontColor('#A8574F')
|
||||||
|
Column({ space: 2 }) {
|
||||||
|
Text('待办任务')
|
||||||
|
.fontSize(12)
|
||||||
|
.fontColor('#2C231D')
|
||||||
|
.maxLines(1)
|
||||||
|
Text(this.hint !== '' ? this.hint
|
||||||
|
: (this.overdueCount > 0 ? '逾期 ' + this.overdueCount.toString() + ' 条' : '点击查看'))
|
||||||
|
.fontSize(10)
|
||||||
|
.fontColor(this.hint !== '' || this.overdueCount > 0 ? '#C0533F' : '#A8988A')
|
||||||
|
.maxLines(1)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
}
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
.layoutWeight(1)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.layoutWeight(1)
|
||||||
|
.alignItems(VerticalAlign.Center)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.padding(14)
|
||||||
|
.backgroundColor('#FFFFFF')
|
||||||
|
.borderRadius(16)
|
||||||
|
// ⭐ 整卡可点
|
||||||
|
.onClick((): void => this.openApp())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
/**
|
||||||
|
* 家书 FamilyBook · 2×4 服务卡片(任务 · 左概览 / 右待办)
|
||||||
|
*
|
||||||
|
* 用户 2026-10-24:「在做几个服务卡片,包括观影、任务,你看着做吧。」
|
||||||
|
* ⇒ 骨架照日程 2×4:左半 = 2×2 那套;右半 = 待办任务(最多三条)。
|
||||||
|
* 排序由数据侧定好(逾期 > 进行中 > 普通,同组按截止时间升序),
|
||||||
|
* 逾期行整条用红色 deadline 文案提示。
|
||||||
|
*
|
||||||
|
* ⚠️ 数据与 2×2 同一份(fbTaskCardBuild 算一次)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
let storageTk4 = new LocalStorage();
|
||||||
|
|
||||||
|
/** 右半「待办」一行 */
|
||||||
|
class FbTaskRow4 {
|
||||||
|
key: string = '';
|
||||||
|
title: string = '';
|
||||||
|
dueText: string = '';
|
||||||
|
status: string = '';
|
||||||
|
boardColor: string = '#A8574F';
|
||||||
|
overdue: boolean = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entry(storageTk4)
|
||||||
|
@Component
|
||||||
|
struct FbTaskCard2x4 {
|
||||||
|
@LocalStorageProp('state') state: string = 'loading';
|
||||||
|
@LocalStorageProp('hint') hint: string = '';
|
||||||
|
@LocalStorageProp('updatedAt') updatedAt: string = '';
|
||||||
|
@LocalStorageProp('openCount') openCount: number = 0;
|
||||||
|
@LocalStorageProp('overdueCount') overdueCount: number = 0;
|
||||||
|
@LocalStorageProp('rowsJson') rowsJson: string = '[]';
|
||||||
|
/* 🔴 v89 教训:ForEach key 必须带 version 前缀,否则下次推送不重绘 */
|
||||||
|
@LocalStorageProp('version') version: string = '';
|
||||||
|
|
||||||
|
/** 右半最多三条(数据侧已裁到 4,这里再兜底截断) */
|
||||||
|
private rows(): FbTaskRow4[] {
|
||||||
|
try {
|
||||||
|
const all: FbTaskRow4[] = JSON.parse(this.rowsJson) as FbTaskRow4[];
|
||||||
|
return all.slice(0, 3);
|
||||||
|
} catch (err) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private openApp(): void {
|
||||||
|
postCardAction(this, {
|
||||||
|
action: 'router',
|
||||||
|
abilityName: 'EntryAbility',
|
||||||
|
params: { fbModule: 'tasks' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 右半单条:左色条 + 标题 + 截止(逾期红) */
|
||||||
|
@Builder
|
||||||
|
taskRow(item: FbTaskRow4) {
|
||||||
|
Row({ space: 8 }) {
|
||||||
|
Column()
|
||||||
|
.width(3)
|
||||||
|
.height(38)
|
||||||
|
.borderRadius(2)
|
||||||
|
.backgroundColor(item.overdue ? '#C0533F' : item.boardColor)
|
||||||
|
Column({ space: 2 }) {
|
||||||
|
Text(item.title)
|
||||||
|
.fontSize(13)
|
||||||
|
.fontColor('#2C231D')
|
||||||
|
.maxLines(1)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
Row({ space: 4 }) {
|
||||||
|
if (item.overdue) {
|
||||||
|
Text('逾期')
|
||||||
|
.fontSize(9)
|
||||||
|
.fontColor('#C0533F')
|
||||||
|
.maxLines(1)
|
||||||
|
}
|
||||||
|
if (item.dueText !== '') {
|
||||||
|
Text(item.dueText)
|
||||||
|
.fontSize(9)
|
||||||
|
.fontColor(item.overdue ? '#C0533F' : '#A8988A')
|
||||||
|
.maxLines(1)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
.layoutWeight(1)
|
||||||
|
} else {
|
||||||
|
Text(item.status === 'doing' ? '进行中' : '待办')
|
||||||
|
.fontSize(9)
|
||||||
|
.fontColor('#A8988A')
|
||||||
|
.maxLines(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
}
|
||||||
|
.layoutWeight(1)
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
}
|
||||||
|
.alignItems(VerticalAlign.Center)
|
||||||
|
.width('100%')
|
||||||
|
.padding({ left: 8, right: 8, top: 5, bottom: 5 })
|
||||||
|
.borderRadius(8)
|
||||||
|
.backgroundColor(item.overdue ? '#FBEDEA' : '#F7F2EC')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 右半空态:没有待办 */
|
||||||
|
@Builder
|
||||||
|
emptyHint() {
|
||||||
|
Column() {
|
||||||
|
Text(this.hint !== '' ? this.hint : '没有待办任务')
|
||||||
|
.fontSize(12)
|
||||||
|
.fontColor(this.hint !== '' ? '#C0533F' : '#A8988A')
|
||||||
|
.maxLines(3)
|
||||||
|
.textAlign(TextAlign.Center)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.layoutWeight(1)
|
||||||
|
.justifyContent(FlexAlign.Center)
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Row({ space: 10 }) {
|
||||||
|
// ===== 左半:与 2×2 一致 =====
|
||||||
|
Column({ space: 6 }) {
|
||||||
|
Row({ space: 6 }) {
|
||||||
|
Text('任务')
|
||||||
|
.fontSize(15)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.fontColor('#2C231D')
|
||||||
|
.maxLines(1)
|
||||||
|
.layoutWeight(1)
|
||||||
|
Text(this.updatedAt)
|
||||||
|
.fontSize(9)
|
||||||
|
.fontColor('#A8988A')
|
||||||
|
.maxLines(1)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
.strokeWidth(0.5)
|
||||||
|
.color('#EFE6D8')
|
||||||
|
|
||||||
|
Row({ space: 6 }) {
|
||||||
|
Text(`${this.openCount}`)
|
||||||
|
.fontSize(38)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.fontColor('#A8574F')
|
||||||
|
Column({ space: 2 }) {
|
||||||
|
Text('待办任务')
|
||||||
|
.fontSize(11)
|
||||||
|
.fontColor('#2C231D')
|
||||||
|
.maxLines(1)
|
||||||
|
Text(this.hint !== '' ? this.hint
|
||||||
|
: (this.overdueCount > 0 ? '逾期 ' + this.overdueCount.toString() + ' 条 · 点击查看'
|
||||||
|
: '点击查看'))
|
||||||
|
.fontSize(9)
|
||||||
|
.fontColor(this.hint !== '' || this.overdueCount > 0 ? '#C0533F' : '#A8988A')
|
||||||
|
.maxLines(1)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
}
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.layoutWeight(1)
|
||||||
|
.alignItems(VerticalAlign.Center)
|
||||||
|
}
|
||||||
|
.layoutWeight(1)
|
||||||
|
.height('100%')
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
.vertical(true)
|
||||||
|
.height('92%')
|
||||||
|
.strokeWidth(0.5)
|
||||||
|
.color('#EFE6D8')
|
||||||
|
|
||||||
|
// ===== 右半:待办(不滚动)=====
|
||||||
|
Column() {
|
||||||
|
if (this.rows().length === 0) {
|
||||||
|
this.emptyHint()
|
||||||
|
} else {
|
||||||
|
Column({ space: 6 }) {
|
||||||
|
ForEach(this.rows(), (item: FbTaskRow4) => {
|
||||||
|
this.taskRow(item)
|
||||||
|
}, (item: FbTaskRow4) => `tk_${this.version}_${item.key}`)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.layoutWeight(1)
|
||||||
|
.justifyContent(FlexAlign.Center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.layoutWeight(1)
|
||||||
|
.height('100%')
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.padding(12)
|
||||||
|
.backgroundColor('#FFFFFF')
|
||||||
|
.borderRadius(16)
|
||||||
|
// ⭐ 整卡可点
|
||||||
|
.onClick((): void => this.openApp())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,6 +35,22 @@
|
|||||||
{
|
{
|
||||||
"name": "card_6x4_name",
|
"name": "card_6x4_name",
|
||||||
"value": "家书 · 日程(宽)"
|
"value": "家书 · 日程(宽)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "card_movie_2x2_name",
|
||||||
|
"value": "家书 · 观影"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "card_movie_2x4_name",
|
||||||
|
"value": "家书 · 最近观影"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "card_task_2x2_name",
|
||||||
|
"value": "家书 · 待办"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "card_task_2x4_name",
|
||||||
|
"value": "家书 · 待办任务"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,74 @@
|
|||||||
"updateDuration": 1,
|
"updateDuration": 1,
|
||||||
"defaultDimension": "6*4",
|
"defaultDimension": "6*4",
|
||||||
"supportDimensions": ["6*4"]
|
"supportDimensions": ["6*4"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FbMovieCard2x2",
|
||||||
|
"displayName": "$string:card_movie_2x2_name",
|
||||||
|
"description": "$string:card_desc",
|
||||||
|
"src": "./ets/pages/card/FbMovieCard2x2.ets",
|
||||||
|
"window": {
|
||||||
|
"designWidth": 720,
|
||||||
|
"autoDesignWidth": true
|
||||||
|
},
|
||||||
|
"colorMode": "light",
|
||||||
|
"uiSyntax": "arkts",
|
||||||
|
"isDefault": false,
|
||||||
|
"updateEnabled": true,
|
||||||
|
"updateDuration": 1,
|
||||||
|
"defaultDimension": "2*2",
|
||||||
|
"supportDimensions": ["2*2"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FbMovieCard2x4",
|
||||||
|
"displayName": "$string:card_movie_2x4_name",
|
||||||
|
"description": "$string:card_desc",
|
||||||
|
"src": "./ets/pages/card/FbMovieCard2x4.ets",
|
||||||
|
"window": {
|
||||||
|
"designWidth": 720,
|
||||||
|
"autoDesignWidth": true
|
||||||
|
},
|
||||||
|
"colorMode": "light",
|
||||||
|
"uiSyntax": "arkts",
|
||||||
|
"isDefault": false,
|
||||||
|
"updateEnabled": true,
|
||||||
|
"updateDuration": 1,
|
||||||
|
"defaultDimension": "2*4",
|
||||||
|
"supportDimensions": ["2*4"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FbTaskCard2x2",
|
||||||
|
"displayName": "$string:card_task_2x2_name",
|
||||||
|
"description": "$string:card_desc",
|
||||||
|
"src": "./ets/pages/card/FbTaskCard2x2.ets",
|
||||||
|
"window": {
|
||||||
|
"designWidth": 720,
|
||||||
|
"autoDesignWidth": true
|
||||||
|
},
|
||||||
|
"colorMode": "light",
|
||||||
|
"uiSyntax": "arkts",
|
||||||
|
"isDefault": false,
|
||||||
|
"updateEnabled": true,
|
||||||
|
"updateDuration": 1,
|
||||||
|
"defaultDimension": "2*2",
|
||||||
|
"supportDimensions": ["2*2"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FbTaskCard2x4",
|
||||||
|
"displayName": "$string:card_task_2x4_name",
|
||||||
|
"description": "$string:card_desc",
|
||||||
|
"src": "./ets/pages/card/FbTaskCard2x4.ets",
|
||||||
|
"window": {
|
||||||
|
"designWidth": 720,
|
||||||
|
"autoDesignWidth": true
|
||||||
|
},
|
||||||
|
"colorMode": "light",
|
||||||
|
"uiSyntax": "arkts",
|
||||||
|
"isDefault": false,
|
||||||
|
"updateEnabled": true,
|
||||||
|
"updateDuration": 1,
|
||||||
|
"defaultDimension": "2*4",
|
||||||
|
"supportDimensions": ["2*4"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user