duoqi-admin/src/components/feedback/columns.tsx
Wang Zhuoxuan fbc8bbb04d feat: 实现 Phase 2 — 用户详情页、反馈管理、订阅管理、CSV 导出
Phase 2a: 用户详情页(资料卡片、游戏统计、答题历史、章节进度)
Phase 2b: 反馈管理页面(类型/状态筛选、详情弹窗、状态变更)
Phase 2c: 订阅等级管理(等级变更对话框、权益对比)
Phase 2d: CSV 导出(用户/反馈/题目列表,Excel 兼容 BOM)
2026-04-08 12:29:06 +08:00

117 lines
3.3 KiB
TypeScript

import type { ColumnDef } from "@tanstack/react-table"
import { Badge } from "@/components/ui/badge"
import {
FEEDBACK_STATUS_LABELS,
FEEDBACK_TYPE_LABELS,
FEEDBACK_RATING_LABELS,
} from "@/lib/constants"
import type { Feedback, FeedbackStatus } from "@/types/feedback"
const STATUS_VARIANTS: Record<FeedbackStatus, "secondary" | "default" | "destructive"> = {
pending: "destructive",
read: "secondary",
resolved: "default",
}
interface ColumnContext {
onMarkRead: (fb: Feedback) => void
onMarkResolved: (fb: Feedback) => void
onViewDetail: (fb: Feedback) => void
}
export function getColumns(ctx: ColumnContext): ColumnDef<Feedback>[] {
return [
{
accessorKey: "type",
header: "类型",
cell: ({ row }) => {
const type = row.getValue("type") as string
return (
<Badge variant="outline">{FEEDBACK_TYPE_LABELS[type as keyof typeof FEEDBACK_TYPE_LABELS] ?? type}</Badge>
)
},
},
{
id: "content",
header: "内容",
cell: ({ row }) => {
const fb = row.original
if (fb.type === "quiz_rating") {
return (
<div className="max-w-xs">
<span className="text-sm">{FEEDBACK_RATING_LABELS[fb.rating ?? ""] ?? fb.rating}</span>
{fb.questionStem && (
<p className="line-clamp-1 text-xs text-muted-foreground">{fb.questionStem}</p>
)}
</div>
)
}
return (
<span className="line-clamp-2 max-w-xs text-sm">
{fb.content ?? "—"}
</span>
)
},
},
{
accessorKey: "userNickname",
header: "用户",
cell: ({ row }) => (
<span className="text-muted-foreground">
{(row.getValue("userNickname") as string) || "匿名"}
</span>
),
},
{
accessorKey: "status",
header: "状态",
cell: ({ row }) => {
const status = row.getValue("status") as FeedbackStatus
return <Badge variant={STATUS_VARIANTS[status]}>{FEEDBACK_STATUS_LABELS[status]}</Badge>
},
},
{
accessorKey: "createdAt",
header: "提交时间",
cell: ({ row }) => (
<span className="text-muted-foreground text-xs">
{new Date(row.getValue("createdAt") as string).toLocaleString("zh-CN")}
</span>
),
},
{
id: "actions",
header: "",
cell: ({ row }) => {
const fb = row.original
return (
<div className="flex items-center gap-1">
<button
className="text-xs text-muted-foreground hover:text-foreground"
onClick={() => ctx.onViewDetail(fb)}
>
</button>
{fb.status === "pending" && (
<button
className="text-xs text-muted-foreground hover:text-foreground"
onClick={() => ctx.onMarkRead(fb)}
>
</button>
)}
{fb.status !== "resolved" && (
<button
className="text-xs text-muted-foreground hover:text-foreground"
onClick={() => ctx.onMarkResolved(fb)}
>
</button>
)}
</div>
)
},
},
]
}