2026-10-22 16:40:44 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 家书 FamilyBook · 任务面板(鸿蒙端 v79)
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 用户 2026-10-22 原话:
|
|
|
|
|
|
* 「鸿蒙端的功能,也需要同样适配,在首页上可以直接修改编辑任务,
|
|
|
|
|
|
* 另外任务模块的功能需要完善。」
|
|
|
|
|
|
*
|
|
|
|
|
|
* 对应网页 v78 的「就地处理面板」(`templates/tasks/_panel.php`):
|
|
|
|
|
|
* 首页点一条任务 / 任务页点一张卡 —— 都在**当前页面**弹出这块面板,
|
|
|
|
|
|
* 不跳模块、不跳整页,就能:
|
|
|
|
|
|
* · 改状态(待办 / 进行中 / 已完成)
|
|
|
|
|
|
* · 挪分组(换看板)
|
|
|
|
|
|
* · 勾子任务、加一条子任务
|
|
|
|
|
|
* · 加图片附件、删附件
|
|
|
|
|
|
* · 发留言
|
|
|
|
|
|
* · 打开编辑表单(标题/备注/期限/看板/负责人/分类/循环)
|
|
|
|
|
|
* · 删任务(二次确认)
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 为什么不直接复用 `FbTaskEditSheet`:那是**表单**(点开就在编辑)。
|
|
|
|
|
|
* 用户此刻多数是"看看它是什么、顺手改一下状态"。拿表单当详情,
|
|
|
|
|
|
* 误触一下就把标题改了(v73 的取舍,见 `FbTaskDetailSheet` 文件头)。
|
|
|
|
|
|
* ⇒ 面板只读为主 + 两颗明确的动作(编辑 / 删除),编辑才进表单。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 三条纪律:
|
|
|
|
|
|
* ① 改状态**只走** `POST /tasks/<id>/status`(PATCH 会忽略 status);
|
|
|
|
|
|
* ② 每个动作成功后再 `getTask()` 重拉一次:一次动作会牵动子任务计数、
|
|
|
|
|
|
* 附件张数、看板色,逐处同步 DOM 必然漏一处(与网页 v78 同一取舍);
|
|
|
|
|
|
* ③ 浮层根必须是 `Stack`:遮罩与卡片要**叠**,当 `Column` 子项"排"会把卡片
|
|
|
|
|
|
* 挤出屏幕(本项目 2026-10-20 真机事故,见 pitfalls)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import { FbColors, FbMetric, FB_MASK, fbAlpha, fbTint } from '../common/Theme';
|
|
|
|
|
|
import {
|
2026-10-28 22:10:15 +08:00
|
|
|
|
FbBoardOpt, FbTaskAttach, FbTaskComment, FbTaskDetail, FbTaskItem, FbTaskKid, FbTaskShare,
|
2026-10-22 16:40:44 +08:00
|
|
|
|
} from '../model/Types';
|
|
|
|
|
|
import { FbSheetRing } from './FbSheetRing';
|
|
|
|
|
|
import { FbSheetClose } from './FbSheetClose';
|
|
|
|
|
|
import { FbCheckCircle } from './Widgets';
|
2026-10-28 22:10:15 +08:00
|
|
|
|
import { FbPersonPickSheet } from './FbPersonPickSheet';
|
|
|
|
|
|
import { FbPersonRepo, FbRosterPerson } from '../data/FbPersonRepo';
|
2026-10-22 16:40:44 +08:00
|
|
|
|
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
|
|
|
|
|
|
import { FbApiResult } from '../data/FbHttp';
|
|
|
|
|
|
import { fbImageSrc } from '../data/FbAttachQueue';
|
|
|
|
|
|
import {
|
|
|
|
|
|
addTaskComment, createTask, deleteTask, delTaskAttach, getTask, moveTask,
|
|
|
|
|
|
setTaskStatus, uploadTaskAttach, FbTaskDraft,
|
2026-10-28 22:10:15 +08:00
|
|
|
|
fetchTaskShares, shareTask, unshareTask,
|
2026-10-22 16:40:44 +08:00
|
|
|
|
} from '../data/FbTaskRepo';
|
|
|
|
|
|
import { parseTaskDetail } from '../data/FbParse';
|
|
|
|
|
|
import { picker } from '@kit.CoreFileKit';
|
|
|
|
|
|
import { BusinessError } from '@kit.BasicServicesKit';
|
|
|
|
|
|
import { hilog } from '@kit.PerformanceAnalysisKit';
|
|
|
|
|
|
|
|
|
|
|
|
const FB_PANEL_BOTTOM_GAP: number = 14;
|
|
|
|
|
|
|
|
|
|
|
|
@ComponentV2
|
|
|
|
|
|
export struct FbTaskPanel {
|
|
|
|
|
|
/** 要看的这条任务 */
|
|
|
|
|
|
@Param taskId: number = 0;
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 列表里那一份(先用它渲染,详情接口回来再替换)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 为什么不直接等接口:手机上网络慢一点,点开就是一片空白/转圈,
|
|
|
|
|
|
* 用户会以为"点不动"。列表里本来就有标题、状态、看板、截止,
|
|
|
|
|
|
* 先画出来,再补子任务/留言/附件。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Param seed: FbTaskItem | null = null;
|
|
|
|
|
|
/** 全部看板(含空看板)—— 父页面用 `?view=boards` 拿,别从任务里反推 */
|
|
|
|
|
|
@Param boards: FbBoardOpt[] = [];
|
|
|
|
|
|
/** 模块主题色(任务 = 赭红) */
|
|
|
|
|
|
@Param accent: string = '#A8574F';
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 外部要求重画的脉冲(宿主改完任务后 +1)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 为什么不用"关掉再打开":会闪一下,还会丢掉滚动位置。
|
|
|
|
|
|
* ⚠️ 只监听这个数字,**不要**把整条任务当 `@Param` 传进来再比对 ——
|
|
|
|
|
|
* 对象相等判断会永远认为"没变"(本项目 v54b 的教训)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Param refreshTick: number = 0;
|
|
|
|
|
|
|
|
|
|
|
|
@Event onClose: () => void = (): void => {
|
|
|
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 点「编辑」→ 宿主打开 `FbTaskEditSheet`。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 传的是**详情**而不是列表行:编辑要回填「分类 / 循环」,
|
|
|
|
|
|
* 而列表行(`FbTaskItem`)里没有这两项 —— 只传列表行会让用户一保存
|
|
|
|
|
|
* 就把分类和循环**静默清空**(端上 v77 已踩过同类"没传=清空")。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Event onEdit: (d: FbTaskDetail) => void = (): void => {
|
|
|
|
|
|
};
|
|
|
|
|
|
/** 有任何变化(状态/看板/子任务/附件/留言)→ 宿主重拉列表 */
|
|
|
|
|
|
@Event onChanged: () => void = (): void => {
|
|
|
|
|
|
};
|
|
|
|
|
|
/** 删掉了 → 宿主关面板 + 重拉 */
|
|
|
|
|
|
@Event onDeleted: () => void = (): void => {
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
@Local detail: FbTaskDetail | null = null;
|
|
|
|
|
|
@Local loading: boolean = true;
|
|
|
|
|
|
/** 正在提交(所有动作共用;防连点打重复请求) */
|
|
|
|
|
|
@Local busy: boolean = false;
|
|
|
|
|
|
/** 一行提示(成功/失败都写这里,就地显示,不弹 toast) */
|
|
|
|
|
|
@Local note: string = '';
|
|
|
|
|
|
@Local kidDraft: string = '';
|
|
|
|
|
|
@Local cmtDraft: string = '';
|
|
|
|
|
|
/** 删除按钮点第一次 → 变成「确定删除?」 */
|
|
|
|
|
|
@Local confirmDel: boolean = false;
|
|
|
|
|
|
@Local uploading: boolean = false;
|
2026-10-28 22:10:15 +08:00
|
|
|
|
// 共享(工单 #26)
|
|
|
|
|
|
@Local roster: FbRosterPerson[] = [];
|
|
|
|
|
|
@Local pickingShare: boolean = false;
|
|
|
|
|
|
@Local shareBusy: boolean = false;
|
2026-10-22 16:40:44 +08:00
|
|
|
|
|
|
|
|
|
|
aboutToAppear(): void {
|
|
|
|
|
|
this.reload();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Monitor('refreshTick')
|
|
|
|
|
|
onTick(): void {
|
|
|
|
|
|
this.reload();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ---------------- 数据 ---------------- */
|
|
|
|
|
|
|
|
|
|
|
|
private token(): string {
|
|
|
|
|
|
return FB_TOKEN_STORE.get();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async reload(): Promise<void> {
|
|
|
|
|
|
if (this.taskId <= 0) {
|
|
|
|
|
|
this.loading = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const res: FbApiResult = await getTask(this.token(), this.taskId);
|
|
|
|
|
|
this.loading = false;
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
|
this.note = res.error !== '' ? res.error : '这条任务没载入,先看基本信息。';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const d: FbTaskDetail | null = parseTaskDetail(res.data);
|
|
|
|
|
|
if (d !== null) {
|
|
|
|
|
|
this.detail = d;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 当前要显示的任务(详情优先,退回列表那一份) */
|
|
|
|
|
|
private cur(): FbTaskItem | null {
|
|
|
|
|
|
const d: FbTaskDetail | null = this.detail;
|
|
|
|
|
|
if (d !== null) {
|
|
|
|
|
|
return d.base;
|
|
|
|
|
|
}
|
|
|
|
|
|
return this.seed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private titleOf(): string {
|
|
|
|
|
|
const t: FbTaskItem | null = this.cur();
|
|
|
|
|
|
return t !== null && t.title !== '' ? t.title : '未命名任务';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private statusOf(): string {
|
|
|
|
|
|
const t: FbTaskItem | null = this.cur();
|
|
|
|
|
|
return t !== null && t.status !== '' ? t.status : 'todo';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private boardIdOf(): number {
|
|
|
|
|
|
const t: FbTaskItem | null = this.cur();
|
|
|
|
|
|
return t !== null ? t.boardId : 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private boardOf(): string {
|
|
|
|
|
|
const t: FbTaskItem | null = this.cur();
|
2026-10-22 18:45:37 +08:00
|
|
|
|
if (t === null) {
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
/* 🔴 v79b:原来只 `return t.board`。`?view=boards` 之外的老接口/某些行
|
|
|
|
|
|
`board` 是空串 ⇒ 面板上就**完全不显示这条属于哪个看板**,
|
|
|
|
|
|
用户没法判断"挪看板"到底挪成了没有(真机验收时就是这么发现 `boardOf()`
|
|
|
|
|
|
定义了却没被任何地方用)。这里补两层兜底:空名 → 按 id 反查 → 默认看板。 */
|
|
|
|
|
|
if (t.board !== '') {
|
|
|
|
|
|
return t.board;
|
|
|
|
|
|
}
|
|
|
|
|
|
for (const b of this.boards) {
|
|
|
|
|
|
if (b.id === t.boardId) {
|
|
|
|
|
|
return b.name;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return t.boardId === 0 ? '默认看板' : '';
|
2026-10-22 16:40:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private boardColorOf(): string {
|
|
|
|
|
|
const t: FbTaskItem | null = this.cur();
|
|
|
|
|
|
return t !== null && t.boardColor !== '' ? t.boardColor : '#9AA0A6';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private dueOf(): string {
|
|
|
|
|
|
const t: FbTaskItem | null = this.cur();
|
|
|
|
|
|
return t !== null ? t.dueText : '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private overdueOf(): boolean {
|
|
|
|
|
|
const t: FbTaskItem | null = this.cur();
|
|
|
|
|
|
return t !== null && t.overdue && this.statusOf() !== 'done';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private assigneeOf(): string {
|
|
|
|
|
|
const t: FbTaskItem | null = this.cur();
|
|
|
|
|
|
return t !== null ? t.assignee : '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private noteOf(): string {
|
|
|
|
|
|
const t: FbTaskItem | null = this.cur();
|
|
|
|
|
|
return t !== null ? t.note : '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private kids(): FbTaskKid[] {
|
|
|
|
|
|
const d: FbTaskDetail | null = this.detail;
|
|
|
|
|
|
return d !== null ? d.kids : [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private kidDoneCount(): number {
|
|
|
|
|
|
let k: number = 0;
|
|
|
|
|
|
for (const one of this.kids()) {
|
|
|
|
|
|
if (one.status === 'done') {
|
|
|
|
|
|
k = k + 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return k;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private comments(): FbTaskComment[] {
|
|
|
|
|
|
const d: FbTaskDetail | null = this.detail;
|
|
|
|
|
|
return d !== null ? d.comments : [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private attachs(): FbTaskAttach[] {
|
|
|
|
|
|
const d: FbTaskDetail | null = this.detail;
|
|
|
|
|
|
return d !== null ? d.attachs : [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-28 22:10:15 +08:00
|
|
|
|
/** 共享给谁(工单 #26) */
|
|
|
|
|
|
private shares(): FbTaskShare[] {
|
|
|
|
|
|
const d: FbTaskDetail | null = this.detail;
|
|
|
|
|
|
return d !== null ? d.shares : [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async openSharePicker(): Promise<void> {
|
|
|
|
|
|
if (this.roster.length === 0) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
this.roster = await FbPersonRepo.inst().list();
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
this.roster = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
this.pickingShare = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async addShares(ids: number[]): Promise<void> {
|
|
|
|
|
|
this.pickingShare = false;
|
|
|
|
|
|
if (ids.length === 0) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.shareBusy = true;
|
|
|
|
|
|
let bad: string = '';
|
|
|
|
|
|
for (const pid of ids) {
|
|
|
|
|
|
const res: FbApiResult = await shareTask(this.token(), this.taskId, pid, 'write');
|
|
|
|
|
|
if (!res.ok && bad === '') {
|
|
|
|
|
|
bad = res.error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
this.shareBusy = false;
|
|
|
|
|
|
this.note = bad !== '' ? bad : '';
|
|
|
|
|
|
await this.reload();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async dropShare(personId: number): Promise<void> {
|
|
|
|
|
|
this.shareBusy = true;
|
|
|
|
|
|
const res: FbApiResult = await unshareTask(this.token(), this.taskId, personId);
|
|
|
|
|
|
this.shareBusy = false;
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
|
this.note = res.error !== '' ? res.error : '没取消成。';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
await this.reload();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 16:40:44 +08:00
|
|
|
|
private categoryOf(): string {
|
|
|
|
|
|
const d: FbTaskDetail | null = this.detail;
|
|
|
|
|
|
return d !== null ? d.category : '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private repeatOf(): string {
|
|
|
|
|
|
const d: FbTaskDetail | null = this.detail;
|
|
|
|
|
|
if (d === null || d.repeat === '') {
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (d.repeat === 'day') {
|
|
|
|
|
|
return '每天';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (d.repeat === 'week') {
|
|
|
|
|
|
return '每周';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (d.repeat === 'month') {
|
|
|
|
|
|
return '每月';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (d.repeat === 'year') {
|
|
|
|
|
|
return '每年';
|
|
|
|
|
|
}
|
|
|
|
|
|
return d.repeat;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private parentOf(): string {
|
|
|
|
|
|
const d: FbTaskDetail | null = this.detail;
|
|
|
|
|
|
return d !== null ? d.parentTitle : '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private isDone(): boolean {
|
|
|
|
|
|
return this.statusOf() === 'done';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 主题色的半透明版(**八位色 `#AARRGGBB`,alpha 在前** —— 别写成 CSS 那种) */
|
|
|
|
|
|
private accentAlpha(aa: string): string {
|
|
|
|
|
|
const c: string = this.accent.replace('#', '');
|
|
|
|
|
|
const rgb: string = c.length >= 8 ? c.substring(2) : c;
|
|
|
|
|
|
return '#' + aa + rgb;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private statusWord(w: string): string {
|
|
|
|
|
|
if (w === 'done') {
|
|
|
|
|
|
return '已完成';
|
|
|
|
|
|
}
|
2026-11-05 12:41:04 +08:00
|
|
|
|
return w === 'doing' ? '进行中' : '待做';
|
2026-10-22 16:40:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 状态的颜色。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 完成用**绿**、进行中用**赭红**、待办用**灰** —— 与任务页/网页同一套语义,
|
|
|
|
|
|
* 不跟模块主题色走(状态是事实,不是"我在哪个模块")。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private statusTint(w: string): string {
|
|
|
|
|
|
if (w === 'done') {
|
|
|
|
|
|
return '#3F7D5A';
|
|
|
|
|
|
}
|
|
|
|
|
|
return w === 'doing' ? '#A8574F' : '#7C8AA0';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private statusWash(w: string): string {
|
|
|
|
|
|
if (w === 'done') {
|
|
|
|
|
|
return '#EAF3EE';
|
|
|
|
|
|
}
|
|
|
|
|
|
return w === 'doing' ? '#FBEFEE' : '#F1F3F5';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ---------------- 动作 ---------------- */
|
|
|
|
|
|
|
|
|
|
|
|
/** 改状态(三段 / 标题左边那颗圈都走这里) */
|
|
|
|
|
|
private async setStatus(next: string): Promise<void> {
|
|
|
|
|
|
if (this.busy || next === this.statusOf()) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.busy = true;
|
|
|
|
|
|
this.note = '';
|
|
|
|
|
|
const res: FbApiResult = await setTaskStatus(this.token(), this.taskId, next);
|
|
|
|
|
|
this.busy = false;
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
|
this.note = res.error !== '' ? res.error : '状态没改过来,请再试一次。';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.note = '已标记为' + this.statusWord(next) + '。';
|
|
|
|
|
|
await this.reload();
|
|
|
|
|
|
this.onChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 挪到另一个看板 */
|
|
|
|
|
|
private async moveTo(boardId: number): Promise<void> {
|
|
|
|
|
|
if (this.busy || boardId === this.boardIdOf()) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.busy = true;
|
|
|
|
|
|
this.note = '';
|
|
|
|
|
|
const res: FbApiResult = await moveTask(this.token(), this.taskId, boardId);
|
|
|
|
|
|
this.busy = false;
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
|
this.note = res.error !== '' ? res.error : '没挪过去,请再试一次。';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.note = '已挪到「' + this.boardNameOf(boardId) + '」。';
|
|
|
|
|
|
await this.reload();
|
|
|
|
|
|
this.onChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private boardNameOf(boardId: number): string {
|
|
|
|
|
|
for (const b of this.boards) {
|
|
|
|
|
|
if (b.id === boardId) {
|
|
|
|
|
|
return b.name;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return boardId === 0 ? '默认看板' : '该看板';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 勾/取消勾一条子任务 */
|
|
|
|
|
|
private async toggleKid(k: FbTaskKid): Promise<void> {
|
|
|
|
|
|
if (this.busy) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.busy = true;
|
|
|
|
|
|
this.note = '';
|
|
|
|
|
|
const next: string = k.status === 'done' ? 'todo' : 'done';
|
|
|
|
|
|
const res: FbApiResult = await setTaskStatus(this.token(), k.id, next);
|
|
|
|
|
|
this.busy = false;
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
|
this.note = res.error !== '' ? res.error : '这条子任务没改成。';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
await this.reload();
|
|
|
|
|
|
this.onChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 加一条子任务(挂在当前任务下) */
|
|
|
|
|
|
private async addKid(): Promise<void> {
|
|
|
|
|
|
const title: string = this.kidDraft.trim();
|
|
|
|
|
|
if (this.busy || title === '') {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.busy = true;
|
|
|
|
|
|
this.note = '';
|
|
|
|
|
|
const d: FbTaskDraft = {
|
|
|
|
|
|
title: title,
|
|
|
|
|
|
note: '',
|
|
|
|
|
|
dueAt: '',
|
|
|
|
|
|
/* 子任务默认跟父任务在同一个看板:用户此刻想的是"这件事拆成几步",
|
|
|
|
|
|
不是"另开一摊"。 */
|
|
|
|
|
|
boardId: this.boardIdOf(),
|
|
|
|
|
|
status: 'todo',
|
|
|
|
|
|
parentId: this.taskId,
|
|
|
|
|
|
assigneeId: 0,
|
|
|
|
|
|
categoryId: 0,
|
|
|
|
|
|
repeat: '',
|
|
|
|
|
|
};
|
|
|
|
|
|
const res: FbApiResult = await createTask(this.token(), d);
|
|
|
|
|
|
this.busy = false;
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
|
this.note = res.error !== '' ? res.error : '子任务没加上。';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.kidDraft = '';
|
|
|
|
|
|
this.note = '子任务已加上。';
|
|
|
|
|
|
await this.reload();
|
|
|
|
|
|
this.onChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 发一条留言 */
|
|
|
|
|
|
private async sendComment(): Promise<void> {
|
|
|
|
|
|
const txt: string = this.cmtDraft.trim();
|
|
|
|
|
|
if (this.busy || txt === '') {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.busy = true;
|
|
|
|
|
|
this.note = '';
|
|
|
|
|
|
const res: FbApiResult = await addTaskComment(this.token(), this.taskId, txt);
|
|
|
|
|
|
this.busy = false;
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
|
this.note = res.error !== '' ? res.error : '留言没发出去。';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.cmtDraft = '';
|
|
|
|
|
|
await this.reload();
|
|
|
|
|
|
this.onChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 选一张图当附件(选完**立刻上传** —— 任务已经存在,不用再点保存) */
|
|
|
|
|
|
private async pickAttach(): Promise<void> {
|
|
|
|
|
|
if (this.uploading) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
const opts: picker.PhotoSelectOptions = new picker.PhotoSelectOptions();
|
|
|
|
|
|
opts.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
|
|
|
|
|
|
opts.maxSelectNumber = 1;
|
|
|
|
|
|
const p: picker.PhotoViewPicker = new picker.PhotoViewPicker();
|
|
|
|
|
|
const sel: picker.PhotoSelectResult = await p.select(opts);
|
|
|
|
|
|
if (sel.photoUris.length === 0) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.uploading = true;
|
|
|
|
|
|
this.note = '';
|
|
|
|
|
|
const err: string = await uploadTaskAttach(this.token(), this.taskId, sel.photoUris[0]);
|
|
|
|
|
|
this.uploading = false;
|
|
|
|
|
|
if (err !== '') {
|
|
|
|
|
|
this.note = err;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.note = '图片已上传。';
|
|
|
|
|
|
await this.reload();
|
|
|
|
|
|
this.onChanged();
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
const be: BusinessError = e as BusinessError;
|
|
|
|
|
|
hilog.warn(0x0000, 'FamilyBook', '选附件失败:%{public}s', JSON.stringify(be));
|
|
|
|
|
|
this.uploading = false;
|
|
|
|
|
|
this.note = '选图失败,请再试一次。';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 删一个附件 */
|
|
|
|
|
|
private async dropAttach(a: FbTaskAttach): Promise<void> {
|
|
|
|
|
|
if (this.busy) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.busy = true;
|
|
|
|
|
|
this.note = '';
|
|
|
|
|
|
const res: FbApiResult = await delTaskAttach(this.token(), this.taskId, a.id);
|
|
|
|
|
|
this.busy = false;
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
|
this.note = res.error !== '' ? res.error : '附件没删掉。';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.note = '附件已删除。';
|
|
|
|
|
|
await this.reload();
|
|
|
|
|
|
this.onChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 删任务(点两次才删) */
|
|
|
|
|
|
private async dropTask(): Promise<void> {
|
|
|
|
|
|
if (this.busy) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!this.confirmDel) {
|
|
|
|
|
|
this.confirmDel = true;
|
|
|
|
|
|
this.note = '再点一次「确定删掉」就删除了,删了不能恢复。';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.busy = true;
|
|
|
|
|
|
const res: FbApiResult = await deleteTask(this.token(), this.taskId);
|
|
|
|
|
|
this.busy = false;
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
|
this.note = res.error !== '' ? res.error : '没删掉,请再试一次。';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.onDeleted();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ---------------- UI ---------------- */
|
|
|
|
|
|
|
|
|
|
|
|
build() {
|
|
|
|
|
|
/* 🔴 根必须 Stack:全屏遮罩与卡片要**叠**(见文件头第 ③ 条) */
|
|
|
|
|
|
Stack({ alignContent: Alignment.Center }) {
|
|
|
|
|
|
Column()
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
|
|
|
|
|
.backgroundColor(FB_MASK)
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.onClose();
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
/* 包裹层只为了挂宽度:环组件自身不能接属性方法(见 FbTaskDetailSheet 注释) */
|
|
|
|
|
|
Column() {
|
|
|
|
|
|
FbSheetRing({ radius: FbMetric.radius, ringColor: this.accent }) {
|
|
|
|
|
|
Column({ space: 0 }) {
|
|
|
|
|
|
this.headBar()
|
|
|
|
|
|
|
|
|
|
|
|
Divider()
|
|
|
|
|
|
.color(FbColors.line)
|
|
|
|
|
|
.margin({ top: 10, bottom: 2 })
|
|
|
|
|
|
|
|
|
|
|
|
Scroll() {
|
|
|
|
|
|
Column({ space: 0 }) {
|
|
|
|
|
|
this.statusSeg()
|
|
|
|
|
|
this.boardChips()
|
|
|
|
|
|
this.infoRows()
|
2026-10-28 22:10:15 +08:00
|
|
|
|
this.shareSection()
|
2026-10-22 16:40:44 +08:00
|
|
|
|
this.kidSection()
|
|
|
|
|
|
this.attachSection()
|
|
|
|
|
|
this.commentSection()
|
|
|
|
|
|
if (this.note !== '') {
|
|
|
|
|
|
Text(this.note)
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(this.note.indexOf('没') >= 0 || this.note.indexOf('不能') >= 0
|
|
|
|
|
|
? '#C0533F' : FbColors.muted)
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 10 })
|
|
|
|
|
|
}
|
|
|
|
|
|
Row().height(6)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
}
|
|
|
|
|
|
/* `flexShrink(1)` 而不是 `layoutWeight(1)`:后者会让短内容也把卡片顶满,
|
|
|
|
|
|
底下留一大片空白(v60b 真机实测) */
|
|
|
|
|
|
.flexShrink(1)
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.scrollBar(BarState.Off)
|
|
|
|
|
|
.align(Alignment.TopStart)
|
|
|
|
|
|
.nestedScroll({
|
|
|
|
|
|
scrollForward: NestedScrollMode.PARENT_FIRST,
|
|
|
|
|
|
scrollBackward: NestedScrollMode.SELF_FIRST
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
this.footBar()
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.constraintSize({ maxHeight: '86%' })
|
|
|
|
|
|
.padding({ left: 16, right: 16, top: 16, bottom: 16 + FB_PANEL_BOTTOM_GAP })
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('94%')
|
2026-10-28 22:10:15 +08:00
|
|
|
|
|
|
|
|
|
|
/* 选人浮层:共享给谁(工单 #26) */
|
|
|
|
|
|
if (this.pickingShare) {
|
|
|
|
|
|
FbPersonPickSheet({
|
|
|
|
|
|
people: this.roster,
|
|
|
|
|
|
selected: [],
|
|
|
|
|
|
title: '共享给谁',
|
|
|
|
|
|
accent: this.accent,
|
|
|
|
|
|
onConfirm: (ids: number[]): void => {
|
|
|
|
|
|
this.addShares(ids);
|
|
|
|
|
|
},
|
|
|
|
|
|
onClose: (): void => {
|
|
|
|
|
|
this.pickingShare = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-10-22 16:40:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
|
|
|
|
|
.hitTestBehavior(HitTestMode.Transparent)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 标题行:状态圈(点=勾完成)+ 标题 + 状态徽标 + 关闭 */
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
headBar() {
|
|
|
|
|
|
Row({ space: 10 }) {
|
|
|
|
|
|
Stack({ alignContent: Alignment.Center }) {
|
|
|
|
|
|
FbCheckCircle({
|
|
|
|
|
|
dia: 18,
|
|
|
|
|
|
done: this.isDone(),
|
|
|
|
|
|
doing: this.statusOf() === 'doing',
|
|
|
|
|
|
color: this.accent
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
.width(30)
|
|
|
|
|
|
.height(30)
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.setStatus(this.isDone() ? 'todo' : 'done');
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
Column({ space: 2 }) {
|
|
|
|
|
|
Text(this.titleOf())
|
|
|
|
|
|
.fontSize(FbMetric.fontTitle)
|
|
|
|
|
|
.fontWeight(FontWeight.Medium)
|
|
|
|
|
|
.fontColor(this.isDone() ? FbColors.faint : FbColors.ink)
|
|
|
|
|
|
.decoration({
|
|
|
|
|
|
type: this.isDone() ? TextDecorationType.LineThrough : TextDecorationType.None,
|
|
|
|
|
|
color: FbColors.faint
|
|
|
|
|
|
})
|
|
|
|
|
|
.maxLines(2)
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
if (this.parentOf() !== '') {
|
|
|
|
|
|
Text('属于「' + this.parentOf() + '」')
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
.maxLines(1)
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.flexShrink(1)
|
|
|
|
|
|
|
|
|
|
|
|
Text(this.statusWord(this.statusOf()))
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(this.statusTint(this.statusOf()))
|
|
|
|
|
|
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
|
|
|
|
|
|
.backgroundColor(this.statusWash(this.statusOf()))
|
|
|
|
|
|
.borderRadius(FbMetric.radiusPill)
|
|
|
|
|
|
|
|
|
|
|
|
FbSheetClose({
|
|
|
|
|
|
onClose: (): void => {
|
|
|
|
|
|
this.onClose();
|
|
|
|
|
|
},
|
|
|
|
|
|
color: ''
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.alignItems(VerticalAlign.Top)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 状态三段(点一下即改,没有"保存"按钮) */
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
statusSeg() {
|
|
|
|
|
|
Row({ space: 8 }) {
|
|
|
|
|
|
this.statusChip('todo')
|
|
|
|
|
|
this.statusChip('doing')
|
|
|
|
|
|
this.statusChip('done')
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 10, bottom: 4 })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
statusChip(w: string) {
|
|
|
|
|
|
Text(this.statusWord(w))
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(this.statusOf() === w ? '#FFFFFF' : this.statusTint(w))
|
|
|
|
|
|
.textAlign(TextAlign.Center)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.height(36)
|
|
|
|
|
|
.backgroundColor(this.statusOf() === w ? this.statusTint(w) : FbColors.inputBg)
|
|
|
|
|
|
.borderRadius(FbMetric.radiusS)
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.setStatus(w);
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 看板 chips(挪分组) */
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
boardChips() {
|
|
|
|
|
|
Column({ space: 6 }) {
|
|
|
|
|
|
Text('分组(看板)')
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
Scroll() {
|
|
|
|
|
|
Row({ space: 8 }) {
|
|
|
|
|
|
this.boardChip(0, '默认看板', '')
|
|
|
|
|
|
ForEach(this.boards, (b: FbBoardOpt) => {
|
|
|
|
|
|
this.boardChip(b.id, b.name, b.color)
|
|
|
|
|
|
}, (b: FbBoardOpt): string => 'pb_' + b.id.toString())
|
|
|
|
|
|
}
|
|
|
|
|
|
.padding({ right: 4 })
|
|
|
|
|
|
}
|
|
|
|
|
|
.scrollable(ScrollDirection.Horizontal)
|
|
|
|
|
|
.scrollBar(BarState.Off)
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 10 })
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
boardChip(id: number, name: string, color: string) {
|
|
|
|
|
|
Row({ space: 5 }) {
|
|
|
|
|
|
Column()
|
|
|
|
|
|
.width(7)
|
|
|
|
|
|
.height(7)
|
|
|
|
|
|
.borderRadius(4)
|
|
|
|
|
|
.backgroundColor(this.boardIdOf() === id ? '#FFFFFF' : (color !== '' ? color : '#9AA0A6'))
|
|
|
|
|
|
Text(name)
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(this.boardIdOf() === id ? '#FFFFFF' : FbColors.muted)
|
|
|
|
|
|
.maxLines(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
.padding({ left: 10, right: 10, top: 7, bottom: 7 })
|
|
|
|
|
|
.backgroundColor(this.boardIdOf() === id
|
|
|
|
|
|
? (color !== '' ? color : this.accent) : FbColors.inputBg)
|
|
|
|
|
|
.borderRadius(FbMetric.radiusPill)
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.moveTo(id);
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 只读信息:截止 / 负责人 / 分类 / 循环 / 备注 */
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
infoRows() {
|
|
|
|
|
|
Column({ space: 0 }) {
|
|
|
|
|
|
if (this.dueOf() !== '') {
|
|
|
|
|
|
Row({ space: 8 }) {
|
|
|
|
|
|
Text('截止')
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
.width(44)
|
|
|
|
|
|
Text(this.dueOf())
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(this.overdueOf() ? '#C0533F' : FbColors.ink)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
if (this.overdueOf()) {
|
|
|
|
|
|
Text('已逾期')
|
|
|
|
|
|
.fontSize(10)
|
|
|
|
|
|
.fontColor('#C0533F')
|
|
|
|
|
|
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
|
|
|
|
|
|
.backgroundColor('#FDECEC')
|
|
|
|
|
|
.borderRadius(FbMetric.radiusPill)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 7, bottom: 7 })
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
}
|
2026-10-22 18:45:37 +08:00
|
|
|
|
if (this.boardOf() !== '') {
|
|
|
|
|
|
this.kvRow('看板', this.boardOf())
|
|
|
|
|
|
}
|
2026-10-22 16:40:44 +08:00
|
|
|
|
if (this.assigneeOf() !== '') {
|
|
|
|
|
|
this.kvRow('负责人', this.assigneeOf())
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.categoryOf() !== '') {
|
|
|
|
|
|
this.kvRow('分类', this.categoryOf())
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.repeatOf() !== '') {
|
|
|
|
|
|
this.kvRow('循环', this.repeatOf())
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.noteOf() !== '') {
|
|
|
|
|
|
this.kvRow('备注', this.noteOf())
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 4 })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
kvRow(label: string, value: string) {
|
|
|
|
|
|
Row({ space: 8 }) {
|
|
|
|
|
|
Text(label)
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
.width(44)
|
|
|
|
|
|
Text(value)
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 7, bottom: 7 })
|
|
|
|
|
|
.alignItems(VerticalAlign.Top)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-28 22:10:15 +08:00
|
|
|
|
/** 共享给谁(工单 #26):列表 + 共享入口 + 逐条取消 */
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
shareSection() {
|
|
|
|
|
|
Column({ space: 6 }) {
|
|
|
|
|
|
Row({ space: 6 }) {
|
|
|
|
|
|
Text('共享给谁')
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontWeight(FontWeight.Medium)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
if (this.shares().length > 0) {
|
|
|
|
|
|
Text(this.shares().length.toString())
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
}
|
|
|
|
|
|
Blank()
|
|
|
|
|
|
Text(this.shareBusy ? '处理中…' : '+ 共享')
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(this.shareBusy ? FbColors.faint : this.accent)
|
|
|
|
|
|
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
|
|
|
|
|
|
.backgroundColor(fbAlpha(this.accent, 0.1))
|
|
|
|
|
|
.borderRadius(FbMetric.radiusPill)
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.openSharePicker();
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 12 })
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
|
|
|
|
|
|
if (this.shares().length === 0) {
|
|
|
|
|
|
Text('还没共享给谁;共享后对方能在任务里看到它,权限分「可改 / 只看」')
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ForEach(this.shares(), (s: FbTaskShare) => {
|
|
|
|
|
|
Row({ space: 8 }) {
|
|
|
|
|
|
Text(s.name)
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.maxLines(1)
|
|
|
|
|
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
|
Text(s.perm === 'read' ? '只看' : '可改')
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
Text('取消')
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.red)
|
|
|
|
|
|
.padding({ left: 6, right: 2, top: 2, bottom: 2 })
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.dropShare(s.personId);
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 6, bottom: 6 })
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
}, (s: FbTaskShare): string => 'tsh_' + s.personId.toString())
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 16:40:44 +08:00
|
|
|
|
/** 子任务:列表 + 加一条 */
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
kidSection() {
|
|
|
|
|
|
Column({ space: 6 }) {
|
|
|
|
|
|
Row({ space: 6 }) {
|
|
|
|
|
|
Text('子任务')
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontWeight(FontWeight.Medium)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
if (this.kids().length > 0) {
|
|
|
|
|
|
Text(this.kidDoneCount().toString() + '/' + this.kids().length.toString())
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
}
|
|
|
|
|
|
Blank()
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 12 })
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
|
|
|
|
|
|
ForEach(this.kids(), (k: FbTaskKid) => {
|
|
|
|
|
|
Row({ space: 8 }) {
|
|
|
|
|
|
Stack({ alignContent: Alignment.Center }) {
|
|
|
|
|
|
FbCheckCircle({
|
|
|
|
|
|
dia: 14,
|
|
|
|
|
|
done: k.status === 'done',
|
|
|
|
|
|
doing: k.status === 'doing',
|
|
|
|
|
|
color: this.accent
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
.width(28)
|
|
|
|
|
|
.height(28)
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.toggleKid(k);
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
Text(k.title)
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(k.status === 'done' ? FbColors.faint : FbColors.ink)
|
|
|
|
|
|
.decoration({
|
|
|
|
|
|
type: k.status === 'done' ? TextDecorationType.LineThrough : TextDecorationType.None
|
|
|
|
|
|
})
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.maxLines(2)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
2026-10-22 17:33:32 +08:00
|
|
|
|
}, (k: FbTaskKid): string => 'pk_' + k.id.toString() + '_' + k.status)
|
2026-10-22 16:40:44 +08:00
|
|
|
|
|
|
|
|
|
|
/* 加一条:输入框 + 「加」 */
|
|
|
|
|
|
Row({ space: 8 }) {
|
|
|
|
|
|
TextInput({ text: this.kidDraft, placeholder: '再加一步(回车即可)' })
|
|
|
|
|
|
.placeholderColor(FbColors.faint)
|
|
|
|
|
|
.placeholderFont({ size: FbMetric.fontTiny })
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
.backgroundColor(FbColors.inputBg)
|
|
|
|
|
|
.borderRadius(FbMetric.radiusS)
|
|
|
|
|
|
.height(38)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.padding({ left: 10, right: 10 })
|
|
|
|
|
|
.enterKeyType(EnterKeyType.Done)
|
|
|
|
|
|
.onChange((v: string): void => {
|
|
|
|
|
|
this.kidDraft = v;
|
|
|
|
|
|
})
|
|
|
|
|
|
.onSubmit((): void => {
|
|
|
|
|
|
this.addKid();
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
Text('加')
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(this.kidDraft.trim() === '' ? FbColors.faint : this.accent)
|
|
|
|
|
|
.textAlign(TextAlign.Center)
|
|
|
|
|
|
.width(52)
|
|
|
|
|
|
.height(38)
|
|
|
|
|
|
.backgroundColor(fbAlpha(this.accent, 0.1))
|
|
|
|
|
|
.borderRadius(FbMetric.radiusS)
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.addKid();
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 附件:缩略图/文件行 + 加图片 */
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
attachSection() {
|
|
|
|
|
|
Column({ space: 8 }) {
|
|
|
|
|
|
Row({ space: 6 }) {
|
|
|
|
|
|
Text('附件')
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontWeight(FontWeight.Medium)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
if (this.attachs().length > 0) {
|
|
|
|
|
|
Text(this.attachs().length.toString())
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
}
|
|
|
|
|
|
Blank()
|
|
|
|
|
|
Text(this.uploading ? '上传中…' : '+ 加图片')
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(this.uploading ? FbColors.faint : this.accent)
|
|
|
|
|
|
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
|
|
|
|
|
|
.backgroundColor(fbAlpha(this.accent, 0.1))
|
|
|
|
|
|
.borderRadius(FbMetric.radiusPill)
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.pickAttach();
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 12 })
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
|
|
|
|
|
|
if (this.attachs().length > 0) {
|
|
|
|
|
|
/* `Flex wrap` 而不是 `Row`:数量不定且要换行。
|
|
|
|
|
|
🔴 `Row` 在 ArkUI 里**不换行**,塞不下就横向溢出父容器(v76 真机事故)。 */
|
|
|
|
|
|
Flex({ wrap: FlexWrap.Wrap }) {
|
|
|
|
|
|
ForEach(this.attachs(), (a: FbTaskAttach) => {
|
|
|
|
|
|
this.attachCell(a)
|
|
|
|
|
|
}, (a: FbTaskAttach): string => 'pa_' + a.id.toString())
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
attachCell(a: FbTaskAttach) {
|
|
|
|
|
|
Stack({ alignContent: Alignment.TopEnd }) {
|
|
|
|
|
|
if (a.isImage) {
|
|
|
|
|
|
Image(fbImageSrc(a.key, this.token()))
|
|
|
|
|
|
.width(64)
|
|
|
|
|
|
.height(64)
|
|
|
|
|
|
.borderRadius(FbMetric.radiusS)
|
|
|
|
|
|
.objectFit(ImageFit.Cover)
|
|
|
|
|
|
.backgroundColor(FbColors.inputBg)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Column({ space: 2 }) {
|
|
|
|
|
|
Text(a.kind.toUpperCase())
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(this.accent)
|
|
|
|
|
|
Text(a.name)
|
|
|
|
|
|
.fontSize(9)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
.maxLines(1)
|
|
|
|
|
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.textAlign(TextAlign.Center)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width(64)
|
|
|
|
|
|
.height(64)
|
|
|
|
|
|
.justifyContent(FlexAlign.Center)
|
|
|
|
|
|
.padding(4)
|
|
|
|
|
|
.backgroundColor(FbColors.inputBg)
|
|
|
|
|
|
.borderRadius(FbMetric.radiusS)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 删除角标:热区做大一点(手指点得中),视觉上仍是小圆 */
|
|
|
|
|
|
Stack({ alignContent: Alignment.Center }) {
|
|
|
|
|
|
Column()
|
|
|
|
|
|
.width(16)
|
|
|
|
|
|
.height(16)
|
|
|
|
|
|
.borderRadius(8)
|
|
|
|
|
|
.backgroundColor('#B3000000')
|
|
|
|
|
|
Text('✕')
|
|
|
|
|
|
.fontSize(9)
|
|
|
|
|
|
.fontColor(Color.White)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width(28)
|
|
|
|
|
|
.height(28)
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.dropAttach(a);
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
.width(64)
|
|
|
|
|
|
.height(64)
|
|
|
|
|
|
.margin({ right: 8, bottom: 8 })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 留言 */
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
commentSection() {
|
|
|
|
|
|
Column({ space: 8 }) {
|
|
|
|
|
|
Row({ space: 6 }) {
|
|
|
|
|
|
Text('留言')
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontWeight(FontWeight.Medium)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
if (this.comments().length > 0) {
|
|
|
|
|
|
Text(this.comments().length.toString())
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
}
|
|
|
|
|
|
Blank()
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 12 })
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
|
|
|
|
|
|
ForEach(this.comments(), (c: FbTaskComment) => {
|
|
|
|
|
|
Row({ space: 8 }) {
|
|
|
|
|
|
/* 头像位:一个小色点(端上没拉头像,用底色 + 名字首字最直观) */
|
|
|
|
|
|
Row() {
|
|
|
|
|
|
Text(c.name.substring(0, 1))
|
|
|
|
|
|
.fontSize(10)
|
|
|
|
|
|
.fontColor(this.accent)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width(22)
|
|
|
|
|
|
.height(22)
|
|
|
|
|
|
.borderRadius(11)
|
|
|
|
|
|
.backgroundColor(c.color !== '' ? c.color : FbColors.inputBg)
|
|
|
|
|
|
.justifyContent(FlexAlign.Center)
|
|
|
|
|
|
|
|
|
|
|
|
Column({ space: 2 }) {
|
|
|
|
|
|
Row({ space: 6 }) {
|
|
|
|
|
|
Text(c.name)
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.muted)
|
|
|
|
|
|
Text(c.createdAt)
|
|
|
|
|
|
.fontSize(9)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
|
|
|
|
|
|
Text(c.content)
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
}
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 4, bottom: 4 })
|
|
|
|
|
|
.alignItems(VerticalAlign.Top)
|
|
|
|
|
|
}, (c: FbTaskComment): string => 'pc_' + c.id.toString())
|
|
|
|
|
|
|
|
|
|
|
|
Row({ space: 8 }) {
|
|
|
|
|
|
TextInput({ text: this.cmtDraft, placeholder: '说一句(回车发送)' })
|
|
|
|
|
|
.placeholderColor(FbColors.faint)
|
|
|
|
|
|
.placeholderFont({ size: FbMetric.fontTiny })
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
.backgroundColor(FbColors.inputBg)
|
|
|
|
|
|
.borderRadius(FbMetric.radiusS)
|
|
|
|
|
|
.height(38)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.padding({ left: 10, right: 10 })
|
|
|
|
|
|
.enterKeyType(EnterKeyType.Send)
|
|
|
|
|
|
.onChange((v: string): void => {
|
|
|
|
|
|
this.cmtDraft = v;
|
|
|
|
|
|
})
|
|
|
|
|
|
.onSubmit((): void => {
|
|
|
|
|
|
this.sendComment();
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
Text('发送')
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(this.cmtDraft.trim() === '' ? FbColors.faint : this.accent)
|
|
|
|
|
|
.textAlign(TextAlign.Center)
|
|
|
|
|
|
.width(56)
|
|
|
|
|
|
.height(38)
|
|
|
|
|
|
.backgroundColor(fbAlpha(this.accent, 0.1))
|
|
|
|
|
|
.borderRadius(FbMetric.radiusS)
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.sendComment();
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 底部:编辑 / 删除 */
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
footBar() {
|
|
|
|
|
|
Row({ space: 10 }) {
|
|
|
|
|
|
Text('编辑')
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(Color.White)
|
|
|
|
|
|
.textAlign(TextAlign.Center)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.height(42)
|
|
|
|
|
|
.backgroundColor(this.busy ? '#C9CDD2' : this.accent)
|
|
|
|
|
|
.borderRadius(FbMetric.radiusS)
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
if (this.busy) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const d: FbTaskDetail | null = this.detail;
|
|
|
|
|
|
if (d === null) {
|
|
|
|
|
|
this.note = '还在载入,稍等一下再点编辑。';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.onEdit(d);
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
Text(this.confirmDel ? '确定删掉' : '删除')
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(this.confirmDel ? Color.White : '#C0533F')
|
|
|
|
|
|
.textAlign(TextAlign.Center)
|
|
|
|
|
|
.width(88)
|
|
|
|
|
|
.height(42)
|
|
|
|
|
|
.backgroundColor(this.confirmDel ? '#C0533F' : '#FDECEC')
|
|
|
|
|
|
.borderRadius(FbMetric.radiusS)
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.dropTask();
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.margin({ top: 12 })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|