完成五人团队正式内用最小收口(07-18-small-team-internal-readiness)

这个提交包含在:
2026-07-18 15:46:14 +08:00
父节点 58c8decef9
当前提交 5d0a5f9a38
共修改 134 个文件,包含 10790 行新增和 1413 行删除
+82 -47
查看文件
@@ -22,6 +22,7 @@ import { isApiError } from "../api/types";
import { useAuth } from "../auth/AuthContext";
import { hasAnyPermission } from "../auth/permissions";
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
import { useUnsavedChangesGuard } from "../components/common/UnsavedChangesGuard";
import { PageHeader } from "../components/common/PageHeader";
import { StatusBadge } from "../components/common/StatusBadge";
import { DateTextInput } from "../components/common/DateTextInput";
@@ -203,6 +204,7 @@ export function ContractsRegistryPage() {
const [searchParams] = useSearchParams();
const initialProjectId = searchParams.get("projectId") ?? "";
const canBrowseEmployees = hasAnyPermission(permissions, ["system.users", "report.dashboard.view"]);
const canManageContracts = hasAnyPermission(permissions, ["project.manage", "project.view_all", "system.settings"]);
const [filters, setFilters] = useState<FiltersState>({
projectId: initialProjectId,
@@ -214,6 +216,7 @@ export function ContractsRegistryPage() {
});
const [editorId, setEditorId] = useState<string | null>(null);
const [editor, setEditor] = useState<EditorState>(() => buildEmptyEditor(initialProjectId));
const [savedEditorSignature, setSavedEditorSignature] = useState(() => JSON.stringify(buildEmptyEditor(initialProjectId)));
const [showExpiring, setShowExpiring] = useState(false);
const [showTrialData, setShowTrialData] = useState(false);
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false);
@@ -262,6 +265,10 @@ export function ContractsRegistryPage() {
: "默认当前用户";
const amountInvalid = Boolean(editor.amount.trim()) && !AMOUNT_PATTERN.test(editor.amount.trim());
const expiringContractCount = expiringQuery.data?.total ?? expiringItems.length;
const runGuarded = useUnsavedChangesGuard(
showEditor && JSON.stringify(editor) !== savedEditorSignature,
"合同表单",
);
const refreshAll = async () => {
await Promise.all([
@@ -271,14 +278,20 @@ export function ContractsRegistryPage() {
};
const resetEditor = () => {
const nextEditor = buildEmptyEditor(filters.projectId);
setEditorId(null);
setEditor(buildEmptyEditor(filters.projectId));
setEditor(nextEditor);
setSavedEditorSignature(JSON.stringify(nextEditor));
};
const openEditor = (item: ContractRegistryItem) => {
setEditorId(item.id);
setEditor(loadEditorFromItem(item));
setShowEditor(true);
runGuarded(() => {
const nextEditor = loadEditorFromItem(item);
setEditorId(item.id);
setEditor(nextEditor);
setSavedEditorSignature(JSON.stringify(nextEditor));
setShowEditor(true);
});
};
const saveMutation = useMutation({
@@ -335,15 +348,19 @@ export function ContractsRegistryPage() {
title="合同"
actions={
<HeaderActionGroup>
<Button
type="button"
onClick={() => {
resetEditor();
setShowEditor(true);
}}
>
新增合同
</Button>
{canManageContracts ? (
<Button
type="button"
onClick={() => {
runGuarded(() => {
resetEditor();
setShowEditor(true);
});
}}
>
新增合同
</Button>
) : null}
</HeaderActionGroup>
}
/>
@@ -359,9 +376,11 @@ export function ContractsRegistryPage() {
<div className="truncate font-medium text-zinc-900">{item.contractNo} · {item.counterpartyName}</div>
<div className="text-xs text-zinc-500">到期日 {formatDisplayDate(item.expiryDate)} · 责任人 {item.owner?.name ?? "-"}</div>
</div>
<Button type="button" size="sm" variant="secondary" onClick={() => openEditor(item)}>
编辑
</Button>
{canManageContracts ? (
<Button type="button" size="sm" variant="secondary" onClick={() => openEditor(item)}>
编辑
</Button>
) : null}
</div>
))}
{!expiringQuery.isLoading && !expiringItems.length ? (
@@ -382,7 +401,7 @@ export function ContractsRegistryPage() {
</div>
<div className="flex flex-wrap gap-2">
{editorId ? (
<Button type="button" variant="secondary" size="sm" onClick={resetEditor}>
<Button type="button" variant="secondary" size="sm" onClick={() => runGuarded(resetEditor)}>
切换到新建
</Button>
) : null}
@@ -391,8 +410,10 @@ export function ContractsRegistryPage() {
variant="secondary"
size="sm"
onClick={() => {
setShowEditor(false);
resetEditor();
runGuarded(() => {
setShowEditor(false);
resetEditor();
});
}}
>
关闭
@@ -705,17 +726,21 @@ export function ContractsRegistryPage() {
type="button"
variant="secondary"
onClick={() => {
setFilters({
projectId: initialProjectId,
status: "",
contractType: "",
counterpartyName: "",
expiryDateFrom: "",
expiryDateTo: "",
runGuarded(() => {
const nextEditor = buildEmptyEditor(initialProjectId);
setFilters({
projectId: initialProjectId,
status: "",
contractType: "",
counterpartyName: "",
expiryDateFrom: "",
expiryDateTo: "",
});
setShowEditor(false);
setEditorId(null);
setEditor(nextEditor);
setSavedEditorSignature(JSON.stringify(nextEditor));
});
setShowEditor(false);
setEditorId(null);
setEditor(buildEmptyEditor(initialProjectId));
}}
>
重置筛选
@@ -810,9 +835,11 @@ export function ContractsRegistryPage() {
{item.amount ? ` · ${item.amount} ${item.currency}` : ""}
{item.expiryDate ? ` · ${formatDisplayDate(item.expiryDate)}` : ""}
</div>
<Button className="mt-3" type="button" size="sm" variant="secondary" onClick={() => openEditor(item)}>
编辑
</Button>
{canManageContracts ? (
<Button className="mt-3" type="button" size="sm" variant="secondary" onClick={() => openEditor(item)}>
编辑
</Button>
) : null}
</article>
))}
{!contractsQuery.isLoading && !visibleContracts.length ? (
@@ -860,9 +887,12 @@ export function ContractsRegistryPage() {
</td>
<td className="table-td">
<div className="flex flex-wrap gap-2">
<Button type="button" size="sm" variant="secondary" onClick={() => openEditor(item)}>
编辑
</Button>
{canManageContracts ? (
<Button type="button" size="sm" variant="secondary" onClick={() => openEditor(item)}>
编辑
</Button>
) : null}
{canManageContracts || item.project ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="button" size="sm" variant="secondary">
@@ -870,25 +900,30 @@ export function ContractsRegistryPage() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
disabled={renewMutation.isPending || !["active", "expired"].includes(item.status)}
onSelect={() => renewMutation.mutate(item)}
>
续约
</DropdownMenuItem>
{canManageContracts ? (
<DropdownMenuItem
disabled={renewMutation.isPending || !["active", "expired"].includes(item.status)}
onSelect={() => renewMutation.mutate(item)}
>
续约
</DropdownMenuItem>
) : null}
{item.project ? (
<DropdownMenuItem asChild>
<Link to={`/projects/${item.project.id}`}>查看项目</Link>
</DropdownMenuItem>
) : null}
<DropdownMenuItem
disabled={toggleStatusMutation.isPending}
onSelect={() => setConfirmStatusTarget(item)}
>
{item.persistedStatus === "archived" ? "恢复生效" : "归档"}
</DropdownMenuItem>
{canManageContracts ? (
<DropdownMenuItem
disabled={toggleStatusMutation.isPending}
onSelect={() => setConfirmStatusTarget(item)}
>
{item.persistedStatus === "archived" ? "恢复生效" : "归档"}
</DropdownMenuItem>
) : null}
</DropdownMenuContent>
</DropdownMenu>
) : null}
</div>
</td>
</tr>