WCM6 收口企业微信多手机移动渲染稳定化
这个提交包含在:
@@ -8,6 +8,7 @@ import {
|
||||
MOBILE_NAV_ITEMS,
|
||||
} from "../../features/mobile/navigation";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { MobileIcon } from "./MobileIcon";
|
||||
|
||||
function resolveBadgeLabel(count: number) {
|
||||
if (count > 99) {
|
||||
@@ -53,8 +54,13 @@ export function MobileBottomNav() {
|
||||
aria-disabled="true"
|
||||
title={`${item.label}不可用`}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[20px]">{item.icon}</span>
|
||||
<span className="max-w-full overflow-hidden text-ellipsis whitespace-nowrap">{item.label}</span>
|
||||
<MobileIcon name={item.icon} className="size-5" />
|
||||
<span
|
||||
className="max-w-full overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
data-testid={`mobile-nav-${item.key}-label`}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -67,29 +73,35 @@ export function MobileBottomNav() {
|
||||
data-testid={`mobile-nav-${item.key}`}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"touch-target pointer-events-auto flex h-12 min-h-0 min-w-0 select-none flex-col items-center justify-center gap-0.5 rounded-xl px-0.5 py-1 text-[11px] font-medium leading-none transition-colors",
|
||||
"touch-target pointer-events-auto relative flex h-12 min-h-0 min-w-0 select-none flex-col items-center justify-center gap-0.5 rounded-xl px-0.5 py-1 text-[11px] font-medium leading-none transition-colors",
|
||||
isActive
|
||||
? "bg-primary text-white shadow-sm"
|
||||
? "bg-primary/10 text-primary ring-1 ring-primary/15"
|
||||
: "text-zinc-500 hover:bg-zinc-50 hover:text-zinc-900",
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
<span className="relative">
|
||||
<span className="material-symbols-outlined text-[19px] leading-none">{item.icon}</span>
|
||||
{badge ? (
|
||||
<span
|
||||
className={cn(
|
||||
"absolute -right-3 -top-2 min-w-5 rounded-full px-1.5 py-0.5 text-[10px] font-semibold leading-none",
|
||||
isActive ? "bg-white text-primary" : "bg-red-500 text-white",
|
||||
)}
|
||||
>
|
||||
{badge}
|
||||
</span>
|
||||
) : null}
|
||||
<span>
|
||||
<MobileIcon name={item.icon} className="size-5" />
|
||||
</span>
|
||||
<span className="max-w-full overflow-hidden text-ellipsis whitespace-nowrap">{item.label}</span>
|
||||
<span
|
||||
className="max-w-full overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
data-testid={`mobile-nav-${item.key}-label`}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
{badge ? (
|
||||
<span
|
||||
data-testid={`mobile-nav-${item.key}-badge`}
|
||||
className={cn(
|
||||
"pointer-events-none absolute right-1 top-1 min-w-4 rounded-full px-1 text-[10px] font-semibold leading-4 shadow-sm",
|
||||
isActive ? "bg-red-500 text-white" : "bg-red-500 text-white",
|
||||
)}
|
||||
>
|
||||
{badge}
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</NavLink>
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { HTMLAttributes } from "react";
|
||||
import {
|
||||
Bell,
|
||||
BriefcaseBusiness,
|
||||
CalendarCheck,
|
||||
Circle,
|
||||
ClipboardCheck,
|
||||
CloudUpload,
|
||||
FileCheck2,
|
||||
FileText,
|
||||
Home,
|
||||
IdCard,
|
||||
Network,
|
||||
PackageSearch,
|
||||
ReceiptText,
|
||||
Settings,
|
||||
UserCog,
|
||||
UserRound,
|
||||
Users,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
const MOBILE_ICON_MAP: Record<string, LucideIcon> = {
|
||||
account_tree: Network,
|
||||
badge: IdCard,
|
||||
cloud_upload: CloudUpload,
|
||||
event_available: CalendarCheck,
|
||||
fact_check: ClipboardCheck,
|
||||
group: Users,
|
||||
home: Home,
|
||||
inventory_2: PackageSearch,
|
||||
manage_accounts: UserCog,
|
||||
notifications: Bell,
|
||||
person: UserRound,
|
||||
picture_as_pdf: FileText,
|
||||
receipt_long: ReceiptText,
|
||||
request_quote: FileCheck2,
|
||||
settings: Settings,
|
||||
workspaces: BriefcaseBusiness,
|
||||
};
|
||||
|
||||
type MobileIconProps = HTMLAttributes<HTMLSpanElement> & {
|
||||
iconClassName?: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export function MobileIcon({ className, iconClassName, name, ...props }: MobileIconProps) {
|
||||
const Icon = MOBILE_ICON_MAP[name] ?? Circle;
|
||||
|
||||
return (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn("inline-flex shrink-0 items-center justify-center leading-none", className)}
|
||||
{...props}
|
||||
>
|
||||
<Icon className={cn("size-5", iconClassName)} strokeWidth={2.1} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -13,12 +13,16 @@
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
min-height: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
color-scheme: light;
|
||||
background: var(--color-bg-canvas);
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
min-height: -webkit-fill-available;
|
||||
background:
|
||||
radial-gradient(circle at 10% -8%, rgba(0, 57, 138, 0.10), transparent 30rem),
|
||||
radial-gradient(circle at 92% 0%, rgba(19, 168, 184, 0.12), transparent 32rem),
|
||||
@@ -29,6 +33,7 @@
|
||||
}
|
||||
|
||||
#root {
|
||||
min-height: -webkit-fill-available;
|
||||
@apply min-h-screen flex flex-col;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { uploadInvoice } from "../api/invoicePool";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { isApiError } from "../api/types";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { MobileIcon } from "../components/mobile/MobileIcon";
|
||||
import { PageScaffold } from "../components/patterns/PageScaffold";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Spinner } from "../components/ui/spinner";
|
||||
@@ -201,7 +202,7 @@ export function MobileCapturePage() {
|
||||
htmlFor="mobile-capture-pdf-file"
|
||||
className="flex min-h-16 cursor-pointer items-center gap-3 rounded-xl 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-lg bg-emerald-50 text-[22px] text-emerald-700">picture_as_pdf</span>
|
||||
<MobileIcon name="picture_as_pdf" className="size-10 rounded-lg bg-emerald-50 p-2 text-emerald-700" />
|
||||
<span className="min-w-0">
|
||||
<span className="block text-sm font-semibold text-zinc-900">选择 PDF 发票</span>
|
||||
<span className="mt-0.5 block text-xs leading-5 text-zinc-500">数电票或扫描件 PDF</span>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useAuth } from "../auth/AuthContext";
|
||||
import { hasAnyPermission } from "../auth/permissions";
|
||||
import { EmployeeAvatar } from "../components/common/EmployeeAvatar";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { MobileIcon } from "../components/mobile/MobileIcon";
|
||||
import { PageScaffold } from "../components/patterns/PageScaffold";
|
||||
import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
import { Button } from "../components/ui/button";
|
||||
@@ -39,9 +40,7 @@ function MobileActionLink({
|
||||
to={to}
|
||||
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-8 shrink-0 items-center justify-center rounded-lg bg-zinc-100 text-[18px] text-zinc-700">
|
||||
{icon}
|
||||
</span>
|
||||
<MobileIcon name={icon} className="size-8 rounded-lg bg-zinc-100 p-1.5 text-zinc-700" />
|
||||
<span className="min-w-0">
|
||||
<span className="block text-sm font-semibold text-zinc-900">{label}</span>
|
||||
{description ? <span className="mt-0.5 block text-xs leading-5 text-zinc-500">{description}</span> : null}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useAuth } from "../auth/AuthContext";
|
||||
import { hasAnyPermission } from "../auth/permissions";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { PageScaffold } from "../components/patterns/PageScaffold";
|
||||
import { MobileIcon } from "../components/mobile/MobileIcon";
|
||||
import { Button } from "../components/ui/button";
|
||||
import {
|
||||
MOBILE_APPROVAL_PERMISSIONS,
|
||||
@@ -139,13 +140,11 @@ function ActionTile({ action }: { action: MobileAction }) {
|
||||
title={`${action.label}:${action.meta}`}
|
||||
aria-label={`${action.label},${action.meta}`}
|
||||
>
|
||||
<span
|
||||
className="material-symbols-outlined mobile-icon-action__icon"
|
||||
<MobileIcon
|
||||
className="mobile-icon-action__icon"
|
||||
data-testid={`mobile-action-${action.key}-icon`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{action.icon}
|
||||
</span>
|
||||
name={action.icon}
|
||||
/>
|
||||
<span className="mobile-icon-action__label" data-testid={`mobile-action-${action.key}-label`}>
|
||||
{action.label}
|
||||
</span>
|
||||
|
||||
@@ -31,6 +31,7 @@ import type { PrecheckResponse } from "../api/feeControl";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasPermission } from "../auth/permissions";
|
||||
import { ConfirmDialog } from "../components/common/ConfirmDialog";
|
||||
import { MobileIcon } from "../components/mobile/MobileIcon";
|
||||
import { PageScaffold } from "../components/patterns/PageScaffold";
|
||||
import { Button } from "../components/ui/button";
|
||||
import {
|
||||
@@ -1175,7 +1176,7 @@ export function MobileReimbursementDetailPage() {
|
||||
htmlFor="mobile-reimbursement-invoice-upload-pdf"
|
||||
className="flex min-h-28 cursor-pointer flex-col items-center justify-center rounded-lg border border-dashed border-zinc-300 bg-zinc-50 px-3 py-4 text-center"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[28px] text-zinc-700">picture_as_pdf</span>
|
||||
<MobileIcon name="picture_as_pdf" className="size-7 text-zinc-700" />
|
||||
<span className="mt-2 text-sm font-semibold text-zinc-900">选择 PDF 发票</span>
|
||||
<span className="mt-1 text-xs text-zinc-500">仅支持 PDF,减少拍照模糊和错传</span>
|
||||
</label>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
:root {
|
||||
--mobile-viewport-height: 100vh;
|
||||
--mobile-safe-area-bottom: env(safe-area-inset-bottom, 0px);
|
||||
--mobile-page-x: clamp(10px, 3vw, 20px);
|
||||
--mobile-page-y: clamp(10px, 2.8vw, 20px);
|
||||
--mobile-card-gap: clamp(8px, 2.5vw, 14px);
|
||||
@@ -81,6 +82,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
background: rgb(255 255 255 / 0.86);
|
||||
font-size: 22px;
|
||||
@@ -88,6 +90,10 @@
|
||||
box-shadow: 0 1px 2px rgb(15 23 42 / 0.07);
|
||||
}
|
||||
|
||||
.mobile-icon-action__icon svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mobile-icon-action__label {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
@@ -129,12 +135,14 @@
|
||||
}
|
||||
|
||||
.mobile-bottom-nav-surface {
|
||||
min-height: calc(var(--mobile-bottom-nav-height) + env(safe-area-inset-bottom, 0px));
|
||||
padding: 0.375rem var(--mobile-page-x) calc(0.35rem + env(safe-area-inset-bottom, 0px));
|
||||
min-height: calc(var(--mobile-bottom-nav-height) + var(--mobile-safe-area-bottom));
|
||||
padding: 0.375rem var(--mobile-page-x) calc(0.35rem + var(--mobile-safe-area-bottom));
|
||||
-webkit-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.mobile-sticky-action-surface {
|
||||
padding: 0.75rem var(--mobile-page-x) max(0.75rem, env(safe-area-inset-bottom, 0px));
|
||||
padding: 0.75rem var(--mobile-page-x) max(0.75rem, var(--mobile-safe-area-bottom));
|
||||
}
|
||||
|
||||
@media (max-width: 359px) {
|
||||
|
||||
在新工单中引用
屏蔽一个用户