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 { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { FbCardData, fbCardBuild } from './FbCardData';
|
||||
import { fbCardDataByKind } from './FbCardMediaData';
|
||||
|
||||
const DOMAIN: number = 0x0000;
|
||||
const TAG: string = 'FbCardForms';
|
||||
@@ -61,6 +61,10 @@ const TAG: string = 'FbCardForms';
|
||||
const STORE_NAME: string = 'fb_card_forms';
|
||||
const K_IDS: string = 'form_ids';
|
||||
const K_OFF: string = 'day_offsets';
|
||||
/** 🔴 v90:formId → 卡片类型(`calendar` / `movies` / `tasks`)。
|
||||
* ⚠️ 旧卡(v90 之前登记的日程卡)没有这一项 ⇒ 读不到一律按 `calendar` 处理,
|
||||
* 绝不能让升级把桌面上已有的日程卡弄瞎。 */
|
||||
const K_KIND: string = 'form_kinds';
|
||||
|
||||
/** 翻页最多到前后一年(防外部输入把日期算成 Invalid Date) */
|
||||
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[]> {
|
||||
@@ -198,7 +248,7 @@ class FbCardFormsImpl {
|
||||
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 === '') {
|
||||
return;
|
||||
}
|
||||
@@ -207,17 +257,21 @@ class FbCardFormsImpl {
|
||||
list.push(formId);
|
||||
await this.saveIds(ctx, list);
|
||||
}
|
||||
// 类型只增不改:同一张卡不会换类型(卡片类型由 form_config 决定,随 formId 固定)
|
||||
await this.setKind(ctx, formId, kind);
|
||||
}
|
||||
|
||||
async unregister(ctx: common.Context, formId: string): Promise<void> {
|
||||
const list: string[] = (await this.ids(ctx)).filter((id: string): boolean => id !== formId);
|
||||
await this.saveIds(ctx, list);
|
||||
await this.clearKind(ctx, formId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用内日程变化后刷新**所有**已添加的卡。
|
||||
* 应用内数据变化后刷新**所有**已添加的卡。
|
||||
*
|
||||
* ⚠️ 按偏移量分组构建:两张卡可能翻在不同日期,逐张构建会重复打同一个接口。
|
||||
* ⚠️ 按类型 + 偏移量分组构建:两张卡可能类型不同 / 翻在不同日期,
|
||||
* 逐张构建会重复打同一个接口。
|
||||
* ⚠️ 推失败的 formId 直接**注销**(卡片多半已被用户删掉),否则每刷一次都白推一遍。
|
||||
*/
|
||||
async pushAll(ctx: common.Context): Promise<void> {
|
||||
@@ -233,16 +287,20 @@ class FbCardFormsImpl {
|
||||
hilog.warn(DOMAIN, TAG, 'pushAll:注册表为空(桌面没有卡片,或跨进程没读到)');
|
||||
return;
|
||||
}
|
||||
const cache: Map<number, formBindingData.FormBindingData> =
|
||||
new Map<number, formBindingData.FormBindingData>();
|
||||
const cache: Map<string, formBindingData.FormBindingData> =
|
||||
new Map<string, formBindingData.FormBindingData>();
|
||||
const stale: string[] = [];
|
||||
for (const formId of list) {
|
||||
/* 🔴 v90:每张卡按自己的类型构建(日程卡才吃偏移量;
|
||||
观影/任务卡没有"看哪一天"的概念,偏移量传 0 即可)。 */
|
||||
const kind: string = await this.kindOf(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) {
|
||||
const data: FbCardData = await fbCardBuild(ctx, off);
|
||||
const data: Object = await fbCardDataByKind(ctx, kind, off);
|
||||
binding = formBindingData.createFormBindingData(data);
|
||||
cache.set(off, binding);
|
||||
cache.set(ck, binding);
|
||||
}
|
||||
try {
|
||||
await formProvider.updateForm(formId, binding);
|
||||
@@ -252,7 +310,7 @@ class FbCardFormsImpl {
|
||||
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);
|
||||
if (stale.length > 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;
|
||||
}
|
||||
Reference in New Issue
Block a user