diff --git a/frontend/components/ops/__tests__/opsFeedbackDropdown.test.ts b/frontend/components/ops/__tests__/opsFeedbackDropdown.test.ts index e7102864..b74f72b9 100644 --- a/frontend/components/ops/__tests__/opsFeedbackDropdown.test.ts +++ b/frontend/components/ops/__tests__/opsFeedbackDropdown.test.ts @@ -32,11 +32,11 @@ export function runTests() { assert( source.includes("积分奖励标准") && source.includes("REWARD_GUIDELINES") && - source.includes("50-100") && - source.includes("200-300") && + source.includes("100") && + source.includes("300") && source.includes("500") && - source.includes("800-1000") && - source.includes("1500+"), - "ops feedback page must document manual reward point guidelines", + source.includes("1000") && + source.includes("1500"), + "ops feedback page must document fixed reward point guidelines", ); } diff --git a/frontend/components/ops/__tests__/opsFeedbackRewardGrant.test.ts b/frontend/components/ops/__tests__/opsFeedbackRewardGrant.test.ts index 53909908..eda70687 100644 --- a/frontend/components/ops/__tests__/opsFeedbackRewardGrant.test.ts +++ b/frontend/components/ops/__tests__/opsFeedbackRewardGrant.test.ts @@ -27,21 +27,33 @@ export function runTests() { assert( opsApiSource.includes("grantFeedbackReward") && opsApiSource.includes("/api/ops/feedback/${feedbackId}/reward") && - opsApiSource.includes("reason"), - "ops API client must expose grantFeedbackReward with reason", + opsApiSource.includes("JSON.stringify({ points })"), + "ops API client must expose grantFeedbackReward with fixed points only", ); assert( - feedbackPageSource.includes("rewardDrafts") && + feedbackPageSource.includes("REWARD_POINT_OPTIONS") && feedbackPageSource.includes("handleRewardGrant") && feedbackPageSource.includes("发放奖励") && - feedbackPageSource.includes("奖励原因") && feedbackPageSource.includes("opsApi.grantFeedbackReward"), - "ops feedback page must provide per-feedback reward grant controls", + "ops feedback page must provide fixed-point reward grant controls", + ); + assert( + feedbackPageSource.includes('value: 100') && + feedbackPageSource.includes('value: 300') && + feedbackPageSource.includes('value: 500') && + feedbackPageSource.includes('value: 1000') && + feedbackPageSource.includes('value: 1500'), + "ops feedback page must use fixed reward point options", + ); + assert( + !feedbackPageSource.includes("奖励原因") && + !feedbackPageSource.includes("rewardDrafts") && + !feedbackPageSource.includes("reason:"), + "ops feedback page must not ask operators for reward reasons", ); assert( feedbackPageSource.includes("已发放") && - feedbackPageSource.includes("reward_points") && - feedbackPageSource.includes("reward_reason"), + feedbackPageSource.includes("reward_points"), "ops feedback page must show existing feedback reward details", ); } diff --git a/frontend/components/ops/feedback/FeedbackPageClient.tsx b/frontend/components/ops/feedback/FeedbackPageClient.tsx index 66e90579..d7a136fe 100644 --- a/frontend/components/ops/feedback/FeedbackPageClient.tsx +++ b/frontend/components/ops/feedback/FeedbackPageClient.tsx @@ -18,19 +18,22 @@ const STATUS_OPTIONS = [ 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: "大面积不可用或严重业务损失,谨慎使用" }, +const REWARD_POINT_OPTIONS = [ + { value: 100, label: "100 分", title: "轻量提醒" }, + { value: 300, label: "300 分", title: "可复现 Bug" }, + { value: 500, label: "500 分", title: "有效数据问题" }, + { value: 1000, label: "1000 分", title: "高影响问题" }, + { value: 1500, label: "1500 分", title: "重大事故" }, ] as const; -type RewardDraft = { - points: string; - reason: string; -}; +const REWARD_GUIDELINES = [ + { points: "0", title: "无效/重复", detail: "重复反馈、无法复现、非问题" }, + { points: "100", title: "轻量提醒", detail: "文案、体验、小范围提示" }, + { points: "300", title: "可复现 Bug", detail: "加载失败、操作异常、局部影响" }, + { points: "500", title: "有效数据问题", detail: "城市数据、图表、关键变量异常" }, + { points: "1000", title: "高影响问题", detail: "支付、账号、订阅、核心终端异常" }, + { points: "1500", title: "重大事故", detail: "大面积不可用或严重业务损失,谨慎使用" }, +] as const; function compactDate(value?: string) { if (!value) return "—"; @@ -82,7 +85,7 @@ export function FeedbackPageClient() { const [payload, setPayload] = useState(null); const [updatingId, setUpdatingId] = useState(null); const [rewardingId, setRewardingId] = useState(null); - const [rewardDrafts, setRewardDrafts] = useState>({}); + const [rewardPointsById, setRewardPointsById] = useState>({}); const load = async () => { setLoading(true); @@ -127,21 +130,16 @@ export function FeedbackPageClient() { } }; - const updateRewardDraft = (rowId: number, patch: Partial) => { - setRewardDrafts((prev) => ({ + const updateRewardPoints = (rowId: number, points: string) => { + setRewardPointsById((prev) => ({ ...prev, - [rowId]: { - points: prev[rowId]?.points || "", - reason: prev[rowId]?.reason || "", - ...patch, - }, + [rowId]: points, })); }; const handleRewardGrant = async (row: UserFeedbackEntry) => { - const draft = rewardDrafts[row.id] || { points: "", reason: "" }; - const points = Number.parseInt(draft.points, 10); - const reason = draft.reason.trim(); + const selectedPoints = rewardPointsById[row.id] || String(REWARD_POINT_OPTIONS[1].value); + const points = Number.parseInt(selectedPoints, 10); if (!row.user_email) { setError("这条反馈没有绑定用户邮箱,不能从反馈页直接发放积分。"); return; @@ -150,15 +148,11 @@ export function FeedbackPageClient() { setError("请输入有效的奖励积分。"); return; } - if (!reason) { - setError("请输入奖励原因,用户账户页会展示这条原因。"); - return; - } setRewardingId(row.id); setError(""); try { - await opsApi.grantFeedbackReward(row.id, points, reason); - setRewardDrafts((prev) => { + await opsApi.grantFeedbackReward(row.id, points); + setRewardPointsById((prev) => { const next = { ...prev }; delete next[row.id]; return next; @@ -253,7 +247,7 @@ export function FeedbackPageClient() { ))}

- 先确认反馈是否有效;未复现的问题可先标为已确认/处理中,奖励原因后续发放时需要写清楚。 + 先确认反馈是否有效;未复现的问题可先标为已确认/处理中,奖励发放后会自动记录到用户账户页。

@@ -301,9 +295,8 @@ export function FeedbackPageClient() { {rows.map((row) => { - const draft = rewardDrafts[row.id] || { points: "", reason: "" }; + const selectedPoints = rewardPointsById[row.id] || String(REWARD_POINT_OPTIONS[1].value); const rewardPoints = Number(row.reward_points || 0); - const rewardReason = String(row.reward_reason || "").trim(); const rewardStatus = String(row.reward_status || "").toLowerCase(); const hasReward = rewardStatus === "granted" && rewardPoints > 0; return ( @@ -330,39 +323,28 @@ export function FeedbackPageClient() { {row.user_email || row.user_id || "—"} {compactDate(row.created_at)} - + {hasReward ? (
已发放 +{rewardPoints.toLocaleString()} 分
- {rewardReason && ( -
- {rewardReason} -
- )}
) : (
-
- updateRewardDraft(row.id, { points: event.target.value })} - placeholder="积分" - aria-label="奖励积分" - className="h-8 w-20 rounded border border-slate-200 bg-white px-2 text-xs font-bold text-slate-700 outline-none focus:border-blue-300 focus:ring-2 focus:ring-blue-100" - /> - updateRewardDraft(row.id, { reason: event.target.value })} - placeholder="奖励原因" - aria-label="奖励原因" - className="h-8 min-w-0 flex-1 rounded border border-slate-200 bg-white px-2 text-xs text-slate-700 outline-none focus:border-blue-300 focus:ring-2 focus:ring-blue-100" - /> -
+