feat: 新增工单反馈系统

This commit is contained in:
2026-09-05 02:42:51 +08:00
parent e505a170b7
commit 8e890d9a96
13 changed files with 996 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
import React, { useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import * as ticketsApi from '../api/tickets.js';
import { getToken } from '../api/client.js';
import { safeSourceUrl } from './Tickets.jsx';
const categories = [['forum_bug', '论坛 Bug'], ['site_bug', '站内 Bug'], ['feature', '功能建议'], ['account', '账号问题'], ['other', '其他问题']];
const priorities = [['low', '普通'], ['normal', '一般'], ['high', '重要'], ['urgent', '紧急']];
export default function TicketCreate() {
const navigate = useNavigate();
const location = useLocation();
const search = new URLSearchParams(location.search);
const sourceUrl = safeSourceUrl((location.state && location.state.sourceUrl) || search.get('source_url') || search.get('from') || window.location.pathname);
const [form, setForm] = useState({ subject: '', description: '', category: 'site_bug', priority: 'normal', source_url: sourceUrl });
const [error, setError] = useState('');
const [busy, setBusy] = useState(false);
const update = (key) => (e) => setForm((prev) => ({ ...prev, [key]: e.target.value }));
if (!getToken()) return <div className="empty-state" style={{ maxWidth: 460, margin: '48px auto' }}><div className="empty-icon" aria-hidden="true">🔒</div><h1 style={{ fontSize: 22 }}>登录后提交工单</h1><p className="text-muted">请先登录再反馈论坛或站内问题</p><Link to="/login.html" state={{ from: location.pathname }} className="btn btn-filled" style={{ marginTop: 16 }}>去登录</Link></div>;
const submit = async (e) => {
e.preventDefault();
const subject = form.subject.trim(); const description = form.description.trim();
if (subject.length < 2) { setError('请填写问题标题(至少 2 个字)'); return; }
if (description.length < 10) { setError('请详细描述问题(至少 10 个字)'); return; }
setError(''); setBusy(true);
try {
const data = await ticketsApi.createTicket({
...form,
subject,
description,
source: form.category === 'forum_bug' ? 'forum' : 'site',
source_url: safeSourceUrl(form.source_url),
browser_info: [navigator.userAgent, `${window.innerWidth}x${window.innerHeight}`].join(' | ').slice(0, 1000),
});
if (!data.ticket || !data.ticket.id) throw new Error('工单创建成功但未返回编号');
navigate('/tickets/' + data.ticket.id, { replace: true });
} catch (err) { setError(err.message || '提交失败,请稍后重试'); }
finally { setBusy(false); }
};
return <div style={{ maxWidth: 760, margin: '0 auto' }}>
<div style={{ marginBottom: 22 }}><Link to="/tickets.html" className="btn btn-text btn-sm"> 返回工单</Link><h1 className="page-title" style={{ margin: '12px 0 6px' }}>提交问题</h1><p className="text-muted" style={{ margin: 0 }}>描述得越具体越有助于快速定位问题</p></div>
<form className="card" onSubmit={submit} noValidate style={{ padding: 22 }}>
{error && <div role="alert" className="empty-state" style={{ padding: 12, marginBottom: 18, textAlign: 'left' }}><p style={{ margin: 0 }}>{error}</p></div>}
<div className="form-group"><label htmlFor="ticket-subject">问题标题 <span aria-hidden="true">*</span></label><input id="ticket-subject" value={form.subject} onChange={update('subject')} maxLength={120} required aria-describedby="ticket-subject-help" placeholder="例如:论坛帖子无法回复" /><p id="ticket-subject-help" className="text-muted" style={{ fontSize: 13 }}>请用一句话概括遇到的问题最多 120 </p></div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 16 }}><div className="form-group"><label htmlFor="ticket-category">问题类型 <span aria-hidden="true">*</span></label><select id="ticket-category" value={form.category} onChange={update('category')} required>{categories.map(([value, label]) => <option key={value} value={value}>{label}</option>)}</select></div><div className="form-group"><label htmlFor="ticket-priority">优先级</label><select id="ticket-priority" value={form.priority} onChange={update('priority')}>{priorities.map(([value, label]) => <option key={value} value={value}>{label}</option>)}</select></div></div>
<div className="form-group"><label htmlFor="ticket-description">问题描述 <span aria-hidden="true">*</span></label><textarea id="ticket-description" value={form.description} onChange={update('description')} maxLength={20000} required rows={8} aria-describedby="ticket-description-help" placeholder="请描述发生了什么、如何复现,以及你期望的结果。" /><p id="ticket-description-help" className="text-muted" style={{ fontSize: 13 }}>至少 10 个字最多 20000 </p></div>
<div className="form-group"><label htmlFor="ticket-source-url">发现问题的页面地址</label><input id="ticket-source-url" type="url" value={form.source_url} onChange={update('source_url')} maxLength={1000} aria-describedby="ticket-source-help" /><p id="ticket-source-help" className="text-muted" style={{ fontSize: 13 }}>已自动记录当前页面地址如果问题来自其他页面可以在这里修改</p></div>
<div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end', flexWrap: 'wrap' }}><Link to="/tickets.html" className="btn btn-tonal">取消</Link><button type="submit" className="btn btn-filled" disabled={busy}>{busy ? '提交中…' : '提交工单'}</button></div>
</form>
</div>;
}