diff --git a/frontend/components/ops/__tests__/opsFeedbackDropdown.test.ts b/frontend/components/ops/__tests__/opsFeedbackDropdown.test.ts new file mode 100644 index 00000000..e7102864 --- /dev/null +++ b/frontend/components/ops/__tests__/opsFeedbackDropdown.test.ts @@ -0,0 +1,42 @@ +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 feedbackPagePath = path.join( + projectRoot, + "components", + "ops", + "feedback", + "FeedbackPageClient.tsx", + ); + const source = fs.readFileSync(feedbackPagePath, "utf8"); + + assert( + source.includes("") && + source.includes("changeStatus(row, event.target.value)") && + source.includes("disabled={updatingId === row.id}") && + source.includes("STATUS_UPDATE_OPTIONS.map"), + "ops feedback action column must use a status dropdown", + ); + assert( + !source.includes("标为{feedbackActionLabel(row.status)}") && + !source.includes("advanceStatus(row)"), + "ops feedback action column must not use one-step status buttons", + ); + assert( + source.includes("积分奖励标准") && + source.includes("REWARD_GUIDELINES") && + source.includes("50-100") && + source.includes("200-300") && + source.includes("500") && + source.includes("800-1000") && + source.includes("1500+"), + "ops feedback page must document manual reward point guidelines", + ); +} diff --git a/frontend/components/ops/feedback/FeedbackPageClient.tsx b/frontend/components/ops/feedback/FeedbackPageClient.tsx index 2cd4aa6e..f54df16c 100644 --- a/frontend/components/ops/feedback/FeedbackPageClient.tsx +++ b/frontend/components/ops/feedback/FeedbackPageClient.tsx @@ -16,13 +16,16 @@ const STATUS_OPTIONS = [ { key: "closed", label: "关闭" }, ] as const; -const NEXT_STATUS: Record = { - open: "triaged", - triaged: "investigating", - investigating: "resolved", - resolved: "closed", - closed: "open", -}; +const STATUS_UPDATE_OPTIONS = STATUS_OPTIONS.filter((item) => item.key); + +const REWARD_GUIDELINES = [ + { points: "0", title: "无效/重复", detail: "重复反馈、无法复现、非问题" }, + { points: "50-100", title: "轻量提醒", detail: "文案、体验、小范围提示" }, + { points: "200-300", title: "可复现 Bug", detail: "加载失败、操作异常、局部影响" }, + { points: "500", title: "有效数据问题", detail: "城市数据、图表、关键变量异常" }, + { points: "800-1000", title: "高影响问题", detail: "支付、账号、订阅、核心终端异常" }, + { points: "1500+", title: "重大事故", detail: "大面积不可用或严重业务损失,谨慎使用" }, +] as const; function compactDate(value?: string) { if (!value) return "—"; @@ -67,11 +70,6 @@ function contextSummary(context?: Record) { return pieces.length ? pieces.join(" · ") : "terminal"; } -function feedbackActionLabel(status?: string) { - const next = NEXT_STATUS[String(status || "open").toLowerCase()] || "triaged"; - return statusLabel(next); -} - export function FeedbackPageClient() { const [loading, setLoading] = useState(true); const [error, setError] = useState(""); @@ -110,8 +108,9 @@ export function FeedbackPageClient() { return acc; }, [rows]); - const advanceStatus = async (row: UserFeedbackEntry) => { - const next = NEXT_STATUS[String(row.status || "open").toLowerCase()] || "triaged"; + const changeStatus = async (row: UserFeedbackEntry, next: string) => { + const current = String(row.status || "open").toLowerCase(); + if (!next || next === current) return; setUpdatingId(row.id); try { await opsApi.updateFeedbackStatus(row.id, next); @@ -179,6 +178,35 @@ export function FeedbackPageClient() { + + + 积分奖励标准 + + +
+ {REWARD_GUIDELINES.map((item) => ( +
+
+ {item.points} 分 +
+
+ {item.title} +
+
+ {item.detail} +
+
+ ))} +
+

+ 先确认反馈是否有效;未复现的问题可先标为已确认/处理中,奖励原因后续发放时需要写清楚。 +

+
+
+ 反馈收件箱 @@ -245,14 +273,19 @@ export function FeedbackPageClient() { {compactDate(row.created_at)} -