From 1ecb783d0c768d1079d0dbffc2a2dfe4699e2216 Mon Sep 17 00:00:00 2001 From: Qiufeng Date: Fri, 4 Sep 2026 14:01:58 +0800 Subject: [PATCH] =?UTF-8?q?chore(release):=201.1.33=20-=20=E5=85=A8?= =?UTF-8?q?=E9=9D=A2=E9=87=8D=E5=A1=91=E7=B3=BB=E7=BB=9F=E5=95=86=E5=8A=A1?= =?UTF-8?q?=E4=B8=8E=E4=B8=93=E4=B8=9A=E5=8C=96=E6=96=87=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 2 +- package.json | 2 +- web-next/src/main.tsx | 8 +-- web-next/src/pages/admins/AdminsPage.tsx | 8 +-- web-next/src/pages/audit/AuditPage.tsx | 6 +- .../src/pages/auth/ChangePasswordPage.tsx | 6 +- web-next/src/pages/auth/LoginPage.tsx | 2 +- web-next/src/pages/common.tsx | 2 +- .../src/pages/dashboard/DashboardPage.tsx | 2 +- web-next/src/pages/expenses/ExpenseDetail.tsx | 12 ++-- web-next/src/pages/expenses/ExpenseDrawer.tsx | 2 +- web-next/src/pages/expenses/ExpensesPage.tsx | 8 +-- web-next/src/pages/trash/TrashPage.tsx | 4 +- web-next/src/pages/update/UpdatePage.tsx | 56 +++++++++---------- web-next/src/pages/update/mockData.ts | 6 +- web-next/src/services/api.ts | 4 +- 16 files changed, 65 insertions(+), 65 deletions(-) diff --git a/index.html b/index.html index 60f83a1..06db0e5 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - TallyNote · 采购报销记录 + TallyNote · 采购报销协同管理平台
diff --git a/package.json b/package.json index ba8216c..daa8314 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tallynote", - "version": "1.1.32", + "version": "1.1.33", "private": true, "type": "module", "packageManager": "pnpm@9.0.6", diff --git a/web-next/src/main.tsx b/web-next/src/main.tsx index e27887c..a368431 100644 --- a/web-next/src/main.tsx +++ b/web-next/src/main.tsx @@ -87,10 +87,10 @@ function App() { // Keep the login form mounted for those requests so the user sees the // button's busy state instead of losing the entire form to a bootstrap // spinner. `bootstrapRequestId` is only set by the initial session check. - if (isSessionBootstrapping(session)) return
; - if (session.status === "error") return

无法连接 TallyNote

{session.error || "请确认本地服务正在运行。"}

; + if (isSessionBootstrapping(session)) return
; + if (session.status === "error") return

无法连接服务

{session.error || "服务暂时无法连接,请稍后重试或检查网络状态。"}

; if (!session.admin) return setPasswordOpen(false)} />; if (session.admin.mustChangePassword) return notify("密码已更新", "success")} />; @@ -104,7 +104,7 @@ function App() { : page === "audit" ? : ; - return { if (!passwordOpen) requestDiscard(() => setPasswordOpen(true)); }}>}>
{content}
; + return { if (!passwordOpen) requestDiscard(() => setPasswordOpen(true)); }}>}>
{content}
; } function RouteErrorPage() { diff --git a/web-next/src/pages/admins/AdminsPage.tsx b/web-next/src/pages/admins/AdminsPage.tsx index 4bdde79..cf725d3 100644 --- a/web-next/src/pages/admins/AdminsPage.tsx +++ b/web-next/src/pages/admins/AdminsPage.tsx @@ -77,18 +77,18 @@ export default function AdminsPage({ currentAdmin, timezone = "Asia/Shanghai", n { colKey: "status", title: "状态", cell: ({ row }: any) => }, { colKey: "lastLoginAt", title: "最近登录", cell: ({ row }: any) => row.lastLoginAt ? dateText(row.lastLoginAt, timezone) : "从未登录" }, { colKey: "version", title: "版本", cell: ({ row }: any) => v{row.version} }, - { colKey: "actions", title: "操作", width: 250, cell: ({ row }: any) => }, + { colKey: "actions", title: "操作", width: 250, cell: ({ row }: any) => }, ]; - return }> + return }>

暂无管理员

