Files
ReporterStationManagementSy…/src/pages/Appeals/index.tsx
T
2026-08-01 23:09:49 +08:00

177 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}