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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user