import { useEffect, useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { uploadInvoice } from "../api/invoicePool"; import { mapApiErrorMessage } from "../api/errorMessages"; import { isApiError } from "../api/types"; import { PageHeader } from "../components/common/PageHeader"; import { Button } from "../components/ui/button"; import { Spinner } from "../components/ui/spinner"; import { enqueueInvoiceUpload, listQueuedInvoiceUploads, queuedItemToFile, removeQueuedInvoiceUpload, type MobileUploadQueueItem, } from "../mobile/uploadQueue"; function formatTime(value: string) { try { return new Date(value).toLocaleString("zh-CN"); } catch { return value; } } function isLikelyNetworkError(error: unknown) { if (isApiError(error)) { return error.status >= 500; } return true; } export function MobileCapturePage() { const queryClient = useQueryClient(); const [error, setError] = useState(""); const [message, setMessage] = useState(""); const [selectedFile, setSelectedFile] = useState(null); const [isOnline, setIsOnline] = useState(navigator.onLine); const queueQuery = useQuery({ queryKey: ["mobile", "invoice-upload-queue"], queryFn: () => listQueuedInvoiceUploads(), }); const uploadMutation = useMutation({ mutationFn: async (file: File) => uploadInvoice(file, "vat"), onSuccess: () => { setSelectedFile(null); setError(""); setMessage(""); toast.success("上传成功"); }, onError: (err) => { if (isApiError(err)) { const msg = `${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}`; setError(msg); toast.error(msg); return; } const msg = err instanceof Error ? err.message : "上传失败"; setError(msg); toast.error(msg); }, }); const syncMutation = useMutation({ mutationFn: async (items: MobileUploadQueueItem[]) => { let succeeded = 0; let failed = 0; for (const item of items) { try { const file = queuedItemToFile(item); await uploadInvoice(file, "vat"); await removeQueuedInvoiceUpload(item.id); succeeded += 1; } catch { failed += 1; } } return { succeeded, failed }; }, onSuccess: async (result) => { await queryClient.invalidateQueries({ queryKey: ["mobile", "invoice-upload-queue"] }); setError(""); setMessage(""); toast.success(`同步完成:成功 ${result.succeeded},失败 ${result.failed}`); }, onError: (err) => { const msg = err instanceof Error ? err.message : "队列同步失败"; setError(msg); toast.error(msg); }, }); const queuedItems = queueQuery.data ?? []; const queuedCountLabel = useMemo(() => { if (!queuedItems.length) return "无待上传项"; return `待上传 ${queuedItems.length} 项`; }, [queuedItems.length]); useEffect(() => { const onOnline = () => { setIsOnline(true); if (queuedItems.length) { syncMutation.mutate(queuedItems); } }; const onOffline = () => { setIsOnline(false); }; window.addEventListener("online", onOnline); window.addEventListener("offline", onOffline); return () => { window.removeEventListener("online", onOnline); window.removeEventListener("offline", onOffline); }; }, [queuedItems, syncMutation]); return (
{error ? (

{error}

) : null} {message ? (

{message}

) : null}

拍照/选择文件

{isOnline ? "在线" : "离线"}
{ const file = event.target.files?.[0] ?? null; setSelectedFile(file); }} type="file" />
当前文件:{selectedFile ? `${selectedFile.name} (${Math.ceil(selectedFile.size / 1024)} KB)` : "未选择"}

离线上传队列

{queuedCountLabel}
{queueQuery.isLoading ?
加载中...
: null} {!queueQuery.isLoading && queuedItems.length === 0 ? (

暂无待上传文件。

) : null} {queuedItems.map((item) => (
{item.fileName}
创建时间:{formatTime(item.createdAt)}
))}
); }