Add recovery Excel export and automatic account detection

This commit is contained in:
巫凤萍
2026-08-05 11:40:29 +08:00
parent b081946752
commit 85110bbfce
15 changed files with 852 additions and 36 deletions

View File

@@ -8,7 +8,10 @@ import {
useMemo,
useState,
} from "react";
import { formatShanghaiDate as formatDate } from "../lib/date-utils";
import {
formatShanghaiDate as formatDate,
formatShanghaiToday,
} from "../lib/date-utils";
type Partner = {
id: string;
@@ -317,6 +320,7 @@ export default function Home() {
const [sourcePreview, setSourcePreview] = useState<FeishuPreview | null>(null);
const [sourceWorking, setSourceWorking] = useState(false);
const [metricForm, setMetricForm] = useState({ exposure: "", views: "" });
const [exportingTaskId, setExportingTaskId] = useState<string | null>(null);
const loadData = useCallback(async () => {
try {
@@ -502,6 +506,37 @@ export default function Home() {
);
};
const exportRecoveryData = async (task: Task) => {
try {
setExportingTaskId(task.id);
const response = await fetch(
`/api/recovery-export?task=${encodeURIComponent(task.id)}`,
{ cache: "no-store" },
);
if (!response.ok) {
const result = await readApiResponse<{ error?: string }>(
response,
"导出失败",
);
throw new Error(result.error || "导出失败");
}
const blob = await response.blob();
const objectUrl = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = objectUrl;
anchor.download = `${task.name}-数据回收.xlsx`;
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
setToast("Excel 已导出,图片和截图已嵌入表格");
} catch (reason) {
setToast(reason instanceof Error ? reason.message : "导出失败");
} finally {
setExportingTaskId(null);
}
};
const uploadScreenshot = async (
distribution: Distribution,
event: ChangeEvent<HTMLInputElement>,
@@ -654,7 +689,7 @@ export default function Home() {
{activeNav === "overview" && (
<div className="date-chip">
<span></span>
<strong>727</strong>
<strong>{formatShanghaiToday()}</strong>
</div>
)}
</div>
@@ -710,6 +745,8 @@ export default function Home() {
onUpload={uploadScreenshot}
onFallback={openMetricFallback}
onRetryFailed={retryFailedMetrics}
onExport={exportRecoveryData}
exportingTaskId={exportingTaskId}
/>
)}
</>
@@ -1373,6 +1410,8 @@ function RecoveryPage({
onUpload,
onFallback,
onRetryFailed,
onExport,
exportingTaskId,
}: {
tasks: Task[];
distributions: Distribution[];
@@ -1386,6 +1425,8 @@ function RecoveryPage({
onUpload: (distribution: Distribution, event: ChangeEvent<HTMLInputElement>) => void;
onFallback: (distribution: Distribution) => void;
onRetryFailed: (taskId: string, failedCount: number) => void;
onExport: (task: Task) => void;
exportingTaskId: string | null;
}) {
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
const selectedTask = tasks.find((task) => task.id === selectedTaskId);
@@ -1431,16 +1472,27 @@ function RecoveryPage({
<h2></h2>
<p>{taskDistributions.length} · </p>
</div>
<button
className="retry-failed-button"
disabled={working || failedCollectionCount === 0}
onClick={() =>
onRetryFailed(selectedTask.id, failedCollectionCount)
}
>
{failedCollectionCount > 0 && <b>{failedCollectionCount}</b>}
</button>
<div className="recovery-panel-actions">
<button
className="export-data-button"
disabled={working || exportingTaskId === selectedTask.id}
onClick={() => onExport(selectedTask)}
>
{exportingTaskId === selectedTask.id
? "正在生成 Excel…"
: "导出全部数据"}
</button>
<button
className="retry-failed-button"
disabled={working || failedCollectionCount === 0}
onClick={() =>
onRetryFailed(selectedTask.id, failedCollectionCount)
}
>
{failedCollectionCount > 0 && <b>{failedCollectionCount}</b>}
</button>
</div>
</div>
<div className="table-scroll">
<table className="data-table recovery-table">