Phase 2a: 用户详情页(资料卡片、游戏统计、答题历史、章节进度) Phase 2b: 反馈管理页面(类型/状态筛选、详情弹窗、状态变更) Phase 2c: 订阅等级管理(等级变更对话框、权益对比) Phase 2d: CSV 导出(用户/反馈/题目列表,Excel 兼容 BOM)
117 lines
3.3 KiB
TypeScript
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>
|
|
)
|
|
},
|
|
},
|
|
]
|
|
}
|