V1-V6 收口移动视口与 PDF 发票上传
这个提交包含在:
@@ -174,7 +174,7 @@ export function MobileCapturePage() {
|
||||
<PageScaffold className="space-y-4">
|
||||
<PageHeader
|
||||
title="票据上传"
|
||||
subtitle="拍照或选择文件。"
|
||||
subtitle="上传 PDF 发票文件。"
|
||||
/>
|
||||
|
||||
{error ? (
|
||||
@@ -189,7 +189,7 @@ export function MobileCapturePage() {
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-zinc-900">选择票据</h3>
|
||||
<p className="mt-1 text-sm leading-5 text-zinc-500">图片或 PDF。</p>
|
||||
<p className="mt-1 text-sm leading-5 text-zinc-500">仅支持 PDF 发票。</p>
|
||||
</div>
|
||||
<span className={isOnline ? "shrink-0 text-sm text-emerald-600" : "shrink-0 text-sm text-orange-600"}>
|
||||
{isOnline ? "在线" : "离线"}
|
||||
@@ -197,58 +197,14 @@ export function MobileCapturePage() {
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2" data-testid="mobile-capture-file">
|
||||
<label
|
||||
htmlFor="mobile-capture-camera-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-blue-50 text-[22px] text-blue-700">photo_camera</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block text-sm font-semibold text-zinc-900">拍照上传</span>
|
||||
<span className="mt-0.5 block text-xs leading-5 text-zinc-500">打开摄像头拍摄发票</span>
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="mobile-capture-camera-file"
|
||||
accept="image/*"
|
||||
capture="environment"
|
||||
className="sr-only"
|
||||
data-testid="mobile-capture-camera-file"
|
||||
onChange={(event) => {
|
||||
setSelectedFiles(Array.from(event.target.files ?? []));
|
||||
}}
|
||||
type="file"
|
||||
/>
|
||||
|
||||
<label
|
||||
htmlFor="mobile-capture-image-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-cyan-50 text-[22px] text-cyan-700">image</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block text-sm font-semibold text-zinc-900">选择图片</span>
|
||||
<span className="mt-0.5 block text-xs leading-5 text-zinc-500">JPG、PNG、WEBP</span>
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="mobile-capture-image-file"
|
||||
accept="image/jpeg,image/png,image/webp"
|
||||
className="sr-only"
|
||||
data-testid="mobile-capture-image-file"
|
||||
multiple
|
||||
onChange={(event) => {
|
||||
setSelectedFiles(Array.from(event.target.files ?? []));
|
||||
}}
|
||||
type="file"
|
||||
/>
|
||||
|
||||
<label
|
||||
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>
|
||||
<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>
|
||||
<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>
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
|
||||
@@ -224,7 +224,7 @@ export function MobilePortalPage() {
|
||||
? {
|
||||
key: "capture",
|
||||
label: "传票据",
|
||||
meta: "拍照或选文件",
|
||||
meta: "上传 PDF",
|
||||
icon: "receipt_long",
|
||||
to: "/mobile/capture",
|
||||
tone: "primary",
|
||||
|
||||
@@ -164,7 +164,6 @@ export function MobileReimbursementDetailPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const { me, permissions, accessToken } = useAuth();
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const fileDocumentInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const attachmentInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const [itemForm, setItemForm] = useState<ItemFormState | null>(null);
|
||||
@@ -563,9 +562,6 @@ export function MobileReimbursementDetailPage() {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
if (fileDocumentInputRef.current) {
|
||||
fileDocumentInputRef.current.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
const messages: string[] = [];
|
||||
@@ -697,9 +693,6 @@ export function MobileReimbursementDetailPage() {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
if (fileDocumentInputRef.current) {
|
||||
fileDocumentInputRef.current.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const toggleInvoiceSelection = (invoice: BindableInvoiceSummary, checked: boolean) => {
|
||||
@@ -763,7 +756,16 @@ export function MobileReimbursementDetailPage() {
|
||||
const showSecondaryActions = Boolean(actionError || canDeleteDraft || !canEdit);
|
||||
|
||||
return (
|
||||
<PageScaffold className={cn("space-y-4", showStickyActions ? "pb-28" : "pb-6")} data-testid="mobile-reimbursement-detail">
|
||||
<PageScaffold
|
||||
className={cn(
|
||||
"space-y-4",
|
||||
showStickyActions ? "mobile-fixed-action-page" : "",
|
||||
showStickyActions
|
||||
? "pb-[calc(var(--mobile-sticky-action-height,76px)+env(safe-area-inset-bottom,0px)+1.25rem)]"
|
||||
: "pb-6",
|
||||
)}
|
||||
data-testid="mobile-reimbursement-detail"
|
||||
>
|
||||
<header className="flex items-center gap-3" data-testid="mobile-reimbursement-detail">
|
||||
<Button asChild size="sm" variant="secondary">
|
||||
<Link to="/mobile/reimbursements">返回</Link>
|
||||
@@ -1088,12 +1090,13 @@ export function MobileReimbursementDetailPage() {
|
||||
|
||||
{showStickyActions ? (
|
||||
<div
|
||||
className="sticky bottom-0 z-20 -mx-4 border-t border-zinc-200 bg-white/95 px-4 pb-[max(0.75rem,env(safe-area-inset-bottom))] pt-3 shadow-[0_-10px_24px_rgba(15,23,42,0.10)] backdrop-blur"
|
||||
className="fixed inset-x-0 bottom-0 z-50 border-t border-zinc-200 bg-white/95 px-4 pb-[max(0.75rem,env(safe-area-inset-bottom))] pt-3 shadow-[0_-10px_24px_rgba(15,23,42,0.10)] backdrop-blur"
|
||||
data-testid="mobile-reimbursement-sticky-actions"
|
||||
>
|
||||
<div className={cn("grid gap-2", canOpenInvoiceDialog && showSubmitAction ? "grid-cols-2" : "grid-cols-1")}>
|
||||
{canOpenInvoiceDialog ? (
|
||||
<Button
|
||||
className="min-h-11 min-w-0 px-2 text-sm whitespace-nowrap"
|
||||
data-testid="mobile-reimbursement-sticky-invoice-add"
|
||||
variant={showSubmitAction ? "secondary" : "default"}
|
||||
type="button"
|
||||
@@ -1104,6 +1107,7 @@ export function MobileReimbursementDetailPage() {
|
||||
) : null}
|
||||
{showSubmitAction ? (
|
||||
<Button
|
||||
className="min-h-11 min-w-0 px-2 text-sm whitespace-nowrap"
|
||||
data-testid={TEST_IDS.reimbursement.submit}
|
||||
disabled={submitMutation.isPending}
|
||||
type="button"
|
||||
@@ -1118,9 +1122,10 @@ export function MobileReimbursementDetailPage() {
|
||||
|
||||
<Dialog open={isInvoiceDialogOpen} onOpenChange={closeInvoiceDialog}>
|
||||
<DialogContent
|
||||
className="max-h-[86dvh] overflow-y-auto p-4"
|
||||
className="overflow-y-auto p-4"
|
||||
data-testid={TEST_IDS.reimbursement.invoiceDialog}
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
style={{ maxHeight: "min(86vh, calc(var(--mobile-viewport-height, 100vh) - 3rem))" }}
|
||||
>
|
||||
<DialogHeader className="text-left">
|
||||
<DialogTitle>添加发票</DialogTitle>
|
||||
@@ -1165,42 +1170,23 @@ export function MobileReimbursementDetailPage() {
|
||||
|
||||
{invoicePanel === "upload" ? (
|
||||
<div className="mt-4 space-y-3">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="grid gap-2">
|
||||
<label
|
||||
htmlFor="mobile-reimbursement-invoice-upload-image"
|
||||
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">photo_camera</span>
|
||||
<span className="mt-2 text-sm font-semibold text-zinc-900">拍照或选图片</span>
|
||||
<span className="mt-1 text-xs text-zinc-500">适合手机拍发票</span>
|
||||
</label>
|
||||
<label
|
||||
htmlFor="mobile-reimbursement-invoice-upload-document"
|
||||
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>
|
||||
<span className="mt-2 text-sm font-semibold text-zinc-900">选择 PDF/文件</span>
|
||||
<span className="mt-1 text-xs text-zinc-500">适合电子发票</span>
|
||||
<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>
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
id="mobile-reimbursement-invoice-upload-image"
|
||||
id="mobile-reimbursement-invoice-upload-pdf"
|
||||
className="sr-only"
|
||||
data-testid={TEST_IDS.reimbursement.invoiceUploadFile}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
capture="environment"
|
||||
multiple
|
||||
onChange={(event) => setSelectedInvoiceFiles(Array.from(event.target.files ?? []))}
|
||||
/>
|
||||
<input
|
||||
ref={fileDocumentInputRef}
|
||||
id="mobile-reimbursement-invoice-upload-document"
|
||||
className="sr-only"
|
||||
data-testid="reimbursement-invoice-upload-document"
|
||||
type="file"
|
||||
accept="application/pdf,image/jpeg,image/png,image/webp"
|
||||
accept="application/pdf"
|
||||
multiple
|
||||
onChange={(event) => setSelectedInvoiceFiles(Array.from(event.target.files ?? []))}
|
||||
/>
|
||||
@@ -1251,7 +1237,10 @@ export function MobileReimbursementDetailPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[44dvh] space-y-2 overflow-y-auto pr-1">
|
||||
<div
|
||||
className="space-y-2 overflow-y-auto pr-1"
|
||||
style={{ maxHeight: "min(44vh, calc(var(--mobile-viewport-height, 100vh) * 0.44))" }}
|
||||
>
|
||||
{invoiceLibraryLoading ? <div className="text-sm text-zinc-500">加载中...</div> : null}
|
||||
{invoiceLibraryError ? (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
|
||||
@@ -1606,7 +1606,7 @@ export function ReimbursementDetailPage() {
|
||||
<div className="rounded-lg border border-dashed border-zinc-300 px-4 py-4">
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm font-medium text-zinc-900">或者上传新发票</div>
|
||||
<p className="text-xs text-zinc-500">支持 PDF 或图片,可多选。</p>
|
||||
<p className="text-xs text-zinc-500">仅支持 PDF 发票,可多选。</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex flex-col gap-3 md:flex-row md:items-center">
|
||||
@@ -1625,7 +1625,7 @@ export function ReimbursementDetailPage() {
|
||||
className="hidden"
|
||||
data-testid={TEST_IDS.reimbursement.invoiceUploadFile}
|
||||
type="file"
|
||||
accept="application/pdf,image/jpeg,image/png,image/webp"
|
||||
accept="application/pdf"
|
||||
multiple
|
||||
onChange={(event) => setSelectedInvoiceFiles(Array.from(event.target.files ?? []))}
|
||||
/>
|
||||
|
||||
@@ -62,7 +62,7 @@ export function InvoiceUploadCard({
|
||||
<input
|
||||
id="file-upload"
|
||||
className="hidden"
|
||||
accept="application/pdf,image/jpeg,image/png,image/webp"
|
||||
accept="application/pdf"
|
||||
data-testid="invoice-upload-file"
|
||||
multiple
|
||||
onChange={(event) => setSelectedFiles(Array.from(event.target.files ?? []))}
|
||||
@@ -90,6 +90,7 @@ export function InvoiceUploadCard({
|
||||
{selectedFiles.length > 4 ? <div>另有 {selectedFiles.length - 4} 个文件</div> : null}
|
||||
</div>
|
||||
) : null}
|
||||
<p className="text-xs text-zinc-500">发票上传仅支持 PDF;附件材料可在对应业务单据中上传图片或其他证明文件。</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户