初始提交:全国记者站管理系统

This commit is contained in:
selfrelease
2026-08-01 23:09:49 +08:00
commit 45fbba0308
96 changed files with 21514 additions and 0 deletions
+176
View File
@@ -0,0 +1,176 @@
import { useState, useEffect, useCallback } from 'react'
import { Gavel, Clock, CheckCircle2, XCircle, Loader2, MessageSquare, X } from 'lucide-react'
import { useRole } from '../../context'
import { useToast } from '../../context'
import { api } from '../../api'
import { EmptyState } from '../../components/ui/EmptyState'
import type { Appeal } from '../../types'
const statusLabels: Record<string, { label: string; color: string }> = {
pending: { label: '待处理', color: 'amber' },
accepted: { label: '已通过', color: 'emerald' },
rejected: { label: '已驳回', color: 'rose' },
}
/**
* 申诉复议页面 — 记者提交申诉,分站/总部处理
*/
export function AppealsPage() {
const { role } = useRole()
const { showToast } = useToast()
const [appeals, setAppeals] = useState<Appeal[]>([])
const [loading, setLoading] = useState(true)
const [filterStatus, setFilterStatus] = useState('')
const [selected, setSelected] = useState<Appeal | null>(null)
const [response, setResponse] = useState('')
const [handling, setHandling] = useState(false)
const load = useCallback(async () => {
setLoading(true)
try {
const data = await api.appeals.list(role, filterStatus || undefined)
setAppeals(data)
} catch (err) {
showToast(err instanceof Error ? err.message : '加载失败')
} finally {
setLoading(false)
}
}, [role, filterStatus, showToast])
useEffect(() => { load() }, [load])
const handleAppeal = async (decision: 'uphold' | 'overturn') => {
if (!selected) return
setHandling(true)
try {
await api.appeals.handle(role, selected.id, decision, response)
showToast(decision === 'uphold' ? '申诉已驳回' : '申诉已通过')
setSelected(null)
setResponse('')
load()
} catch (err) {
showToast(err instanceof Error ? err.message : '操作失败')
} finally {
setHandling(false)
}
}
return (
<div className="page-content">
<div className="page-heading small">
<div>
<div className="page-heading-icon"><Gavel size={20} /></div>
<div>
<h1></h1>
<span></span>
</div>
</div>
</div>
<div className="filter-bar">
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)}>
<option value=""></option>
<option value="pending"></option>
<option value="accepted"></option>
<option value="rejected"></option>
</select>
</div>
{loading ? (
<div className="loading-center"><Loader2 size={24} className="spin" /></div>
) : appeals.length === 0 ? (
<EmptyState icon={Gavel} text="暂无申诉记录" />
) : (
<div className="appeal-list">
{appeals.map(a => {
const st = statusLabels[a.status] || statusLabels.pending
return (
<div key={a.id} className={`appeal-card status-${st.color}`} onClick={() => { setSelected(a); setResponse('') }}>
<div className="appeal-header">
<span className="appeal-code">{a.code}</span>
<span className={`appeal-status ${st.color}`}>{st.label}</span>
</div>
<div className="appeal-body">
<div className="appeal-meta">
<span>{a.appellant}</span>
<span>{a.station}</span>
<span>ID{a.recordId}</span>
</div>
<div className="appeal-reason">
<MessageSquare size={14} />
<span>{a.reason}</span>
</div>
{a.response && (
<div className="appeal-response">
<strong></strong>{a.response}
</div>
)}
<div className="appeal-footer">
<Clock size={13} />
<span> {a.createdAt}</span>
{a.handledAt && <span> {a.handledAt}</span>}
{a.handler && <span>{a.handler}</span>}
</div>
</div>
</div>
)
})}
</div>
)}
{selected && (
<div className="modal-layer" onClick={() => { setSelected(null); setResponse('') }}>
<div className="modal" role="dialog" aria-modal="true" onClick={e => e.stopPropagation()} style={{ maxWidth: 520 }}>
<div className="modal-head">
<h2 style={{ fontSize: 16, margin: 0 }}> {selected.code}</h2>
<button className="icon-button" onClick={() => { setSelected(null); setResponse('') }}>
<X size={18} />
</button>
</div>
<div className="modal-body" style={{ padding: 16 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 12 }}>
<div><strong></strong>{selected.appellant}{selected.station}</div>
<div><strong>ID</strong>{selected.recordId}</div>
<div><strong></strong>{selected.reason}</div>
<div><strong></strong>{statusLabels[selected.status]?.label}</div>
{selected.response && <div><strong></strong>{selected.response}</div>}
{selected.handler && <div><strong></strong>{selected.handler}</div>}
<div><strong></strong>{selected.createdAt}</div>
{selected.handledAt && <div><strong></strong>{selected.handledAt}</div>}
</div>
{role !== 'reporter' && selected.status === 'pending' && (
<>
<textarea
placeholder="输入处理意见..."
value={response}
onChange={e => setResponse(e.target.value)}
rows={3}
style={{ width: '100%', marginBottom: 12, padding: 8, borderRadius: 6, border: '1px solid var(--border)' }}
/>
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
<button
className="danger-button"
style={{ height: 38, padding: '0 15px', fontSize: 13, fontWeight: 600, borderRadius: 4, border: '1px solid var(--red)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 7 }}
disabled={handling}
onClick={() => handleAppeal('uphold')}
>
<XCircle size={16} />
</button>
<button
className="primary-button"
disabled={handling}
onClick={() => handleAppeal('overturn')}
>
<CheckCircle2 size={16} />
</button>
</div>
</>
)}
</div>
</div>
</div>
)}
</div>
)
}