统一首批页面成熟设计基线(05-30-unified-page-design-review)

这个提交包含在:
2026-05-30 17:47:00 +08:00
父节点 6ae6d82355
当前提交 218ea9ad10
共修改 24 个文件,包含 837 行新增和 326 行删除
+117 -23
查看文件
@@ -38,6 +38,14 @@ type EmptyAction = {
variant?: "primary" | "secondary" | "ghost";
};
type QuickEntry = {
key: string;
label: string;
href: string;
actionLabel: string;
icon: string;
};
const LEAVE_PORTAL_PERMISSIONS = ["leave.view_own", "leave.view_all", "leave.create", "leave.submit"];
const EXPENSE_PORTAL_PERMISSIONS = ["expense.view_own", "expense.view_all", "expense.create", "expense.proxy_manage"];
const PROCUREMENT_PORTAL_PERMISSIONS = ["procurement.view_own", "procurement.view_all", "procurement.create", "procurement.submit"];
@@ -45,6 +53,10 @@ const TRAVEL_PORTAL_PERMISSIONS = ["travel.view_own", "travel.view_all", "travel
const APPROVAL_PORTAL_PERMISSIONS = ["expense.approve", "leave.approve", "procurement.approve", "travel.approve"];
const FINANCE_PORTAL_PERMISSIONS = ["payment.batch.manage", "risk.review.manage", "expense.finance.pay"];
const SYSTEM_PORTAL_PERMISSIONS = ["system.users", "system.settings", "system.audit.view", "notification.channel.manage"];
const CONTRACT_PORTAL_PERMISSIONS = ["project.view_all", "project.manage", "system.settings"];
const PROJECT_PORTAL_PERMISSIONS = ["project.view_own", "project.view_all", "project.manage"];
const ASSET_PORTAL_PERMISSIONS = ["asset.view_own", "asset.view_all", "asset.manage", "asset.transfer", "asset.inventory"];
const INVOICE_PORTAL_PERMISSIONS = ["invoice.manage"];
const SEVERITY_RANK = {
high: 3,
@@ -398,8 +410,8 @@ function buildEmptyActions(permissions: string[], roleCapabilities: RoleCapabili
const actions: EmptyAction[] = [];
if (!roleCapabilities.isAdminOnly && hasAnyPermission(permissions, EXPENSE_PORTAL_PERMISSIONS)) {
actions.push({ key: "new-expense", label: "发起报销", href: "/reimbursements", variant: "primary" });
actions.push({ key: "my-progress", label: "查看我的进度", href: "/reimbursements", variant: "secondary" });
actions.push({ key: "new-expense", label: "新建报销", href: "/reimbursements", variant: "primary" });
actions.push({ key: "my-progress", label: "查看进度", href: "/reimbursements", variant: "secondary" });
}
if (roleCapabilities.canManageSystem) {
@@ -409,6 +421,82 @@ function buildEmptyActions(permissions: string[], roleCapabilities: RoleCapabili
return actions.slice(0, roleCapabilities.canManageSystem && actions.length > 2 ? 3 : 2);
}
function buildQuickEntries(permissions: string[], roleCapabilities: RoleCapabilities) {
const entries: QuickEntry[] = [];
if (!roleCapabilities.isAdminOnly && hasAnyPermission(permissions, EXPENSE_PORTAL_PERMISSIONS)) {
entries.push({
key: "reimbursements",
label: "报销",
href: "/reimbursements",
actionLabel: hasAnyPermission(permissions, ["expense.create", "expense.proxy_manage"]) ? "新建报销" : "查看报销",
icon: "request_quote",
});
}
if (hasAnyPermission(permissions, PROCUREMENT_PORTAL_PERMISSIONS)) {
entries.push({
key: "purchase",
label: "采购",
href: "/purchase-requests",
actionLabel: hasAnyPermission(permissions, ["procurement.create", "procurement.submit"]) ? "新建采购" : "查看采购",
icon: "shopping_cart",
});
}
if (hasAnyPermission(permissions, CONTRACT_PORTAL_PERMISSIONS)) {
entries.push({
key: "contracts",
label: "合同",
href: "/contracts-registry",
actionLabel: "查看合同",
icon: "description",
});
}
if (hasAnyPermission(permissions, PROJECT_PORTAL_PERMISSIONS)) {
entries.push({
key: "projects",
label: "项目",
href: "/projects",
actionLabel: "查看项目",
icon: "workspaces",
});
}
if (hasAnyPermission(permissions, ASSET_PORTAL_PERMISSIONS)) {
entries.push({
key: "assets",
label: "资产",
href: "/assets",
actionLabel: "查看资产",
icon: "inventory_2",
});
}
if (roleCapabilities.canUseFinance) {
entries.push({
key: "payments",
label: "付款",
href: "/payment-batches",
actionLabel: "处理付款",
icon: "payments",
});
}
if (hasAnyPermission(permissions, INVOICE_PORTAL_PERMISSIONS)) {
entries.push({
key: "invoices",
label: "票据",
href: "/invoice-pool",
actionLabel: "查看票据",
icon: "receipt_long",
});
}
return entries.slice(0, 6);
}
function sumWorkItemCount(items: WorkItem[]) {
return items.reduce((total, item) => total + Math.max(1, item.count), 0);
}
@@ -419,16 +507,12 @@ function SummaryStrip({
progressCount,
blockerCount,
reminderCount,
primaryItem,
emptyActions,
}: {
userName: string;
actionCount: number;
progressCount: number;
blockerCount: number;
reminderCount: number;
primaryItem: WorkItem | null;
emptyActions: EmptyAction[];
}) {
const attentionCount = blockerCount + reminderCount;
const summaryText = actionCount > 0
@@ -440,23 +524,9 @@ function SummaryStrip({
return (
<section className="portal-work-summary" data-testid="portal-work-summary">
<div className="min-w-0">
<div className="portal-work-summary__eyebrow">工作台</div>
<div className="portal-work-summary__title">{summaryText}</div>
<div className="portal-work-summary__meta">{userName} · 当前工作区</div>
</div>
{primaryItem ? (
<Button asChild type="button">
<Link to={primaryItem.href}>处理最急的一件</Link>
</Button>
) : emptyActions[0] ? (
<Button
asChild
variant={emptyActions[0].variant === "secondary" || emptyActions[0].variant === "ghost" ? emptyActions[0].variant : "default"}
type="button"
>
<Link to={emptyActions[0].href}>{emptyActions[0].label}</Link>
</Button>
) : null}
</section>
);
}
@@ -493,6 +563,30 @@ function WorkItemList({ items }: { items: WorkItem[] }) {
);
}
function QuickEntryPanel({ entries }: { entries: QuickEntry[] }) {
if (!entries.length) {
return null;
}
return (
<SectionPanel title="常用入口">
<div className="portal-quick-entry-grid" data-testid="portal-quick-entries">
{entries.map((entry) => (
<Link className="portal-quick-entry" key={entry.key} to={entry.href}>
<span className="portal-quick-entry__icon" aria-hidden="true">
<span className="material-symbols-outlined text-[18px]">{entry.icon}</span>
</span>
<span className="min-w-0">
<span className="portal-quick-entry__title">{entry.label}</span>
<span className="portal-quick-entry__action">{entry.actionLabel}</span>
</span>
</Link>
))}
</div>
</SectionPanel>
);
}
function TodayActionPanel({
items,
emptyActions,
@@ -578,11 +672,11 @@ export function PortalHomePage() {
roleCapabilities.canManageSystem ||
isManagerRole(permissions);
const todayActionItems = buildTodayActionItems(payload, roleCapabilities);
const quickEntries = buildQuickEntries(permissions, roleCapabilities);
const progressItems = buildProgressItems(payload);
const teamBlockers = isManagerView ? buildTeamBlockers(payload, roleCapabilities) : [];
const managementReminders = buildManagementReminders(payload, roleCapabilities, canSetupWorkspace);
const emptyActions = buildEmptyActions(permissions, roleCapabilities);
const primaryItem = todayActionItems[0] ?? null;
const actionCount = sumWorkItemCount(todayActionItems);
const progressCount = sumWorkItemCount(progressItems);
const blockerCount = sumWorkItemCount(teamBlockers);
@@ -605,10 +699,10 @@ export function PortalHomePage() {
progressCount={progressCount}
blockerCount={blockerCount}
reminderCount={reminderCount}
primaryItem={primaryItem}
emptyActions={emptyActions}
/>
<QuickEntryPanel entries={quickEntries} />
<TodayActionPanel items={todayActionItems} emptyActions={emptyActions} />
<div className="portal-task-secondary-grid">