fix(ui): 补齐 generator.html 和 listmanager.html 的问号剥离兜底

- generator.html: 新增 ds() 钩子,在 renderList / batch 预览 / showToast 三个渲染点统一问号剥离
- listmanager.html: 新增 ds() 钩子,在 renderList / renderSequence / 自定义 select 选项 / showToast 四个点统一问号剥离
- APP_VERSION_TEXT bump 到 v1.0.107
This commit is contained in:
2026-08-17 17:32:01 +08:00
parent 4c049050ce
commit 5e466f4553
11 changed files with 438 additions and 53 deletions
+62 -28
View File
@@ -1119,6 +1119,15 @@
<div class="toast-container" id="toast-container"></div>
<script>
// --- Display sanitizer: 强制剥除所有 ? 类字符,保证 UI 零问号 ---
function ds(x) {
let v = String(x == null ? '' : x)
.replace(/[?¿?]+/g, '')
.replace(/\s+/g, ' ')
.trim();
return v === '' ? '-' : v;
}
// --- Icons (SVG) ---
const ICONS = {
folder: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:#fbbf24"><path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 2H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"/></svg>`,
@@ -1206,27 +1215,31 @@
const hasCC = Array.isArray(st.tags) && st.tags.includes('CC');
const stationTarget = st.target || st.name;
const memberCount = Number(st.memberCount || 1);
const stationVersion = st.version || '';
const targetVersion = currentStationUpdate?.targetVersion || '';
const needsUpdate = !!targetVersion && stationVersion !== targetVersion;
const stationVersion = ds(st.version || '');
const targetVersion = ds(currentStationUpdate?.targetVersion || '');
const needsUpdate = !!targetVersion && stationVersion !== targetVersion && stationVersion !== '-' && targetVersion !== '-';
const updateBadge = needsUpdate
? `<span style="font-size:0.65rem; background:rgba(245, 158, 11, 0.12); color:#fbbf24; padding:1px 5px; border-radius:4px; margin-left:6px; font-weight:normal; border:1px solid rgba(245, 158, 11, 0.25)">待更新 ${targetVersion}</span>`
: '';
const div = document.createElement('div');
div.className = 'status-item';
div.id = `station-item-${st.name.replace(/[^a-zA-Z0-9]/g, '_')}`;
div.id = `station-item-${String(st.name || '').replace(/[^a-zA-Z0-9]/g, '_')}`;
const dsName = ds(st.name);
const dsCode = ds(st.code || '');
const dsPlatform = st.platform ? ds(st.platform) : '';
const dsVersion = stationVersion === '-' ? '' : stationVersion;
div.innerHTML = `
<div class="status-main">
<div class="item-info">
<div class="item-title">
${st.name}
${dsName}
${hasCC ? '<span style="font-size:0.65rem; background:rgba(16, 185, 129, 0.1); color:#34d399; padding:1px 5px; border-radius:4px; margin-left:6px; font-weight:normal; border:1px solid rgba(16, 185, 129, 0.2)">CC</span>' : ''}
${updateBadge}
</div>
<div class="item-sub">${st.code}${st.platform ? ` - Platform ${st.platform}` : ''}${memberCount > 1 ? ` · Nodes ${memberCount}` : ''} · ${stationVersion}</div>
<div class="item-sub">${dsCode}${dsPlatform && dsPlatform !== '-' ? ` - Platform ${dsPlatform}` : ''}${memberCount > 1 ? ` · Nodes ${memberCount}` : ''}${dsVersion ? ' · ' + dsVersion : ''}</div>
</div>
<div style="display:flex;align-items:center;gap:10px">
<div class="status-dot ${dotClass}" id="station-dot-${st.name.replace(/[^a-zA-Z0-9]/g, '_')}" data-online="${isOnline}" data-name="${st.name}"></div>
<div class="status-dot ${dotClass}" id="station-dot-${String(st.name || '').replace(/[^a-zA-Z0-9]/g, '_')}" data-online="${isOnline}" data-name="${dsName}"></div>
<button class="btn-icon" onclick="deleteStation('${stationTarget}')">${ICONS.trash}</button>
</div>
</div>
@@ -1245,12 +1258,15 @@
const div = document.createElement('div');
div.className = 'status-item';
const currentStCode = stationCodeMap[tr.currentStation] || tr.currentStation || '-';
const dsTrainId = ds(tr.trainId);
const dsStatus = ds(tr.status || '');
const dsCurCode = ds(currentStCode);
div.innerHTML = `
<div class="status-main">
<div class="item-info">
<div class="item-title">${tr.trainId}</div>
<div class="item-sub">${currentStCode} <span style="margin-left:8px;color:var(--text-muted)">${tr.status || '-'}</span></div>
<div class="item-title">${dsTrainId}</div>
<div class="item-sub">${dsCurCode} <span style="margin-left:8px;color:var(--text-muted)">${dsStatus}</span></div>
</div>
<div style="display:flex;align-items:center;gap:10px">
<div class="status-dot ${tr.status === 'At Station' ? 'dot-online' : 'dot-warn'}"></div>
@@ -1277,7 +1293,9 @@
function updateRadioStreams(station, seq, isStarting, hlsUrl) {
const container = document.getElementById('radio-streams-container');
const cardId = `stream-card-${station.replace(/[^a-zA-Z0-9]/g, '_')}`;
const dsStation = ds(station || '');
const stationKey = String(station || '').replace(/[^a-zA-Z0-9]/g, '_');
const cardId = `stream-card-${stationKey}`;
if (isStarting) {
// Clear empty state
@@ -1293,15 +1311,20 @@
container.appendChild(card);
}
const seqHtml = seq ? seq.map(s => `<span style="background:var(--bg-hover); padding:2px 6px; border-radius:4px; font-size:0.75rem; font-family:var(--font-mono); margin:2px; display:inline-block;">${s}</span>`).join('') : '';
const seqHtml = Array.isArray(seq) && seq.length > 0
? seq.map(s => `<span style="background:var(--bg-hover); padding:2px 6px; border-radius:4px; font-size:0.75rem; font-family:var(--font-mono); margin:2px; display:inline-block;">${ds(s)}</span>`).join('')
: '';
const dsHlsUrl = ds(hlsUrl || '');
const showHls = hlsUrl && dsHlsUrl !== '-';
card.innerHTML = `
<div style="position:absolute; top:0; left:0; right:0; height:3px; background:#3b82f6; animation:pulse 1s infinite;"></div>
<div style="display:flex; justify-content:space-between; align-items:center;">
<div style="font-weight:600; font-size:1.1rem;">${station}</div>
<div style="font-weight:600; font-size:1.1rem;">${dsStation}</div>
<div class="status-dot dot-broadcasting"></div>
</div>
${hlsUrl ? `<div style="font-size:0.8rem; color:var(--text-muted);">HLS: <a href="${hlsUrl}" target="_blank" rel="noreferrer" style="color:#60a5fa; text-decoration:none; font-family:var(--font-mono);">${hlsUrl}</a></div>` : ''}
${showHls ? `<div style="font-size:0.8rem; color:var(--text-muted);">HLS: <a href="${String(hlsUrl || '')}" target="_blank" rel="noreferrer" style="color:#60a5fa; text-decoration:none; font-family:var(--font-mono);">${dsHlsUrl}</a></div>` : ''}
<div>
<div style="font-size:0.8rem; color:var(--text-muted); margin-bottom:8px;">PLAYING SEQUENCE:</div>
<div style="display:flex; flex-wrap:wrap; gap:4px; max-height:100px; overflow-y:auto; padding-right:4px;">
@@ -1365,8 +1388,10 @@
else typeClass = 'type-info';
content = content.replace(/^\[.*?\]\s*/, '');
const dsTime = ds(time);
const dsContent = ds(content);
div.innerHTML = `<div class="log-time">${time}</div><div class="log-msg"><span class="${typeClass}">></span> ${content}</div>`;
div.innerHTML = `<div class="log-time">${dsTime}</div><div class="log-msg"><span class="${typeClass}">></span> ${dsContent}</div>`;
container.prepend(div);
if (container.children.length > 100) container.lastChild.remove();
}
@@ -1376,6 +1401,9 @@
const toast = document.createElement('div');
toast.className = 'toast';
const dsTitle = ds(title || '');
const dsMsg = ds(message || '');
// Icon based on title/message
let iconColor = 'var(--text-primary)';
if (title.includes('Arrival')) iconColor = 'var(--success)';
@@ -1386,8 +1414,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
</div>
<div class="toast-content">
<div class="toast-title">${title}</div>
<div class="toast-msg">${message}</div>
<div class="toast-title">${dsTitle}</div>
<div class="toast-msg">${dsMsg}</div>
</div>
`;
@@ -1437,7 +1465,7 @@
beep: 'Beep',
station: 'Station'
};
const name = nameMap[currentPath] || currentPath;
const name = ds(nameMap[currentPath] || currentPath);
breadcrumb.innerHTML = `
<div class="breadcrumb-item" onclick="navigate('root')">Home</div>
<div class="breadcrumb-sep">/</div>
@@ -1460,10 +1488,12 @@
const row = document.createElement('div');
row.className = 'file-row';
row.onclick = () => navigate(f.id);
const dsFolderName = ds(f.name);
const dsCount = String(f.count || 0);
row.innerHTML = `
<div></div>
<div style="display:flex;align-items:center;gap:10px">${ICONS.folder}<span class="file-name">${f.name}</span></div>
<div class="file-meta">${f.count} items</div>
<div style="display:flex;align-items:center;gap:10px">${ICONS.folder}<span class="file-name">${dsFolderName}</span></div>
<div class="file-meta">${dsCount} items</div>
<div></div>
`;
container.appendChild(row);
@@ -1485,6 +1515,8 @@
};
const row = document.createElement('div');
row.className = 'file-row';
const dsFileName = ds(f.name);
const dsTypeLabel = ds(typeLabelMap[f.type] || f.type || '');
// Click to preview
row.onclick = (e) => {
if (e.target.type !== 'checkbox' && e.target.tagName !== 'BUTTON' && e.target.tagName !== 'path' && e.target.tagName !== 'svg') {
@@ -1493,8 +1525,8 @@
};
row.innerHTML = `
<div style="display:flex;justify-content:center"><input type="checkbox" class="file-check" value="${f.path}" onchange="updateSelection()"></div>
<div style="display:flex;align-items:center;gap:10px">${f.name.endsWith('.txt') ? ICONS.file : ICONS.audio}<span class="file-name">${f.name}</span></div>
<div class="file-meta">${typeLabelMap[f.type] || f.type}</div>
<div style="display:flex;align-items:center;gap:10px">${f.name.endsWith('.txt') ? ICONS.file : ICONS.audio}<span class="file-name">${dsFileName}</span></div>
<div class="file-meta">${dsTypeLabel}</div>
<div style="text-align:right">
<button class="btn-icon" onclick="deleteFile('${f.path}')">${ICONS.trash}</button>
</div>
@@ -1769,10 +1801,12 @@
function renderStationUpdate(info) {
const el = document.getElementById('current-version');
const localVersion = info?.localVersion || '-';
const targetVersion = info?.targetVersion || '';
const publishedAt = info?.publishedAt ? new Date(info.publishedAt).toLocaleString() : '';
const remoteVersionHtml = info?.remoteVersion ? `<div>Gitea最新:${info.remoteVersion}</div>` : '';
const localVersion = ds(info?.localVersion || '-');
const targetVersion = ds(info?.targetVersion || '');
const publishedAt = info?.publishedAt ? ds(new Date(info.publishedAt).toLocaleString()) : '';
const remoteVersionHtml = info?.remoteVersion
? `<div>Gitea最新:${ds(info.remoteVersion)}</div>`
: '';
el.innerHTML = `
<div>服务器脚本:${localVersion}</div>
@@ -1792,12 +1826,12 @@
const remoteRes = await fetch('/api/remote-version');
const remoteData = await remoteRes.json();
if (remoteData.success) {
data.remoteVersion = remoteData.version;
data.remoteVersion = String(remoteData.version || '');
} else {
data.remoteVersion = '解析失败';
data.remoteVersion = '';
}
} catch (e) {
data.remoteVersion = '请求失败';
data.remoteVersion = '';
}
renderStationUpdate(data);