统一首批页面成熟设计基线(05-30-unified-page-design-review)
这个提交包含在:
+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">
|
||||
|
||||
在新工单中引用
屏蔽一个用户