初始提交:全国记者站管理系统
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user