Compare commits

...
2 Commits
Author SHA1 Message Date
hz4th_coder 339dd89ba6 修复: 分页每页条数下拉框被全局 select 样式撑满整行
- 根因: 全局 input/select/textarea{width:100%} 导致 .psize-sel 独占一行
- 修复: .psize-sel 覆盖 width:auto, 与页码按钮/跳页框同一行居中排列
2026-08-16 19:55:37 +08:00
hz4th_coder b6b9e9148c UI 优化: Markdown 切换开关移入标题行 + 分页控件顶部复制/每页条数可调
- 任务详情弹窗: 📝 Markdown/原文 切换开关移到「任务指令」「产出物」标题右侧
  (标题左、开关右, flex 布局, 不再挤占文本区域宽度)
- 运行日志/告警中心: 分页控件顶部底部各一份; 新增每页条数选择(10/20/50/100),
  改条数自动回到第1页, 顶部/底部跳页双向同步
2026-08-16 19:51:32 +08:00
2 changed files with 45 additions and 20 deletions
+40 -19
View File
@@ -102,10 +102,13 @@ function mdToHtml(md) {
/* ---------- Markdown 区块(带 原文/Markdown 切换,默认 Markdown ---------- */
const mdRaw = {}; // id -> {md: bool, text: string}
function mdBlock(id, text, cls = '') {
function mdToggleBtn(id) {
// 切换开关:放入区块标题行右侧(由调用方放到 .sec-title 内)
return `<span class="md-toggle"><button id="md-btn-${id}" class="on" onclick="mdToggle('${id}')">📝 Markdown</button></span>`;
}
function mdBody(id, text, cls = '') {
mdRaw[id] = {md: true, text: text || ''};
return `<div class="md-toggle"><button id="md-btn-${id}" class="on" onclick="mdToggle('${id}')">📝 Markdown</button></div>
<div class="md-box ${cls}" id="md-body-${id}">${mdToHtml(text)}</div>`;
return `<div class="md-box ${cls}" id="md-body-${id}">${mdToHtml(text)}</div>`;
}
function mdToggle(id) {
const rec = mdRaw[id];
@@ -1436,10 +1439,10 @@ async function openTaskDetail(tid) {
<dt>创建/更新</dt><dd class="mono">${fmtTime(t.created_at)} / ${fmtTime(t.updated_at)}</dd>
<dt>截止</dt><dd>${t.deadline || '—'}</dd>
</div>
<div class="sec-title" style="margin-top:12px">任务指令</div>
${mdBlock('td-desc-' + t.id, t.description || t.title, 'sm')}
${t.output_text ? `<div class="sec-title">产出物 v${t.output_version}${t.output_text.length} 字)</div>
${mdBlock('td-out-' + t.id, t.output_text)}` : ''}
<div class="sec-title md-title" style="margin-top:12px"><span>任务指令</span>${mdToggleBtn('td-desc-' + t.id)}</div>
${mdBody('td-desc-' + t.id, t.description || t.title, 'sm')}
${t.output_text ? `<div class="sec-title md-title"><span>产出物 v${t.output_version}${t.output_text.length} 字)</span>${mdToggleBtn('td-out-' + t.id)}</div>
${mdBody('td-out-' + t.id, t.output_text)}` : ''}
${t.error ? `<div class="sec-title" style="color:var(--danger)">错误信息</div>
<div class="output-box" style="border-color:var(--danger)">${esc(t.error)}</div>` : ''}
<div class="sec-title">执行日志</div>
@@ -1839,6 +1842,7 @@ async function pageReports() {
/* ---------- 运行日志 ---------- */
let logsState = {page: 1, pageSize: 50, level: '', q: ''};
window.logsState = logsState; // 供分页组件通过 window[stateVar] 读写
async function pageLogs() {
const st = logsState;
@@ -1861,9 +1865,10 @@ async function pageLogs() {
${st.q ? `<button class="fbtn" onclick="logsState.q='';logsState.page=1;pageLogs()">✕ 清除</button>` : ''}
</div>
</div>
${pagerHtml(r.page, totalPages, r.total, 'logsState', 'pageLogs', 'top')}
<div class="card">${r.data.map(l => `<div class="log-line ${l.level}"><span class="ts">${fmtTime(l.created_at)}</span>
[任务#${l.task_id} ${esc(l.task_title || '')}] ${esc(l.message)}</div>`).join('') || '<div class="empty">暂无日志</div>'}</div>
${pagerHtml(r.page, totalPages, r.total, 'logsState', 'pageLogs')}`;
${pagerHtml(r.page, totalPages, r.total, 'logsState', 'pageLogs', 'bottom')}`;
bindPagerGo('logsState', totalPages, 'pageLogs');
const qel = $('#logs-q');
if (qel) qel.addEventListener('keydown', e => { if (e.key === 'Enter') { logsState.q = qel.value.trim(); logsState.page = 1; pageLogs(); } });
@@ -1871,6 +1876,7 @@ async function pageLogs() {
/* ---------- 告警中心 ---------- */
let alertsState = {page: 1, pageSize: 20, level: '', type: '', read: ''};
window.alertsState = alertsState; // 供分页组件通过 window[stateVar] 读写
async function pageAlerts() {
const st = alertsState;
@@ -1901,13 +1907,14 @@ async function pageAlerts() {
<div style="flex:1"></div>
<button class="btn" onclick="readAllAlerts()">✔ 全部标记已读</button>
</div>
${pagerHtml(r.page, totalPages, r.total, 'alertsState', 'pageAlerts', 'top')}
<div class="card">${r.data.map(a => `
<div class="alert-row ${a.read ? '' : 'unread'} ${a.level === 'critical' ? 'critical' : ''}" onclick="readAlert(${a.id})">
<div class="t">${ALERT_LEVEL[a.level] || '️'} ${esc(a.title)} ${a.read ? '' : '<span class="badge st-review">未读</span>'}</div>
<div class="d">${esc(a.detail)}</div>
<div class="tm">${fmtTime(a.created_at)} · ${esc(a.type)}</div>
</div>`).join('') || '<div class="empty">暂无告警 🎉</div>'}</div>
${pagerHtml(r.page, totalPages, r.total, 'alertsState', 'pageAlerts')}`;
${pagerHtml(r.page, totalPages, r.total, 'alertsState', 'pageAlerts', 'bottom')}`;
bindPagerGo('alertsState', totalPages, 'pageAlerts');
refreshAlertBadge();
}
@@ -1920,9 +1927,14 @@ async function readAllAlerts() {
toast('已全部标记为已读', 'ok'); pageAlerts();
}
/* ---------- 分页组件(快速定位页数) ---------- */
function pagerHtml(page, totalPages, total, stateVar, refreshFn) {
if (totalPages <= 1) return total ? `<div class="pager"><span class="pg-info">共 ${total} 条</span></div>` : '';
/* ---------- 分页组件(快速定位页数,顶部/底部各一份 + 每页条数 ---------- */
function pagerHtml(page, totalPages, total, stateVar, refreshFn, pos = 'bottom') {
const st = window[stateVar] || {};
const sizes = [10, 20, 50, 100];
const sizeSel = `<select class="psize-sel" onchange="setPageSize('${stateVar}', this.value, '${refreshFn}')" title="每页条数">
${sizes.map(s => `<option value="${s}" ${st.pageSize === s ? 'selected' : ''}>${s} 条/页</option>`).join('')}
</select>`;
if (totalPages <= 1) return total ? `<div class="pager ${pos}"><span class="pg-info">共 ${total} 条</span>${sizeSel}</div>` : '';
const pages = [];
const add = p => { if (pages[pages.length - 1] !== p && p >= 1 && p <= totalPages) pages.push(p); };
add(1);
@@ -1931,27 +1943,36 @@ function pagerHtml(page, totalPages, total, stateVar, refreshFn) {
for (let p = lo; p <= hi; p++) add(p);
if (hi < totalPages - 1) pages.push('…');
add(totalPages);
return `<div class="pager">
return `<div class="pager ${pos}">
${sizeSel}
<span class="pg-info">共 ${total} 条 · ${page}/${totalPages} 页</span>
<button class="pbtn" ${page <= 1 ? 'disabled' : ''} onclick="${stateVar}.page=${page - 1};${refreshFn}()"></button>
${pages.map(p => p === '…' ? '<span class="pgap">…</span>' :
`<button class="pbtn ${p === page ? 'cur' : ''}" onclick="${stateVar}.page=${p};${refreshFn}()">${p}</button>`).join('')}
<button class="pbtn" ${page >= totalPages ? 'disabled' : ''} onclick="${stateVar}.page=${page + 1};${refreshFn}()"></button>
<span class="pgo">跳至 <input id="pager-go" type="number" min="1" max="${totalPages}" value="${page}"> 页 <button class="pbtn" onclick="goPageInput('${stateVar}', ${totalPages}, '${refreshFn}')">GO</button></span>
<span class="pgo">跳至 <input id="pager-go-${pos}" type="number" min="1" max="${totalPages}" value="${page}"> 页 <button class="pbtn" onclick="goPageInput('${stateVar}', ${totalPages}, '${refreshFn}', '${pos}')">GO</button></span>
</div>`;
}
function goPageInput(stateVar, totalPages, refreshFn) {
const v = parseInt($('#pager-go')?.value || '');
function setPageSize(stateVar, size, refreshFn) {
window[stateVar].pageSize = parseInt(size) || 50;
window[stateVar].page = 1;
window[refreshFn]();
}
function goPageInput(stateVar, totalPages, refreshFn, pos) {
const el = document.getElementById('pager-go-' + pos);
const v = parseInt(el?.value || '');
if (!v || v < 1 || v > totalPages) return toast(`页码需在 1-${totalPages} 之间`, 'err');
window[stateVar].page = v;
window[refreshFn]();
}
function bindPagerGo(stateVar, totalPages, refreshFn) {
const el = $('#pager-go');
if (!el) return;
el.addEventListener('keydown', e => { if (e.key === 'Enter') goPageInput(stateVar, totalPages, refreshFn); });
['top', 'bottom'].forEach(pos => {
const el = document.getElementById('pager-go-' + pos);
if (el) el.addEventListener('keydown', e => { if (e.key === 'Enter') goPageInput(stateVar, totalPages, refreshFn, pos); });
});
}
/* ---------- 开放 API ---------- */
+5 -1
View File
@@ -139,6 +139,9 @@ tr:hover td{background:var(--panel2)}
.filter-bar .fbtn.on.warn-on{background:var(--warn)}
.filter-bar .fbtn.on.danger-on{background:var(--danger)}
.pager{display:flex;gap:4px;align-items:center;flex-wrap:wrap;margin-top:14px;justify-content:center}
.pager.top{margin-top:0;margin-bottom:14px}
.psize-sel{background:var(--panel2);border:1px solid var(--border);color:var(--text);border-radius:7px;padding:4px 6px;font-size:12px;cursor:pointer;height:30px;width:auto}
.psize-sel:hover{border-color:var(--accent)}
.pager .pbtn{min-width:30px;height:30px;padding:0 8px;border:1px solid var(--border);background:var(--panel2);color:var(--text);border-radius:7px;font-size:12px;cursor:pointer}
.pager .pbtn:hover{border-color:var(--accent);color:var(--accent)}
.pager .pbtn.cur{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:700}
@@ -166,9 +169,10 @@ tr:hover td{background:var(--panel2)}
.md-box code{background:var(--panel2);border:1px solid var(--border);border-radius:4px;padding:1px 5px;font-family:ui-monospace,Consolas,monospace;font-size:12px}
.md-box pre.md-code code{background:none;border:none;padding:0}
.md-box a{color:var(--accent)}
.md-toggle{font-size:11px;float:right}
.md-toggle{font-size:11px;white-space:nowrap}
.md-toggle button{border:1px solid var(--border);background:var(--panel2);color:var(--muted);padding:2px 8px;cursor:pointer;font-size:11px;border-radius:5px}
.md-toggle button.on{background:var(--accent);color:#fff;border-color:var(--accent)}
.md-title{display:flex;align-items:center;justify-content:space-between;gap:10px}
/* V1tabs / DAG / 告警 / 开关 */
.tabs{display:flex;gap:4px;margin-bottom:16px;border-bottom:1px solid var(--border)}