feat(web): 工单页与升级面板,修掉视图竞态与标签缺失导致的崩溃

- 新增 /w/:wsId/tickets:派给我的 / 我发起的、状态过滤、列表内联快操作
- 详情抽屉按角色显示受理/开工/提交/验收/驳回/改派/取消;任务态底部可升级成工单
- 导航抽成 WorkspaceNav,带未读角标(30 秒对齐,操作后立刻刷新)
- 修竞态:切视图/切 tab 连发请求时,慢的回包会把新结果盖掉(reqId 序号丢弃过期响应)
- 登录支持 ?next= 站内回跳,邀请链接不再'登录完掉回首页'
- e2e/tickets.mjs:双人双上下文的完整闭环,8 项断言全绿
This commit is contained in:
dadao
2026-10-28 13:20:18 +08:00
parent 30caaaf1d9
commit b1dc3cffa6
11 changed files with 750 additions and 45 deletions
+15 -6
View File
@@ -1,4 +1,4 @@
import type { ItemStatus } from "./types";
import type { ItemStatus, TaskStatus } from "./types";
const PRIORITY_ALIASES: Record<string, number> = {
"!0": 0,
@@ -53,17 +53,26 @@ export const ROLE_LABEL: Record<string, string> = {
guest: "访客",
};
export const STATUS_LABEL: Record<ItemStatus, string> = {
export const STATUS_LABEL: Record<string, string> = {
// 任务
inbox: "收件箱",
todo: "待办",
doing: "进行中",
done: "已完成",
blocked: "被阻塞",
canceled: "已取消",
// 工单(与任务的 done/canceled 共用)
open: "待接收",
accepted: "待办理",
in_progress: "办理中",
pending_verify: "待验收",
};
/** 状态机前端影子:只用来置灰拖拽目标,服务端仍会再校验一次。 */
export const ALLOWED: Record<ItemStatus, ItemStatus[]> = {
/**
* 任务状态机的前端影子:只用来置灰拖拽目标,服务端仍会再校验一次。
* 工单的状态机在服务端(动作驱动),前端不做影子,直接看按钮。
*/
export const ALLOWED: Record<string, TaskStatus[]> = {
inbox: ["todo", "canceled"],
todo: ["inbox", "doing", "done", "canceled"],
doing: ["todo", "done", "blocked", "canceled"],
@@ -72,8 +81,8 @@ export const ALLOWED: Record<ItemStatus, ItemStatus[]> = {
canceled: ["todo"],
};
export function canTransition(from: ItemStatus, to: ItemStatus): boolean {
return from === to || (ALLOWED[from] ?? []).includes(to);
export function canTransition(from: string, to: string): boolean {
return from === to || (ALLOWED[from] ?? []).includes(to as TaskStatus);
}
export const PRIORITY_LABEL: Record<number, string> = {
+38 -1
View File
@@ -39,7 +39,16 @@ export interface Invitation {
expires_at: string;
}
export type ItemStatus = "inbox" | "todo" | "doing" | "done" | "blocked" | "canceled";
export type TaskStatus = "inbox" | "todo" | "doing" | "done" | "blocked" | "canceled";
export type TicketStatus =
| "open"
| "accepted"
| "in_progress"
| "pending_verify"
| "done"
| "canceled";
export type ItemStatus = TaskStatus | TicketStatus;
export interface Tag {
id: string;
@@ -74,4 +83,32 @@ export interface Activity {
export interface ItemDetail extends Item {
activities: Activity[];
ticket?: TicketInfo;
}
export interface TicketInfo {
requester_id: string;
assignee_id: string;
parent_item_id: string | null;
accepted_at: string | null;
submitted_at: string | null;
verified_at: string | null;
verified_by: string | null;
reject_count: number;
resolution_md: string;
}
export interface Ticket extends Item {
ticket: TicketInfo;
}
export interface Notification {
id: string;
kind: string;
item_id: string | null;
item_title: string | null;
title: string;
body: string;
read_at: string | null;
created_at: string;
}