R89 A4-A5 移动导航与工作台重排

这个提交包含在:
2026-05-05 19:51:55 +08:00
父节点 bf6deac6d0
当前提交 ee64909811
共修改 6 个文件,包含 116 行新增和 23 行删除
@@ -44,6 +44,7 @@ export function MobileBottomNav() {
<button <button
key={item.key} key={item.key}
type="button" type="button"
data-testid={`mobile-nav-${item.key}`}
className="touch-target flex flex-col items-center justify-center gap-1 rounded-2xl px-2 py-2 text-xs font-medium text-zinc-400" className="touch-target flex flex-col items-center justify-center gap-1 rounded-2xl px-2 py-2 text-xs font-medium text-zinc-400"
disabled disabled
aria-disabled="true" aria-disabled="true"
@@ -60,6 +61,7 @@ export function MobileBottomNav() {
key={item.key} key={item.key}
end end
to={item.to} to={item.to}
data-testid={`mobile-nav-${item.key}`}
className={({ isActive }) => className={({ isActive }) =>
cn( cn(
"touch-target flex flex-col items-center justify-center gap-1 rounded-2xl px-2 py-2 text-xs font-medium transition-colors", "touch-target flex flex-col items-center justify-center gap-1 rounded-2xl px-2 py-2 text-xs font-medium transition-colors",
+1 -1
查看文件
@@ -145,7 +145,7 @@ export function Topbar() {
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="left-0 top-0 h-full w-80 max-w-[84vw] rounded-none rounded-r-3xl border-r border-slate-200 p-0"> <DialogContent className="left-0 top-0 h-dvh w-80 max-w-[88vw] translate-x-0 translate-y-0 rounded-none rounded-r-3xl border-r border-slate-200 p-0">
<div className="flex h-full flex-col bg-white"> <div className="flex h-full flex-col bg-white">
<div className="flex items-start justify-between border-b border-slate-200 px-5 py-5"> <div className="flex items-start justify-between border-b border-slate-200 px-5 py-5">
<AppBrand /> <AppBrand />
+1 -1
查看文件
@@ -30,6 +30,6 @@ export type MobileNavItem = {
export const MOBILE_NAV_ITEMS: MobileNavItem[] = [ export const MOBILE_NAV_ITEMS: MobileNavItem[] = [
{ key: "home", to: "/mobile", label: "首页", icon: "home" }, { key: "home", to: "/mobile", label: "首页", icon: "home" },
{ key: "approvals", to: "/mobile/approvals", label: "审批", icon: "fact_check", anyOf: MOBILE_APPROVAL_PERMISSIONS }, { key: "approvals", to: "/mobile/approvals", label: "审批", icon: "fact_check", anyOf: MOBILE_APPROVAL_PERMISSIONS },
{ key: "capture", to: "/mobile/capture", label: "拍照", icon: "photo_camera", anyOf: MOBILE_CAPTURE_PERMISSIONS }, { key: "capture", to: "/mobile/capture", label: "票据", icon: "receipt_long", anyOf: MOBILE_CAPTURE_PERMISSIONS },
{ key: "me", to: "/mobile/me", label: "我的", icon: "person" }, { key: "me", to: "/mobile/me", label: "我的", icon: "person" },
]; ];
+4 -2
查看文件
@@ -30,9 +30,11 @@ export function AppShellLayout() {
<Sidebar /> <Sidebar />
<main className="flex min-w-0 flex-1 flex-col lg:ml-72"> <main className="flex min-w-0 flex-1 flex-col lg:ml-72">
<Topbar /> <div className={isMobileRoute ? "hidden md:block" : ""}>
<Topbar />
</div>
<div className={`flex-1 px-4 py-5 sm:px-7 sm:py-7 lg:px-8 ${isMobileRoute ? "pb-24 md:pb-8" : ""}`}> <div className={`flex-1 px-4 py-5 sm:px-7 sm:py-7 lg:px-8 ${isMobileRoute ? "pb-24 pt-4 md:pb-8" : ""}`}>
<div className="layout-container max-w-none"> <div className="layout-container max-w-none">
<ErrorBoundary> <ErrorBoundary>
<Outlet /> <Outlet />
+66 -8
查看文件
@@ -6,6 +6,7 @@ import { fetchEmployeeProfile } from "../api/employees";
import { mapApiErrorMessage } from "../api/errorMessages"; import { mapApiErrorMessage } from "../api/errorMessages";
import { isApiError } from "../api/types"; import { isApiError } from "../api/types";
import { useAuth } from "../auth/AuthContext"; import { useAuth } from "../auth/AuthContext";
import { hasAnyPermission } from "../auth/permissions";
import { EmployeeAvatar } from "../components/common/EmployeeAvatar"; import { EmployeeAvatar } from "../components/common/EmployeeAvatar";
import { PageHeader } from "../components/common/PageHeader"; import { PageHeader } from "../components/common/PageHeader";
import { PageScaffold } from "../components/patterns/PageScaffold"; import { PageScaffold } from "../components/patterns/PageScaffold";
@@ -22,8 +23,39 @@ function resolveErrorMessage(error: unknown) {
return "请求失败,请稍后重试。"; return "请求失败,请稍后重试。";
} }
function MobileActionLink({
to,
label,
description,
icon,
}: {
to: string;
label: string;
description: string;
icon: string;
}) {
return (
<Link
to={to}
className="flex min-h-16 items-center gap-3 rounded-2xl border border-zinc-200 bg-white px-4 py-3 text-left shadow-sm transition-colors hover:bg-zinc-50"
>
<span className="material-symbols-outlined flex size-10 shrink-0 items-center justify-center rounded-xl bg-zinc-100 text-[20px] text-zinc-700">
{icon}
</span>
<span className="min-w-0">
<span className="block text-sm font-semibold text-zinc-900">{label}</span>
<span className="mt-0.5 block text-xs leading-5 text-zinc-500">{description}</span>
</span>
</Link>
);
}
export function MobileMePage() { export function MobileMePage() {
const { me, refreshMe } = useAuth(); const { me, permissions, refreshMe, logout } = useAuth();
const canAccessApprovals = hasAnyPermission(permissions, ["expense.approve", "leave.approve", "procurement.approve", "travel.approve"]);
const canAccessAdmin = hasAnyPermission(permissions, ["*", "system.settings", "system.users", "report.dashboard.view"]);
const canAccessAssets = hasAnyPermission(permissions, ["asset.view_own", "asset.view_all", "asset.manage", "asset.transfer", "asset.inventory"]);
const canAccessProjects = hasAnyPermission(permissions, ["project.view_own", "project.view_all", "project.manage"]);
const profileQuery = useQuery({ const profileQuery = useQuery({
enabled: Boolean(me?.id), enabled: Boolean(me?.id),
@@ -49,7 +81,7 @@ export function MobileMePage() {
<PageScaffold className="space-y-4"> <PageScaffold className="space-y-4">
<PageHeader <PageHeader
title="我的" title="我的"
subtitle="查看个人信息、进入完整档案并管理外部推送开关。" subtitle="个人信息、常用入口和低频管理操作。"
/> />
<SectionPanel title="个人信息" description="头像与部门信息从完整员工档案读取。"> <SectionPanel title="个人信息" description="头像与部门信息从完整员工档案读取。">
@@ -70,14 +102,36 @@ export function MobileMePage() {
) : null} ) : null}
</SectionPanel> </SectionPanel>
<SectionPanel title="完整员工档案" description="办公地点、紧急联系人、绩效和薪资等完整信息仍在员工档案页维护或查看。"> <SectionPanel title="账号与绑定" description="个人设置里可维护企业微信绑定、通知偏好和账号资料。">
{me?.id ? ( <div className="grid gap-3">
<Button asChild type="button" className="w-full"> <MobileActionLink to="/settings/profile" label="个人设置" description="绑定企业微信、维护账号与通知偏好" icon="manage_accounts" />
<Link to={`/people/${me.id}`}>进入完整员工档案</Link> {me?.id ? (
</Button> <MobileActionLink to={`/people/${me.id}`} label="完整员工档案" description="办公地点、紧急联系人和完整档案" icon="badge" />
) : null} ) : null}
</div>
</SectionPanel> </SectionPanel>
<SectionPanel title="更多入口" description="低频模块放在这里,避免干扰移动首页。">
<div className="grid gap-3" data-testid="mobile-more-links">
<MobileActionLink to="/notifications" label="通知中心" description="查看全部站内通知" icon="notifications" />
<MobileActionLink to="/leave-requests" label="请假" description="提交或查看请假申请" icon="event_available" />
<MobileActionLink to="/reimbursements" label="报销" description="提交报销和查看报销进度" icon="request_quote" />
{canAccessApprovals ? <MobileActionLink to="/mobile/approvals" label="审批" description="处理移动审批待办" icon="fact_check" /> : null}
{canAccessAssets ? <MobileActionLink to="/assets" label="资产" description="查看资产台账和盘点入口" icon="inventory_2" /> : null}
{canAccessProjects ? <MobileActionLink to="/projects" label="项目" description="查看项目、风险和里程碑" icon="workspaces" /> : null}
</div>
</SectionPanel>
{canAccessAdmin ? (
<SectionPanel title="管理入口" description="管理员和负责人低频使用。">
<div className="grid gap-3">
<MobileActionLink to="/people" label="人员管理" description="用户、角色和员工资料" icon="group" />
<MobileActionLink to="/organization" label="组织架构" description="部门和汇报关系" icon="account_tree" />
<MobileActionLink to="/system/settings" label="系统设置" description="工作区和系统参数" icon="settings" />
</div>
</SectionPanel>
) : null}
<SectionPanel title="外部通知推送" description="关闭后仅停止外部渠道推送,站内通知仍保留。"> <SectionPanel title="外部通知推送" description="关闭后仅停止外部渠道推送,站内通知仍保留。">
<label className="flex items-center gap-3 text-sm text-zinc-800"> <label className="flex items-center gap-3 text-sm text-zinc-800">
<input <input
@@ -93,6 +147,10 @@ export function MobileMePage() {
适用于审批待办、报销付款和资产流转提醒。关闭后不会影响站内通知或系统登录。 适用于审批待办、报销付款和资产流转提醒。关闭后不会影响站内通知或系统登录。
</p> </p>
</SectionPanel> </SectionPanel>
<Button type="button" variant="secondary" className="w-full border-red-200 text-red-600 hover:bg-red-50" onClick={() => void logout()}>
退出登录
</Button>
</PageScaffold> </PageScaffold>
); );
} }
+42 -11
查看文件
@@ -315,14 +315,45 @@ export function MobilePortalPage() {
const teamStatusItems = teamStatusQuery.data ?? []; const teamStatusItems = teamStatusQuery.data ?? [];
const pendingTasks = pendingCountQuery.data?.items ?? []; const pendingTasks = pendingCountQuery.data?.items ?? [];
const notifications = notificationsQuery.data?.items ?? []; const notifications = notificationsQuery.data?.items ?? [];
const pendingApprovalCount = canAccessApprovals ? pendingCountQuery.data?.total ?? 0 : 0;
const unreadNotificationCount = notifications.filter((item) => !item.readAt).length;
return ( return (
<PageScaffold className="space-y-4"> <PageScaffold className="space-y-4">
<PageHeader <PageHeader
title="移动首页" title="今天"
subtitle="处理今日考勤、审批和高频入口。" subtitle="先处理待办,再进入报销、票据和团队事项。"
/> />
<section className="card-std space-y-4 p-4" data-testid="mobile-today-focus">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-sm font-semibold text-zinc-900">今日重点</div>
<div className="mt-1 text-xs leading-5 text-zinc-500">移动端只保留高频动作,低频模块放到“我的”。</div>
</div>
<span className={pendingApprovalCount > 0 ? "badge-warning" : "badge-success"}>
{pendingApprovalCount > 0 ? `${pendingApprovalCount} 条待审批` : "暂无审批待办"}
</span>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="rounded-2xl border border-zinc-200 bg-white px-4 py-4">
<div className="text-2xl font-semibold text-zinc-900">{pendingApprovalCount}</div>
<div className="mt-1 text-xs text-zinc-500">审批待办</div>
</div>
<div className="rounded-2xl border border-zinc-200 bg-white px-4 py-4">
<div className="text-2xl font-semibold text-zinc-900">{unreadNotificationCount}</div>
<div className="mt-1 text-xs text-zinc-500">未读通知</div>
</div>
</div>
<div className="grid gap-3">
<QuickLinkCard label="处理审批" description="查看待办并快速处理" to="/mobile/approvals" disabled={!canAccessApprovals} />
<QuickLinkCard label="发起报销" description="提交报销和查看进度" to="/reimbursements" disabled={!canAccessReimbursements} />
<QuickLinkCard label="上传票据" description="拍照或选择文件上传" to="/mobile/capture" disabled={!canAccessCapture} />
</div>
</section>
{!attendanceFeatureDisabled ? ( {!attendanceFeatureDisabled ? (
<section className="card-std space-y-4 p-4"> <section className="card-std space-y-4 p-4">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
@@ -524,15 +555,15 @@ export function MobilePortalPage() {
)} )}
</section> </section>
<section className="card-std space-y-3 p-4"> {(canAccessLeave || canAccessAssetScan) ? (
<div className="text-sm font-semibold text-zinc-900">快捷入口</div> <section className="card-std space-y-3 p-4">
<div className="grid gap-3 sm:grid-cols-2"> <div className="text-sm font-semibold text-zinc-900">其他快捷入口</div>
<QuickLinkCard label="提交请假" description="进入请假申请页" to="/leave-requests" disabled={!canAccessLeave} /> <div className="grid gap-3 sm:grid-cols-2">
<QuickLinkCard label="提交报销" description="进入报销申请页" to="/reimbursements" disabled={!canAccessReimbursements} /> <QuickLinkCard label="提交请假" description="进入请假申请页" to="/leave-requests" disabled={!canAccessLeave} />
<QuickLinkCard label="拍照上传" description="打开移动拍照上传" to="/mobile/capture" disabled={!canAccessCapture} /> <QuickLinkCard label="资产扫描" description="进入扫码与盘点" to="/mobile/assets/scan" disabled={!canAccessAssetScan} />
<QuickLinkCard label="资产扫描" description="进入扫码与盘点" to="/mobile/assets/scan" disabled={!canAccessAssetScan} /> </div>
</div> </section>
</section> ) : null}
<section className="card-std space-y-3 p-4"> <section className="card-std space-y-3 p-4">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">