feat(web,android): 看板拖拽、详情侧栏编辑、快速录入与安卓任务列表

Web:
- 看板四列可拖拽,非法流转当场说明原因;勾选后可批量移动
- 详情侧栏全部可编辑(标题/优先级/截止/标签/备注/评论),409 自动刷新
- 快速录入 #标签 !优先级;键盘流 j/k、Enter、e、Esc
- Playwright 真浏览器 e2e 7 项断言全绿(web/e2e/smoke.mjs)
- 1000 条任务实测首屏 P50 38-55ms

Android(未编译验证:本机无 JDK/Gradle):
- 任务列表/详情/勾选完成/快速新建,冲突时提示并刷新
- 修:User/Member 字段名与后端 nickmame/phone 对齐,原先必然反序列化失败
This commit is contained in:
dadao
2026-10-28 04:05:41 +08:00
parent c4868f04cf
commit 6e33e1024d
13 changed files with 1316 additions and 93 deletions
+84
View File
@@ -0,0 +1,84 @@
import type { ItemStatus } from "./types";
const PRIORITY_ALIASES: Record<string, number> = {
"!0": 0,
"!1": 1,
"!2": 2,
"!3": 3,
"!低": 1,
"!中": 2,
"!高": 3,
};
export interface QuickAdd {
title: string;
tagNames: string[];
priority: number;
status: ItemStatus;
}
/**
* 解析快速录入:`#标签` 与 `!优先级` 会被吃掉,剩下的做标题。
*
* 只认行内 `#` 开头且后面不是空格的片段——中文正文里 `#` 极少自然出现,
* 真要以 # 开头就加个空格。`@人` 暂时不解析:重名的人比标签多,
* 悄悄指错负责人比不支持更糟(等 M2 有成员选择器再做)。
*/
export function parseQuickAdd(raw: string, defaultStatus: ItemStatus): QuickAdd {
const tagNames: string[] = [];
let priority = 1;
const words: string[] = [];
for (const token of raw.split(/\s+/)) {
if (!token) continue;
if (token.length > 1 && token.startsWith("#")) {
tagNames.push(token.slice(1));
continue;
}
if (PRIORITY_ALIASES[token] !== undefined) {
priority = PRIORITY_ALIASES[token];
continue;
}
words.push(token);
}
const title = words.join(" ").trim() || raw.trim();
return { title, tagNames, priority, status: defaultStatus };
}
export const ROLE_LABEL: Record<string, string> = {
owner: "负责人",
admin: "管理员",
member: "成员",
guest: "访客",
};
export const STATUS_LABEL: Record<ItemStatus, string> = {
inbox: "收件箱",
todo: "待办",
doing: "进行中",
done: "已完成",
blocked: "被阻塞",
canceled: "已取消",
};
/** 状态机前端影子:只用来置灰拖拽目标,服务端仍会再校验一次。 */
export const ALLOWED: Record<ItemStatus, ItemStatus[]> = {
inbox: ["todo", "canceled"],
todo: ["inbox", "doing", "done", "canceled"],
doing: ["todo", "done", "blocked", "canceled"],
blocked: ["doing", "canceled"],
done: ["todo"],
canceled: ["todo"],
};
export function canTransition(from: ItemStatus, to: ItemStatus): boolean {
return from === to || (ALLOWED[from] ?? []).includes(to);
}
export const PRIORITY_LABEL: Record<number, string> = {
0: "无",
1: "低",
2: "中",
3: "高",
};