R109/A1,A5 优化移动端底部导航和常用页面
这个提交包含在:
@@ -33,8 +33,8 @@ export function MobileBottomNav() {
|
|||||||
const pendingCount = pendingCountQuery.data ?? 0;
|
const pendingCount = pendingCountQuery.data ?? 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="z-20 shrink-0 border-t border-zinc-200 bg-white/95 px-2 pb-[max(0.5rem,env(safe-area-inset-bottom))] pt-2 shadow-[0_-8px_24px_rgba(15,23,42,0.08)] backdrop-blur md:hidden">
|
<nav className="z-20 shrink-0 border-t border-zinc-200 bg-white/95 px-2 pb-[max(0.25rem,env(safe-area-inset-bottom))] pt-1.5 shadow-[0_-6px_18px_rgba(15,23,42,0.06)] backdrop-blur md:hidden">
|
||||||
<div className="grid grid-cols-4 gap-2">
|
<div className="grid grid-cols-4 gap-1">
|
||||||
{MOBILE_NAV_ITEMS.map((item) => {
|
{MOBILE_NAV_ITEMS.map((item) => {
|
||||||
const disabled = item.anyOf ? !hasAnyPermission(permissions, [...item.anyOf]) : false;
|
const disabled = item.anyOf ? !hasAnyPermission(permissions, [...item.anyOf]) : false;
|
||||||
const badge = item.key === "approvals" && pendingCount > 0 ? resolveBadgeLabel(pendingCount) : null;
|
const badge = item.key === "approvals" && pendingCount > 0 ? resolveBadgeLabel(pendingCount) : null;
|
||||||
@@ -45,7 +45,7 @@ export function MobileBottomNav() {
|
|||||||
key={item.key}
|
key={item.key}
|
||||||
type="button"
|
type="button"
|
||||||
data-testid={`mobile-nav-${item.key}`}
|
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 h-12 min-h-0 flex-col items-center justify-center gap-0.5 rounded-xl px-1 py-1 text-[11px] font-medium leading-none text-zinc-400"
|
||||||
disabled
|
disabled
|
||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
title={`${item.label}不可用`}
|
title={`${item.label}不可用`}
|
||||||
@@ -59,14 +59,14 @@ export function MobileBottomNav() {
|
|||||||
return (
|
return (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={item.key}
|
key={item.key}
|
||||||
end
|
end={item.to === "/mobile"}
|
||||||
to={item.to}
|
to={item.to}
|
||||||
data-testid={`mobile-nav-${item.key}`}
|
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 h-12 min-h-0 flex-col items-center justify-center gap-0.5 rounded-xl px-1 py-1 text-[11px] font-medium leading-none transition-colors",
|
||||||
isActive
|
isActive
|
||||||
? "bg-primary-50 text-primary ring-1 ring-primary-100"
|
? "bg-primary text-white shadow-sm"
|
||||||
: "text-zinc-500 hover:bg-zinc-50 hover:text-zinc-900",
|
: "text-zinc-500 hover:bg-zinc-50 hover:text-zinc-900",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -74,12 +74,12 @@ export function MobileBottomNav() {
|
|||||||
{({ isActive }) => (
|
{({ isActive }) => (
|
||||||
<>
|
<>
|
||||||
<span className="relative">
|
<span className="relative">
|
||||||
<span className="material-symbols-outlined text-[20px]">{item.icon}</span>
|
<span className="material-symbols-outlined text-[19px] leading-none">{item.icon}</span>
|
||||||
{badge ? (
|
{badge ? (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute -right-3 -top-2 min-w-5 rounded-full px-1.5 py-0.5 text-[10px] font-semibold leading-none",
|
"absolute -right-3 -top-2 min-w-5 rounded-full px-1.5 py-0.5 text-[10px] font-semibold leading-none",
|
||||||
isActive ? "bg-primary text-white" : "bg-red-500 text-white",
|
isActive ? "bg-white text-primary" : "bg-red-500 text-white",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{badge}
|
{badge}
|
||||||
|
|||||||
@@ -248,10 +248,10 @@ export function MobileLeavePage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageScaffold className="space-y-4">
|
<PageScaffold className="space-y-3 pb-4">
|
||||||
<PageHeader title="请假" />
|
<PageHeader title="请假" />
|
||||||
|
|
||||||
<section className="rounded-lg border border-zinc-200 bg-white p-4 shadow-sm" data-testid="mobile-leave-summary">
|
<section className="rounded-lg border border-zinc-200 bg-white p-3 shadow-sm" data-testid="mobile-leave-summary">
|
||||||
<div className="grid grid-cols-3 gap-3 text-sm">
|
<div className="grid grid-cols-3 gap-3 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-zinc-500">可用</div>
|
<div className="text-xs text-zinc-500">可用</div>
|
||||||
@@ -268,7 +268,7 @@ export function MobileLeavePage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="rounded-lg border border-zinc-200 bg-white p-4 shadow-sm" data-testid="mobile-leave-form">
|
<section className="rounded-lg border border-zinc-200 bg-white p-3 shadow-sm" data-testid="mobile-leave-form">
|
||||||
<div className="mb-4 flex items-center justify-between gap-3">
|
<div className="mb-4 flex items-center justify-between gap-3">
|
||||||
<h3 className="text-base font-semibold text-zinc-900">{editingItem ? "编辑请假" : "新建请假"}</h3>
|
<h3 className="text-base font-semibold text-zinc-900">{editingItem ? "编辑请假" : "新建请假"}</h3>
|
||||||
{editingItem ? (
|
{editingItem ? (
|
||||||
@@ -278,7 +278,7 @@ export function MobileLeavePage() {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-3">
|
||||||
<label className="block space-y-2 text-sm text-zinc-700">
|
<label className="block space-y-2 text-sm text-zinc-700">
|
||||||
<span>假期类型</span>
|
<span>假期类型</span>
|
||||||
<select className="select-std" value={leaveTypeId} onChange={(event) => setLeaveTypeId(event.target.value)}>
|
<select className="select-std" value={leaveTypeId} onChange={(event) => setLeaveTypeId(event.target.value)}>
|
||||||
@@ -353,7 +353,7 @@ export function MobileLeavePage() {
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="w-full"
|
className="min-h-12 w-full"
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!canCreateLeave || isSaving || !leaveTypeId || !startDate || !endDate}
|
disabled={!canCreateLeave || isSaving || !leaveTypeId || !startDate || !endDate}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -370,7 +370,7 @@ export function MobileLeavePage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="rounded-lg border border-zinc-200 bg-white p-4 shadow-sm" data-testid="mobile-leave-balances">
|
<section className="rounded-lg border border-zinc-200 bg-white p-3 shadow-sm" data-testid="mobile-leave-balances">
|
||||||
<h3 className="text-base font-semibold text-zinc-900">额度</h3>
|
<h3 className="text-base font-semibold text-zinc-900">额度</h3>
|
||||||
<div className="mt-3 space-y-2">
|
<div className="mt-3 space-y-2">
|
||||||
{balancesQuery.isLoading ? <div className="text-sm text-zinc-500">加载中...</div> : null}
|
{balancesQuery.isLoading ? <div className="text-sm text-zinc-500">加载中...</div> : null}
|
||||||
@@ -400,7 +400,7 @@ export function MobileLeavePage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="rounded-lg border border-zinc-200 bg-white p-4 shadow-sm" data-testid="mobile-leave-records">
|
<section className="rounded-lg border border-zinc-200 bg-white p-3 shadow-sm" data-testid="mobile-leave-records">
|
||||||
<h3 className="text-base font-semibold text-zinc-900">请假记录</h3>
|
<h3 className="text-base font-semibold text-zinc-900">请假记录</h3>
|
||||||
<Tabs value={statusFilter} onValueChange={(value) => setStatusFilter(value as LeaveStatusFilter)}>
|
<Tabs value={statusFilter} onValueChange={(value) => setStatusFilter(value as LeaveStatusFilter)}>
|
||||||
<TabsList className="mt-3 grid h-auto w-full grid-cols-4">
|
<TabsList className="mt-3 grid h-auto w-full grid-cols-4">
|
||||||
|
|||||||
@@ -31,20 +31,20 @@ function MobileActionLink({
|
|||||||
}: {
|
}: {
|
||||||
to: string;
|
to: string;
|
||||||
label: string;
|
label: string;
|
||||||
description: string;
|
description?: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to={to}
|
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"
|
className="flex min-h-12 items-center gap-3 rounded-lg border border-zinc-200 bg-white px-3 py-2 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">
|
<span className="material-symbols-outlined flex size-8 shrink-0 items-center justify-center rounded-lg bg-zinc-100 text-[18px] text-zinc-700">
|
||||||
{icon}
|
{icon}
|
||||||
</span>
|
</span>
|
||||||
<span className="min-w-0">
|
<span className="min-w-0">
|
||||||
<span className="block text-sm font-semibold text-zinc-900">{label}</span>
|
<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>
|
{description ? <span className="mt-0.5 block text-xs leading-5 text-zinc-500">{description}</span> : null}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
@@ -78,12 +78,12 @@ export function MobileMePage() {
|
|||||||
const departmentName = profile?.department?.name ?? (me?.departmentId ? "已关联组织" : "待管理员维护");
|
const departmentName = profile?.department?.name ?? (me?.departmentId ? "已关联组织" : "待管理员维护");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageScaffold className="space-y-4">
|
<PageScaffold className="space-y-3 pb-4">
|
||||||
<PageHeader title="我的" />
|
<PageHeader title="我的" />
|
||||||
|
|
||||||
<SectionPanel title="个人信息">
|
<SectionPanel title="个人信息">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-3">
|
||||||
<EmployeeAvatar name={me?.name ?? "员工"} src={profile?.avatarUrl} size="lg" />
|
<EmployeeAvatar name={me?.name ?? "员工"} src={profile?.avatarUrl} size="md" />
|
||||||
<div className="min-w-0 space-y-1">
|
<div className="min-w-0 space-y-1">
|
||||||
<div className="text-lg font-semibold text-zinc-900">{me?.name ?? "-"}</div>
|
<div className="text-lg font-semibold text-zinc-900">{me?.name ?? "-"}</div>
|
||||||
<div className="text-sm text-zinc-500">{departmentName}</div>
|
<div className="text-sm text-zinc-500">{departmentName}</div>
|
||||||
@@ -100,7 +100,7 @@ export function MobileMePage() {
|
|||||||
</SectionPanel>
|
</SectionPanel>
|
||||||
|
|
||||||
<SectionPanel title="账号与绑定">
|
<SectionPanel title="账号与绑定">
|
||||||
<div className="grid gap-3">
|
<div className="grid gap-2">
|
||||||
<MobileActionLink to="/settings/profile" label="个人设置" description="企业微信、账号与通知" icon="manage_accounts" />
|
<MobileActionLink to="/settings/profile" label="个人设置" description="企业微信、账号与通知" icon="manage_accounts" />
|
||||||
{me?.id ? (
|
{me?.id ? (
|
||||||
<MobileActionLink to={`/people/${me.id}`} label="员工档案" description="办公地点和紧急联系人" icon="badge" />
|
<MobileActionLink to={`/people/${me.id}`} label="员工档案" description="办公地点和紧急联系人" icon="badge" />
|
||||||
@@ -109,22 +109,22 @@ export function MobileMePage() {
|
|||||||
</SectionPanel>
|
</SectionPanel>
|
||||||
|
|
||||||
<SectionPanel title="更多入口">
|
<SectionPanel title="更多入口">
|
||||||
<div className="grid gap-3" data-testid="mobile-more-links">
|
<div className="grid grid-cols-2 gap-2" data-testid="mobile-more-links">
|
||||||
<MobileActionLink to="/notifications" label="通知中心" description="查看全部站内通知" icon="notifications" />
|
<MobileActionLink to="/notifications" label="通知中心" icon="notifications" />
|
||||||
<MobileActionLink to="/mobile/leave" label="请假" description="申请和记录" icon="event_available" />
|
<MobileActionLink to="/mobile/leave" label="请假" icon="event_available" />
|
||||||
<MobileActionLink to="/mobile/reimbursements" label="报销" description="草稿和进度" icon="request_quote" />
|
<MobileActionLink to="/mobile/reimbursements" label="报销" icon="request_quote" />
|
||||||
{canAccessApprovals ? <MobileActionLink to="/mobile/approvals" label="审批" description="待办处理" icon="fact_check" /> : null}
|
{canAccessApprovals ? <MobileActionLink to="/mobile/approvals" label="审批" icon="fact_check" /> : null}
|
||||||
{canAccessAssets ? <MobileActionLink to="/assets" label="资产" description="台账和盘点" icon="inventory_2" /> : null}
|
{canAccessAssets ? <MobileActionLink to="/assets" label="资产" icon="inventory_2" /> : null}
|
||||||
{canAccessProjects ? <MobileActionLink to="/projects" label="项目" description="项目和里程碑" icon="workspaces" /> : null}
|
{canAccessProjects ? <MobileActionLink to="/projects" label="项目" icon="workspaces" /> : null}
|
||||||
</div>
|
</div>
|
||||||
</SectionPanel>
|
</SectionPanel>
|
||||||
|
|
||||||
{canAccessAdmin ? (
|
{canAccessAdmin ? (
|
||||||
<SectionPanel title="管理入口">
|
<SectionPanel title="管理入口">
|
||||||
<div className="grid gap-3">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<MobileActionLink to="/people" label="人员管理" description="用户、角色和员工资料" icon="group" />
|
<MobileActionLink to="/people" label="人员管理" icon="group" />
|
||||||
<MobileActionLink to="/organization" label="组织架构" description="部门和汇报关系" icon="account_tree" />
|
<MobileActionLink to="/organization" label="组织架构" icon="account_tree" />
|
||||||
<MobileActionLink to="/system/settings" label="系统设置" description="工作区和系统参数" icon="settings" />
|
<MobileActionLink to="/system/settings" label="系统设置" icon="settings" />
|
||||||
</div>
|
</div>
|
||||||
</SectionPanel>
|
</SectionPanel>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户