oa-frontend-pretrial-cleanup 收敛PC试运行前页面风格

这个提交包含在:
2026-05-26 17:02:21 +08:00
父节点 91304bb40b
当前提交 66b3ceb08f
共修改 19 个文件,包含 193 行新增和 95 行删除
@@ -21,3 +21,7 @@ reasonable.
Frozen modules and Enterprise WeChat-owned HR/attendance entries must be labeled
or hidden according to the active Issue; do not silently re-enable them.
- Use the shared `WorkspaceNotice` pattern for trial boundaries, safety notes,
and post-trial capabilities instead of page-local warning blocks.
- Trial-facing PC lists must hide obvious TEST/SMOKE records by default when a
page-level toggle exists; the toggle may reveal them for troubleshooting.
@@ -3,7 +3,7 @@
"name": "oa-frontend-pretrial-cleanup",
"title": "OA 前端试运行前 PC 清障",
"description": "试运行前收敛 PC 高频页面风格、文案、默认视图和冻结模块边界,补齐视觉证据,不改业务状态机。",
"status": "planning",
"status": "in_progress",
"dev_type": null,
"scope": "frontend-pretrial-cleanup",
"package": null,
+5 -5
查看文件
@@ -13,16 +13,16 @@ type PageHeaderProps = {
export function PageHeader({ title, subtitle, actions, eyebrow, meta, className }: PageHeaderProps) {
return (
<header className={cn("flex flex-wrap items-start justify-between gap-4", className)}>
<header className={cn("flex flex-col gap-4 md:flex-row md:items-end md:justify-between", className)}>
<div className="min-w-0 max-w-3xl">
{eyebrow ? (
<div className="text-xs font-bold uppercase tracking-[0.2em] text-primary">{eyebrow}</div>
<div className="text-xs font-bold uppercase text-primary">{eyebrow}</div>
) : null}
<h2 className="mt-1 text-2xl font-semibold text-[var(--color-text-primary)]">{title}</h2>
{subtitle ? <p className="mt-1.5 text-sm text-[var(--color-text-secondary)]">{subtitle}</p> : null}
<h1 className="mt-1 text-2xl font-semibold text-[var(--color-text-primary)] md:text-3xl">{title}</h1>
{subtitle ? <p className="mt-2 text-sm leading-6 text-[var(--color-text-secondary)]">{subtitle}</p> : null}
{meta ? <div className="mt-3 flex flex-wrap gap-2">{meta}</div> : null}
</div>
{actions ? <div className="flex shrink-0 flex-wrap gap-2">{actions}</div> : null}
{actions ? <div className="flex shrink-0 flex-wrap items-center gap-2">{actions}</div> : null}
</header>
);
}
@@ -102,3 +102,35 @@ export function WorkspaceEmptyState({
</div>
);
}
export function WorkspaceNotice({
title,
children,
tone = "info",
actions,
className,
}: {
title: string;
children?: ReactNode;
tone?: "info" | "warning" | "neutral";
actions?: ReactNode;
className?: string;
}) {
const toneClass = {
info: "border-primary/20 bg-primary/5 text-primary",
warning: "border-amber-200 bg-amber-50 text-amber-800",
neutral: "border-slate-200 bg-slate-50 text-slate-700",
}[tone];
return (
<section className={cn("rounded-xl border px-4 py-3 text-sm", toneClass, className)}>
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div className="min-w-0">
<div className="font-semibold">{title}</div>
{children ? <div className="mt-1 leading-6 text-slate-600">{children}</div> : null}
</div>
{actions ? <div className="flex shrink-0 flex-wrap items-center gap-2">{actions}</div> : null}
</div>
</section>
);
}
+11 -27
查看文件
@@ -55,26 +55,15 @@
.workbench-hero {
position: relative;
overflow: hidden;
background:
radial-gradient(circle at 12% 4%, rgba(0, 57, 138, 0.14), transparent 17rem),
radial-gradient(circle at 86% 12%, rgba(19, 168, 184, 0.15), transparent 19rem),
linear-gradient(135deg, rgba(255, 255, 255, 0.98), rgba(230, 236, 244, 0.94));
background: rgba(255, 255, 255, 0.96);
border: 1px solid rgba(226, 232, 240, 0.92);
border-radius: var(--radius-2xl);
border-radius: var(--radius-panel);
box-shadow: var(--shadow-sm);
@apply flex flex-col gap-5 px-6 py-7 md:flex-row md:items-end md:justify-between md:px-7;
@apply flex flex-col gap-4 px-5 py-5 md:flex-row md:items-end md:justify-between md:px-6;
}
.workbench-hero::after {
position: absolute;
inset: auto -5rem -7rem auto;
width: 18rem;
height: 18rem;
content: "";
background:
repeating-radial-gradient(ellipse at center, rgba(0, 57, 138, 0.13) 0 1px, transparent 1px 16px);
border-radius: 9999px;
transform: rotate(-8deg);
display: none;
}
.workbench-hero > * {
@@ -84,17 +73,17 @@
.workbench-hero__eyebrow {
color: var(--color-accent-primary);
@apply text-xs font-bold uppercase tracking-[0.22em];
@apply text-xs font-bold uppercase;
}
.workbench-hero__title {
color: var(--color-text-primary);
@apply mt-2 text-3xl font-semibold tracking-tight md:text-4xl;
@apply mt-1 text-2xl font-semibold md:text-3xl;
}
.workbench-hero__subtitle {
color: var(--color-text-secondary);
@apply mt-3 max-w-3xl text-sm leading-6 md:text-base md:leading-7;
@apply mt-2 max-w-3xl text-sm leading-6;
}
.workbench-hero--compact {
@@ -135,16 +124,11 @@
.section-panel {
position: relative;
overflow: hidden;
@apply p-5 md:p-6;
@apply p-4 md:p-5;
}
.section-panel::before {
position: absolute;
inset: 0 0 auto 0;
height: 3px;
content: "";
background: linear-gradient(90deg, var(--color-accent-primary), rgba(19, 168, 184, 0.72), transparent);
opacity: 0.7;
display: none;
}
.section-panel__header {
@@ -155,7 +139,7 @@
.section-panel__title {
color: var(--color-text-primary);
@apply text-base font-semibold tracking-tight;
@apply text-base font-semibold;
}
.section-panel__desc {
@@ -166,7 +150,7 @@
.section-panel__body {
position: relative;
z-index: 1;
@apply mt-5;
@apply mt-4;
}
.metric-card {
+5
查看文件
@@ -0,0 +1,5 @@
const TRIAL_NOISE_PATTERN = /(?:^|[^a-z0-9])(?:test|smoke)(?:$|[^a-z0-9]|\d)|测试|冒烟/i;
export function hasTrialNoise(...values: Array<string | null | undefined>) {
return values.some((value) => TRIAL_NOISE_PATTERN.test(value ?? ""));
}
+6 -6
查看文件
@@ -272,7 +272,7 @@ export function ApOperationsPage() {
const canReviewRisk = hasPermission(permissions, "risk.review.manage");
const canManageTravelPayments = hasPermission(permissions, "travel.payment.manage");
const [activeTab, setActiveTab] = useState<OperationsTab>("ledger");
const [activeTab, setActiveTab] = useState<OperationsTab>("pending");
const [pendingEntityType, setPendingEntityType] = useState<PendingEntityFilter>("all");
const [pendingCreatedAtFrom, setPendingCreatedAtFrom] = useState("");
const [pendingCreatedAtTo, setPendingCreatedAtTo] = useState("");
@@ -618,7 +618,7 @@ export function ApOperationsPage() {
<ListWorkspace>
<WorkspaceHeader
title="财务付款与对账"
subtitle="集中处理待付款批次、异常付款、风险标记和对账异常。"
subtitle="待付款、异常付款、风险和对账。"
actions={
<>
<Button
@@ -662,29 +662,29 @@ export function ApOperationsPage() {
) : null}
<div className="grid gap-4 md:grid-cols-5">
<SummaryCard title="台账记录" value={financeLedgerQuery.data?.total ?? "-"} />
<SummaryCard title="待付款" value={summaryQuery.data?.summary.pendingPaymentCount ?? "-"} />
<SummaryCard title="异常" value={summaryQuery.data?.summary.exceptionCount ?? "-"} tone="red" />
<SummaryCard
title="部分匹配"
value={summaryQuery.data?.summary.partialMatchCount ?? "-"}
tone="amber"
/>
<SummaryCard title="异常" value={summaryQuery.data?.summary.exceptionCount ?? "-"} tone="red" />
<SummaryCard title="台账记录" value={financeLedgerQuery.data?.total ?? "-"} />
<SummaryCard
title="预算超支"
value={summaryQuery.data?.summary.budgetOverspendCount ?? "-"}
tone="emerald"
tone="amber"
/>
</div>
<div className="card-std p-4 md:p-5">
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as OperationsTab)}>
<TabsList className="flex w-full flex-wrap justify-start gap-1">
<TabsTrigger value="ledger">财务台账 {financeLedgerQuery.data?.total ?? 0}</TabsTrigger>
<TabsTrigger value="pending">待付款 {pendingBatches.length}</TabsTrigger>
<TabsTrigger value="failed">异常付款 {failedItems.length}</TabsTrigger>
<TabsTrigger value="risk">风险标记 {filteredRiskFlags.length}</TabsTrigger>
<TabsTrigger value="reconciliation">对账异常 {partialMatchItems.length}</TabsTrigger>
<TabsTrigger value="ledger">财务台账 {financeLedgerQuery.data?.total ?? 0}</TabsTrigger>
</TabsList>
<TabsContent value="ledger" className="mt-6 space-y-4">
+11 -6
查看文件
@@ -34,7 +34,6 @@ import { DateTextInput } from "../components/common/DateTextInput";
import { StatusBadge } from "../components/common/StatusBadge";
import { TableSkeleton } from "../components/common/TableSkeleton";
import { FilterBar, ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import { SectionPanel } from "../components/patterns/SectionPanel";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
@@ -1415,10 +1414,15 @@ export function AssetRegistryPage() {
) : null}
{selectedAssetId && !isCreatingAsset && canIntegrationManage ? (
<SectionPanel
title="ERP 同步状态"
description="仅同步资产法体、成本中心与退役/归档等财务台账摘要,不接管实物流转。"
>
<details className="card-std p-4 md:p-5">
<summary className="flex cursor-pointer list-none items-start justify-between gap-3">
<div>
<div className="text-base font-semibold text-zinc-900">ERP 同步状态</div>
<div className="mt-1 text-sm text-zinc-500">高级轨道,默认收起。</div>
</div>
<span className="text-sm font-medium text-primary">展开</span>
</summary>
<div className="mt-4">
{assetErpTrackQuery.isLoading ? (
<p className="rounded-lg bg-zinc-50 px-3 py-2 text-sm text-zinc-500">ERP 同步轨道加载中...</p>
) : assetErpTrackQuery.isError ? (
@@ -1476,7 +1480,8 @@ export function AssetRegistryPage() {
) : (
<p className="rounded-lg bg-zinc-50 px-3 py-2 text-sm text-zinc-500">暂无 ERP 轨道信息</p>
)}
</SectionPanel>
</div>
</details>
) : null}
{selectedAssetId && !isCreatingAsset && currentAsset ? (
+7
查看文件
@@ -21,6 +21,7 @@ import { PageHeader } from "../components/common/PageHeader";
import { TableSkeleton } from "../components/common/TableSkeleton";
import { MetricCard } from "../components/patterns/MetricCard";
import { SectionPanel } from "../components/patterns/SectionPanel";
import { WorkspaceNotice } from "../components/patterns/ListWorkspace";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
import { Label } from "../components/ui/label";
@@ -247,6 +248,9 @@ export function AttendancePage() {
return (
<PageScaffoldLike>
<PageHeader title="考勤" subtitle="日常打卡在企业微信,审批和解释在 OA。" />
<WorkspaceNotice title="企业微信主办" tone="info">
日常打卡、请假和假期余额由企业微信承载;OA 只保留汇总、解释和审计入口。
</WorkspaceNotice>
<SectionPanel title="企微考勤统一汇总未启用">
<div className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
当前 `attendance_unified_v1` 未开启。历史 OA 自建上下班打卡已关闭;正式启用前请由管理员打开统一考勤功能开关。
@@ -259,6 +263,9 @@ export function AttendancePage() {
return (
<PageScaffoldLike>
<PageHeader title="考勤" subtitle="企业微信采集打卡;OA 统一处理请假、补卡、外出、加班和月度解释。" />
<WorkspaceNotice title="企业微信主办" tone="info">
日常打卡仍在企业微信完成;OA 不写回企业微信,只展示汇总、修正审批和同步审计。
</WorkspaceNotice>
<div className="grid gap-4 md:grid-cols-4">
<MetricCard title="人员" value={monthly?.summary.employeeCount ?? 0} />
+5 -1
查看文件
@@ -18,6 +18,7 @@ import { isApiError } from "../api/types";
import { useAuth } from "../auth/AuthContext";
import { hasPermission } from "../auth/permissions";
import { PageHeader } from "../components/common/PageHeader";
import { WorkspaceNotice } from "../components/patterns/ListWorkspace";
import { Button } from "../components/ui/button";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../components/ui/tabs";
@@ -377,8 +378,11 @@ export function BudgetsPage() {
<div className="space-y-6">
<PageHeader
title="预算管理"
subtitle="支持预算池维护,并在同一页面下维护预算控制规则中心。"
subtitle="预算池和控制规则维护。"
/>
<WorkspaceNotice title="后置能力" tone="warning">
试运行主路径不启用预算硬控;本页仅供管理员维护规则和查看配置。
</WorkspaceNotice>
{error ? (
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
+45 -21
查看文件
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useMemo, useState } from "react";
import { Link, useSearchParams } from "react-router-dom";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
@@ -24,12 +24,14 @@ import { hasAnyPermission } from "../auth/permissions";
import { PageHeader } from "../components/common/PageHeader";
import { StatusBadge } from "../components/common/StatusBadge";
import { DateTextInput } from "../components/common/DateTextInput";
import { WorkspaceNotice } from "../components/patterns/ListWorkspace";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
import { Label } from "../components/ui/label";
import { Textarea } from "../components/ui/textarea";
import { downloadBlob } from "../lib/download";
import { formatDisplayDate } from "../lib/displayFormat";
import { hasTrialNoise } from "../lib/trialNoise";
import { toast } from "sonner";
const CONFIDENTIALITY_OPTIONS: ConfidentialityLevel[] = ["public", "internal", "confidential", "restricted"];
@@ -200,6 +202,7 @@ export function ContractsRegistryPage() {
const [editorId, setEditorId] = useState<string | null>(null);
const [editor, setEditor] = useState<EditorState>(() => buildEmptyEditor(initialProjectId));
const [showExpiring, setShowExpiring] = useState(false);
const [showTrialData, setShowTrialData] = useState(false);
const filterPayload = buildFiltersPayload(filters);
const contractsQuery = useQuery({
@@ -223,6 +226,16 @@ export function ContractsRegistryPage() {
const projectOptions = projectsQuery.data?.items ?? [];
const employeeOptions = canBrowseEmployees ? employeesQuery.data?.items ?? [] : [];
const contracts = contractsQuery.data?.items ?? [];
const visibleContracts = useMemo(
() =>
showTrialData
? contracts
: contracts.filter(
(item) => !hasTrialNoise(item.contractNo, item.counterpartyName, item.summary, item.project?.name),
),
[contracts, showTrialData],
);
const hiddenTrialContractCount = contracts.length - visibleContracts.length;
const expiringItems = expiringQuery.data?.items ?? [];
const editingItem = editorId ? contracts.find((item) => item.id === editorId) ?? null : null;
const readonlyOwnerLabel = editingItem?.owner
@@ -633,24 +646,29 @@ export function ContractsRegistryPage() {
<h3 className="text-base font-semibold text-zinc-900">台账列表</h3>
<div className="mt-1 text-sm text-zinc-500">{filters.projectId ? "已按项目过滤" : "显示全部可见合同"}</div>
</div>
<Button
type="button"
variant="secondary"
onClick={() => {
setFilters({
projectId: initialProjectId,
status: "",
contractType: "",
counterpartyName: "",
expiryDateFrom: "",
expiryDateTo: "",
});
setEditorId(null);
setEditor(buildEmptyEditor(initialProjectId));
}}
>
重置筛选
</Button>
<div className="flex flex-wrap gap-2">
<Button
type="button"
variant="secondary"
onClick={() => {
setFilters({
projectId: initialProjectId,
status: "",
contractType: "",
counterpartyName: "",
expiryDateFrom: "",
expiryDateTo: "",
});
setEditorId(null);
setEditor(buildEmptyEditor(initialProjectId));
}}
>
重置筛选
</Button>
<Button type="button" variant={showTrialData ? "default" : "secondary"} onClick={() => setShowTrialData((value) => !value)}>
{showTrialData ? "隐藏测试数据" : "显示测试数据"}
</Button>
</div>
</div>
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
@@ -733,8 +751,14 @@ export function ContractsRegistryPage() {
<div className="text-sm text-red-600">{resolveError(contractsQuery.error, "加载合同台账失败")}</div>
) : null}
{!showTrialData && hiddenTrialContractCount > 0 ? (
<WorkspaceNotice title={`已隐藏 ${hiddenTrialContractCount} 条 TEST/SMOKE 合同`} tone="neutral">
试运行默认只展示正式合同;需要核对测试样本时可临时显示。
</WorkspaceNotice>
) : null}
<div className="space-y-3">
{contracts.map((item) => (
{visibleContracts.map((item) => (
<div key={item.id} className="rounded-lg border border-zinc-200 p-4">
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
<div className="space-y-2">
@@ -802,7 +826,7 @@ export function ContractsRegistryPage() {
</div>
))}
{!contractsQuery.isLoading && !contracts.length ? (
{!contractsQuery.isLoading && !visibleContracts.length ? (
<div className="text-sm text-zinc-500">暂无符合筛选条件的合同台账。</div>
) : null}
</div>
+4
查看文件
@@ -12,6 +12,7 @@ import {
import { PageHeader } from "../components/common/PageHeader";
import { TableSkeleton } from "../components/common/TableSkeleton";
import { SectionPanel } from "../components/patterns/SectionPanel";
import { WorkspaceNotice } from "../components/patterns/ListWorkspace";
import { Button } from "../components/ui/button";
import {
Dialog,
@@ -320,6 +321,9 @@ export function CutoverConsolePage() {
return (
<div className="space-y-6">
<PageHeader title="切换控制台" subtitle="范围开关和导入演练。" />
<WorkspaceNotice title="仅管理入口" tone="warning">
切换范围会影响试运行边界,默认不作为日常业务入口;正式变更需另行确认。
</WorkspaceNotice>
<div className="grid gap-4 md:grid-cols-3">
<div className="card-std p-5">
+5 -1
查看文件
@@ -21,6 +21,7 @@ import { isApiError } from "../api/types";
import { PageHeader } from "../components/common/PageHeader";
import { Pagination } from "../components/common/Pagination";
import { TableSkeleton } from "../components/common/TableSkeleton";
import { WorkspaceNotice } from "../components/patterns/ListWorkspace";
import { Button } from "../components/ui/button";
import {
Dialog,
@@ -262,7 +263,7 @@ export function ErpIntegrationPage() {
<div className="space-y-6">
<PageHeader
title="ERP 集成"
subtitle="主数据同步、业务同步队列和 ID 映射。"
subtitle="主数据同步、业务队列和 ID 映射。"
actions={
tab === "master-data" ? (
<Button onClick={() => syncMutation.mutate(undefined)} disabled={syncMutation.isPending}>
@@ -271,6 +272,9 @@ export function ErpIntegrationPage() {
) : null
}
/>
<WorkspaceNotice title="ERP 写入后置" tone="warning">
试运行期间不把 ERP 写入作为主路径;本页用于管理员查看同步状态和队列排障。
</WorkspaceNotice>
{error ? (
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">{error}</p>
+6 -2
查看文件
@@ -21,6 +21,7 @@ import { PageHeader } from "../components/common/PageHeader";
import { Pagination } from "../components/common/Pagination";
import { TableSkeleton } from "../components/common/TableSkeleton";
import { SectionPanel } from "../components/patterns/SectionPanel";
import { WorkspaceNotice } from "../components/patterns/ListWorkspace";
import { Button } from "../components/ui/button";
import {
Dialog,
@@ -318,8 +319,11 @@ export function FeatureFlagsPage() {
<div className={batchMode ? "space-y-6 pb-32" : "space-y-6"}>
<PageHeader
title="功能开关"
subtitle="统一治理外部桥接、验真与请假日历灰度。Env 仍是硬闸,在线开关用于部门级灰度。"
subtitle="外部桥接、验真和灰度开关。"
/>
<WorkspaceNotice title="安全摘要" tone="warning">
Env 仍是硬闸;批量开启、关闭或灰度变更会影响试运行边界,执行前必须保留变更说明。
</WorkspaceNotice>
{error ? (
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">{error}</p>
@@ -327,7 +331,7 @@ export function FeatureFlagsPage() {
<SectionPanel
title="开关列表"
description="单条编辑保留原能力;批量模式只统一切换 overrideMode,并要求共享变更说明。"
description="单条编辑或批量切换,必须填写变更说明。"
actions={
<Button
type="button"
+11 -15
查看文件
@@ -639,8 +639,7 @@ function QuickActionsPanel({
}) {
return (
<SectionPanel
title="快捷操作栏"
description="高频动作"
title="常用"
className={layout === "rail" ? "portal-panel-fill" : undefined}
>
<div className={layout === "rail" ? "portal-action-stack" : "grid gap-3 sm:grid-cols-2 xl:grid-cols-4"}>
@@ -667,8 +666,7 @@ function TodoAggregatePanel({ items }: { items: TodoAggregateItem[] }) {
return (
<SectionPanel
title="今日待办聚合"
description="今天要处理的事"
title="待处理"
className="portal-panel-fill"
>
<div className="space-y-3">
@@ -713,8 +711,7 @@ function TeamStatusBoardPanel({ items }: { items: PortalTeamStatusItem[] }) {
return (
<SectionPanel
title="团队全员状态"
description="今日状态"
title="团队状态"
actions={<Link to="/team" className="text-sm font-medium text-primary hover:underline">查看团队页</Link>}
className="portal-panel-fill"
>
@@ -772,8 +769,7 @@ function ActivityAnnouncementsPanel({
}) {
return (
<SectionPanel
title="近期动态流"
description="动态和公告"
title="通知"
actions={<Link to="/announcements" className="text-sm font-medium text-primary hover:underline">公告页</Link>}
className="portal-panel-fill"
>
@@ -1156,8 +1152,8 @@ export function PortalHomePage() {
{isManagerView ? (
<>
<SecondaryWorkspacePanel
title="角色重点 / 常用入口"
description="按需展开。"
title="角色重点"
description="常用入口"
>
<div className="grid gap-6 xl:grid-cols-[minmax(0,1fr),minmax(0,1fr)] 2xl:grid-cols-[minmax(0,1fr),minmax(0,1fr),360px]">
{roleCapabilities.canApproveTasks ? (
@@ -1185,8 +1181,8 @@ export function PortalHomePage() {
</div>
<SecondaryCardsPanel
title="项目跟进与组织协同"
description="项目、组织和风险。"
title="项目与组织"
description="项目、组织、风险"
cards={secondaryCards}
/>
</SecondaryWorkspacePanel>
@@ -1196,7 +1192,7 @@ export function PortalHomePage() {
) : (
<SecondaryWorkspacePanel
title="我的申请与资产 / 继续工作"
description="按需展开。"
description="申请、资产、最近访问"
>
<div className="grid gap-6 xl:grid-cols-[minmax(0,1fr),minmax(0,1fr)]">
<SummaryPanel title="我的申请与资产" items={employeeSummaryItems} />
@@ -1206,8 +1202,8 @@ export function PortalHomePage() {
<div className="grid gap-6 xl:grid-cols-[minmax(0,1fr),minmax(0,1fr)]">
<QuickLinksPanel items={quickLinkItems} />
<SecondaryCardsPanel
title="项目跟进与个人协同"
description="项目和参考指标。"
title="项目与协同"
description="项目、参考指标"
cards={secondaryCards}
/>
</div>
+28 -7
查看文件
@@ -14,7 +14,7 @@ import { useAuth } from "../auth/AuthContext";
import { hasAnyPermission } from "../auth/permissions";
import { DateTextInput } from "../components/common/DateTextInput";
import { StatusBadge } from "../components/common/StatusBadge";
import { FilterBar, ListWorkspace, StatStrip, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import { FilterBar, ListWorkspace, StatStrip, WorkspaceHeader, WorkspaceNotice } from "../components/patterns/ListWorkspace";
import { MetricCard } from "../components/patterns/MetricCard";
import { SectionPanel } from "../components/patterns/SectionPanel";
import { Button } from "../components/ui/button";
@@ -22,6 +22,7 @@ import { Input } from "../components/ui/input";
import { Label } from "../components/ui/label";
import { Textarea } from "../components/ui/textarea";
import { formatDisplayDate } from "../lib/displayFormat";
import { hasTrialNoise } from "../lib/trialNoise";
import { toast } from "sonner";
const STATUS_OPTIONS: Array<{ value: "all" | ProjectStatus; label: string }> = [
@@ -76,6 +77,7 @@ export function ProjectsPage() {
const [startsAt, setStartsAt] = useState("");
const [targetEndAt, setTargetEndAt] = useState("");
const [isConfidential, setIsConfidential] = useState(false);
const [showTrialData, setShowTrialData] = useState(false);
const listQuery = useQuery({
queryKey: ["projects", "list", status, mine, q],
@@ -99,8 +101,18 @@ export function ProjectsPage() {
);
const projectItems = listQuery.data?.items ?? [];
const visibleProjectItems = useMemo(
() =>
showTrialData
? projectItems
: projectItems.filter(
(item) => !hasTrialNoise(item.projectNo, item.name, item.clientName, item.clientAlias, item.summary),
),
[projectItems, showTrialData],
);
const hiddenTrialProjectCount = projectItems.length - visibleProjectItems.length;
const projectStats = useMemo(() => {
return projectItems.reduce(
return visibleProjectItems.reduce(
(acc, item) => {
acc.total += 1;
if (item.status === "active") {
@@ -112,7 +124,7 @@ export function ProjectsPage() {
},
{ total: 0, active: 0, overdueMilestones: 0, openRisks: 0 },
);
}, [projectItems]);
}, [visibleProjectItems]);
const createMutation = useMutation({
mutationFn: () =>
@@ -183,7 +195,7 @@ export function ProjectsPage() {
<div className="page-shell-grid">
<div className="space-y-6">
<FilterBar className="grid gap-3 md:grid-cols-[minmax(0,1fr),180px,160px]">
<FilterBar className="grid gap-3 md:grid-cols-[minmax(0,1fr),180px,160px,160px]">
<Input placeholder="搜索项目编号、项目名、客户名" value={q} onChange={(event) => updateSearch({ q: event.target.value })} />
<select className="select-std" value={status} onChange={(event) => updateSearch({ status: event.target.value })}>
{STATUS_OPTIONS.map((item) => (
@@ -195,18 +207,27 @@ export function ProjectsPage() {
<Button type="button" variant={mine ? "default" : "secondary"} onClick={() => updateSearch({ mine: !mine })}>
{mine ? "只看我的" : "切换到我的"}
</Button>
<Button type="button" variant={showTrialData ? "default" : "secondary"} onClick={() => setShowTrialData((value) => !value)}>
{showTrialData ? "隐藏测试数据" : "显示测试数据"}
</Button>
</FilterBar>
{!showTrialData && hiddenTrialProjectCount > 0 ? (
<WorkspaceNotice title={`已隐藏 ${hiddenTrialProjectCount} 条 TEST/SMOKE 项目`} tone="neutral">
试运行默认只展示正式项目;需要排障时可临时显示测试数据。
</WorkspaceNotice>
) : null}
<SectionPanel
title="项目列表"
description={projectItems.length ? `当前筛出 ${projectItems.length} 个项目` : "按项目名、客户或状态快速缩小范围"}
description={visibleProjectItems.length ? `当前显示 ${visibleProjectItems.length} 个项目` : "按项目名、客户或状态快速缩小范围"}
>
<div className="space-y-4">
{listQuery.isLoading ? <div className="text-sm text-zinc-500">项目加载中...</div> : null}
{listQuery.isError ? <div className="text-sm text-red-600">{resolveError(listQuery.error, "加载项目失败")}</div> : null}
<div className="grid gap-4">
{projectItems.map((item) => (
{visibleProjectItems.map((item) => (
<Link key={item.id} to={`/projects/${item.id}`} className="card-std block space-y-3 p-5 hover:border-primary/40">
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div>
@@ -228,7 +249,7 @@ export function ProjectsPage() {
<div className="text-sm text-zinc-500">{summaryLine(item)}</div>
</Link>
))}
{!listQuery.isLoading && !projectItems.length ? (
{!listQuery.isLoading && !visibleProjectItems.length ? (
<div className="rounded-xl border border-dashed border-zinc-200 bg-zinc-50 px-4 py-8 text-sm text-zinc-500">
当前没有符合筛选条件的项目。
</div>
+1 -1
查看文件
@@ -363,7 +363,7 @@ export function ReimbursementsPage() {
<ListWorkspace>
<WorkspaceHeader
title="我的报销"
subtitle="草稿、审批、支付。"
subtitle="草稿、审批和付款状态。"
actions={
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
{showProxyOwnerSelector ? (
+5 -1
查看文件
@@ -28,6 +28,7 @@ import { CardSkeleton } from "../components/common/CardSkeleton";
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
import { EmptyStateCard } from "../components/common/EmptyStateCard";
import { PageHeader } from "../components/common/PageHeader";
import { WorkspaceNotice } from "../components/patterns/ListWorkspace";
import { RiskFlagsCard } from "../components/common/RiskFlagsCard";
import { Pagination } from "../components/common/Pagination";
import { StatusBadge } from "../components/common/StatusBadge";
@@ -517,13 +518,16 @@ export function TravelRequestsPage() {
<div className="space-y-6">
<PageHeader
title="差旅管理"
subtitle="差旅申请、对公支付轨道与批次对账在同一路径统一收口。"
subtitle="差旅申请、支付轨道和批次对账。"
actions={
activeTab === "requests" && canCreate ? (
<Button type="button" onClick={() => selectTravelRequest("")}>新建差旅申请</Button>
) : undefined
}
/>
<WorkspaceNotice title="后置能力" tone="warning">
差旅不是本次 7 天试运行主路径;如需使用,仅按已有权限查看或维护,不扩展新流程。
</WorkspaceNotice>
<div className="flex flex-wrap gap-2">
{canAccessRequestsTab ? (
+1 -1
查看文件
@@ -210,7 +210,7 @@ export function WorkflowTasksPage() {
<WorkspaceHeader
eyebrow="审批"
title="审批中心"
subtitle="集中处理待办、查看已办和业务审批历史。"
subtitle="待处理、已处理和业务类型。"
/>
<div data-testid="workflow-task-summary">