import { useCallback, useEffect, useRef, useState } from "react"; import { useParams, useSearchParams } from "react-router-dom"; import { AppShell } from "../components/AppShell"; import { TaskDetail } from "../components/TaskDetail"; import { ChevronRight, Plus, TicketIcon } from "../components/icons"; import { Button, EmptyState, InlineAlert, PageHeader, Sheet, SkeletonRows, StatusPill, useToast, } from "../components/ui"; import { ApiError, api } from "../lib/client"; import { STATUS_LABEL } from "../lib/parse"; import type { Item, Member, Ticket } from "../lib/types"; const ROLES = [ { key: "assigned", label: "派给我的", desc: "别人等你的" }, { key: "requested", label: "我发起的", desc: "你等别人的" }, { key: "all", label: "全部", desc: "工作区所有工单" }, ] as const; const STATUS_FILTERS = [ { key: "", label: "全部状态" }, { key: "open", label: "待接收" }, { key: "accepted", label: "待办理" }, { key: "in_progress", label: "办理中" }, { key: "pending_verify", label: "待验收" }, { key: "done", label: "已完成" }, ] as const; export function TicketsPage() { const { wsId = "" } = useParams(); const [searchParams, setSearchParams] = useSearchParams(); const [role, setRole] = useState<(typeof ROLES)[number]["key"]>("assigned"); const [status, setStatus] = useState(""); const [tickets, setTickets] = useState([]); const [cursor, setCursor] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // 从通知点进来时带 ?open=,落地即展开那一条 const [openId, setOpenId] = useState(searchParams.get("open")); const reqId = useRef(0); const { push: toast } = useToast(); // 手工开单:这条路径不经过任何任务,所以人得自己选 const [members, setMembers] = useState([]); const [creating, setCreating] = useState(false); const [formTitle, setFormTitle] = useState(""); const [formBody, setFormBody] = useState(""); const [formAssignee, setFormAssignee] = useState(""); const [formPriority, setFormPriority] = useState(1); const [formDue, setFormDue] = useState(""); const [saving, setSaving] = useState(false); useEffect(() => { if (!wsId) return; let alive = true; void api .get<{ items: Member[] }>(`/workspaces/${wsId}/members?limit=100`) .then((page) => { if (alive) setMembers(page.items); }) .catch(() => { /* 成员拉不到时,表单里就没人可选,但不该拦住列表 */ }); return () => { alive = false; }; }, [wsId]); const load = useCallback( async (nextCursor: string | null) => { // 切 tab / 切状态会连发请求,慢的回来会盖掉新结果,用序号丢弃过期响应 const mine = ++reqId.current; setLoading(true); setError(null); try { const qs = new URLSearchParams({ role }); if (status) qs.set("status", status); if (nextCursor) qs.set("cursor", nextCursor); const page = await api.get<{ tickets: Ticket[]; next_cursor: string | null; has_more: boolean; }>(`/workspaces/${wsId}/tickets?${qs.toString()}`); if (mine !== reqId.current) return; setTickets((prev) => (nextCursor ? [...prev, ...page.tickets] : page.tickets)); setCursor(page.has_more ? page.next_cursor : null); } catch (err) { if (mine !== reqId.current) return; setError(err instanceof ApiError ? err.message : "工单加载失败"); } finally { if (mine === reqId.current) setLoading(false); } }, [wsId, role, status], ); useEffect(() => { void load(null); }, [load]); async function quickAction(ticket: Ticket, action: string, body: Record = {}) { try { await api.post(`/workspaces/${wsId}/items/${ticket.id}/ticket/${action}`, { version: ticket.version, ...body, }); void load(null); } catch (err) { if (err instanceof ApiError && err.code === 4009) { setError("这条刚被别人改过,已为你刷新列表"); void load(null); return; } setError(err instanceof ApiError ? err.message : "操作失败"); } } const currentRole = ROLES.find((r) => r.key === role); async function createTicket() { if (!formTitle.trim()) { setError("工单得有个标题,别人才能知道要办什么"); return; } if (!formAssignee) { setError("先选一个人——工单不指派就等于没人接"); return; } setSaving(true); setError(null); try { const created = await api.post(`/workspaces/${wsId}/tickets`, { title: formTitle.trim(), body_md: formBody.trim() || null, assignee_id: formAssignee, priority: formPriority, due_at: formDue ? new Date(formDue).toISOString() : null, }); setCreating(false); setFormTitle(""); setFormBody(""); setFormAssignee(""); setFormDue(""); setFormPriority(1); setRole("requested"); toast("工单已开出,对方会收到通知", "success"); setOpenId(created.id); } catch (err) { setError(err instanceof ApiError ? err.message : "开单失败"); } finally { setSaving(false); } } return ( 需要别人搭把手的事,都在这里} actions={ <> } />
{ROLES.map((r) => { const active = role === r.key; return ( ); })}
{error && (
{error}
)} {loading && tickets.length === 0 ? (
) : tickets.length === 0 ? (
} title={role === "assigned" ? "没有等你的工单" : "这里还空着"} hint={ role === "assigned" ? "别人把事交给你办时,这里会出现;要主动找人帮忙,去任务详情点「升级成工单」。" : currentRole?.desc } />
) : (
    {tickets.map((t) => (
  • {t.status === "open" && ( )} {t.status === "accepted" && ( )} {t.status === "pending_verify" && ( )}
  • ))}
)} {cursor && (
)} {/* 手工开单:不经过任务,直接把一件要别人帮忙的事开成工单 */} setCreating(false)} size="xl" title="新建工单" subtitle="不是从任务升级来的也可以——写清楚要办什么,指定一个人。" footer={ <> } >
setFormTitle(e.target.value)} placeholder="一句能直接看懂的话,例如:核对 9 月发票与报销单" maxLength={200} autoFocus />