R89 A4-A5 移动导航与工作台重排
这个提交包含在:
@@ -44,6 +44,7 @@ export function MobileBottomNav() {
|
||||
<button
|
||||
key={item.key}
|
||||
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"
|
||||
disabled
|
||||
aria-disabled="true"
|
||||
@@ -60,6 +61,7 @@ export function MobileBottomNav() {
|
||||
key={item.key}
|
||||
end
|
||||
to={item.to}
|
||||
data-testid={`mobile-nav-${item.key}`}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"touch-target flex flex-col items-center justify-center gap-1 rounded-2xl px-2 py-2 text-xs font-medium transition-colors",
|
||||
|
||||
@@ -145,7 +145,7 @@ export function Topbar() {
|
||||
</Button>
|
||||
</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 items-start justify-between border-b border-slate-200 px-5 py-5">
|
||||
<AppBrand />
|
||||
|
||||
@@ -30,6 +30,6 @@ export type MobileNavItem = {
|
||||
export const MOBILE_NAV_ITEMS: MobileNavItem[] = [
|
||||
{ key: "home", to: "/mobile", label: "首页", icon: "home" },
|
||||
{ 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" },
|
||||
];
|
||||
|
||||
@@ -30,9 +30,11 @@ export function AppShellLayout() {
|
||||
<Sidebar />
|
||||
|
||||
<main className="flex min-w-0 flex-1 flex-col lg:ml-72">
|
||||
<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">
|
||||
<ErrorBoundary>
|
||||
<Outlet />
|
||||
|
||||
@@ -6,6 +6,7 @@ import { fetchEmployeeProfile } from "../api/employees";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { isApiError } from "../api/types";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasAnyPermission } from "../auth/permissions";
|
||||
import { EmployeeAvatar } from "../components/common/EmployeeAvatar";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { PageScaffold } from "../components/patterns/PageScaffold";
|
||||
@@ -22,8 +23,39 @@ function resolveErrorMessage(error: unknown) {
|
||||
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() {
|
||||
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({
|
||||
enabled: Boolean(me?.id),
|
||||
@@ -49,7 +81,7 @@ export function MobileMePage() {
|
||||
<PageScaffold className="space-y-4">
|
||||
<PageHeader
|
||||
title="我的"
|
||||
subtitle="查看个人信息、进入完整档案并管理外部推送开关。"
|
||||
subtitle="个人信息、常用入口和低频管理操作。"
|
||||
/>
|
||||
|
||||
<SectionPanel title="个人信息" description="头像与部门信息从完整员工档案读取。">
|
||||
@@ -70,14 +102,36 @@ export function MobileMePage() {
|
||||
) : null}
|
||||
</SectionPanel>
|
||||
|
||||
<SectionPanel title="完整员工档案" description="办公地点、紧急联系人、绩效和薪资等完整信息仍在员工档案页维护或查看。">
|
||||
<SectionPanel title="账号与绑定" description="个人设置里可维护企业微信绑定、通知偏好和账号资料。">
|
||||
<div className="grid gap-3">
|
||||
<MobileActionLink to="/settings/profile" label="个人设置" description="绑定企业微信、维护账号与通知偏好" icon="manage_accounts" />
|
||||
{me?.id ? (
|
||||
<Button asChild type="button" className="w-full">
|
||||
<Link to={`/people/${me.id}`}>进入完整员工档案</Link>
|
||||
</Button>
|
||||
<MobileActionLink to={`/people/${me.id}`} label="完整员工档案" description="办公地点、紧急联系人和完整档案" icon="badge" />
|
||||
) : null}
|
||||
</div>
|
||||
</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="关闭后仅停止外部渠道推送,站内通知仍保留。">
|
||||
<label className="flex items-center gap-3 text-sm text-zinc-800">
|
||||
<input
|
||||
@@ -93,6 +147,10 @@ export function MobileMePage() {
|
||||
适用于审批待办、报销付款和资产流转提醒。关闭后不会影响站内通知或系统登录。
|
||||
</p>
|
||||
</SectionPanel>
|
||||
|
||||
<Button type="button" variant="secondary" className="w-full border-red-200 text-red-600 hover:bg-red-50" onClick={() => void logout()}>
|
||||
退出登录
|
||||
</Button>
|
||||
</PageScaffold>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -315,14 +315,45 @@ export function MobilePortalPage() {
|
||||
const teamStatusItems = teamStatusQuery.data ?? [];
|
||||
const pendingTasks = pendingCountQuery.data?.items ?? [];
|
||||
const notifications = notificationsQuery.data?.items ?? [];
|
||||
const pendingApprovalCount = canAccessApprovals ? pendingCountQuery.data?.total ?? 0 : 0;
|
||||
const unreadNotificationCount = notifications.filter((item) => !item.readAt).length;
|
||||
|
||||
return (
|
||||
<PageScaffold className="space-y-4">
|
||||
<PageHeader
|
||||
title="移动首页"
|
||||
subtitle="处理今日考勤、审批和高频入口。"
|
||||
title="今天"
|
||||
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 ? (
|
||||
<section className="card-std space-y-4 p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
@@ -524,15 +555,15 @@ export function MobilePortalPage() {
|
||||
)}
|
||||
</section>
|
||||
|
||||
{(canAccessLeave || canAccessAssetScan) ? (
|
||||
<section className="card-std space-y-3 p-4">
|
||||
<div className="text-sm font-semibold text-zinc-900">快捷入口</div>
|
||||
<div className="text-sm font-semibold text-zinc-900">其他快捷入口</div>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<QuickLinkCard label="提交请假" description="进入请假申请页" to="/leave-requests" disabled={!canAccessLeave} />
|
||||
<QuickLinkCard label="提交报销" description="进入报销申请页" to="/reimbursements" disabled={!canAccessReimbursements} />
|
||||
<QuickLinkCard label="拍照上传" description="打开移动拍照上传" to="/mobile/capture" disabled={!canAccessCapture} />
|
||||
<QuickLinkCard label="资产扫描" description="进入扫码与盘点" to="/mobile/assets/scan" disabled={!canAccessAssetScan} />
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className="card-std space-y-3 p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
|
||||
在新工单中引用
屏蔽一个用户