Add ops feedback reward guide

This commit is contained in:
2569718930@qq.com
2026-06-08 18:54:43 +08:00
parent 961c9bd37c
commit 7d1bfea8da
2 changed files with 95 additions and 20 deletions
@@ -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("<select") &&
source.includes("onChange={(event) =>") &&
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",
);
}
@@ -16,13 +16,16 @@ const STATUS_OPTIONS = [
{ key: "closed", label: "关闭" },
] as const;
const NEXT_STATUS: Record<string, string> = {
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<string, unknown>) {
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() {
</Card>
</div>
<Card>
<CardHeader className="pb-2">
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-2 md:grid-cols-3 xl:grid-cols-6">
{REWARD_GUIDELINES.map((item) => (
<div
key={item.points}
className="rounded-lg border border-slate-200 bg-slate-50 px-3 py-2"
>
<div className="font-mono text-sm font-black text-blue-700">
{item.points}
</div>
<div className="mt-1 text-xs font-bold text-slate-900">
{item.title}
</div>
<div className="mt-1 text-[11px] leading-4 text-slate-500">
{item.detail}
</div>
</div>
))}
</div>
<p className="mt-2 text-xs text-slate-500">
/
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between gap-3">
<CardTitle></CardTitle>
@@ -245,14 +273,19 @@ export function FeedbackPageClient() {
</td>
<td className="whitespace-nowrap py-3 pr-4 text-xs text-slate-500">{compactDate(row.created_at)}</td>
<td className="py-3 pr-4">
<button
type="button"
onClick={() => advanceStatus(row)}
<select
value={String(row.status || "open").toLowerCase()}
onChange={(event) => changeStatus(row, event.target.value)}
disabled={updatingId === row.id}
className="rounded border border-slate-200 bg-white px-2.5 py-1 text-xs font-bold text-slate-600 transition hover:bg-slate-50 disabled:cursor-wait disabled:opacity-60"
className="h-8 min-w-[108px] rounded border border-slate-200 bg-white px-2 text-xs font-bold text-slate-600 outline-none transition hover:bg-slate-50 focus:border-blue-300 focus:ring-2 focus:ring-blue-100 disabled:cursor-wait disabled:opacity-60"
aria-label="更新反馈状态"
>
{feedbackActionLabel(row.status)}
</button>
{STATUS_UPDATE_OPTIONS.map((item) => (
<option key={item.key} value={item.key}>
{item.label}
</option>
))}
</select>
</td>
</tr>
))}