import { useState, useEffect } from 'react';
import { useParams, useNavigate, Link } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
const STATUS_LABELS = { open: '待处理', in_progress: '处理中', closed: '已关闭' };
const STATUS_COLORS = {
open: { color: '#f59e0b', bg: '#fffbeb' },
in_progress: { color: '#3b82f6', bg: '#eff6ff' },
closed: { color: '#10b981', bg: '#ecfdf5' }
};
const TYPE_LABELS = { bug: '🐛 Bug 报告', feature: '💡 功能建议', other: '📌 其他' };
export default function IssueDetail() {
const { id } = useParams();
const { user } = useAuth();
const navigate = useNavigate();
const [issue, setIssue] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
// 回复表单
const [replyContent, setReplyContent] = useState('');
const [replying, setReplying] = useState(false);
const [replyMsg, setReplyMsg] = useState('');
useEffect(() => {
if (!user) {
navigate('/login');
return;
}
fetchIssue();
}, [user, id, navigate]);
const fetchIssue = async () => {
try {
const token = localStorage.getItem('token');
const res = await fetch(`/api/issues/${id}`, {
headers: { Authorization: `Bearer ${token}` }
});
const data = await res.json();
if (!res.ok) throw new Error(data.detail || '获取失败');
setIssue(data.issue);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
const handleReply = async (e) => {
e.preventDefault();
if (!replyContent.trim()) return;
setReplying(true);
setReplyMsg('');
try {
const token = localStorage.getItem('token');
const res = await fetch(`/api/issues/${id}/reply`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({ content: replyContent.trim() })
});
const data = await res.json();
if (!res.ok) throw new Error(data.detail || '回复失败');
setReplyContent('');
setReplyMsg('回复成功');
fetchIssue();
} catch (err) {
setReplyMsg(err.message);
} finally {
setReplying(false);
}
};
if (!user) return null;
if (loading) {
return
加载中...
;
}
if (error) {
return (
);
}
if (!issue) return null;
return (
← 返回反馈列表
{/* Issue 主体 */}
{issue.title}
{STATUS_LABELS[issue.status] || issue.status}
{TYPE_LABELS[issue.type] || issue.type}
提交者: {issue.username}
{new Date(issue.created_at).toLocaleString('zh-CN')}
{/* 附带的 Markdown 用例 */}
{issue.markdown_sample && (
📎 附带的 Markdown 用例
{issue.markdown_sample}
)}
{/* 回复列表 */}
💬 回复 ({issue.replies?.length || 0})
{(!issue.replies || issue.replies.length === 0) ? (
暂无回复
) : (
{issue.replies.map(reply => (
{reply.author === 'admin' ? '🔧 管理员' : reply.author}
{new Date(reply.created_at).toLocaleString('zh-CN')}
{reply.content}
))}
)}
{/* 回复表单 */}
{issue.status !== 'closed' && (
<>
>
)}
{issue.status === 'closed' && (
🔒 该 Issue 已关闭,无法继续回复。
)}
);
}
const styles = {
container: {
maxWidth: '700px',
margin: '0 auto',
padding: '30px 20px'
},
center: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '60vh',
fontSize: '16px',
color: '#9ca3af'
},
backLink: {
display: 'inline-block',
marginBottom: '16px',
fontSize: '14px',
color: '#3b82f6',
textDecoration: 'none',
fontWeight: '500'
},
card: {
backgroundColor: 'white',
borderRadius: '8px',
padding: '24px',
boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
marginBottom: '16px'
},
headerRow: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
gap: '12px',
marginBottom: '12px'
},
title: {
fontSize: '20px',
fontWeight: 'bold',
color: '#111827',
margin: 0,
flex: 1
},
statusBadge: {
fontSize: '12px',
fontWeight: '600',
padding: '4px 12px',
borderRadius: '20px',
whiteSpace: 'nowrap'
},
metaRow: {
display: 'flex',
gap: '12px',
alignItems: 'center',
flexWrap: 'wrap'
},
typeBadge: {
fontSize: '12px',
color: '#6b7280',
backgroundColor: '#f3f4f6',
padding: '3px 10px',
borderRadius: '4px'
},
metaText: {
fontSize: '13px',
color: '#9ca3af'
},
divider: {
border: 'none',
borderTop: '1px solid #e5e7eb',
margin: '20px 0'
},
sectionTitle: {
fontSize: '15px',
fontWeight: '600',
color: '#374151',
marginBottom: '12px'
},
descBlock: {
marginTop: '20px'
},
descText: {
fontSize: '14px',
color: '#4b5563',
lineHeight: '1.7',
whiteSpace: 'pre-wrap'
},
sampleBlock: {
marginTop: '20px'
},
codeBlock: {
backgroundColor: '#1f2937',
color: '#e5e7eb',
padding: '16px',
borderRadius: '6px',
fontSize: '13px',
lineHeight: '1.5',
overflow: 'auto',
maxHeight: '400px',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word'
},
emptyText: {
color: '#9ca3af',
fontSize: '14px',
textAlign: 'center',
padding: '20px 0'
},
replyList: {
display: 'flex',
flexDirection: 'column',
gap: '12px'
},
replyItem: {
padding: '14px',
backgroundColor: '#f9fafb',
borderRadius: '8px',
border: '1px solid #e5e7eb'
},
adminReply: {
backgroundColor: '#fef2f2',
borderColor: '#fecaca'
},
replyHeader: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '8px'
},
replyAuthor: {
fontSize: '13px',
fontWeight: '600',
color: '#374151'
},
replyDate: {
fontSize: '12px',
color: '#9ca3af'
},
replyContent: {
fontSize: '14px',
color: '#4b5563',
lineHeight: '1.6',
margin: 0,
whiteSpace: 'pre-wrap'
},
replyTextarea: {
width: '100%',
padding: '10px 12px',
border: '1px solid #d1d5db',
borderRadius: '6px',
fontSize: '14px',
boxSizing: 'border-box',
resize: 'vertical',
fontFamily: 'inherit',
lineHeight: '1.5',
marginBottom: '10px'
},
replyBtn: {
padding: '10px 24px',
backgroundColor: '#3b82f6',
color: 'white',
border: 'none',
borderRadius: '6px',
fontSize: '14px',
cursor: 'pointer',
fontWeight: '500'
},
closedHint: {
marginTop: '16px',
padding: '12px',
backgroundColor: '#f3f4f6',
borderRadius: '6px',
fontSize: '13px',
color: '#6b7280',
textAlign: 'center'
},
errorBox: {
color: '#ef4444',
backgroundColor: '#fef2f2',
padding: '12px 16px',
borderRadius: '6px',
marginBottom: '16px',
fontSize: '14px',
border: '1px solid #fecaca'
}
};