: undefined} onRetry={() => void load()}>
{ if (!busy) requestDiscard(() => setShowCreate(false)); }} footer={}>
{ void create(); }}> { setForm({ ...form, username: value }); setFormFields(current => ({ ...current, username: undefined })); setFormError(""); }} onEnter={(_, context) => { context.e.preventDefault(); void create(); }} maxlength={64} autocomplete="off" /> { setForm({ ...form, displayName: value }); setFormFields(current => ({ ...current, displayName: undefined })); setFormError(""); }} onEnter={(_, context) => { context.e.preventDefault(); void create(); }} maxlength={80} />{formError &&
{formError}
}
{ if (!busy) setAction(null); }} onConfirm={() => void (action?.kind === "reset" ? reset() : toggle())} onCancel={() => { if (!busy) setAction(null); }}> - {action?.kind === "reset" ? <>将生成一次性临时密码,并立即使“{action.admin.displayName}”的现有会话失效。 : action?.admin.status === "active" ? "停用后该管理员的现有会话会立即失效。" : "启用后该管理员可以重新登录。"} + {action?.kind === "reset" ? <>将生成一次性临时密码,同时让管理员“{action.admin.displayName}”已登录的会话安全退出。 : action?.admin.status === "active" ? "停用后该管理员将无法访问系统,已登录的会话会立即注销。" : "启用后该管理员可恢复系统访问并正常登录。"} - setSecret("")} onConfirm={() => setSecret("")} onCancel={() => setSecret("")}>
{secret}

请通过安全渠道交给管理员。首次登录必须修改密码。

+ setSecret("")} onConfirm={() => setSecret("")} onCancel={() => setSecret("")}>
{secret}

请妥善保管并将临时密码交付给管理员,该密码在首次登录时会被强制更新。

; async function copySecret(value: string) { diff --git a/web-next/src/pages/audit/AuditPage.tsx b/web-next/src/pages/audit/AuditPage.tsx index 2bf540b..8c6d221 100644 --- a/web-next/src/pages/audit/AuditPage.tsx +++ b/web-next/src/pages/audit/AuditPage.tsx @@ -23,7 +23,7 @@ const ACTION_LABELS: Record = { "auth.login_failed": "登录失败", "expense.created": "创建账目", "expense.updated": "更新账目", - "expense.status_changed": "切换报销状态", + "expense.status_changed": "更新报销状态", "expense.trashed": "移入回收站", "expense.restored": "恢复账目", "expense.purged": "永久删除账目", @@ -133,8 +133,8 @@ export default function AuditPage({ timezone = "Asia/Shanghai" }: { timezone?: s { colKey: "outcome", title: "结果", cell: ({ row }: any) => {outcomeLabel(row.outcome)} }, ]; - return void load()} disabled={loading || loadingMore} icon={}>刷新}> -
{ e.preventDefault(); applyFilters(); }}>} /> setTargetDraft(String(v))} options={[{ label: "全部目标", value: "" }, { label: "账目", value: "expense" }, { label: "管理员", value: "admin" }, { label: "导出", value: "export" }, { label: "会话", value: "session" }, { label: "更新任务", value: "update" }, { label: "系统检查", value: "system" }]} />

{action || targetType ? "没有符合当前筛选条件的审计记录" : "暂无审计记录"}

{(action || targetType) && } : undefined} onRetry={() => void load()}>
{hasMore &&
}; } diff --git a/web-next/src/pages/auth/ChangePasswordPage.tsx b/web-next/src/pages/auth/ChangePasswordPage.tsx index f42227b..6078142 100644 --- a/web-next/src/pages/auth/ChangePasswordPage.tsx +++ b/web-next/src/pages/auth/ChangePasswordPage.tsx @@ -108,14 +108,14 @@ export default function ChangePasswordPage({ admin, onSuccess, onCancel, returnL ; if (!isFirstLogin) { - return }>{returnLabel} : undefined} className="tn-password-page">{panel}; + return }>{returnLabel} : undefined} className="tn-password-page">{panel}; } return
-

首次登录保护

-

管理员 {displayName} 需要先设置新密码。

+

初始安全设置

+

欢迎使用系统,管理员 {displayName},为保障账户安全,首次登录请先设置新密码。

