修复后台工单处理布局与控件

This commit is contained in:
2026-09-05 03:12:14 +08:00
parent e7bb30b4bd
commit c539216146
2 changed files with 172 additions and 20 deletions
+39 -16
View File
@@ -79,6 +79,7 @@ export default function TicketManage() {
const [reply, setReply] = useState(''); const [reply, setReply] = useState('');
const [internal, setInternal] = useState(''); const [internal, setInternal] = useState('');
const [assignees, setAssignees] = useState([]); const [assignees, setAssignees] = useState([]);
const [saveFeedback, setSaveFeedback] = useState({ type: '', text: '' });
const loadStats = useCallback(() => { const loadStats = useCallback(() => {
request('/tickets/admin/stats').then((data) => setStats(data || {})).catch(() => {}); request('/tickets/admin/stats').then((data) => setStats(data || {})).catch(() => {});
@@ -110,19 +111,41 @@ export default function TicketManage() {
useEffect(() => { loadList(); }, [loadList]); useEffect(() => { loadList(); }, [loadList]);
useEffect(() => { loadStats(); }, [loadStats]); useEffect(() => { loadStats(); }, [loadStats]);
useEffect(() => { 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 totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
const selectedTicket = detail && detail.ticket; 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 updateFilter = (key, value) => { setPage(1); setFilters((old) => ({ ...old, [key]: value })); };
const refresh = () => { loadList(); loadStats(); if (selectedId) loadDetail(selectedId); }; const refresh = () => { loadList(); loadStats(); if (selectedId) loadDetail(selectedId); };
const updateTicket = async (path, body, message) => { const updateTicket = async (path, body, message) => {
setSaving(true); setSaving(true);
try { await request(path, { method: 'PUT', body }); showSnack(message); loadDetail(selectedId); loadList(); loadStats(); } setSaveFeedback({ type: 'saving', text: '正在保存…' });
catch (e) { showSnack(e.message || '保存失败', 'error'); } try {
finally { setSaving(false); } 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 sendMessage = async (internalMessage) => {
const content = (internalMessage ? internal : reply).trim(); const content = (internalMessage ? internal : reply).trim();
@@ -157,24 +180,24 @@ export default function TicketManage() {
</Stack> </Stack>
<Paper variant="outlined" sx={{ p: 1.5, mb: 2 }} component="form" onSubmit={(e) => { e.preventDefault(); setPage(1); loadList(); }}> <Paper variant="outlined" sx={{ p: 1.5, mb: 2 }} component="form" onSubmit={(e) => { e.preventDefault(); setPage(1); loadList(); }}>
<Grid container spacing={1.25} alignItems="center"> <Box className="ticket-filter-grid">
<Grid item xs={12} md={4}><TextField fullWidth size="small" label="搜索工单" placeholder="编号、标题或内容" value={filters.q} onChange={(e) => updateFilter('q', e.target.value)} /></Grid> <TextField className="ticket-filter-search" fullWidth size="small" label="搜索工单" placeholder="编号、标题或内容" value={filters.q} onChange={(e) => updateFilter('q', e.target.value)} />
<Grid item xs={6} sm={4} md={2}><TextField fullWidth select size="small" label="状态" value={filters.status} onChange={(e) => updateFilter('status', e.target.value)}><MenuItem value="">全部状态</MenuItem>{Object.entries(STATUS).map(([key, item]) => <MenuItem key={key} value={key}>{item.label}</MenuItem>)}</TextField></Grid> <TextField fullWidth select size="small" label="状态" value={filters.status} onChange={(e) => updateFilter('status', e.target.value)}><MenuItem value="">全部状态</MenuItem>{Object.entries(STATUS).map(([key, item]) => <MenuItem key={key} value={key}>{item.label}</MenuItem>)}</TextField>
<Grid item xs={6} sm={4} md={2}><TextField fullWidth select size="small" label="优先级" value={filters.priority} onChange={(e) => updateFilter('priority', e.target.value)}><MenuItem value="">全部优先级</MenuItem>{Object.entries(PRIORITY).map(([key, item]) => <MenuItem key={key} value={key}>{item.label}</MenuItem>)}</TextField></Grid> <TextField fullWidth select size="small" label="优先级" value={filters.priority} onChange={(e) => updateFilter('priority', e.target.value)}><MenuItem value="">全部优先级</MenuItem>{Object.entries(PRIORITY).map(([key, item]) => <MenuItem key={key} value={key}>{item.label}</MenuItem>)}</TextField>
<Grid item xs={12} sm={4} md={2}><TextField fullWidth select size="small" label="类型" value={filters.category} onChange={(e) => updateFilter('category', e.target.value)}><MenuItem value="">全部类型</MenuItem>{Object.entries(CATEGORY).map(([key, label]) => <MenuItem key={key} value={key}>{label}</MenuItem>)}</TextField></Grid> <TextField fullWidth select size="small" label="类型" value={filters.category} onChange={(e) => updateFilter('category', e.target.value)}><MenuItem value="">全部类型</MenuItem>{Object.entries(CATEGORY).map(([key, label]) => <MenuItem key={key} value={key}>{label}</MenuItem>)}</TextField>
<Grid item xs={12} md={2}><Button fullWidth type="submit" variant="contained" sx={{ minHeight: 40 }}>搜索</Button></Grid> <Button fullWidth type="submit" variant="contained" sx={{ minHeight: 40 }}>搜索</Button>
</Grid> </Box>
</Paper> </Paper>
{error && <Alert severity="error" role="alert" action={<Button color="inherit" size="small" onClick={loadList}>重试</Button>} sx={{ mb: 2 }}>{error}</Alert>} {error && <Alert severity="error" role="alert" action={<Button color="inherit" size="small" onClick={loadList}>重试</Button>} sx={{ mb: 2 }}>{error}</Alert>}
<Box className="ticket-work-area"> <Box className="ticket-work-area">
<Box className="ticket-list-pane"> <Box className="ticket-list-pane">
<Paper variant="outlined" sx={{ height: '100%', overflow: 'hidden' }}> <Paper className="ticket-list-card" variant="outlined" sx={{ height: '100%', overflow: 'hidden' }}>
<Box sx={{ p: 1.5, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}><Typography variant="subtitle1" fontWeight={700}>最近工单</Typography><Typography variant="caption" color="text.secondary"> {total} </Typography></Box> <Box sx={{ p: 1.5, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}><Typography variant="subtitle1" fontWeight={700}>最近工单</Typography><Typography variant="caption" color="text.secondary"> {total} </Typography></Box>
<Divider /> <Divider />
{loading ? <Box role="status" aria-label="正在加载工单" sx={{ p: 4, textAlign: 'center' }}><CircularProgress size={28} /></Box> : list.length === 0 ? <Box sx={{ p: 4, textAlign: 'center' }}><Typography color="text.secondary">暂无匹配工单</Typography></Box> : ( {loading ? <Box role="status" aria-label="正在加载工单" sx={{ p: 4, textAlign: 'center' }}><CircularProgress size={28} /></Box> : list.length === 0 ? <Box sx={{ p: 4, textAlign: 'center' }}><Typography color="text.secondary">暂无匹配工单</Typography></Box> : (
<Stack component="ul" aria-label="工单列表" sx={{ listStyle: 'none', m: 0, p: 0 }}> <Stack component="ul" className="ticket-list-scroll" aria-label="工单列表" sx={{ listStyle: 'none', m: 0, p: 0 }}>
{list.map((ticket) => <Box component="li" key={ticket.id}><Button fullWidth onClick={() => loadDetail(ticket.id)} aria-pressed={String(selectedId) === String(ticket.id)} sx={{ p: 1.5, textAlign: 'left', textTransform: 'none', justifyContent: 'flex-start', borderRadius: 0, borderBottom: '1px solid', borderColor: 'divider', bgcolor: String(selectedId) === String(ticket.id) ? 'action.selected' : 'transparent' }}><Box sx={{ width: '100%', minWidth: 0 }}><Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 0.5 }}><Typography variant="caption" color="text.secondary">{ticket.ticket_no || `#${ticket.id}`}</Typography><StatusChip value={ticket.status} /><PriorityChip value={ticket.priority} /></Stack><Typography variant="body2" fontWeight={600} noWrap>{ticket.subject || ticket.title || '无标题工单'}</Typography><Typography variant="caption" color="text.secondary" noWrap>{ticket.requester_name || ticket.requester_username || '未知用户'} · {fmtTime(ticket.updated_at || ticket.created_at)}</Typography></Box></Button></Box>)} {list.map((ticket) => <Box component="li" key={ticket.id}><Button fullWidth onClick={() => loadDetail(ticket.id)} aria-pressed={String(selectedId) === String(ticket.id)} sx={{ p: 1.5, textAlign: 'left', textTransform: 'none', justifyContent: 'flex-start', borderRadius: 0, borderBottom: '1px solid', borderColor: 'divider', bgcolor: String(selectedId) === String(ticket.id) ? 'action.selected' : 'transparent' }}><Box sx={{ width: '100%', minWidth: 0 }}><Stack className="ticket-list-meta" direction="row" spacing={1} alignItems="center" useFlexGap sx={{ mb: 0.5 }}><Typography className="ticket-list-number" variant="caption" color="text.secondary">{ticket.ticket_no || `#${ticket.id}`}</Typography><StatusChip value={ticket.status} /><PriorityChip value={ticket.priority} /></Stack><Typography variant="body2" fontWeight={600} noWrap>{ticket.subject || ticket.title || '无标题工单'}</Typography><Typography variant="caption" color="text.secondary" noWrap>{ticket.requester_name || ticket.requester_username || '未知用户'} · {fmtTime(ticket.updated_at || ticket.created_at)}</Typography></Box></Button></Box>)}
</Stack> </Stack>
)} )}
<Stack direction="row" alignItems="center" justifyContent="center" spacing={1} sx={{ p: 1.25 }}><IconButton aria-label="上一页" size="small" disabled={page <= 1 || loading} onClick={() => setPage(page - 1)}><NavigateBeforeIcon /></IconButton><Typography variant="caption"> {page} / {totalPages} </Typography><IconButton aria-label="下一页" size="small" disabled={page >= totalPages || loading} onClick={() => setPage(page + 1)}><NavigateNextIcon /></IconButton></Stack> <Stack direction="row" alignItems="center" justifyContent="center" spacing={1} sx={{ p: 1.25 }}><IconButton aria-label="上一页" size="small" disabled={page <= 1 || loading} onClick={() => setPage(page - 1)}><NavigateBeforeIcon /></IconButton><Typography variant="caption"> {page} / {totalPages} </Typography><IconButton aria-label="下一页" size="small" disabled={page >= totalPages || loading} onClick={() => setPage(page + 1)}><NavigateNextIcon /></IconButton></Stack>
@@ -182,11 +205,11 @@ export default function TicketManage() {
</Box> </Box>
<Box className="ticket-detail-pane"> <Box className="ticket-detail-pane">
<Paper variant="outlined" sx={{ p: { xs: 2, md: 2.5 }, minHeight: 420 }}> <Paper className="ticket-detail-card" variant="outlined" sx={{ p: { xs: 2, md: 2.5 }, minHeight: 420 }}>
{!selectedId ? <Box sx={{ py: 10, textAlign: 'center' }}><Typography variant="h6" color="text.secondary">选择一个工单开始处理</Typography><Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>工单详情回复和状态操作会显示在这里</Typography></Box> : detailLoading ? <Box role="status" aria-label="正在加载工单详情" sx={{ py: 10, textAlign: 'center' }}><CircularProgress /></Box> : selectedTicket ? ( {!selectedId ? <Box sx={{ py: 10, textAlign: 'center' }}><Typography variant="h6" color="text.secondary">选择一个工单开始处理</Typography><Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>工单详情回复和状态操作会显示在这里</Typography></Box> : detailLoading ? <Box role="status" aria-label="正在加载工单详情" sx={{ py: 10, textAlign: 'center' }}><CircularProgress /></Box> : selectedTicket ? (
<Stack spacing={2}> <Stack spacing={2}>
<Box><Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap" useFlexGap><Typography variant="h6" component="h2" sx={{ overflowWrap: 'anywhere' }}>{selectedTicket.subject || selectedTicket.title}</Typography><StatusChip value={selectedTicket.status} /><PriorityChip value={selectedTicket.priority} /></Stack><Typography variant="caption" color="text.secondary">{selectedTicket.ticket_no || `#${selectedTicket.id}`} · {selectedTicket.requester_name || selectedTicket.requester_username || '未知用户'} · 创建于 {fmtTime(selectedTicket.created_at)}</Typography></Box> <Box><Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap" useFlexGap><Typography variant="h6" component="h2" sx={{ overflowWrap: 'anywhere' }}>{selectedTicket.subject || selectedTicket.title}</Typography><StatusChip value={selectedTicket.status} /><PriorityChip value={selectedTicket.priority} /></Stack><Typography variant="caption" color="text.secondary">{selectedTicket.ticket_no || `#${selectedTicket.id}`} · {selectedTicket.requester_name || selectedTicket.requester_username || '未知用户'} · 创建于 {fmtTime(selectedTicket.created_at)}</Typography></Box>
<Grid container spacing={1.25}><Grid item xs={12} sm={4}><TextField fullWidth select size="small" label="状态" value={selectedTicket.status || 'open'} disabled={saving} onChange={(e) => updateTicket(`/tickets/${selectedId}/status`, { status: e.target.value }, '状态已更新')}>{Object.entries(STATUS).map(([key, item]) => <MenuItem key={key} value={key}>{item.label}</MenuItem>)}</TextField></Grid><Grid item xs={12} sm={4}><TextField fullWidth select size="small" label="优先级" value={selectedTicket.priority || 'normal'} disabled={saving} onChange={(e) => updateTicket(`/tickets/${selectedId}/priority`, { priority: e.target.value }, '优先级已更新')}>{Object.entries(PRIORITY).map(([key, item]) => <MenuItem key={key} value={key}>{item.label}</MenuItem>)}</TextField></Grid><Grid item xs={12} sm={4}><TextField fullWidth select size="small" label="负责人" value={selectedTicket.assignee_id || ''} disabled={saving} onChange={(e) => updateTicket(`/tickets/${selectedId}/assignee`, { assignee_id: e.target.value || null }, '负责人已更新')}><MenuItem value="">未分配</MenuItem>{assignees.filter((user) => user.role === 'admin' || user.role === undefined).map((user) => <MenuItem key={user.id} value={user.id}>{user.nickname || user.username}</MenuItem>)}</TextField></Grid></Grid> <Box><Grid container spacing={1.25}><Grid item xs={12} sm={4}><TextField fullWidth select size="small" label="状态" helperText="管理员可更新处理阶段" value={selectedTicket.status || 'open'} disabled={saving} onChange={(e) => updateTicket(`/tickets/${selectedId}/status`, { status: e.target.value }, '状态已更新')}>{Object.entries(STATUS).map(([key, item]) => <MenuItem key={key} value={key}>{item.label}</MenuItem>)}</TextField></Grid><Grid item xs={12} sm={4}><TextField fullWidth select size="small" label="优先级" helperText="用于安排处理顺序" value={selectedTicket.priority || 'normal'} disabled={saving} onChange={(e) => updateTicket(`/tickets/${selectedId}/priority`, { priority: e.target.value }, '优先级已更新')}>{Object.entries(PRIORITY).map(([key, item]) => <MenuItem key={key} value={key}>{item.label}</MenuItem>)}</TextField></Grid><Grid item xs={12} sm={4}><TextField fullWidth select size="small" label="负责人" helperText="可选择管理员或设为未分配" value={selectedTicket.assignee_id == null ? '' : String(selectedTicket.assignee_id)} disabled={saving} SelectProps={{ renderValue: (value) => { const person = normalizedAssignees.find((user) => user.id === String(value)); return <Box component="span" className="ticket-select-value" title={person ? person.displayName : '未分配'}>{person ? person.displayName : '未分配'}</Box>; } }} onChange={(e) => updateTicket(`/tickets/${selectedId}/assignee`, { assignee_id: e.target.value ? Number(e.target.value) || e.target.value : null }, '负责人已更新')}><MenuItem value="">未分配</MenuItem>{normalizedAssignees.map((user) => <MenuItem key={user.id} value={user.id} sx={{ whiteSpace: 'normal', overflowWrap: 'anywhere' }}>{user.displayName}</MenuItem>)}</TextField></Grid></Grid>{saveFeedback.text && <Typography className={`ticket-save-feedback ticket-save-${saveFeedback.type}`} variant="caption" role={saveFeedback.type === 'error' ? 'alert' : 'status'} sx={{ display: 'block', mt: 0.75 }}>{saveFeedback.text}</Typography>}</Box>
<Paper variant="outlined" sx={{ p: 1.5, bgcolor: 'action.hover' }}><Typography variant="subtitle2" gutterBottom>问题描述</Typography><Typography variant="body2" sx={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{selectedTicket.description || '暂无描述'}</Typography>{selectedTicket.source_url && <Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 1, overflowWrap: 'anywhere' }}>来源{selectedTicket.source_url}</Typography>}</Paper> <Paper variant="outlined" sx={{ p: 1.5, bgcolor: 'action.hover' }}><Typography variant="subtitle2" gutterBottom>问题描述</Typography><Typography variant="body2" sx={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{selectedTicket.description || '暂无描述'}</Typography>{selectedTicket.source_url && <Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 1, overflowWrap: 'anywhere' }}>来源{selectedTicket.source_url}</Typography>}</Paper>
<Box><Typography variant="subtitle2" sx={{ mb: 1 }}>处理记录</Typography><Stack component="ol" spacing={1.25} sx={{ m: 0, pl: 2.5 }}>{messages.map((message) => <Box component="li" key={`m-${message.id}`}><Paper variant="outlined" sx={{ p: 1.25 }}><Stack direction="row" spacing={1} alignItems="center"><Avatar sx={{ width: 26, height: 26, fontSize: 12 }}>{String(message.author_name || message.author_username || '管')[0]}</Avatar><Typography variant="body2" fontWeight={600}>{message.author_name || message.author_username || '管理员'}</Typography>{message.is_internal ? <Chip size="small" label="内部备注" color="warning" /> : <Chip size="small" label="公开回复" variant="outlined" />}<Typography variant="caption" color="text.secondary" sx={{ ml: 'auto' }}>{fmtTime(message.created_at)}</Typography></Stack><Typography variant="body2" sx={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', mt: 1 }}>{message.content}</Typography></Paper></Box>)}{events.map((event) => <Box component="li" key={`e-${event.id}`}><Typography variant="caption" color="text.secondary">{fmtTime(event.created_at)} · {event.detail || `${event.field_name || '工单'}已更新`}</Typography></Box>)}</Stack></Box> <Box><Typography variant="subtitle2" sx={{ mb: 1 }}>处理记录</Typography><Stack component="ol" spacing={1.25} sx={{ m: 0, pl: 2.5 }}>{messages.map((message) => <Box component="li" key={`m-${message.id}`}><Paper variant="outlined" sx={{ p: 1.25 }}><Stack direction="row" spacing={1} alignItems="center"><Avatar sx={{ width: 26, height: 26, fontSize: 12 }}>{String(message.author_name || message.author_username || '管')[0]}</Avatar><Typography variant="body2" fontWeight={600}>{message.author_name || message.author_username || '管理员'}</Typography>{message.is_internal ? <Chip size="small" label="内部备注" color="warning" /> : <Chip size="small" label="公开回复" variant="outlined" />}<Typography variant="caption" color="text.secondary" sx={{ ml: 'auto' }}>{fmtTime(message.created_at)}</Typography></Stack><Typography variant="body2" sx={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', mt: 1 }}>{message.content}</Typography></Paper></Box>)}{events.map((event) => <Box component="li" key={`e-${event.id}`}><Typography variant="caption" color="text.secondary">{fmtTime(event.created_at)} · {event.detail || `${event.field_name || '工单'}已更新`}</Typography></Box>)}</Stack></Box>
<Divider /> <Divider />
+133 -4
View File
@@ -50,7 +50,7 @@ body {
gap: 16px; gap: 16px;
width: 100%; width: 100%;
min-width: 0; min-width: 0;
min-height: calc(100vh - 280px); height: max(520px, calc(100vh - 280px));
align-items: stretch; align-items: stretch;
} }
@@ -68,11 +68,116 @@ body {
} }
.ticket-list-pane > .MuiPaper-root { .ticket-list-pane > .MuiPaper-root {
min-height: calc(100vh - 280px); min-height: 0;
} }
.ticket-detail-pane > .MuiPaper-root { .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) { @media (max-width: 1199px) and (min-width: 700px) {
@@ -89,7 +194,7 @@ body {
.ticket-work-area { .ticket-work-area {
grid-template-columns: minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
min-height: 0; height: auto;
gap: 16px; gap: 16px;
} }
@@ -98,9 +203,33 @@ body {
min-height: 0; min-height: 0;
} }
.ticket-list-card {
max-height: 480px;
}
.ticket-list-scroll {
flex: 0 1 auto;
max-height: 350px;
}
.ticket-detail-pane > .MuiPaper-root { .ticket-detail-pane > .MuiPaper-root {
min-height: 480px; 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);
}
} }
/* 减弱动效偏好 */ /* 减弱动效偏好 */