2026-10-20 15:27:32 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 家书 FamilyBook · 任务的**写操作**(鸿蒙端)
|
|
|
|
|
|
*
|
|
|
|
|
|
* 对应服务端 `app/ApiHandler.php` 的 `tasks()`:
|
|
|
|
|
|
* POST /api/v1/tasks 新建(title / note / due_at / board_id / status)
|
|
|
|
|
|
* PATCH /api/v1/tasks/<id> 改(标题/备注/截止/看板/负责人/分类)
|
|
|
|
|
|
* POST /api/v1/tasks/<id>/status 改状态(todo / doing / done)
|
|
|
|
|
|
* DELETE /api/v1/tasks/<id> 删
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 用户 2026-10-20:「任务也是一样啊,任务也点不进去,也没法那个完成,
|
|
|
|
|
|
* 新建任务、设置这些东西都这都没有。」
|
|
|
|
|
|
* ⇒ 端上原来这三件事**一件都没接**(按钮没 onTap、列表项没 onClick)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ **状态只走 `/status` 子动作**:服务端 `PATCH /tasks/<id>` 里
|
|
|
|
|
|
* `'status' => (string)$t['status']` 是**原样回填**,显式忽略请求里的 status
|
|
|
|
|
|
* (见 ApiHandler 那行注释「状态只走 /status 子动作」)。
|
|
|
|
|
|
* 所以端上 toggle 完成必须打 `/status`,打 PATCH 会"看起来成功但状态没变"。
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import { fbApi, FbApiResult, FbApiOptions } from './FbHttp';
|
2026-10-22 16:40:44 +08:00
|
|
|
|
import { fbUploadOne } from './FbUpload';
|
2026-10-20 15:27:32 +08:00
|
|
|
|
|
|
|
|
|
|
/** 新建/编辑任务提交的字段 */
|
|
|
|
|
|
export interface FbTaskDraft {
|
|
|
|
|
|
title: string;
|
|
|
|
|
|
note: string;
|
|
|
|
|
|
/** `YYYY-MM-DD HH:MM`;空串 = 没有期限 */
|
|
|
|
|
|
dueAt: string;
|
|
|
|
|
|
/** 看板 id(0 = 不归指定看板 → 服务端落到「其他」那一列) */
|
|
|
|
|
|
boardId: number;
|
|
|
|
|
|
/** todo / doing / done(只在**新建**时生效;改状态走 `setTaskStatus`) */
|
|
|
|
|
|
status: string;
|
2026-10-22 16:40:44 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 父任务 id(v79;0 = 顶层任务)。
|
|
|
|
|
|
* 面板里「加一条子任务」就是 `parentId = 当前任务` 的新建请求。
|
|
|
|
|
|
*/
|
|
|
|
|
|
parentId: number;
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 负责人 id。
|
|
|
|
|
|
*
|
|
|
|
|
|
* `> 0` = 指定这个人;`0` = 新建时"用默认值(我自己)"、编辑时"清空负责人";
|
|
|
|
|
|
* **`-1` = 这次不改**(编辑表单目前没有负责人这一项 ⇒ 不能传 0 把它抹掉)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
assigneeId: number;
|
|
|
|
|
|
/** 分类 id(0 = 不分类) */
|
|
|
|
|
|
categoryId: number;
|
|
|
|
|
|
/** 循环规则:空 / day / week / month / year */
|
|
|
|
|
|
repeat: string;
|
2026-10-20 15:27:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 新建任务 */
|
|
|
|
|
|
export async function createTask(token: string, d: FbTaskDraft): Promise<FbApiResult> {
|
|
|
|
|
|
/* ⚠️ ArkTS 的 `arkts-no-untyped-obj-literals`:请求体必须
|
|
|
|
|
|
`Record<string, Object> = {}` 逐键赋值(见 FbTripRepo 同一说明)。 */
|
|
|
|
|
|
const body: Record<string, Object> = {};
|
|
|
|
|
|
body['title'] = d.title;
|
|
|
|
|
|
body['note'] = d.note;
|
|
|
|
|
|
body['due_at'] = d.dueAt;
|
|
|
|
|
|
body['board_id'] = d.boardId;
|
|
|
|
|
|
body['status'] = d.status;
|
2026-10-22 16:40:44 +08:00
|
|
|
|
/* v79:这三个原来没传 —— 端上因此**建不出子任务**、也指定不了负责人/分类。
|
|
|
|
|
|
⚠️ `assignee_id` 只在 >0 时才传:服务端 `Api::int('assignee_id', $pid)`
|
|
|
|
|
|
的默认值是"我自己",传 0 会把它覆盖成"没人负责"。 */
|
|
|
|
|
|
if (d.parentId > 0) {
|
|
|
|
|
|
body['parent_id'] = d.parentId;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (d.assigneeId > 0) {
|
|
|
|
|
|
body['assignee_id'] = d.assigneeId;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (d.categoryId > 0) {
|
|
|
|
|
|
body['category_id'] = d.categoryId;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (d.repeat !== '') {
|
|
|
|
|
|
body['repeat_rule'] = d.repeat;
|
|
|
|
|
|
}
|
2026-10-20 15:27:32 +08:00
|
|
|
|
const opts: FbApiOptions = { method: 'POST', token: token, body: body };
|
|
|
|
|
|
return await fbApi('/api/v1/tasks', opts);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-10-22 16:40:44 +08:00
|
|
|
|
* 改任务(标题 / 备注 / 截止 / 看板 / 负责人 / 分类 / 循环)。
|
2026-10-20 15:27:32 +08:00
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ **不带 status**:服务端 PATCH 会忽略它(见文件头说明),
|
|
|
|
|
|
* 带上只会让人以为"改成功了",实际状态没动。
|
|
|
|
|
|
*/
|
|
|
|
|
|
export async function updateTask(token: string, id: number, d: FbTaskDraft): Promise<FbApiResult> {
|
|
|
|
|
|
const body: Record<string, Object> = {};
|
|
|
|
|
|
body['title'] = d.title;
|
|
|
|
|
|
body['note'] = d.note;
|
|
|
|
|
|
body['due_at'] = d.dueAt;
|
|
|
|
|
|
body['board_id'] = d.boardId;
|
2026-10-22 16:40:44 +08:00
|
|
|
|
/* v79:负责人/分类/循环也走 PATCH(服务端这三个字段都认)。
|
|
|
|
|
|
⚠️ `assignee_id` 只在**明确要改**(>=0)时才传:编辑表单里没有负责人这一项,
|
|
|
|
|
|
传 0 会把原来指派的负责人**静默清空**(与 v77「没传 ≠ 清空」同一类 bug)。
|
|
|
|
|
|
⚠️ 分类和循环则**必须**照传(0 / 空串就是"清空",那是合法操作)。 */
|
|
|
|
|
|
if (d.assigneeId >= 0) {
|
|
|
|
|
|
body['assignee_id'] = d.assigneeId;
|
|
|
|
|
|
}
|
|
|
|
|
|
body['category_id'] = d.categoryId;
|
|
|
|
|
|
body['repeat_rule'] = d.repeat;
|
2026-10-20 15:27:32 +08:00
|
|
|
|
const opts: FbApiOptions = { method: 'PATCH', token: token, body: body };
|
|
|
|
|
|
return await fbApi('/api/v1/tasks/' + id.toString(), opts);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 改状态(勾完成 / 取消完成 / 标记进行中) */
|
|
|
|
|
|
export async function setTaskStatus(token: string, id: number, status: string): Promise<FbApiResult> {
|
|
|
|
|
|
const body: Record<string, Object> = {};
|
|
|
|
|
|
body['status'] = status;
|
|
|
|
|
|
const opts: FbApiOptions = { method: 'POST', token: token, body: body };
|
|
|
|
|
|
return await fbApi('/api/v1/tasks/' + id.toString() + '/status', opts);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 删任务 */
|
|
|
|
|
|
export async function deleteTask(token: string, id: number): Promise<FbApiResult> {
|
|
|
|
|
|
const opts: FbApiOptions = { method: 'DELETE', token: token };
|
|
|
|
|
|
return await fbApi('/api/v1/tasks/' + id.toString(), opts);
|
|
|
|
|
|
}
|
2026-10-22 16:40:44 +08:00
|
|
|
|
|
|
|
|
|
|
/* ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
v79(用户 2026-10-22):「鸿蒙端的功能,也需要同样适配……
|
|
|
|
|
|
任务模块的功能需要完善。」
|
|
|
|
|
|
⇒ 下面这些是网页 v78「就地面板」在端上的对等物。
|
|
|
|
|
|
⚠️ 除 `getTask` 外全部返回 `FbApiResult`,且**服务端都会回整条任务** ——
|
|
|
|
|
|
调用方拿到 `res.data` 直接重画面板,不用再补一次 GET。
|
|
|
|
|
|
═══════════════════════════════════════════════════════════════ */
|
|
|
|
|
|
|
|
|
|
|
|
/** 取一条任务的详情(含子任务 / 留言 / 附件 / 共享人) */
|
|
|
|
|
|
export async function getTask(token: string, id: number): Promise<FbApiResult> {
|
|
|
|
|
|
const opts: FbApiOptions = { method: 'GET', token: token };
|
|
|
|
|
|
return await fbApi('/api/v1/tasks/' + id.toString(), opts);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 挪到另一个看板(网页版「分组」的等价物)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 走 `/move` 而不是 `PATCH`:`/move` 会顺手把这一列重排 position;
|
|
|
|
|
|
* PATCH 只改 board_id,同列里会留下重复的 position(网页端拖拽用的是 /move)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
export async function moveTask(token: string, id: number, boardId: number): Promise<FbApiResult> {
|
|
|
|
|
|
const body: Record<string, Object> = {};
|
|
|
|
|
|
body['board_id'] = boardId;
|
|
|
|
|
|
const opts: FbApiOptions = { method: 'POST', token: token, body: body };
|
|
|
|
|
|
return await fbApi('/api/v1/tasks/' + id.toString() + '/move', opts);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 发一条留言(服务端只回 `{id}`,要整块内容就再 `getTask` 一次) */
|
|
|
|
|
|
export async function addTaskComment(token: string, id: number, content: string): Promise<FbApiResult> {
|
|
|
|
|
|
const body: Record<string, Object> = {};
|
|
|
|
|
|
body['content'] = content;
|
|
|
|
|
|
const opts: FbApiOptions = { method: 'POST', token: token, body: body };
|
|
|
|
|
|
return await fbApi('/api/v1/tasks/' + id.toString() + '/comments', opts);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 传一个附件(图片/文档都行,服务端按 mime 自己分)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @return 空串 = 成功;否则是给人看的错误说明(与 `fbUploadOne` 的约定一致)
|
|
|
|
|
|
*/
|
|
|
|
|
|
export async function uploadTaskAttach(token: string, id: number, uri: string): Promise<string> {
|
|
|
|
|
|
return await fbUploadOne('/api/v1/tasks/' + id.toString() + '/attach', 'file', uri, token);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 删一个附件(列表里点 ✕) */
|
|
|
|
|
|
export async function delTaskAttach(token: string, id: number, aid: number): Promise<FbApiResult> {
|
|
|
|
|
|
/* ⚠️ 两处都踩过,别改回去:
|
|
|
|
|
|
① 路由只解析「资源/id/子动作」**三段** ⇒ 不能写成 `/attach/<aid>`,
|
|
|
|
|
|
那样服务端只会拿到 `$sub='attach'`,附件 id 被静默丢掉;
|
|
|
|
|
|
② 附件 id 走 **query**,不要塞 JSON body —— 实测 `DELETE` 带 JSON 体时
|
|
|
|
|
|
服务端读到的是空(`Api::param` 拿不到),症状是
|
|
|
|
|
|
「要指定删哪个附件」400,而请求看起来完全正常。 */
|
|
|
|
|
|
const q: Record<string, string> = {};
|
|
|
|
|
|
q['attachment_id'] = aid.toString();
|
|
|
|
|
|
const opts: FbApiOptions = { method: 'DELETE', token: token, query: q };
|
|
|
|
|
|
return await fbApi('/api/v1/tasks/' + id.toString() + '/attach', opts);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-28 22:10:15 +08:00
|
|
|
|
/** 共享给谁(工单 #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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|