From 555d3fa68fe2f5fb28f83eb48d0fb00aed1a6246 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Sat, 6 Jun 2026 23:15:55 +0800 Subject: [PATCH] Show user feedback history in account --- frontend/components/account/AccountCenter.tsx | 8 + .../account/AccountFeedbackPanel.tsx | 145 ++++++++++++++++++ .../__tests__/accountFeedbackPanel.test.ts | 34 ++++ frontend/components/account/account-copy.ts | 4 + .../UserFeedbackStatusButton.tsx | 2 +- .../feedbackStatusNotification.test.ts | 2 + .../scan-terminal/feedback-status.ts | 2 + 7 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 frontend/components/account/AccountFeedbackPanel.tsx create mode 100644 frontend/components/account/__tests__/accountFeedbackPanel.test.ts diff --git a/frontend/components/account/AccountCenter.tsx b/frontend/components/account/AccountCenter.tsx index 8eecb7cd..705f6ef3 100644 --- a/frontend/components/account/AccountCenter.tsx +++ b/frontend/components/account/AccountCenter.tsx @@ -52,6 +52,7 @@ import { WALLETCONNECT_PROJECT_ID, } from "./constants"; import { InfoRow, PlusIcon } from "./AccountInfoRow"; +import { AccountFeedbackPanel } from "./AccountFeedbackPanel"; import { chainIdToDisplayName, clearStoredPaymentRecovery, @@ -943,6 +944,13 @@ export function AccountCenter() { )} + + {/* Telegram Bot Section & Payment Details */} {showSecondarySections ? (
([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [available, setAvailable] = useState(true); + + const load = useCallback(async (signal?: AbortSignal) => { + if (typeof fetch !== "function") return; + setLoading(true); + setError(""); + try { + const res = await fetch("/api/feedback?limit=10", { + cache: "no-store", + headers: { Accept: "application/json" }, + signal, + }); + if (res.status === 401 || res.status === 403) { + setAvailable(false); + setEntries([]); + return; + } + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const payload = (await res.json()) as UserFeedbackPayload; + setEntries(Array.isArray(payload.feedback) ? payload.feedback : []); + setAvailable(true); + } catch (err) { + if (signal?.aborted) return; + setError(String(err).slice(0, 140)); + } finally { + if (!signal?.aborted) setLoading(false); + } + }, []); + + useEffect(() => { + const controller = new AbortController(); + void load(controller.signal); + return () => controller.abort(); + }, [load]); + + const emptyText = useMemo(() => { + if (loading) return isEn ? "Loading feedback..." : "正在加载反馈..."; + return isEn ? "No submitted feedback yet." : "暂无已提交反馈。"; + }, [isEn, loading]); + + if (!available) return null; + + return ( +
+
+
+

+ + {title} +

+

{description}

+
+ +
+ + {error && ( +
+ {isEn ? "Failed to load feedback: " : "反馈加载失败:"}{error} +
+ )} + + {entries.length === 0 ? ( +
+ {emptyText} +
+ ) : ( +
+ {entries.map((entry) => ( +
+
+
+ + {feedbackStatusLabel(entry.status, isEn)} + + + {categoryLabel(entry.category, isEn)} + + + {compactDate(entry.created_at)} + +
+

+ {entry.message || (isEn ? "Feedback" : "反馈")} +

+
+
+
{compactDate(entry.updated_at)}
+
+ {isEn ? "Updated" : "最近更新"} +
+
+
+ ))} +
+ )} +
+ ); +} diff --git a/frontend/components/account/__tests__/accountFeedbackPanel.test.ts b/frontend/components/account/__tests__/accountFeedbackPanel.test.ts new file mode 100644 index 00000000..8dcd53ee --- /dev/null +++ b/frontend/components/account/__tests__/accountFeedbackPanel.test.ts @@ -0,0 +1,34 @@ +import fs from "node:fs"; +import path from "node:path"; + +function assert(condition: unknown, message: string): asserts condition { + if (!condition) throw new Error(message); +} + +export function runTests() { + const projectRoot = process.cwd(); + const accountDir = path.join(projectRoot, "components", "account"); + const accountCenterSource = fs.readFileSync( + path.join(accountDir, "AccountCenter.tsx"), + "utf8", + ); + const feedbackPanelPath = path.join(accountDir, "AccountFeedbackPanel.tsx"); + + assert(fs.existsSync(feedbackPanelPath), "account center must ship a My Feedback panel component"); + + const feedbackPanelSource = fs.readFileSync(feedbackPanelPath, "utf8"); + assert( + accountCenterSource.includes('import { AccountFeedbackPanel } from "./AccountFeedbackPanel";') && + accountCenterSource.includes(" { weeklyRewards: isEn ? "Referral Rewards" : "邀请奖励", membershipDetails: isEn ? "Membership Details" : "会员权限详情", identityStatus: isEn ? "Identity Status" : "身份状态", + accountFeedbackTitle: isEn ? "My Feedback" : "我的反馈", + accountFeedbackDescription: isEn + ? "Recent reports you submitted and their current handling status." + : "你最近提交过的反馈及当前处理状态。", authMode: isEn ? "Auth Mode" : "鉴权模式", weatherEngine: isEn ? "Weather Engine" : "气象引擎", intradayAnalysis: isEn ? "Intraday Analysis" : "今日内分析", diff --git a/frontend/components/dashboard/scan-terminal/UserFeedbackStatusButton.tsx b/frontend/components/dashboard/scan-terminal/UserFeedbackStatusButton.tsx index 8bb7ba76..a248bb59 100644 --- a/frontend/components/dashboard/scan-terminal/UserFeedbackStatusButton.tsx +++ b/frontend/components/dashboard/scan-terminal/UserFeedbackStatusButton.tsx @@ -6,12 +6,12 @@ import type { UserFeedbackEntry, UserFeedbackPayload } from "@/types/ops"; import { buildFeedbackNotificationKey, countUnseenFeedbackUpdates, + FEEDBACK_STATUS_POLL_MS, feedbackStatusLabel, feedbackStatusTone, } from "./feedback-status"; const FEEDBACK_STATUS_SEEN_KEY = "polyweather_feedback_status_seen_v1"; -const FEEDBACK_STATUS_POLL_MS = 60_000; function loadSeenKeys() { if (typeof window === "undefined") return new Set(); diff --git a/frontend/components/dashboard/scan-terminal/__tests__/feedbackStatusNotification.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/feedbackStatusNotification.test.ts index fd2ebefb..59266107 100644 --- a/frontend/components/dashboard/scan-terminal/__tests__/feedbackStatusNotification.test.ts +++ b/frontend/components/dashboard/scan-terminal/__tests__/feedbackStatusNotification.test.ts @@ -1,6 +1,7 @@ import { buildFeedbackNotificationKey, countUnseenFeedbackUpdates, + FEEDBACK_STATUS_POLL_MS, feedbackStatusLabel, } from "@/components/dashboard/scan-terminal/feedback-status"; @@ -9,6 +10,7 @@ function assert(condition: unknown, message: string): asserts condition { } export function runTests() { + assert(FEEDBACK_STATUS_POLL_MS === 600_000, "feedback bell background polling should run every 10 minutes"); assert(feedbackStatusLabel("open", false) === "已收到", "open feedback should read as received to users"); assert(feedbackStatusLabel("triaged", false) === "已确认", "triaged feedback should read as confirmed to users"); assert(feedbackStatusLabel("investigating", false) === "处理中", "investigating feedback should read as in progress"); diff --git a/frontend/components/dashboard/scan-terminal/feedback-status.ts b/frontend/components/dashboard/scan-terminal/feedback-status.ts index 7e5dcd0e..c8e68832 100644 --- a/frontend/components/dashboard/scan-terminal/feedback-status.ts +++ b/frontend/components/dashboard/scan-terminal/feedback-status.ts @@ -1,5 +1,7 @@ import type { UserFeedbackEntry } from "@/types/ops"; +export const FEEDBACK_STATUS_POLL_MS = 10 * 60 * 1000; + export function feedbackStatusLabel(status: string | undefined, isEn: boolean) { const key = String(status || "open").toLowerCase(); if (key === "triaged") return isEn ? "Confirmed" : "已确认";