任务共享:面板补「共享给谁」区块 laoyang/FamilyBook#26

用户原话(2026-10-28):「鸿蒙端对齐网页端」。任务面板原来没有共享入口,现在:

1) FbTaskShare 类型 + parseTaskDetail 解析 Task::find 已带的 shares
2) FbTaskRepo 补 fetchTaskShares/shareTask/unshareTask(删除走 query)
3) FbTaskPanel 加「共享给谁」区块:列表(可改/只看)+ 逐条取消 +
   「+ 共享」复用选人组件(与资源/财务共享同一套)

编译 BUILD SUCCESSFUL;服务端冒烟 _i26_task_share_api.py 13/0 + 反证命中。
This commit is contained in:
2026-10-28 22:10:15 +08:00
parent f93dd92cdb
commit 59a9d3df39
4 changed files with 188 additions and 1 deletions
+135 -1
View File
@@ -31,17 +31,20 @@
import { FbColors, FbMetric, FB_MASK, fbAlpha, fbTint } from '../common/Theme'; import { FbColors, FbMetric, FB_MASK, fbAlpha, fbTint } from '../common/Theme';
import { import {
FbBoardOpt, FbTaskAttach, FbTaskComment, FbTaskDetail, FbTaskItem, FbTaskKid, FbBoardOpt, FbTaskAttach, FbTaskComment, FbTaskDetail, FbTaskItem, FbTaskKid, FbTaskShare,
} from '../model/Types'; } from '../model/Types';
import { FbSheetRing } from './FbSheetRing'; import { FbSheetRing } from './FbSheetRing';
import { FbSheetClose } from './FbSheetClose'; import { FbSheetClose } from './FbSheetClose';
import { FbCheckCircle } from './Widgets'; import { FbCheckCircle } from './Widgets';
import { FbPersonPickSheet } from './FbPersonPickSheet';
import { FbPersonRepo, FbRosterPerson } from '../data/FbPersonRepo';
import { FB_TOKEN_STORE } from '../data/FbTokenStore'; import { FB_TOKEN_STORE } from '../data/FbTokenStore';
import { FbApiResult } from '../data/FbHttp'; import { FbApiResult } from '../data/FbHttp';
import { fbImageSrc } from '../data/FbAttachQueue'; import { fbImageSrc } from '../data/FbAttachQueue';
import { import {
addTaskComment, createTask, deleteTask, delTaskAttach, getTask, moveTask, addTaskComment, createTask, deleteTask, delTaskAttach, getTask, moveTask,
setTaskStatus, uploadTaskAttach, FbTaskDraft, setTaskStatus, uploadTaskAttach, FbTaskDraft,
fetchTaskShares, shareTask, unshareTask,
} from '../data/FbTaskRepo'; } from '../data/FbTaskRepo';
import { parseTaskDetail } from '../data/FbParse'; import { parseTaskDetail } from '../data/FbParse';
import { picker } from '@kit.CoreFileKit'; import { picker } from '@kit.CoreFileKit';
@@ -104,6 +107,10 @@ export struct FbTaskPanel {
/** 删除按钮点第一次 → 变成「确定删除?」 */ /** 删除按钮点第一次 → 变成「确定删除?」 */
@Local confirmDel: boolean = false; @Local confirmDel: boolean = false;
@Local uploading: boolean = false; @Local uploading: boolean = false;
// 共享(工单 #26)
@Local roster: FbRosterPerson[] = [];
@Local pickingShare: boolean = false;
@Local shareBusy: boolean = false;
aboutToAppear(): void { aboutToAppear(): void {
this.reload(); this.reload();
@@ -231,6 +238,52 @@ export struct FbTaskPanel {
return d !== null ? d.attachs : []; return d !== null ? d.attachs : [];
} }
/** 共享给谁(工单 #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();
}
private categoryOf(): string { private categoryOf(): string {
const d: FbTaskDetail | null = this.detail; const d: FbTaskDetail | null = this.detail;
return d !== null ? d.category : ''; return d !== null ? d.category : '';
@@ -515,6 +568,7 @@ export struct FbTaskPanel {
this.statusSeg() this.statusSeg()
this.boardChips() this.boardChips()
this.infoRows() this.infoRows()
this.shareSection()
this.kidSection() this.kidSection()
this.attachSection() this.attachSection()
this.commentSection() this.commentSection()
@@ -551,6 +605,22 @@ export struct FbTaskPanel {
} }
} }
.width('94%') .width('94%')
/* 选人浮层:共享给谁(工单 #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;
}
})
}
} }
.width('100%') .width('100%')
.height('100%') .height('100%')
@@ -754,6 +824,70 @@ export struct FbTaskPanel {
.alignItems(VerticalAlign.Top) .alignItems(VerticalAlign.Top)
} }
/** 共享给谁(工单 #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)
}
/** 子任务:列表 + 加一条 */ /** 子任务:列表 + 加一条 */
@Builder @Builder
kidSection() { kidSection() {
+20
View File
@@ -20,6 +20,7 @@ import {
FbLedgerBook, FbLedgerDetail, FbLedgerEntry, FbLedgerPoint, FbNoteItem, FbNoteBlock, FbNotificationItem, FbResourceItem, FbLedgerBook, FbLedgerDetail, FbLedgerEntry, FbLedgerPoint, FbNoteItem, FbNoteBlock, FbNotificationItem, FbResourceItem,
FbTaskItem, FbTripItem, FbWeather, FbStat, FbMemoryItem, FbRecentDay, FbTaskItem, FbTripItem, FbWeather, FbStat, FbMemoryItem, FbRecentDay,
FbTaskAttach, FbTaskComment, FbTaskDetail, FbTaskKid, FbTaskMeta, FbCatOpt, FbBoardOpt, FbTaskAttach, FbTaskComment, FbTaskDetail, FbTaskKid, FbTaskMeta, FbCatOpt, FbBoardOpt,
FbTaskShare,
} from '../model/Types'; } from '../model/Types';
// ============================================================ // ============================================================
@@ -200,6 +201,13 @@ interface RawTaskDetail extends RawTask {
children?: RawTaskKid[]; children?: RawTaskKid[];
comments?: RawTaskComment[]; comments?: RawTaskComment[];
attachs?: RawTaskAttach[]; attachs?: RawTaskAttach[];
shares?: RawTaskShare[];
}
/** 任务共享行(工单 #26;服务端 `Task::shares()`:s.person_id/perm + p.name + 昵称) */
interface RawTaskShare {
person_id?: number; perm?: string | null;
name?: string | null; my_nickname?: string | null;
} }
/** `?view=boards` 的看板元数据(v79) */ /** `?view=boards` 的看板元数据(v79) */
@@ -1016,6 +1024,17 @@ export function parseTaskDetail(json: string): FbTaskDetail | null {
attachs.push(one); attachs.push(one);
} }
// 共享给谁(工单 #26;服务端 Task::find 已带 shares)
const shares: FbTaskShare[] = [];
for (const sh of arr<RawTaskShare>(d.shares)) {
const nick: string = s(sh.my_nickname);
shares.push({
personId: n(sh.person_id),
name: nick !== '' ? nick : s(sh.name),
perm: s(sh.perm) !== '' ? s(sh.perm) : 'write',
});
}
const creatorNick: string = s(d.creator_nick); const creatorNick: string = s(d.creator_nick);
const detail: FbTaskDetail = { const detail: FbTaskDetail = {
base: base, base: base,
@@ -1028,6 +1047,7 @@ export function parseTaskDetail(json: string): FbTaskDetail | null {
kids: kids, kids: kids,
comments: comments, comments: comments,
attachs: attachs, attachs: attachs,
shares: shares,
}; };
return detail; return detail;
} }
+24
View File
@@ -174,3 +174,27 @@ export async function delTaskAttach(token: string, id: number, aid: number): Pro
return await fbApi('/api/v1/tasks/' + id.toString() + '/attach', opts); return await fbApi('/api/v1/tasks/' + id.toString() + '/attach', opts);
} }
/** 共享给谁(工单 #26) */
export async function fetchTaskShares(token: string, id: number): Promise<FbApiResult> {
const opts: FbApiOptions = { token: token, authProbe: true };
return await fbApi('/api/v1/tasks/' + id.toString() + '/shares', opts);
}
/** 共享给某人(perm = read / write;任务默认 write,与 Web 端 tasks/share 一致) */
export async function shareTask(token: string, id: number, personId: number,
perm: string): Promise<FbApiResult> {
const body: Record<string, Object> = {};
body['person_id'] = personId;
body['perm'] = perm;
const opts: FbApiOptions = { method: 'POST', token: token, body: body };
return await fbApi('/api/v1/tasks/' + id.toString() + '/shares', opts);
}
/** 取消某人的共享(person_id 走 query,DELETE 带 body 服务端读不到) */
export async function unshareTask(token: string, id: number, personId: number): Promise<FbApiResult> {
const q: Record<string, string> = {};
q['person_id'] = personId.toString();
const opts: FbApiOptions = { method: 'DELETE', token: token, query: q };
return await fbApi('/api/v1/tasks/' + id.toString() + '/shares', opts);
}
+9
View File
@@ -779,6 +779,13 @@ export interface FbTaskAttach {
sizeWord: string; sizeWord: string;
} }
/** 任务共享给谁(工单 #26;服务端 `Task::shares()`) */
export interface FbTaskShare {
personId: number;
name: string;
perm: string;
}
/** /**
* 任务详情(v79 面板用):列表行 + 只有详情接口才给的那几块。 * 任务详情(v79 面板用):列表行 + 只有详情接口才给的那几块。
* *
@@ -801,6 +808,8 @@ export interface FbTaskDetail {
kids: FbTaskKid[]; kids: FbTaskKid[];
comments: FbTaskComment[]; comments: FbTaskComment[];
attachs: FbTaskAttach[]; attachs: FbTaskAttach[];
/** 共享给谁(工单 #26) */
shares: FbTaskShare[];
} }
// ============================================================ // ============================================================