From c539216146b19eb8d3d6c1f6dad5577cfe40ef7f Mon Sep 17 00:00:00 2001
From: miaomiao <2733773236@qq.com>
Date: Sat, 5 Sep 2026 03:12:14 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=90=8E=E5=8F=B0=E5=B7=A5?=
=?UTF-8?q?=E5=8D=95=E5=A4=84=E7=90=86=E5=B8=83=E5=B1=80=E4=B8=8E=E6=8E=A7?=
=?UTF-8?q?=E4=BB=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
frontend/src/admin/pages/TicketManage.jsx | 55 ++++++---
frontend/src/admin/theme.css | 137 +++++++++++++++++++++-
2 files changed, 172 insertions(+), 20 deletions(-)
diff --git a/frontend/src/admin/pages/TicketManage.jsx b/frontend/src/admin/pages/TicketManage.jsx
index 380aa45..7196c37 100644
--- a/frontend/src/admin/pages/TicketManage.jsx
+++ b/frontend/src/admin/pages/TicketManage.jsx
@@ -79,6 +79,7 @@ export default function TicketManage() {
const [reply, setReply] = useState('');
const [internal, setInternal] = useState('');
const [assignees, setAssignees] = useState([]);
+ const [saveFeedback, setSaveFeedback] = useState({ type: '', text: '' });
const loadStats = useCallback(() => {
request('/tickets/admin/stats').then((data) => setStats(data || {})).catch(() => {});
@@ -110,19 +111,41 @@ export default function TicketManage() {
useEffect(() => { loadList(); }, [loadList]);
useEffect(() => { loadStats(); }, [loadStats]);
useEffect(() => {
- request('/tickets/admin/assignees').then((data) => setAssignees(Array.isArray(data) ? data : (data.users || data.list || []))).catch(() => {});
+ request('/tickets/admin/assignees').then((data) => setAssignees(Array.isArray(data) ? data : (data.users || data.list || []))).catch((e) => setSaveFeedback({ type: 'error', text: e.message || '负责人列表加载失败' }));
}, []);
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
const selectedTicket = detail && detail.ticket;
+ const normalizedAssignees = useMemo(() => {
+ const values = assignees
+ .filter((user) => user && (user.role === 'admin' || user.role === undefined))
+ .map((user) => ({
+ ...user,
+ id: String(user.id),
+ displayName: user.nickname || user.username || user.name || `管理员 #${user.id}`,
+ }));
+ if (selectedTicket?.assignee_id != null && !values.some((user) => user.id === String(selectedTicket.assignee_id))) {
+ values.push({
+ id: String(selectedTicket.assignee_id),
+ displayName: selectedTicket.assignee_name || selectedTicket.assignee_username || `负责人 #${selectedTicket.assignee_id}`,
+ });
+ }
+ return values;
+ }, [assignees, selectedTicket]);
const updateFilter = (key, value) => { setPage(1); setFilters((old) => ({ ...old, [key]: value })); };
const refresh = () => { loadList(); loadStats(); if (selectedId) loadDetail(selectedId); };
const updateTicket = async (path, body, message) => {
setSaving(true);
- try { await request(path, { method: 'PUT', body }); showSnack(message); loadDetail(selectedId); loadList(); loadStats(); }
- catch (e) { showSnack(e.message || '保存失败', 'error'); }
- finally { setSaving(false); }
+ setSaveFeedback({ type: 'saving', text: '正在保存…' });
+ try {
+ await request(path, { method: 'PUT', body });
+ setSaveFeedback({ type: 'success', text: '已保存' });
+ showSnack(message); loadDetail(selectedId); loadList(); loadStats();
+ } catch (e) {
+ setSaveFeedback({ type: 'error', text: e.message || '保存失败,请重试' });
+ showSnack(e.message || '保存失败', 'error');
+ } finally { setSaving(false); }
};
const sendMessage = async (internalMessage) => {
const content = (internalMessage ? internal : reply).trim();
@@ -157,24 +180,24 @@ export default function TicketManage() {
{ e.preventDefault(); setPage(1); loadList(); }}>
-
- updateFilter('q', e.target.value)} />
- updateFilter('status', e.target.value)}>{Object.entries(STATUS).map(([key, item]) => )}
- updateFilter('priority', e.target.value)}>{Object.entries(PRIORITY).map(([key, item]) => )}
- updateFilter('category', e.target.value)}>{Object.entries(CATEGORY).map(([key, label]) => )}
-
-
+
+ updateFilter('q', e.target.value)} />
+ updateFilter('status', e.target.value)}>{Object.entries(STATUS).map(([key, item]) => )}
+ updateFilter('priority', e.target.value)}>{Object.entries(PRIORITY).map(([key, item]) => )}
+ updateFilter('category', e.target.value)}>{Object.entries(CATEGORY).map(([key, label]) => )}
+
+
{error && 重试} sx={{ mb: 2 }}>{error}}
-
+
最近工单共 {total} 条
{loading ? : list.length === 0 ? 暂无匹配工单 : (
-
- {list.map((ticket) => )}
+
+ {list.map((ticket) => )}
)}
setPage(page - 1)}>第 {page} / {totalPages} 页= totalPages || loading} onClick={() => setPage(page + 1)}>
@@ -182,11 +205,11 @@ export default function TicketManage() {
-
+
{!selectedId ? 选择一个工单开始处理工单详情、回复和状态操作会显示在这里 : detailLoading ? : selectedTicket ? (
{selectedTicket.subject || selectedTicket.title}{selectedTicket.ticket_no || `#${selectedTicket.id}`} · {selectedTicket.requester_name || selectedTicket.requester_username || '未知用户'} · 创建于 {fmtTime(selectedTicket.created_at)}
- updateTicket(`/tickets/${selectedId}/status`, { status: e.target.value }, '状态已更新')}>{Object.entries(STATUS).map(([key, item]) => )} updateTicket(`/tickets/${selectedId}/priority`, { priority: e.target.value }, '优先级已更新')}>{Object.entries(PRIORITY).map(([key, item]) => )} updateTicket(`/tickets/${selectedId}/assignee`, { assignee_id: e.target.value || null }, '负责人已更新')}>{assignees.filter((user) => user.role === 'admin' || user.role === undefined).map((user) => )}
+ updateTicket(`/tickets/${selectedId}/status`, { status: e.target.value }, '状态已更新')}>{Object.entries(STATUS).map(([key, item]) => )} updateTicket(`/tickets/${selectedId}/priority`, { priority: e.target.value }, '优先级已更新')}>{Object.entries(PRIORITY).map(([key, item]) => )} { const person = normalizedAssignees.find((user) => user.id === String(value)); return {person ? person.displayName : '未分配'}; } }} onChange={(e) => updateTicket(`/tickets/${selectedId}/assignee`, { assignee_id: e.target.value ? Number(e.target.value) || e.target.value : null }, '负责人已更新')}>{normalizedAssignees.map((user) => )}{saveFeedback.text && {saveFeedback.text}}
问题描述{selectedTicket.description || '暂无描述'}{selectedTicket.source_url && 来源:{selectedTicket.source_url}}
处理记录{messages.map((message) => {String(message.author_name || message.author_username || '管')[0]}{message.author_name || message.author_username || '管理员'}{message.is_internal ? : }{fmtTime(message.created_at)}{message.content})}{events.map((event) => {fmtTime(event.created_at)} · {event.detail || `${event.field_name || '工单'}已更新`})}
diff --git a/frontend/src/admin/theme.css b/frontend/src/admin/theme.css
index 686c98b..995b4ca 100644
--- a/frontend/src/admin/theme.css
+++ b/frontend/src/admin/theme.css
@@ -50,7 +50,7 @@ body {
gap: 16px;
width: 100%;
min-width: 0;
- min-height: calc(100vh - 280px);
+ height: max(520px, calc(100vh - 280px));
align-items: stretch;
}
@@ -68,11 +68,116 @@ body {
}
.ticket-list-pane > .MuiPaper-root {
- min-height: calc(100vh - 280px);
+ min-height: 0;
}
.ticket-detail-pane > .MuiPaper-root {
- min-height: calc(100vh - 280px);
+ min-height: 0;
+ overflow: auto;
+}
+
+.ticket-list-card {
+ display: flex;
+ flex-direction: column;
+}
+
+.ticket-list-scroll {
+ flex: 1 1 auto;
+ min-height: 0;
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+
+.ticket-list-scroll > li,
+.ticket-list-scroll > li > .MuiButton-root {
+ min-width: 0;
+ max-width: 100%;
+}
+
+.ticket-list-scroll > li > .MuiButton-root > .MuiBox-root {
+ min-width: 0;
+}
+
+.ticket-list-scroll .MuiTypography-root {
+ min-width: 0;
+ overflow-wrap: anywhere;
+}
+
+.ticket-list-scroll .MuiTypography-noWrap {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+ white-space: normal;
+ overflow: hidden;
+}
+
+.ticket-list-scroll .MuiChip-root {
+ flex: 0 0 auto;
+ max-width: 100%;
+}
+
+.ticket-list-meta {
+ flex-wrap: wrap;
+}
+
+.ticket-list-number {
+ min-width: 0;
+ overflow-wrap: anywhere;
+}
+
+.ticket-detail-card {
+ overflow: auto;
+}
+
+.ticket-filter-grid {
+ display: grid;
+ grid-template-columns: minmax(240px, 2fr) repeat(3, minmax(130px, 1fr)) minmax(96px, 0.85fr);
+ gap: 10px;
+ align-items: center;
+ min-width: 0;
+}
+
+.ticket-filter-grid > * {
+ min-width: 0;
+}
+
+.ticket-filter-grid .MuiInputBase-root,
+.ticket-filter-grid .MuiButton-root {
+ width: 100%;
+ min-width: 0;
+}
+
+.ticket-select-value {
+ display: block;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.ticket-save-feedback {
+ overflow-wrap: anywhere;
+}
+
+.ticket-save-saving { color: var(--mui-palette-text-secondary, inherit); }
+.ticket-save-success { color: #2e7d32; }
+.ticket-save-error { color: #d32f2f; }
+
+[data-theme="dark"] .ticket-save-success { color: #81c784; }
+[data-theme="dark"] .ticket-save-error { color: #ef9a9a; }
+
+@media (max-width: 1199px) and (min-width: 700px) {
+ .ticket-filter-grid {
+ grid-template-columns: minmax(220px, 1.6fr) repeat(3, minmax(118px, 1fr));
+ }
+
+ .ticket-filter-grid .ticket-filter-search {
+ grid-column: 1 / -1;
+ }
+
+ .ticket-filter-grid > .MuiButton-root {
+ grid-column: 1 / -1;
+ }
}
@media (max-width: 1199px) and (min-width: 700px) {
@@ -89,7 +194,7 @@ body {
.ticket-work-area {
grid-template-columns: minmax(0, 1fr);
- min-height: 0;
+ height: auto;
gap: 16px;
}
@@ -98,9 +203,33 @@ body {
min-height: 0;
}
+ .ticket-list-card {
+ max-height: 480px;
+ }
+
+ .ticket-list-scroll {
+ flex: 0 1 auto;
+ max-height: 350px;
+ }
+
.ticket-detail-pane > .MuiPaper-root {
min-height: 480px;
}
+
+ .ticket-filter-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .ticket-filter-grid .ticket-filter-search,
+ .ticket-filter-grid > .MuiButton-root {
+ grid-column: 1 / -1;
+ }
+}
+
+@media (max-width: 459px) {
+ .ticket-filter-grid {
+ grid-template-columns: minmax(0, 1fr);
+ }
}
/* 减弱动效偏好 */