diff --git a/.trellis/spec/hyb-oa/frontend-react-vite-design.md b/.trellis/spec/hyb-oa/frontend-react-vite-design.md index 58551b58..dd526638 100644 --- a/.trellis/spec/hyb-oa/frontend-react-vite-design.md +++ b/.trellis/spec/hyb-oa/frontend-react-vite-design.md @@ -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. diff --git a/.trellis/tasks/05-26-oa-frontend-pretrial-cleanup/task.json b/.trellis/tasks/05-26-oa-frontend-pretrial-cleanup/task.json index 90d6a5c4..47ea693e 100644 --- a/.trellis/tasks/05-26-oa-frontend-pretrial-cleanup/task.json +++ b/.trellis/tasks/05-26-oa-frontend-pretrial-cleanup/task.json @@ -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, diff --git a/client/src/components/common/PageHeader.tsx b/client/src/components/common/PageHeader.tsx index f9e388cb..628e2e1f 100644 --- a/client/src/components/common/PageHeader.tsx +++ b/client/src/components/common/PageHeader.tsx @@ -13,16 +13,16 @@ type PageHeaderProps = { export function PageHeader({ title, subtitle, actions, eyebrow, meta, className }: PageHeaderProps) { return ( -
+
{eyebrow ? ( -
{eyebrow}
+
{eyebrow}
) : null} -

{title}

- {subtitle ?

{subtitle}

: null} +

{title}

+ {subtitle ?

{subtitle}

: null} {meta ?
{meta}
: null}
- {actions ?
{actions}
: null} + {actions ?
{actions}
: null}
); } diff --git a/client/src/components/patterns/ListWorkspace.tsx b/client/src/components/patterns/ListWorkspace.tsx index 45b32937..4258be8f 100644 --- a/client/src/components/patterns/ListWorkspace.tsx +++ b/client/src/components/patterns/ListWorkspace.tsx @@ -102,3 +102,35 @@ export function WorkspaceEmptyState({ ); } + +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 ( +
+
+
+
{title}
+ {children ?
{children}
: null} +
+ {actions ?
{actions}
: null} +
+
+ ); +} diff --git a/client/src/index.css b/client/src/index.css index 5e9d64e1..e43ab518 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -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 { diff --git a/client/src/lib/trialNoise.ts b/client/src/lib/trialNoise.ts new file mode 100644 index 00000000..d3682ab1 --- /dev/null +++ b/client/src/lib/trialNoise.ts @@ -0,0 +1,5 @@ +const TRIAL_NOISE_PATTERN = /(?:^|[^a-z0-9])(?:test|smoke)(?:$|[^a-z0-9]|\d)|测试|冒烟/i; + +export function hasTrialNoise(...values: Array) { + return values.some((value) => TRIAL_NOISE_PATTERN.test(value ?? "")); +} diff --git a/client/src/pages/ApOperationsPage.tsx b/client/src/pages/ApOperationsPage.tsx index 10c14dea..b972779c 100644 --- a/client/src/pages/ApOperationsPage.tsx +++ b/client/src/pages/ApOperationsPage.tsx @@ -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("ledger"); + const [activeTab, setActiveTab] = useState("pending"); const [pendingEntityType, setPendingEntityType] = useState("all"); const [pendingCreatedAtFrom, setPendingCreatedAtFrom] = useState(""); const [pendingCreatedAtTo, setPendingCreatedAtTo] = useState(""); @@ -618,7 +618,7 @@ export function ApOperationsPage() { +
+ + +
@@ -733,8 +751,14 @@ export function ContractsRegistryPage() {
{resolveError(contractsQuery.error, "加载合同台账失败")}
) : null} + {!showTrialData && hiddenTrialContractCount > 0 ? ( + + 试运行默认只展示正式合同;需要核对测试样本时可临时显示。 + + ) : null} +
- {contracts.map((item) => ( + {visibleContracts.map((item) => (
@@ -802,7 +826,7 @@ export function ContractsRegistryPage() {
))} - {!contractsQuery.isLoading && !contracts.length ? ( + {!contractsQuery.isLoading && !visibleContracts.length ? (
暂无符合筛选条件的合同台账。
) : null}
diff --git a/client/src/pages/CutoverConsolePage.tsx b/client/src/pages/CutoverConsolePage.tsx index 91cc6c0d..35b3d37a 100644 --- a/client/src/pages/CutoverConsolePage.tsx +++ b/client/src/pages/CutoverConsolePage.tsx @@ -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 (
+ + 切换范围会影响试运行边界,默认不作为日常业务入口;正式变更需另行确认。 +
diff --git a/client/src/pages/ErpIntegrationPage.tsx b/client/src/pages/ErpIntegrationPage.tsx index aa736d3b..f8d6ed70 100644 --- a/client/src/pages/ErpIntegrationPage.tsx +++ b/client/src/pages/ErpIntegrationPage.tsx @@ -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() {
syncMutation.mutate(undefined)} disabled={syncMutation.isPending}> @@ -271,6 +272,9 @@ export function ErpIntegrationPage() { ) : null } /> + + 试运行期间不把 ERP 写入作为主路径;本页用于管理员查看同步状态和队列排障。 + {error ? (

{error}

diff --git a/client/src/pages/FeatureFlagsPage.tsx b/client/src/pages/FeatureFlagsPage.tsx index f97869f4..6ef3203e 100644 --- a/client/src/pages/FeatureFlagsPage.tsx +++ b/client/src/pages/FeatureFlagsPage.tsx @@ -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() {
+ + Env 仍是硬闸;批量开启、关闭或灰度变更会影响试运行边界,执行前必须保留变更说明。 + {error ? (

{error}

@@ -327,7 +331,7 @@ export function FeatureFlagsPage() {
@@ -667,8 +666,7 @@ function TodoAggregatePanel({ items }: { items: TodoAggregateItem[] }) { return (
@@ -713,8 +711,7 @@ function TeamStatusBoardPanel({ items }: { items: PortalTeamStatusItem[] }) { return ( 查看团队页} className="portal-panel-fill" > @@ -772,8 +769,7 @@ function ActivityAnnouncementsPanel({ }) { return ( 公告页} className="portal-panel-fill" > @@ -1156,8 +1152,8 @@ export function PortalHomePage() { {isManagerView ? ( <>
{roleCapabilities.canApproveTasks ? ( @@ -1185,8 +1181,8 @@ export function PortalHomePage() {
@@ -1196,7 +1192,7 @@ export function PortalHomePage() { ) : (
@@ -1206,8 +1202,8 @@ export function PortalHomePage() {
diff --git a/client/src/pages/ProjectsPage.tsx b/client/src/pages/ProjectsPage.tsx index 6b1c580f..406275b7 100644 --- a/client/src/pages/ProjectsPage.tsx +++ b/client/src/pages/ProjectsPage.tsx @@ -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() {
- + updateSearch({ q: event.target.value })} />