feat: 新增错误问题追踪与管理功能
- 升级程序版本至 v1.5.25 - 新增对比度文本颜色计算与垂直滚动条绘制工具函数,优化选择界面滚动显示 - 网页端新增问题记录管理页面,配套完整的表格与详情展示样式 - 实现issue记录标准化处理、实时事件推送接收与按时间倒序排序展示 - 重构原有选择界面的滚动逻辑,替换为通用滚动条绘制函数
This commit is contained in:
+137
-1
@@ -27,6 +27,7 @@ createApp({
|
||||
tickets: '车票记录',
|
||||
vouchers: '凭证管理',
|
||||
iccards: 'IC 卡管理',
|
||||
issues: '问题汇总',
|
||||
assets: '线路图',
|
||||
settings: '系统设置',
|
||||
logs: '日志'
|
||||
@@ -163,6 +164,132 @@ createApp({
|
||||
try { return JSON.parse(t); } catch (e) { return null; }
|
||||
};
|
||||
|
||||
const safeJsonStringify = (value) => {
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch (e) {
|
||||
return String(value);
|
||||
}
|
||||
};
|
||||
|
||||
const formatIssueKey = (key) => {
|
||||
const map = {
|
||||
device: '设备',
|
||||
device_id: '设备编号',
|
||||
machine_id: '机器 ID',
|
||||
station_code: '车站编号',
|
||||
station_name: '车站名称',
|
||||
station: '车站',
|
||||
error_msg: '错误信息',
|
||||
ticket_id: '车票 ID',
|
||||
card_id: '卡号',
|
||||
holder_name: '持卡人',
|
||||
balance: '余额',
|
||||
amount: '金额',
|
||||
price: '价格',
|
||||
order_code: '订单号',
|
||||
voucher_code: '凭证码',
|
||||
method: '方法',
|
||||
source: '来源',
|
||||
reason: '原因',
|
||||
status: '状态',
|
||||
action: '动作',
|
||||
line: '线路',
|
||||
route: '区间',
|
||||
from: '起点',
|
||||
to: '终点',
|
||||
time: '时间',
|
||||
ts: '时间戳'
|
||||
};
|
||||
const raw = String(key || '').trim();
|
||||
if (!raw) return '字段';
|
||||
if (map[raw]) return map[raw];
|
||||
return raw
|
||||
.replace(/[_-]+/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.replace(/^\w/, (ch) => ch.toUpperCase());
|
||||
};
|
||||
|
||||
const formatIssueValue = (value) => {
|
||||
if (value == null || value === '') return '---';
|
||||
if (typeof value === 'boolean') return value ? '是' : '否';
|
||||
if (typeof value === 'number') return Number.isFinite(value) ? String(value) : '---';
|
||||
if (typeof value === 'string') return value.trim() || '---';
|
||||
if (Array.isArray(value)) {
|
||||
if (!value.length) return '---';
|
||||
if (value.every((item) => item == null || ['string', 'number', 'boolean'].includes(typeof item))) {
|
||||
return value.map(formatIssueValue).join(' / ');
|
||||
}
|
||||
return safeJsonStringify(value);
|
||||
}
|
||||
if (typeof value === 'object') return safeJsonStringify(value);
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const buildIssueDetailView = (detail) => {
|
||||
const rawText = String(detail || '').trim();
|
||||
if (!rawText) return { entries: [], rawText: '', rawExpanded: false };
|
||||
|
||||
const hiddenKeys = new Set(['device', 'device_id', 'machine_id', 'station_code', 'station_name', 'error_msg', 'detail', 'type']);
|
||||
const parsed = parseJsonSafe(rawText);
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
const entries = Object.entries(parsed)
|
||||
.filter(([key, value]) => !hiddenKeys.has(String(key || '').trim()) && value != null && value !== '')
|
||||
.map(([key, value]) => ({
|
||||
key: String(key),
|
||||
label: formatIssueKey(key),
|
||||
value: formatIssueValue(value)
|
||||
}));
|
||||
return {
|
||||
entries,
|
||||
rawText: entries.length ? rawText : '',
|
||||
rawExpanded: false
|
||||
};
|
||||
}
|
||||
|
||||
const segments = rawText
|
||||
.split(/\r?\n|;|;/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
const entries = [];
|
||||
for (const segment of segments) {
|
||||
const matched = /^([^:=:]{1,30})\s*[:=:]\s*(.+)$/.exec(segment);
|
||||
if (!matched) continue;
|
||||
const key = String(matched[1] || '').trim();
|
||||
if (!key || hiddenKeys.has(key)) continue;
|
||||
entries.push({
|
||||
key,
|
||||
label: formatIssueKey(key),
|
||||
value: formatIssueValue(matched[2])
|
||||
});
|
||||
}
|
||||
|
||||
const hasPlainBody = segments.length <= 1 && entries.length <= 1;
|
||||
return {
|
||||
entries: hasPlainBody ? [] : entries,
|
||||
rawText,
|
||||
rawExpanded: hasPlainBody
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeIssueRecord = (issue) => {
|
||||
const item = issue && typeof issue === 'object' ? issue : {};
|
||||
const device = String(item.device || item.machine_id || '').trim();
|
||||
const stationCode = String(item.station_code || '').trim();
|
||||
const stationName = stationCode ? getStationName(stationCode) : '';
|
||||
const detailView = buildIssueDetailView(item.detail);
|
||||
return {
|
||||
...item,
|
||||
displayDevice: device || '---',
|
||||
displayStation: stationName && stationName !== stationCode ? `${stationName} (${stationCode})` : (stationCode || '---'),
|
||||
displayError: String(item.error_msg || '').trim() || '未提供错误信息',
|
||||
detailEntries: detailView.entries,
|
||||
detailRaw: detailView.rawText,
|
||||
detailRawExpanded: detailView.rawExpanded
|
||||
};
|
||||
};
|
||||
|
||||
const parseCsvSimple = (text) => {
|
||||
const out = { headers: [], rows: [] };
|
||||
const raw = String(text || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||
@@ -871,7 +998,9 @@ createApp({
|
||||
try {
|
||||
const res = await requestJson('/api/issues');
|
||||
if (res && res.ok) {
|
||||
issues.value = res.issues || [];
|
||||
issues.value = (res.issues || [])
|
||||
.map(normalizeIssueRecord)
|
||||
.sort((a, b) => Number(b?.ts || 0) - Number(a?.ts || 0));
|
||||
issueDataLoaded = true;
|
||||
markSynced();
|
||||
}
|
||||
@@ -1638,6 +1767,12 @@ createApp({
|
||||
stats.sold_tickets += item.sold_tickets;
|
||||
stats.revenue += item.revenue;
|
||||
});
|
||||
socket.on('issue:new', (item) => {
|
||||
issues.value = [normalizeIssueRecord(item), ...issues.value]
|
||||
.sort((a, b) => Number(b?.ts || 0) - Number(a?.ts || 0));
|
||||
issueDataLoaded = true;
|
||||
markSynced();
|
||||
});
|
||||
socket.on('ic-card:created', () => { fetchIcCards(false).catch(console.error); });
|
||||
socket.on('ic-card:opened', () => { fetchIcCards(false).catch(console.error); });
|
||||
socket.on('ic-card:check', () => {
|
||||
@@ -1950,6 +2085,7 @@ createApp({
|
||||
tickets: `已加载 ${ticketList.value.length} 条车票记录`,
|
||||
vouchers: `已加载 ${orders.value.length} 条凭证记录`,
|
||||
iccards: `当前检索到 ${icCards.value.length} 张 IC 卡`,
|
||||
issues: `当前共有 ${issues.value.length} 条问题记录`,
|
||||
assets: assetsManifest.routeMap ? `已上传线路图 ${assetsManifest.routeMap}` : '尚未上传线路图',
|
||||
settings: '可维护优惠活动与导出数据',
|
||||
logs: `当前筛选结果 ${logs.value.length} 条日志`
|
||||
|
||||
Reference in New Issue
Block a user