import { useCallback, useEffect, useState } from 'react'; import { motion, AnimatePresence } from 'motion/react'; import { Inbox, RotateCcw, X, Send, CheckCircle2, AlertCircle, Image as ImageIcon, Loader2, ArrowLeft } from 'lucide-react'; import { fetchJson } from '../../auth/api-client'; import { EmptyState, LoadingState, PageFrame, SurfaceCard } from '../../components/ui/surface'; interface FeedbackItem { id: number; type: 'dimension' | 'bug' | 'ux' | 'other'; module: string | null; content: string; contact: string | null; screenshots: string[] | string | null; user_id: string | null; user_name: string | null; status: 'open' | 'in_progress' | 'done' | 'rejected'; reply_content: string | null; reply_user: string | null; reply_at: string | null; created_at: string; } const TYPE_LABEL: Record = { dimension: '新维度', bug: 'Bug', ux: '体验', other: '其他', }; const STATUS_OPTIONS: { key: FeedbackItem['status']; label: string; cls: string }[] = [ { key: 'open', label: '待处理', cls: 'bg-slate-100 text-slate-500 border-slate-200' }, { key: 'in_progress', label: '处理中', cls: 'bg-amber-100 text-amber-600 border-amber-200' }, { key: 'done', label: '已完成', cls: 'bg-emerald-100 text-emerald-600 border-emerald-200' }, { key: 'rejected', label: '已忽略', cls: 'bg-rose-100 text-rose-500 border-rose-200' }, ]; const MODULE_LABELS: Record = { assets: '资产管理', mileage: '里程管理', energy: '能源管理', scheduling: '智能调度', ele: '充电导入', }; function parseScreenshots(s: FeedbackItem['screenshots']): string[] { if (!s) return []; if (Array.isArray(s)) return s; try { return JSON.parse(String(s)); } catch { return []; } } async function patchItem(id: number, data: { status?: string; reply?: string }): Promise { const token = sessionStorage.getItem('bi_jwt'); const res = await fetch(`/api/feedback/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) }, body: JSON.stringify(data), }); const json = await res.json(); if (!res.ok || !json.ok) throw new Error(json.message || `更新失败 (${res.status})`); } export default function FeedbackAdminPage() { const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [statusFilter, setStatusFilter] = useState<'' | FeedbackItem['status']>(''); const [active, setActive] = useState(null); const [replyDraft, setReplyDraft] = useState(''); const [replyStatus, setReplyStatus] = useState('done'); const [saving, setSaving] = useState(false); const [hint, setHint] = useState(null); const reload = useCallback(async () => { setLoading(true); setError(null); try { const params = new URLSearchParams(); if (statusFilter) params.set('status', statusFilter); params.set('limit', '200'); const d = await fetchJson<{ items: FeedbackItem[] }>(`/api/feedback/list?${params.toString()}`); setItems(d.items); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setLoading(false); } }, [statusFilter]); useEffect(() => { reload(); }, [reload]); const open = (it: FeedbackItem) => { setActive(it); setReplyDraft(it.reply_content || ''); setReplyStatus(it.status === 'open' ? 'done' : it.status); }; const save = async () => { if (!active) return; setSaving(true); setHint(null); try { await patchItem(active.id, { status: replyStatus, reply: replyDraft }); setHint('已保存'); setActive(null); await reload(); } catch (e) { setHint(e instanceof Error ? e.message : String(e)); } finally { setSaving(false); setTimeout(() => setHint(null), 3000); } }; const setStatusOnly = async (it: FeedbackItem, status: FeedbackItem['status']) => { try { await patchItem(it.id, { status }); await reload(); } catch (e) { setHint(e instanceof Error ? e.message : String(e)); setTimeout(() => setHint(null), 3000); } }; const counters = items.reduce>((m, it) => { m[it.status] = (m[it.status] || 0) + 1; return m; }, {}); return ( )} maxWidth="max-w-5xl" > {/* 状态过滤 */}
{STATUS_OPTIONS.map(o => ( ))}
{error && (
{error}
)} {hint && ( {hint} )} {/* 列表 */}
{loading && items.length === 0 ? ( ) : items.length === 0 ? ( ) : items.map(it => { const shots = parseScreenshots(it.screenshots); const statusOpt = STATUS_OPTIONS.find(o => o.key === it.status); return (
open(it)} >
{TYPE_LABEL[it.type] || it.type} {it.module && {MODULE_LABELS[it.module] || it.module}} {statusOpt?.label || it.status} {(it.user_name || it.user_id || '匿名')} · {(it.created_at || '').replace('T', ' ').slice(0, 16)}
{it.content}
{(shots.length > 0 || it.contact) && (
{shots.length > 0 && {shots.length} 张} {it.contact && 联系 {it.contact}}
)} {it.reply_content && (
回复: {it.reply_content}
)} {it.status === 'open' && (
e.stopPropagation()}>
)}
); })}
{/* 详情 / 回复弹窗 */} {active && ( setActive(null)} > e.stopPropagation()} >
反馈详情 #{active.id}
{active.user_name || active.user_id || '匿名'} · {(active.created_at || '').replace('T', ' ').slice(0, 16)}
{TYPE_LABEL[active.type] || active.type} {active.module && 板块: {MODULE_LABELS[active.module] || active.module}} {active.contact && 联系: {active.contact}}
{active.content}
{parseScreenshots(active.screenshots).length > 0 && (
{parseScreenshots(active.screenshots).map((u, i) => ( ))}
)}

状态

{STATUS_OPTIONS.map(o => ( ))}

回复内容