修复 R7E 真机反馈回流与移动入口 WF8-11A R7E-16A R7E-16B BIZ8-12A

这个提交包含在:
2026-05-14 21:53:20 +08:00
父节点 45900e0673
当前提交 5491e922e6
共修改 26 个文件,包含 281 行新增和 56 行删除
+15 -2
查看文件
@@ -9,6 +9,7 @@ import { MobileIcon } from "../components/mobile/MobileIcon";
import { PageScaffold } from "../components/patterns/PageScaffold";
import { Button } from "../components/ui/button";
import { Spinner } from "../components/ui/spinner";
import { PDF_FILE_ACCEPT, rejectedPdfMessage, splitPdfFiles } from "../lib/pdfFiles";
import { withUploadTimeout } from "../lib/uploadTimeout";
import {
enqueueInvoiceUpload,
@@ -145,6 +146,17 @@ export function MobileCapturePage() {
const queuedItems = queueQuery.data ?? [];
function handlePdfSelection(files: FileList | null) {
const { accepted, rejected } = splitPdfFiles(files);
setSelectedFiles(accepted);
const rejectMessage = rejectedPdfMessage(rejected);
setError(rejectMessage);
setMessage("");
if (rejectMessage) {
toast.warning(rejectMessage);
}
}
const queuedCountLabel = useMemo(() => {
if (!queuedItems.length) return "无待上传项";
return `待上传 ${queuedItems.length} 项`;
@@ -210,12 +222,13 @@ export function MobileCapturePage() {
</label>
<input
id="mobile-capture-pdf-file"
accept="application/pdf"
accept={PDF_FILE_ACCEPT}
className="sr-only"
data-testid="mobile-capture-pdf-file"
multiple
onChange={(event) => {
setSelectedFiles(Array.from(event.target.files ?? []));
handlePdfSelection(event.target.files);
event.target.value = "";
}}
type="file"
/>
+3 -1
查看文件
@@ -6,7 +6,7 @@ import { fetchEmployeeProfile } from "../api/employees";
import { mapApiErrorMessage } from "../api/errorMessages";
import { isApiError } from "../api/types";
import { useAuth } from "../auth/AuthContext";
import { hasAnyPermission, hasBusinessApproverRole } from "../auth/permissions";
import { hasAnyPermission, hasBusinessApproverRole, hasFinanceRole } from "../auth/permissions";
import { EmployeeAvatar } from "../components/common/EmployeeAvatar";
import { PageHeader } from "../components/common/PageHeader";
import { MobileIcon } from "../components/mobile/MobileIcon";
@@ -65,6 +65,7 @@ export function MobileMePage() {
const canAccessProjects = hasAnyPermission(permissions, ["project.view_own", "project.view_all", "project.manage"]);
const canAccessPurchase = hasAnyPermission(permissions, [...MOBILE_PURCHASE_PERMISSIONS]);
const canAccessApprovals = hasBusinessApproverRole(me?.roleCodes, permissions);
const canAccessFinance = hasFinanceRole(me?.roleCodes, permissions);
const profileQuery = useQuery({
enabled: Boolean(me?.id),
@@ -128,6 +129,7 @@ export function MobileMePage() {
<MobileActionLink to="/mobile/reimbursements" label="报销" icon="request_quote" />
{canAccessPurchase ? <MobileActionLink to="/mobile/purchase-requests" label="采购" icon="shopping_cart" /> : null}
<MobileActionLink to="/mobile/approvals" label={canAccessApprovals ? "审批" : "审批进度"} icon="fact_check" />
{canAccessFinance ? <MobileActionLink to="/mobile#finance" label="财务" description="审批与台账" icon="account_balance_wallet" /> : null}
{canAccessAssets ? <MobileActionLink to="/mobile/assets/scan" label="资产扫码" icon="inventory_2" /> : null}
{canAccessProjects ? <MobileActionLink to="/mobile" label="项目动态" icon="workspaces" /> : null}
</div>
+24 -15
查看文件
@@ -257,6 +257,7 @@ export function MobilePortalPage() {
.filter((item) => !(canAccessApprovals && item.type.startsWith("workflow.task_")))
.slice(0, 3);
const financeSummary = portalHomeQuery.data?.financeSummary ?? null;
const paymentBatchEnabled = Boolean(financeSummary?.paymentBatchEnabled);
async function openHomeNotification(item: NotificationItem) {
const targetLink = resolveMobileNotificationTargetLink(item);
@@ -378,10 +379,10 @@ export function MobilePortalPage() {
) : null}
{canUseFinance ? (
<section className="mobile-card mobile-page-stack" data-testid="mobile-finance-summary">
<section id="finance" className="mobile-card mobile-page-stack" data-testid="mobile-finance-summary">
<div className="mobile-section-header">
<div className="mobile-section-title">财务</div>
<span className="badge-neutral">复核与付款</span>
<span className="badge-neutral">{paymentBatchEnabled ? "复核与付款" : "审批与台账"}</span>
</div>
{portalHomeQuery.isLoading ? <p className="text-sm text-zinc-500">读取中...</p> : null}
{portalHomeQuery.isError ? (
@@ -389,22 +390,30 @@ export function MobilePortalPage() {
{resolveErrorMessage(portalHomeQuery.error)}
</p>
) : null}
<div className="mobile-metric-grid text-sm">
<div className="rounded-lg bg-zinc-50 px-3 py-2">
<div className="text-xs text-zinc-500">待付款</div>
<div className="mt-1 text-xl font-semibold text-zinc-900">{financeSummary?.pendingBatchCount ?? 0}</div>
</div>
<div className="rounded-lg bg-zinc-50 px-3 py-2">
<div className="text-xs text-zinc-500">异常</div>
<div className="mt-1 text-xl font-semibold text-zinc-900">
{(financeSummary?.failedItemCount ?? 0) + (financeSummary?.riskCount ?? 0)}
{!portalHomeQuery.isLoading && !portalHomeQuery.isError && paymentBatchEnabled ? (
<div className="mobile-metric-grid text-sm">
<div className="rounded-lg bg-zinc-50 px-3 py-2">
<div className="text-xs text-zinc-500">待付款</div>
<div className="mt-1 text-xl font-semibold text-zinc-900">{financeSummary?.pendingBatchCount ?? 0}</div>
</div>
<div className="rounded-lg bg-zinc-50 px-3 py-2">
<div className="text-xs text-zinc-500">异常</div>
<div className="mt-1 text-xl font-semibold text-zinc-900">
{(financeSummary?.failedItemCount ?? 0) + (financeSummary?.riskCount ?? 0)}
</div>
</div>
<div className="rounded-lg bg-zinc-50 px-3 py-2">
<div className="text-xs text-zinc-500">台账/导出</div>
<div className="mt-1 text-xl font-semibold text-zinc-900">可用</div>
</div>
</div>
<div className="rounded-lg bg-zinc-50 px-3 py-2">
<div className="text-xs text-zinc-500">台账/导出</div>
<div className="mt-1 text-xl font-semibold text-zinc-900">可用</div>
) : null}
{!portalHomeQuery.isLoading && !portalHomeQuery.isError && !paymentBatchEnabled ? (
<div className="mobile-subcard text-sm text-zinc-600" data-testid="mobile-finance-disabled-state">
<div className="font-medium text-zinc-900">付款批次未启用</div>
<div className="mt-1 leading-5">可查看已流转到财务审批的报销单,待付款和异常批次暂不展示。</div>
</div>
</div>
) : null}
</section>
) : null}
@@ -45,6 +45,7 @@ import {
} from "../components/ui/dialog";
import { Input } from "../components/ui/input";
import { Textarea } from "../components/ui/textarea";
import { PDF_FILE_ACCEPT, rejectedPdfMessage, splitPdfFiles } from "../lib/pdfFiles";
import { buildPrecheckDialogMessage } from "../lib/precheck";
import {
formatInvoiceDisplayTitle,
@@ -224,6 +225,16 @@ export function MobileReimbursementDetailPage() {
const canManageAttachments = Boolean(canEdit && (isParticipant || canViewAllExpense));
const selectedInvoiceEntries = Object.entries(selectedInvoices);
function handleInvoicePdfSelection(files: FileList | null) {
const { accepted, rejected } = splitPdfFiles(files);
setSelectedInvoiceFiles(accepted);
const message = rejectedPdfMessage(rejected);
setInvoiceActionError(message);
if (message) {
toast.warning(message);
}
}
const bindableInvoicesQuery = useQuery({
enabled: Boolean(reimbursementId && isInvoiceDialogOpen && canOpenInvoiceDialog),
queryKey: ["invoice-pool", "bindable", reimbursementId, invoiceSearch],
@@ -1266,9 +1277,12 @@ export function MobileReimbursementDetailPage() {
className="sr-only"
data-testid={TEST_IDS.reimbursement.invoiceUploadFile}
type="file"
accept="application/pdf"
accept={PDF_FILE_ACCEPT}
multiple
onChange={(event) => setSelectedInvoiceFiles(Array.from(event.target.files ?? []))}
onChange={(event) => {
handleInvoicePdfSelection(event.target.files);
event.target.value = "";
}}
/>
{selectedInvoiceFiles.length ? (
+16 -2
查看文件
@@ -63,6 +63,7 @@ import {
import { useInvoiceOcrTracker } from "../hooks/useInvoiceOcrTracker";
import { resolveReimbursementDisplayCode } from "../lib/reimbursementDisplay";
import { TEST_IDS } from "../lib/testIds";
import { PDF_FILE_ACCEPT, rejectedPdfMessage, splitPdfFiles } from "../lib/pdfFiles";
import { ReimbursementActions, type PayPayload } from "./reimbursement-detail/ReimbursementActions";
import {
ReimbursementItemEditor,
@@ -806,6 +807,16 @@ export function ReimbursementDetailPage() {
const submitInvoiceNotice = canEditItems && detail.invoices.length < 1 ? missingInvoiceSubmitMessage : "";
const selectedInvoiceEntries = Object.entries(selectedInvoices);
function handleInvoicePdfSelection(files: FileList | null) {
const { accepted, rejected } = splitPdfFiles(files);
setSelectedInvoiceFiles(accepted);
const message = rejectedPdfMessage(rejected);
setActionError(message);
if (message) {
toast.warning(message);
}
}
const expenseTypes = expenseTypesQuery.data?.items ?? [];
const computeEditorDefaults = (item?: ReimbursementItem): ItemEditorInitialValues => {
@@ -1730,9 +1741,12 @@ export function ReimbursementDetailPage() {
className="hidden"
data-testid={TEST_IDS.reimbursement.invoiceUploadFile}
type="file"
accept="application/pdf"
accept={PDF_FILE_ACCEPT}
multiple
onChange={(event) => setSelectedInvoiceFiles(Array.from(event.target.files ?? []))}
onChange={(event) => {
handleInvoicePdfSelection(event.target.files);
event.target.value = "";
}}
/>
<Button
data-testid={TEST_IDS.reimbursement.invoiceUploadSubmit}
@@ -1,5 +1,6 @@
import type { FetchMyDelegationsResponse } from "../../api/delegations";
import { Button } from "../../components/ui/button";
import { PDF_FILE_ACCEPT, splitPdfFiles } from "../../lib/pdfFiles";
export type InvoiceUploadCardProps = {
selectedFiles: File[];
@@ -62,10 +63,13 @@ export function InvoiceUploadCard({
<input
id="file-upload"
className="hidden"
accept="application/pdf"
accept={PDF_FILE_ACCEPT}
data-testid="invoice-upload-file"
multiple
onChange={(event) => setSelectedFiles(Array.from(event.target.files ?? []))}
onChange={(event) => {
setSelectedFiles(splitPdfFiles(event.target.files).accepted);
event.target.value = "";
}}
type="file"
/>