{panel}
diff --git a/web-next/src/pages/auth/LoginPage.tsx b/web-next/src/pages/auth/LoginPage.tsx index 9494766..05b9ffd 100644 --- a/web-next/src/pages/auth/LoginPage.tsx +++ b/web-next/src/pages/auth/LoginPage.tsx @@ -76,7 +76,7 @@ export default function LoginPage({ notice, onSuccess }: LoginPageProps) {
-

登录到 TallyNote

+

登录 TallyNote 工作台

; if (error && empty) return

{error}

{onRetry && }
; return <> - {loading &&
} + {loading &&
} {error && } {empty || children} ; diff --git a/web-next/src/pages/dashboard/DashboardPage.tsx b/web-next/src/pages/dashboard/DashboardPage.tsx index 8e3e874..3c8d6d5 100644 --- a/web-next/src/pages/dashboard/DashboardPage.tsx +++ b/web-next/src/pages/dashboard/DashboardPage.tsx @@ -140,7 +140,7 @@ export default function DashboardPage({ timezone = "Asia/Shanghai", onNavigate }
{error && hasCurrentSnapshot && } message={`刷新失败:${error}。当前展示的是本月最近一次成功加载的数据。`} />} - {loading ?
: !hasCurrentSnapshot ?
} message={error || "暂时无法读取仪表盘数据"} />
: <> + {loading ?
: !hasCurrentSnapshot ?
} message={error || "暂时无法读取仪表盘数据"} />
: <>
diff --git a/web-next/src/pages/expenses/ExpenseDetail.tsx b/web-next/src/pages/expenses/ExpenseDetail.tsx index b5cb2fb..6b54813 100644 --- a/web-next/src/pages/expenses/ExpenseDetail.tsx +++ b/web-next/src/pages/expenses/ExpenseDetail.tsx @@ -10,7 +10,7 @@ type Props = { expense: Expense; timezone?: string; onClose: () => void; onUpdat const TIMELINE_LABELS: Record = { "expense.created": "创建账目", "expense.updated": "更新账目", - "expense.status_changed": "切换报销状态", + "expense.status_changed": "更新报销状态", "expense.trashed": "移入回收站", "expense.restored": "从回收站恢复", "attachment.added": "添加附件", @@ -49,7 +49,7 @@ export default function ExpenseDetail({ expense, timezone = "Asia/Shanghai", onC const current = (caught.details as { current?: Expense } | undefined)?.current; if (!current) return false; setDetail(current); - setMessage("这笔账目刚被其他管理员修改,已加载最新版本,请确认后重试。"); + setMessage("此笔账目已被其他管理员更新,已为您自动同步最新记录,请确认后重试。"); return true; }; const updateStatus = async () => { setBusy(true); try { const next = detail.status === "reimbursed" ? "unreimbursed" : "reimbursed"; const result = await api<{ expense: Expense }>(`/api/expenses/${detail.id}/status`, { method: "POST", body: JSON.stringify({ status: next, version: detail.version }) }); setDetail(result.expense); setAction(null); notify?.(next === "reimbursed" ? "已标记为已报销" : "已改回未报销", "success"); onUpdated(); } catch (caught) { if (!recoverConflict(caught, setError)) setError((caught as Error).message); setAction(null); } finally { setBusy(false); } }; @@ -57,16 +57,16 @@ export default function ExpenseDetail({ expense, timezone = "Asia/Shanghai", onC const remove = async () => { if (!removeTarget) return; const requires = removeTarget.kind === "invoice" && detail.invoiceCount <= 1 && !detail.invoiceMissingReason?.trim(); if (requires && !removeReason.trim()) { setRemoveError("请填写无发票原因"); return; } setBusy(true); setRemoveError(""); try { const body: { version: number; invoiceMissingReason?: string } = { version: detail.version }; if (requires) body.invoiceMissingReason = removeReason.trim(); const result = await api<{ expense: Expense }>(`/api/attachments/${removeTarget.id}`, { method: "DELETE", body: JSON.stringify(body) }); setDetail(result.expense); setRemoveTarget(null); notify?.("附件已删除", "success"); onUpdated(); } catch (caught) { if (!recoverConflict(caught, setRemoveError)) setRemoveError((caught as Error).message); } finally { setBusy(false); } }; return <> }> - {loading ?
: error ?
{error}
:
+ {loading ?
: error ?
{error}
:
{money(detail.amountCents)}
支付时间
{dateText(detail.paidAt, timezone)}
发票
{detail.invoiceCount > 0 ? `${detail.invoiceCount} 张` : detail.invoiceMissingReason ? <>无发票:{detail.invoiceMissingReason} : 未说明}
状态
{detail.status === "reimbursed" ? "已报销" : "未报销"}
备注
{detail.note || "无"}

附件 {detail.attachments?.length || 0}

{(detail.attachments || []).map(a => { const protectsLastProof = a.kind === "payment_proof" && detail.paymentProofCount <= 1; return
{a.mimeType.startsWith("image/") ? : } {a.originalName}{formatBytes(a.sizeBytes)}{a.previewable && }
; })}
{timeline.length > 0 && <>

操作记录

{timeline.slice(0, 12).map(t =>
{dateText(t.occurredAt, timezone)}{TIMELINE_LABELS[t.action] || t.action}{t.actorUsername || "系统"}
)}
}
} - { if (!busy) setAction(null); }} onConfirm={() => void updateStatus()} onCancel={() => { if (!busy) setAction(null); }}>{detail.status === "reimbursed" ? "这笔账目会重新出现在未报销列表。" : "确认这笔账目已完成报销,并从未报销列表移出?"} - { if (!busy) setAction(null); }} onConfirm={() => void trash()} onCancel={() => { if (!busy) setAction(null); }}>账目会从普通列表和导出结果中隐藏,附件会保留,可在回收站恢复。 - { if (!busy) setRemoveTarget(null); }} onConfirm={() => void remove()} onCancel={() => { if (!busy) setRemoveTarget(null); }}>{removeTarget && <>

{removeTarget.kind === "payment_proof" ? "账目至少需要保留一张付款凭证。" : detail.invoiceCount <= 1 && !detail.invoiceMissingReason?.trim() ? "这是最后一张发票。删除后必须填写无发票原因。" : "将删除这张发票。"}

{removeTarget.kind === "invoice" && detail.invoiceCount <= 1 && !detail.invoiceMissingReason?.trim() &&