This commit is contained in:
MarkLo 2025-12-16 18:45:43 +08:00
parent 5569f1097c
commit 488eeac64c
1 changed files with 211 additions and 12 deletions

View File

@ -3,7 +3,7 @@
*/
"use client";
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import { useRouter } from "next/navigation";
import { format } from "date-fns";
import { zhTW } from "date-fns/locale";
@ -34,7 +34,7 @@ import {
getReportCountByMarketType,
type SavedReport,
} from "@/lib/reports-db";
import { getCloudReports, deleteCloudReport, isCloudSyncEnabled } from "@/lib/user-api";
import { getCloudReports, deleteCloudReport, saveCloudReport, isCloudSyncEnabled } from "@/lib/user-api";
import { LoginPrompt } from "@/components/auth/login-button";
import { PendingTaskRecovery } from "@/components/PendingTaskRecovery";
@ -151,6 +151,11 @@ export default function HistoryPage() {
null
);
const [deleting, setDeleting] = useState(false);
// Sync state
const [syncing, setSyncing] = useState(false);
const [syncResult, setSyncResult] = useState<{ success: number; failed: number } | null>(null);
const hasAutoSyncedRef = useRef(false);
// Load reports when tab changes or auth state changes
useEffect(() => {
@ -161,6 +166,75 @@ export default function HistoryPage() {
useEffect(() => {
loadCounts();
}, [isAuthenticated]);
// Auto-sync local reports to cloud when page loads (if authenticated)
useEffect(() => {
const autoSync = async () => {
// Only sync once per session, and only if authenticated
if (hasAutoSyncedRef.current || !isAuthenticated || !isCloudSyncEnabled()) {
return;
}
hasAutoSyncedRef.current = true;
try {
// Get all local reports
const [usLocal, twseLocal, tpexLocal] = await Promise.all([
getReportsByMarketType("us"),
getReportsByMarketType("twse"),
getReportsByMarketType("tpex"),
]);
const allLocal = [...usLocal, ...twseLocal, ...tpexLocal];
if (allLocal.length === 0) return;
// Get cloud reports to check for duplicates
const cloudReports = await getCloudReports();
const cloudKeys = new Set(
cloudReports.map(r => `${r.ticker}_${r.analysis_date}`)
);
// Find local-only reports to upload
const toUpload = allLocal.filter(
r => !cloudKeys.has(`${r.ticker}_${r.analysis_date}`)
);
if (toUpload.length === 0) {
console.log("☁️ Auto-sync: All reports already in cloud");
return;
}
console.log(`☁️ Auto-sync: Uploading ${toUpload.length} local reports to cloud...`);
// Upload each report silently
let success = 0;
for (const report of toUpload) {
try {
const cloudId = await saveCloudReport({
ticker: report.ticker,
market_type: report.market_type,
analysis_date: report.analysis_date,
result: report.result,
});
if (cloudId) success++;
} catch (e) {
// Silently continue on error
}
}
if (success > 0) {
console.log(`☁️ Auto-sync: Successfully uploaded ${success} reports`);
// Reload to show updated data
await loadReports();
await loadCounts();
}
} catch (error) {
console.error("☁️ Auto-sync failed:", error);
}
};
autoSync();
}, [isAuthenticated]);
const loadReports = async () => {
setLoading(true);
@ -227,26 +301,134 @@ export default function HistoryPage() {
const loadCounts = async () => {
try {
// Always get local counts first
const localCounts = await getReportCountByMarketType();
if (isAuthenticated && isCloudSyncEnabled()) {
const cloudReports = await getCloudReports();
const cloudCounts = {
us: cloudReports.filter(r => r.market_type === "us").length,
twse: cloudReports.filter(r => r.market_type === "twse").length,
tpex: cloudReports.filter(r => r.market_type === "tpex").length,
};
if (cloudReports.length > 0) {
setCounts(cloudCounts);
// Get local reports to check for duplicates
const [usLocal, twseLocal, tpexLocal] = await Promise.all([
getReportsByMarketType("us"),
getReportsByMarketType("twse"),
getReportsByMarketType("tpex"),
]);
// Cloud report keys for deduplication
const cloudKeys = new Set(
cloudReports.map(r => `${r.ticker}_${r.analysis_date}_${r.market_type}`)
);
// Count local-only reports (not in cloud)
const usLocalOnly = usLocal.filter(
r => !cloudKeys.has(`${r.ticker}_${r.analysis_date}_us`)
).length;
const twseLocalOnly = twseLocal.filter(
r => !cloudKeys.has(`${r.ticker}_${r.analysis_date}_twse`)
).length;
const tpexLocalOnly = tpexLocal.filter(
r => !cloudKeys.has(`${r.ticker}_${r.analysis_date}_tpex`)
).length;
// Cloud counts
const usCoud = cloudReports.filter(r => r.market_type === "us").length;
const twseCloud = cloudReports.filter(r => r.market_type === "twse").length;
const tpexCloud = cloudReports.filter(r => r.market_type === "tpex").length;
// Merged counts: cloud + local-only
setCounts({
us: usCoud + usLocalOnly,
twse: twseCloud + twseLocalOnly,
tpex: tpexCloud + tpexLocalOnly,
});
return;
}
}
const data = await getReportCountByMarketType();
setCounts(data);
setCounts(localCounts);
} catch (error) {
console.error("Failed to load counts:", error);
}
};
// Sync local reports to cloud
const handleSyncToCloud = async () => {
if (!isAuthenticated || !isCloudSyncEnabled()) {
alert("請先登入以啟用雲端同步");
return;
}
setSyncing(true);
setSyncResult(null);
try {
// Get all local reports
const [usLocal, twseLocal, tpexLocal] = await Promise.all([
getReportsByMarketType("us"),
getReportsByMarketType("twse"),
getReportsByMarketType("tpex"),
]);
const allLocal = [...usLocal, ...twseLocal, ...tpexLocal];
// Get cloud reports to check for duplicates
const cloudReports = await getCloudReports();
const cloudKeys = new Set(
cloudReports.map(r => `${r.ticker}_${r.analysis_date}`)
);
// Find local-only reports to upload
const toUpload = allLocal.filter(
r => !cloudKeys.has(`${r.ticker}_${r.analysis_date}`)
);
if (toUpload.length === 0) {
setSyncResult({ success: 0, failed: 0 });
alert("所有報告已同步到雲端!");
return;
}
// Upload each report
let success = 0;
let failed = 0;
for (const report of toUpload) {
try {
const cloudId = await saveCloudReport({
ticker: report.ticker,
market_type: report.market_type,
analysis_date: report.analysis_date,
result: report.result,
});
if (cloudId) {
success++;
} else {
failed++;
}
} catch (e) {
failed++;
}
}
setSyncResult({ success, failed });
// Reload data after sync
await loadReports();
await loadCounts();
if (failed === 0) {
alert(`成功同步 ${success} 份報告到雲端!`);
} else {
alert(`同步完成:${success} 成功,${failed} 失敗`);
}
} catch (error) {
console.error("Sync failed:", error);
alert("同步失敗,請稍後再試");
} finally {
setSyncing(false);
}
};
const handleViewReport = (report: SavedReport) => {
// Set the context with the saved report data
setAnalysisResult(report.result);
@ -410,8 +592,25 @@ export default function HistoryPage() {
(marketType) => (
<TabsContent key={marketType} value={marketType} className="mt-6">
<div className="space-y-4">
{/* Refresh button */}
<div className="flex justify-end">
{/* Action buttons */}
<div className="flex justify-end gap-2">
{/* Sync to Cloud button - only show when authenticated */}
{isAuthenticated && (
<Button
variant="outline"
size="sm"
onClick={handleSyncToCloud}
disabled={syncing || loading}
className="gap-2"
>
<Cloud
className={`h-4 w-4 ${syncing ? "animate-pulse" : ""}`}
/>
{syncing ? "同步中..." : "同步到雲端"}
</Button>
)}
{/* Refresh button */}
<Button
variant="outline"
size="sm"