sync from local backup

This commit is contained in:
chunyu
2026-08-05 23:59:22 +08:00
commit a59f46b894
409 changed files with 166027 additions and 0 deletions
+550
View File
@@ -0,0 +1,550 @@
.bugdetail-page {
min-height: 100vh;
background: #f5f7fa;
}
.bugdetail-hero {
position: relative;
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
padding: 48px 20px 40px;
overflow: hidden;
}
.bugdetail-particles {
position: absolute;
inset: 0;
pointer-events: none;
}
.bugdetail-particle {
position: absolute;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
animation: bugdetail-float linear infinite;
}
@keyframes bugdetail-float {
0%, 100% { transform: translateY(0) scale(1); opacity: 0.3; }
50% { transform: translateY(-18px) scale(1.2); opacity: 0.6; }
}
.bugdetail-hero-content {
position: relative;
max-width: 700px;
margin: 0 auto;
text-align: center;
z-index: 1;
}
.bugdetail-hero-icon {
width: 64px;
height: 64px;
margin: 0 auto 16px;
background: rgba(255, 255, 255, 0.15);
border-radius: 18px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
color: #fff;
backdrop-filter: blur(8px);
}
.bugdetail-hero-title {
font-size: 26px;
font-weight: 700;
color: #fff;
margin: 0 0 12px;
}
.bugdetail-hero-tags {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
}
.bugdetail-container {
max-width: 1100px;
margin: 0 auto;
padding: 24px 20px 60px;
}
.bugdetail-top-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.bugdetail-id {
font-size: 14px;
color: #999;
font-weight: 600;
}
.bugdetail-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 60vh;
gap: 16px;
text-align: center;
}
.bugdetail-empty h2 {
font-size: 22px;
color: #333;
margin: 0;
}
.bugdetail-empty p {
color: #999;
margin: 0;
}
.bugdetail-reward-card {
display: flex;
align-items: flex-start;
gap: 16px;
padding: 20px 24px;
background: linear-gradient(135deg, #fef9c3 0%, #fef3c7 50%, #fff7ed 100%);
border: 1px solid #fde68a;
border-radius: 14px;
margin-bottom: 20px;
}
.bugdetail-reward-icon {
width: 48px;
height: 48px;
background: linear-gradient(135deg, #f59e0b, #ef4444);
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
color: #fff;
flex-shrink: 0;
}
.bugdetail-reward-content {
flex: 1;
}
.bugdetail-reward-title {
font-size: 16px;
font-weight: 700;
color: #92400e;
margin-bottom: 6px;
display: flex;
align-items: center;
gap: 6px;
}
.bugdetail-reward-desc {
font-size: 13px;
color: #a16207;
margin-bottom: 10px;
line-height: 1.6;
}
.bugdetail-reward-details {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.bugdetail-reward-points {
display: flex;
align-items: center;
gap: 4px;
font-size: 14px;
font-weight: 700;
color: #b45309;
background: #fff;
padding: 4px 12px;
border-radius: 20px;
}
.bugdetail-reward-badge {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
font-weight: 600;
color: #7c3aed;
background: #f3e8ff;
padding: 4px 12px;
border-radius: 20px;
}
.bugdetail-body {
display: grid;
grid-template-columns: 300px 1fr;
gap: 20px;
align-items: start;
}
.bugdetail-info-panel {
display: flex;
flex-direction: column;
gap: 16px;
}
.bugdetail-info-card {
border-radius: 14px !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05) !important;
border: none !important;
}
.bugdetail-info-card .ant-card-body {
padding: 20px;
}
.bugdetail-info-title {
font-size: 14px;
font-weight: 600;
color: #1a1a2e;
margin: 0 0 16px;
display: flex;
align-items: center;
gap: 6px;
}
.bugdetail-info-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.bugdetail-info-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.bugdetail-info-label {
font-size: 13px;
color: #999;
}
.bugdetail-info-value {
font-size: 13px;
font-weight: 500;
color: #333;
}
.bugdetail-description {
font-size: 13px;
color: #555;
line-height: 1.8;
margin: 0;
}
.bugdetail-description p {
margin: 0 0 4px;
}
.bugdetail-images {
margin-top: 16px;
}
.bugdetail-images h4 {
font-size: 13px;
font-weight: 600;
color: #555;
margin: 0 0 10px;
display: flex;
align-items: center;
gap: 4px;
}
.bugdetail-images-grid {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.bugdetail-chat-panel {
min-width: 0;
}
.bugdetail-chat-card {
border-radius: 14px !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05) !important;
border: none !important;
}
.bugdetail-chat-card .ant-card-body {
padding: 20px;
}
.bugdetail-chat-title {
font-size: 15px;
font-weight: 600;
color: #1a1a2e;
margin: 0 0 20px;
display: flex;
align-items: center;
gap: 6px;
}
.bugdetail-chat-count {
font-size: 12px;
font-weight: 400;
color: #999;
margin-left: auto;
}
.bugdetail-chat-messages {
display: flex;
flex-direction: column;
gap: 16px;
max-height: 500px;
overflow-y: auto;
padding-right: 4px;
margin-bottom: 20px;
}
.bugdetail-chat-messages::-webkit-scrollbar {
width: 4px;
}
.bugdetail-chat-messages::-webkit-scrollbar-thumb {
background: #ddd;
border-radius: 2px;
}
.bugdetail-msg {
display: flex;
gap: 12px;
animation: bugdetail-msg-in 0.3s ease;
}
@keyframes bugdetail-msg-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.bugdetail-msg.user {
flex-direction: row;
}
.bugdetail-msg.admin {
flex-direction: row;
}
.bugdetail-msg-avatar.ant-avatar {
flex-shrink: 0;
}
.bugdetail-msg-avatar.admin {
background: linear-gradient(135deg, #667eea, #764ba2);
}
.bugdetail-msg-avatar.user {
background: linear-gradient(135deg, #10b981, #059669);
}
.bugdetail-msg-body {
flex: 1;
min-width: 0;
}
.bugdetail-msg-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
}
.bugdetail-msg-name {
font-size: 13px;
font-weight: 600;
color: #333;
}
.bugdetail-msg-tag {
font-size: 10px !important;
padding: 0 5px !important;
line-height: 16px !important;
margin: 0 !important;
}
.bugdetail-msg-time {
font-size: 11px;
color: #bbb;
margin-left: auto;
}
.bugdetail-msg-content {
background: #fff;
padding: 12px 16px;
border-radius: 4px 12px 12px 12px;
font-size: 13px;
color: #333;
line-height: 1.7;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
white-space: pre-wrap;
}
.bugdetail-msg.admin .bugdetail-msg-content {
background: #f0f2ff;
border-radius: 4px 12px 12px 12px;
}
.bugdetail-msg-images {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 10px;
}
.bugdetail-reply-area {
border-top: 1px solid #f0f0f0;
padding-top: 16px;
}
.bugdetail-reply-input {
margin-bottom: 10px;
}
.bugdetail-reply-input textarea {
border-radius: 10px !important;
resize: none !important;
}
.bugdetail-reply-actions {
display: flex;
align-items: center;
justify-content: space-between;
}
.bugdetail-resolved-bar {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 14px;
background: #f6ffed;
border: 1px solid #b7eb8f;
border-radius: 10px;
font-size: 13px;
color: #52c41a;
margin-top: 4px;
}
@media (max-width: 800px) {
.bugdetail-body {
grid-template-columns: 1fr;
}
.bugdetail-info-panel {
order: 1;
}
.bugdetail-chat-panel {
order: 0;
}
}
@media (max-width: 600px) {
.bugdetail-hero {
padding: 32px 16px 28px;
}
.bugdetail-hero-title {
font-size: 20px;
}
.bugdetail-msg-time {
display: none;
}
}
[data-theme="dark"] .bugdetail-page {
background: #0f172a;
}
[data-theme="dark"] .bugdetail-info-card,
[data-theme="dark"] .bugdetail-chat-card {
background: #1e293b !important;
}
[data-theme="dark"] .bugdetail-info-card .ant-card-body,
[data-theme="dark"] .bugdetail-chat-card .ant-card-body {
background: #1e293b;
}
[data-theme="dark"] .bugdetail-id {
color: #64748b;
}
[data-theme="dark"] .bugdetail-empty h2 {
color: #f1f5f9;
}
[data-theme="dark"] .bugdetail-empty p {
color: #64748b;
}
[data-theme="dark"] .bugdetail-reward-points {
background: #1e293b;
}
[data-theme="dark"] .bugdetail-info-title {
color: #f1f5f9;
}
[data-theme="dark"] .bugdetail-info-label {
color: #64748b;
}
[data-theme="dark"] .bugdetail-info-value {
color: #f1f5f9;
}
[data-theme="dark"] .bugdetail-description {
color: #94a3b8;
}
[data-theme="dark"] .bugdetail-images h4 {
color: #94a3b8;
}
[data-theme="dark"] .bugdetail-chat-title {
color: #f1f5f9;
}
[data-theme="dark"] .bugdetail-chat-count {
color: #64748b;
}
[data-theme="dark"] .bugdetail-chat-messages::-webkit-scrollbar-thumb {
background: #334155;
}
[data-theme="dark"] .bugdetail-msg-name {
color: #f1f5f9;
}
[data-theme="dark"] .bugdetail-msg-time {
color: #64748b;
}
[data-theme="dark"] .bugdetail-msg-content {
background: #1e293b;
color: #f1f5f9;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
[data-theme="dark"] .bugdetail-msg.admin .bugdetail-msg-content {
background: #1e293b;
}
[data-theme="dark"] .bugdetail-reply-area {
border-top: 1px solid #334155;
}
+583
View File
@@ -0,0 +1,583 @@
import React, { useState, useRef, useEffect } from "react";
import {
Avatar,
Button,
Tag,
Input,
message,
Image,
Tooltip,
Card,
Space,
Spin,
} from "antd";
import {
BugOutlined,
ArrowLeftOutlined,
SendOutlined,
CheckCircleOutlined,
ClockCircleOutlined,
ExclamationCircleOutlined,
UserOutlined,
CustomerServiceOutlined,
PictureOutlined,
SmileOutlined,
PaperClipOutlined,
EnvironmentOutlined,
InfoCircleOutlined,
CloseCircleOutlined,
FileOutlined,
} from "@ant-design/icons";
import { useNavigate, useSearchParams } from "react-router-dom";
import { api_request } from "@/utils/request";
import { formatDateTime } from "@/utils/time";
import "./BugDetail.css";
const { TextArea } = Input;
const EMOJI_LIST = [
"😀", "😂", "🤣", "😊", "😍", "🥰", "😘", "😜",
"🤔", "😤", "😭", "😱", "👍", "👎", "👏", "🙌",
"🎉", "🔥", "❤️", "💯", "✅", "❌", "⭐", "💪",
];
const ATTACHMENT_ACCEPT = ".pdf,.doc,.docx,.txt,.zip,.rar";
interface ApiCommentImage {
id: number;
image: string;
created_at: string;
}
interface ApiCommentAttachment {
id: number;
file: string;
filename: string;
file_size: number;
created_at: string;
}
interface ApiComment {
id: number;
user: number;
user_name: string;
user_avatar: string;
content: string;
is_admin: boolean;
created_at: string;
images: ApiCommentImage[];
attachments: ApiCommentAttachment[];
}
interface ApiBugImage {
id: number;
image: string;
created_at: string;
}
interface ApiBugAttachment {
id: number;
file: string;
filename: string;
file_size: number;
created_at: string;
}
interface ApiBugDetail {
id: number;
title: string;
bug_type: string;
priority: string;
status: string;
description: string;
created_at: string;
updated_at: string;
user: number;
user_name: string;
user_avatar: string;
images: ApiBugImage[];
attachments: ApiBugAttachment[];
comments: ApiComment[];
}
const priorityColor: Record<string, string> = { "high": "red", "medium": "orange", "low": "blue" };
const priorityLabel: Record<string, string> = { "high": "高", "medium": "中", "low": "低" };
const statusConfig: Record<string, { color: string; icon: React.ReactNode }> = {
"resolved": { color: "green", icon: <CheckCircleOutlined /> },
"processing": { color: "orange", icon: <ClockCircleOutlined /> },
"pending": { color: "red", icon: <ExclamationCircleOutlined /> },
"rejected": { color: "default", icon: <ExclamationCircleOutlined /> },
};
const statusLabel: Record<string, string> = { "pending": "待处理", "processing": "处理中", "resolved": "已解决", "rejected": "已拒绝" };
const typeLabel: Record<string, string> = { "feature": "功能问题", "ui": "界面问题", "performance": "性能问题", "other": "其他" };
const BugDetail: React.FC = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const chatEndRef = useRef<HTMLDivElement>(null);
const imageInputRef = useRef<HTMLInputElement>(null);
const attachmentInputRef = useRef<HTMLInputElement>(null);
const bugId = Number(searchParams.get("id")) || 0;
const [bug, setBug] = useState<ApiBugDetail | null>(null);
const [loading, setLoading] = useState(true);
const [replyText, setReplyText] = useState("");
const [sending, setSending] = useState(false);
const [selectedImages, setSelectedImages] = useState<File[]>([]);
const [imagePreviews, setImagePreviews] = useState<string[]>([]);
const [selectedAttachments, setSelectedAttachments] = useState<File[]>([]);
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const loadBugDetail = async () => {
if (!bugId) {
setLoading(false);
return;
}
try {
const res: any = await api_request.bug.get_report_detail(bugId);
if (res?.data) {
setBug(res.data);
}
} catch {
setBug(null);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadBugDetail();
}, [bugId]);
useEffect(() => {
chatEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [bug?.comments]);
useEffect(() => {
return () => {
imagePreviews.forEach((url) => URL.revokeObjectURL(url));
};
}, [imagePreviews]);
const handleImageSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files) return;
const newFiles = Array.from(files);
const remaining = 3 - selectedImages.length;
const toAdd = newFiles.slice(0, remaining);
if (toAdd.length < newFiles.length) {
message.warning("最多上传3张图片");
}
const oversized = toAdd.find((f) => f.size > 10 * 1024 * 1024);
if (oversized) {
message.error(`图片 ${oversized.name} 超过10MB限制`);
return;
}
setSelectedImages((prev) => [...prev, ...toAdd]);
const newPreviews = toAdd.map((f) => URL.createObjectURL(f));
setImagePreviews((prev) => [...prev, ...newPreviews]);
e.target.value = "";
};
const handleRemoveImage = (index: number) => {
URL.revokeObjectURL(imagePreviews[index]);
setSelectedImages((prev) => prev.filter((_, i) => i !== index));
setImagePreviews((prev) => prev.filter((_, i) => i !== index));
};
const handleAttachmentSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files) return;
const newFiles = Array.from(files);
const remaining = 2 - selectedAttachments.length;
const toAdd = newFiles.slice(0, remaining);
if (toAdd.length < newFiles.length) {
message.warning("最多上传2个附件");
}
const oversized = toAdd.find((f) => f.size > 20 * 1024 * 1024);
if (oversized) {
message.error(`附件 ${oversized.name} 超过20MB限制`);
return;
}
setSelectedAttachments((prev) => [...prev, ...toAdd]);
e.target.value = "";
};
const handleRemoveAttachment = (index: number) => {
setSelectedAttachments((prev) => prev.filter((_, i) => i !== index));
};
const handleEmojiClick = (emoji: string) => {
setReplyText((prev) => prev + emoji);
setShowEmojiPicker(false);
};
const formatFileSize = (bytes: number) => {
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
};
const handleSendReply = async () => {
if (!replyText.trim()) {
message.warning("请输入回复内容");
return;
}
if (!bug) return;
setSending(true);
try {
const formData = new FormData();
formData.append("content", replyText.trim());
selectedImages.forEach((file) => {
formData.append("images", file);
});
selectedAttachments.forEach((file) => {
formData.append("attachments", file);
});
await api_request.bug.create_comment(bug.id, formData);
message.success("回复发送成功");
setReplyText("");
imagePreviews.forEach((url) => URL.revokeObjectURL(url));
setSelectedImages([]);
setImagePreviews([]);
setSelectedAttachments([]);
setShowEmojiPicker(false);
loadBugDetail();
} catch {
message.error("回复发送失败,请稍后重试");
} finally {
setSending(false);
}
};
if (loading) {
return (
<div className="bugdetail-page">
<div className="bugdetail-empty">
<Spin size="large" />
<p>加载中...</p>
</div>
</div>
);
}
if (!bug) {
return (
<div className="bugdetail-page">
<div className="bugdetail-empty">
<BugOutlined style={{ fontSize: 48, color: "#667eea" }} />
<h2>反馈记录不存在</h2>
<p>该反馈可能已被删除或不存在</p>
<Button type="primary" onClick={() => navigate("/bug")}>返回反馈页面</Button>
</div>
</div>
);
}
const bugImages = bug.images.map((img) => img.image);
const comments = bug.comments || [];
return (
<div className="bugdetail-page">
<div className="bugdetail-hero">
<div className="bugdetail-particles">
{Array.from({ length: 10 }).map((_, i) => (
<span
key={i}
className="bugdetail-particle"
style={{
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
width: `${Math.random() * 4 + 2}px`,
height: `${Math.random() * 4 + 2}px`,
animationDelay: `${Math.random() * 5}s`,
animationDuration: `${Math.random() * 3 + 3}s`,
}}
/>
))}
</div>
<div className="bugdetail-hero-content">
<div className="bugdetail-hero-icon"><BugOutlined /></div>
<h1 className="bugdetail-hero-title">{bug.title}</h1>
<div className="bugdetail-hero-tags">
<Tag color={priorityColor[bug.priority]}>优先级:{priorityLabel[bug.priority]}</Tag>
<Tag color={statusConfig[bug.status]?.color} icon={statusConfig[bug.status]?.icon}>
{statusLabel[bug.status] || bug.status}
</Tag>
<Tag>{typeLabel[bug.bug_type] || bug.bug_type}</Tag>
</div>
</div>
</div>
<div className="bugdetail-container">
<div className="bugdetail-top-bar">
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate("/bug")}>
返回反馈列表
</Button>
<span className="bugdetail-id">#{bug.id}</span>
</div>
<div className="bugdetail-body">
<div className="bugdetail-info-panel">
<Card className="bugdetail-info-card">
<h3 className="bugdetail-info-title"><InfoCircleOutlined /> 反馈详情</h3>
<div className="bugdetail-info-list">
<div className="bugdetail-info-row">
<span className="bugdetail-info-label">问题类型</span>
<span className="bugdetail-info-value"><Tag>{typeLabel[bug.bug_type] || bug.bug_type}</Tag></span>
</div>
<div className="bugdetail-info-row">
<span className="bugdetail-info-label">优先级</span>
<span className="bugdetail-info-value">
<Tag color={priorityColor[bug.priority]}>{priorityLabel[bug.priority] || bug.priority}</Tag>
</span>
</div>
<div className="bugdetail-info-row">
<span className="bugdetail-info-label">状态</span>
<span className="bugdetail-info-value">
<Tag color={statusConfig[bug.status]?.color} icon={statusConfig[bug.status]?.icon}>
{statusLabel[bug.status] || bug.status}
</Tag>
</span>
</div>
<div className="bugdetail-info-row">
<span className="bugdetail-info-label">提交时间</span>
<span className="bugdetail-info-value">{formatDateTime(bug.created_at)}</span>
</div>
<div className="bugdetail-info-row">
<span className="bugdetail-info-label">反馈编号</span>
<span className="bugdetail-info-value">#{bug.id}</span>
</div>
</div>
</Card>
<Card className="bugdetail-info-card">
<h3 className="bugdetail-info-title"><EnvironmentOutlined /> 问题描述</h3>
<div className="bugdetail-description">
{bug.description.split("\n").map((line, i) => (
<p key={i}>{line || <br />}</p>
))}
</div>
{bugImages.length > 0 && (
<div className="bugdetail-images">
<h4><PictureOutlined /> 问题截图</h4>
<div className="bugdetail-images-grid">
{bugImages.map((img, i) => (
<Image
key={i}
src={img}
width={120}
height={80}
style={{ objectFit: "cover", borderRadius: 8, cursor: "pointer" }}
/>
))}
</div>
</div>
)}
</Card>
</div>
<div className="bugdetail-chat-panel">
<Card className="bugdetail-chat-card">
<h3 className="bugdetail-chat-title">
<CustomerServiceOutlined /> 沟通记录
<span className="bugdetail-chat-count">{comments.length} 条消息</span>
</h3>
<div className="bugdetail-chat-messages">
{comments.map((msg) => (
<div
key={msg.id}
className={`bugdetail-msg ${msg.is_admin ? "admin" : "user"}`}
>
<Avatar
size={36}
src={msg.user_avatar || undefined}
icon={msg.is_admin ? <CustomerServiceOutlined /> : <UserOutlined />}
className={`bugdetail-msg-avatar ${msg.is_admin ? "admin" : "user"}`}
/>
<div className="bugdetail-msg-body">
<div className="bugdetail-msg-header">
<span className="bugdetail-msg-name">{msg.user_name}</span>
{msg.is_admin && <Tag color="blue" className="bugdetail-msg-tag">官方</Tag>}
<span className="bugdetail-msg-time">{formatDateTime(msg.created_at)}</span>
</div>
<div className="bugdetail-msg-content">
{msg.content}
{msg.images && msg.images.length > 0 && (
<div className="bugdetail-msg-images">
{msg.images.map((img, i) => (
<Image
key={i}
src={img.image}
width={160}
height={100}
style={{ objectFit: "cover", borderRadius: 8, cursor: "pointer" }}
/>
))}
</div>
)}
{msg.attachments && msg.attachments.length > 0 && (
<div style={{ marginTop: 8, display: "flex", gap: 8, flexWrap: "wrap" }}>
{msg.attachments.map((att) => (
<a
key={att.id}
href={att.file}
target="_blank"
rel="noopener noreferrer"
style={{ fontSize: 12, color: "#667eea" }}
>
<FileOutlined /> {att.filename} ({formatFileSize(att.file_size)})
</a>
))}
</div>
)}
</div>
</div>
</div>
))}
<div ref={chatEndRef} />
</div>
{bug.status !== "resolved" && bug.status !== "rejected" && (
<div className="bugdetail-reply-area">
<div className="bugdetail-reply-input">
<TextArea
value={replyText}
onChange={(e) => setReplyText(e.target.value)}
placeholder="输入回复内容..."
autoSize={{ minRows: 2, maxRows: 4 }}
onPressEnter={(e) => {
if (!e.shiftKey) {
e.preventDefault();
handleSendReply();
}
}}
/>
</div>
{selectedImages.length > 0 && (
<div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginBottom: 10 }}>
{imagePreviews.map((url, i) => (
<div key={i} style={{ position: "relative" }}>
<img
src={url}
alt={`预览 ${i}`}
style={{ width: 80, height: 60, objectFit: "cover", borderRadius: 6, border: "1px solid #eee" }}
/>
<CloseCircleOutlined
style={{ position: "absolute", top: -6, right: -6, fontSize: 16, color: "#ff4d4f", cursor: "pointer", background: "#fff", borderRadius: "50%" }}
onClick={() => handleRemoveImage(i)}
/>
</div>
))}
</div>
)}
{selectedAttachments.length > 0 && (
<div style={{ display: "flex", flexDirection: "column", gap: 4, marginBottom: 10 }}>
{selectedAttachments.map((file, i) => (
<div key={i} style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 12, color: "#666", background: "#f5f5f5", padding: "4px 10px", borderRadius: 6 }}>
<FileOutlined />
<span>{file.name}</span>
<span style={{ color: "#999" }}>({formatFileSize(file.size)})</span>
<CloseCircleOutlined
style={{ fontSize: 14, color: "#ff4d4f", cursor: "pointer", marginLeft: "auto" }}
onClick={() => handleRemoveAttachment(i)}
/>
</div>
))}
</div>
)}
{showEmojiPicker && (
<div style={{ display: "flex", flexWrap: "wrap", gap: 4, padding: "8px 12px", background: "#fff", border: "1px solid #eee", borderRadius: 10, marginBottom: 10, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}>
{EMOJI_LIST.map((emoji) => (
<span
key={emoji}
style={{ fontSize: 20, cursor: "pointer", padding: 4, borderRadius: 4, transition: "background 0.2s" }}
onMouseEnter={(e) => (e.currentTarget.style.background = "#f0f0f0")}
onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}
onClick={() => handleEmojiClick(emoji)}
>
{emoji}
</span>
))}
</div>
)}
<div className="bugdetail-reply-actions">
<Space>
<Tooltip title="上传图片">
<Button
type="text"
icon={<PictureOutlined />}
size="small"
disabled={selectedImages.length >= 3}
onClick={() => imageInputRef.current?.click()}
/>
</Tooltip>
<Tooltip title="表情">
<Button
type="text"
icon={<SmileOutlined />}
size="small"
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
/>
</Tooltip>
<Tooltip title="附件">
<Button
type="text"
icon={<PaperClipOutlined />}
size="small"
disabled={selectedAttachments.length >= 2}
onClick={() => attachmentInputRef.current?.click()}
/>
</Tooltip>
</Space>
<Button
type="primary"
icon={<SendOutlined />}
loading={sending}
onClick={handleSendReply}
disabled={!replyText.trim()}
>
发送回复
</Button>
</div>
</div>
)}
{(bug.status === "resolved" || bug.status === "rejected") && (
<div className="bugdetail-resolved-bar">
<CheckCircleOutlined style={{ color: "#52c41a" }} />
<span>此反馈已{statusLabel[bug.status] || bug.status},如需继续沟通请提交新的反馈</span>
</div>
)}
</Card>
</div>
</div>
</div>
<input
ref={imageInputRef}
type="file"
accept="image/*"
multiple
onChange={handleImageSelect}
style={{ display: "none" }}
/>
<input
ref={attachmentInputRef}
type="file"
accept={ATTACHMENT_ACCEPT}
multiple
onChange={handleAttachmentSelect}
style={{ display: "none" }}
/>
</div>
);
};
export default BugDetail;