Polish static UI flows
This commit is contained in:
+296
-14
@@ -16,8 +16,13 @@
|
||||
flex: 1; min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 240px 1fr;
|
||||
transition: grid-template-columns var(--t-base);
|
||||
}
|
||||
.io-app.side-collapsed { grid-template-columns: 0 1fr; }
|
||||
@media (max-width: 1100px) {
|
||||
.io-app { grid-template-columns: 200px 1fr; }
|
||||
.io-app.side-collapsed { grid-template-columns: 0 1fr; }
|
||||
}
|
||||
@media (max-width: 1100px) { .io-app { grid-template-columns: 200px 1fr; } }
|
||||
|
||||
/* ========== 左 · 会话栏 ========== */
|
||||
.io-side {
|
||||
@@ -25,6 +30,12 @@
|
||||
background: var(--surface);
|
||||
display: flex; flex-direction: column;
|
||||
min-height: 0; overflow: hidden;
|
||||
transition: opacity var(--t-base), transform var(--t-base);
|
||||
}
|
||||
.io-app.side-collapsed .io-side {
|
||||
opacity: 0;
|
||||
transform: translateX(-8px);
|
||||
pointer-events: none;
|
||||
}
|
||||
.io-side-h {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
@@ -147,6 +158,74 @@
|
||||
background: var(--surface);
|
||||
}
|
||||
.io-toolbar .spacer { flex: 1; }
|
||||
.io-toolbar .side-restore-btn {
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-sm);
|
||||
color: var(--black-alpha-72);
|
||||
font-family: inherit;
|
||||
font-size: 12.5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.io-toolbar .side-restore-btn:hover { border-color: var(--heat-20); color: var(--heat); }
|
||||
.io-toolbar .side-restore-btn[hidden] { display: none; }
|
||||
.io-toolbar .side-restore-btn svg { width: 14px; height: 14px; }
|
||||
.io-toolbar-search {
|
||||
position: relative;
|
||||
width: min(320px, 32vw);
|
||||
min-width: 220px;
|
||||
}
|
||||
.io-toolbar-search[hidden] { display: none; }
|
||||
.io-toolbar-search svg {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
color: var(--black-alpha-48);
|
||||
pointer-events: none;
|
||||
}
|
||||
.io-toolbar-search input {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 0 32px 0 30px;
|
||||
background: var(--background-lighter);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-sm);
|
||||
color: var(--accent-black);
|
||||
font-family: inherit;
|
||||
font-size: 12.5px;
|
||||
outline: none;
|
||||
}
|
||||
.io-toolbar-search input:focus { border-color: var(--heat-40); background: var(--surface); }
|
||||
.io-toolbar-search .clear-search {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 0;
|
||||
border-radius: var(--r-sm);
|
||||
background: transparent;
|
||||
color: var(--black-alpha-48);
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.io-toolbar-search .clear-search:hover { background: var(--black-alpha-4); color: var(--accent-black); }
|
||||
.io-toolbar-search .clear-search svg {
|
||||
position: static;
|
||||
transform: none;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
.io-toolbar .search-btn {
|
||||
width: 32px; height: 32px;
|
||||
background: var(--surface);
|
||||
@@ -157,7 +236,9 @@
|
||||
display: grid; place-items: center;
|
||||
}
|
||||
.io-toolbar .search-btn:hover { border-color: var(--heat-20); color: var(--heat); }
|
||||
.io-toolbar .search-btn.active { background: var(--heat-12); border-color: var(--heat-20); color: var(--heat); }
|
||||
.io-toolbar .search-btn svg { width: 14px; height: 14px; }
|
||||
.io-tool-filter { position: relative; display: inline-flex; }
|
||||
.io-toolbar .tb-chip {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
height: 32px; padding: 0 10px;
|
||||
@@ -169,7 +250,48 @@
|
||||
transition: border-color var(--t-base), color var(--t-base);
|
||||
}
|
||||
.io-toolbar .tb-chip:hover { border-color: var(--heat-20); color: var(--heat); }
|
||||
.io-toolbar .tb-chip.active { background: var(--heat-12); border-color: var(--heat-20); color: var(--heat); }
|
||||
.io-toolbar .tb-chip svg { width: 10px; height: 10px; opacity: .6; }
|
||||
.io-tool-filter.open .tb-chip svg { transform: rotate(180deg); }
|
||||
.io-tool-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
right: 0;
|
||||
min-width: 156px;
|
||||
display: none;
|
||||
padding: 4px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
box-shadow: var(--shadow-floating);
|
||||
z-index: 30;
|
||||
}
|
||||
.io-tool-filter.open .io-tool-menu { display: block; }
|
||||
.io-tool-menu .mi {
|
||||
width: 100%;
|
||||
min-height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 10px;
|
||||
border: 0;
|
||||
border-radius: var(--r-sm);
|
||||
background: transparent;
|
||||
color: var(--accent-black);
|
||||
font-family: inherit;
|
||||
font-size: 12.5px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.io-tool-menu .mi:hover { background: var(--black-alpha-4); }
|
||||
.io-tool-menu .mi.selected { background: var(--heat-12); color: var(--heat); font-weight: 500; }
|
||||
.io-tool-menu .mi svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
opacity: 0;
|
||||
color: var(--heat);
|
||||
}
|
||||
.io-tool-menu .mi.selected svg { opacity: 1; }
|
||||
|
||||
/* 对话流主体 */
|
||||
.io-stream {
|
||||
@@ -711,22 +833,33 @@
|
||||
<section class="io-main">
|
||||
|
||||
<div class="io-toolbar">
|
||||
<button class="side-restore-btn" type="button" id="io-side-restore" hidden title="展开会话栏">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18"/></svg>
|
||||
会话
|
||||
</button>
|
||||
<span class="spacer"></span>
|
||||
<button class="search-btn" type="button" title="搜索">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
</button>
|
||||
<button class="tb-chip" type="button">
|
||||
时间
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
|
||||
</button>
|
||||
<button class="tb-chip" type="button">
|
||||
生成模式
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
|
||||
</button>
|
||||
<button class="tb-chip" type="button">
|
||||
操作类型
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
|
||||
</button>
|
||||
<div class="io-toolbar-search" id="io-toolbar-search" hidden>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
<input id="io-toolbar-search-input" type="text" placeholder="搜索当前对话结果">
|
||||
<button class="clear-search" type="button" id="io-toolbar-search-clear" title="清空搜索">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="io-tool-filter" data-filter="time">
|
||||
<button class="tb-chip" type="button"><span data-filter-label>时间</span><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
|
||||
<div class="io-tool-menu" data-filter-menu></div>
|
||||
</div>
|
||||
<div class="io-tool-filter" data-filter="mode">
|
||||
<button class="tb-chip" type="button"><span data-filter-label>生成模式</span><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
|
||||
<div class="io-tool-menu" data-filter-menu></div>
|
||||
</div>
|
||||
<div class="io-tool-filter" data-filter="action">
|
||||
<button class="tb-chip" type="button"><span data-filter-label>操作类型</span><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
|
||||
<div class="io-tool-menu" data-filter-menu></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="io-stream" id="io-stream">
|
||||
@@ -893,6 +1026,13 @@ Shell.render({
|
||||
const convList = $('#io-conv-list');
|
||||
const newConvBtn = $('#io-new-conv');
|
||||
const jumpBtn = $('#io-jump-bottom');
|
||||
const ioApp = $('.io-app');
|
||||
const foldBtn = $('.io-side-h .fold');
|
||||
const sideRestore = $('#io-side-restore');
|
||||
const toolbarSearchBtn = $('.io-toolbar .search-btn');
|
||||
const toolbarSearch = $('#io-toolbar-search');
|
||||
const toolbarSearchInput = $('#io-toolbar-search-input');
|
||||
const toolbarSearchClear = $('#io-toolbar-search-clear');
|
||||
|
||||
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
||||
function uid() { return 'm-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5); }
|
||||
@@ -913,6 +1053,12 @@ Shell.render({
|
||||
messages: [], // 当前激活会话的对话流(从 convMessages 镜像出来)
|
||||
convMessages: { default: [] }, // 所有会话的 messages 按 convId 持久化
|
||||
refImages: [],
|
||||
toolbar: {
|
||||
q: '',
|
||||
time: 'all',
|
||||
mode: 'all',
|
||||
action: 'all',
|
||||
},
|
||||
param: {
|
||||
model: 'studio-v2',
|
||||
ratio: '1:1',
|
||||
@@ -921,6 +1067,27 @@ Shell.render({
|
||||
},
|
||||
};
|
||||
|
||||
const TOOL_FILTERS = {
|
||||
time: [
|
||||
{ id: 'all', label: '时间' },
|
||||
{ id: 'today', label: '今天' },
|
||||
{ id: 'week', label: '近 7 天' },
|
||||
],
|
||||
mode: [
|
||||
{ id: 'all', label: '生成模式' },
|
||||
{ id: 'studio-v2', label: 'Airshelf v2' },
|
||||
{ id: 'studio-v2-pro', label: 'v2 Pro' },
|
||||
{ id: 'realistic', label: '写实增强' },
|
||||
{ id: 'anime', label: '国风动漫' },
|
||||
],
|
||||
action: [
|
||||
{ id: 'all', label: '操作类型' },
|
||||
{ id: 'ok', label: '已完成' },
|
||||
{ id: 'gen', label: '生成中' },
|
||||
{ id: 'err', label: '失败' },
|
||||
],
|
||||
};
|
||||
|
||||
function slimMessages(msgs) {
|
||||
// dataUrl 体积大,持久化时剥离;只保留元数据
|
||||
return (msgs || []).map(m => ({
|
||||
@@ -1074,6 +1241,29 @@ Shell.render({
|
||||
}
|
||||
|
||||
/* ---------- 渲染:中央对话流 ---------- */
|
||||
function getVisibleMessages() {
|
||||
const t = state.toolbar;
|
||||
const q = (t.q || '').trim().toLowerCase();
|
||||
const now = Date.now();
|
||||
return state.messages.filter(m => {
|
||||
if (q) {
|
||||
const hay = [
|
||||
m.prompt,
|
||||
m.ratio,
|
||||
modelLabel(m.model),
|
||||
styleLabel(m.style),
|
||||
...(m.results || []).map(r => r.label || r.status),
|
||||
].join(' ').toLowerCase();
|
||||
if (!hay.includes(q)) return false;
|
||||
}
|
||||
if (t.time === 'today' && now - (m.createdAt || now) > 86400000) return false;
|
||||
if (t.time === 'week' && now - (m.createdAt || now) > 86400000 * 7) return false;
|
||||
if (t.mode !== 'all' && m.model !== t.mode) return false;
|
||||
if (t.action !== 'all' && !(m.results || []).some(r => r.status === t.action)) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function renderStream() {
|
||||
if (!state.messages.length) {
|
||||
streamInner.innerHTML = `
|
||||
@@ -1099,7 +1289,27 @@ Shell.render({
|
||||
});
|
||||
return;
|
||||
}
|
||||
streamInner.innerHTML = state.messages.map(messageHTML).join('');
|
||||
const visible = getVisibleMessages();
|
||||
if (!visible.length) {
|
||||
streamInner.innerHTML = `
|
||||
<div class="io-empty">
|
||||
<div class="ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
</div>
|
||||
<div class="badge">// NO MATCH</div>
|
||||
<h2>没有符合筛选的结果</h2>
|
||||
<p>当前对话里没有匹配的批次,可以清空搜索或切回全部筛选。</p>
|
||||
<div class="examples"><button class="ex" type="button" id="io-clear-toolbar-filters">清空筛选</button></div>
|
||||
</div>`;
|
||||
$('#io-clear-toolbar-filters')?.addEventListener('click', () => {
|
||||
state.toolbar = { q: '', time: 'all', mode: 'all', action: 'all' };
|
||||
if (toolbarSearchInput) toolbarSearchInput.value = '';
|
||||
syncToolbarFilters();
|
||||
renderStream();
|
||||
});
|
||||
return;
|
||||
}
|
||||
streamInner.innerHTML = visible.map(messageHTML).join('');
|
||||
bindMessageEvents();
|
||||
}
|
||||
|
||||
@@ -1608,6 +1818,9 @@ Shell.render({
|
||||
if (!e.target.closest('.io-input-bottom .param')) {
|
||||
document.querySelectorAll('.io-input-bottom .param.open').forEach(x => x.classList.remove('open'));
|
||||
}
|
||||
if (!e.target.closest('.io-tool-filter')) {
|
||||
document.querySelectorAll('.io-tool-filter.open').forEach(x => x.classList.remove('open'));
|
||||
}
|
||||
if (!e.target.closest('.cell-more-wrap')) {
|
||||
document.querySelectorAll('.cell-more-wrap.open').forEach(x => x.classList.remove('open'));
|
||||
}
|
||||
@@ -1616,6 +1829,73 @@ Shell.render({
|
||||
}
|
||||
});
|
||||
|
||||
/* ---------- 顶部工具栏:折叠 / 搜索 / 筛选 ---------- */
|
||||
function setSideCollapsed(collapsed) {
|
||||
ioApp?.classList.toggle('side-collapsed', collapsed);
|
||||
if (sideRestore) sideRestore.hidden = !collapsed;
|
||||
try { localStorage.setItem('fs-io-side-collapsed', collapsed ? '1' : '0'); } catch (e) {}
|
||||
}
|
||||
foldBtn?.addEventListener('click', () => setSideCollapsed(true));
|
||||
sideRestore?.addEventListener('click', () => setSideCollapsed(false));
|
||||
|
||||
toolbarSearchBtn?.addEventListener('click', () => {
|
||||
const open = toolbarSearch?.hidden;
|
||||
if (toolbarSearch) toolbarSearch.hidden = !open;
|
||||
toolbarSearchBtn.classList.toggle('active', !!open || !!state.toolbar.q);
|
||||
if (open) requestAnimationFrame(() => toolbarSearchInput?.focus());
|
||||
});
|
||||
toolbarSearchInput?.addEventListener('input', e => {
|
||||
state.toolbar.q = e.target.value.trim();
|
||||
toolbarSearchBtn?.classList.toggle('active', !!state.toolbar.q || !toolbarSearch?.hidden);
|
||||
renderStream();
|
||||
});
|
||||
toolbarSearchClear?.addEventListener('click', () => {
|
||||
state.toolbar.q = '';
|
||||
if (toolbarSearchInput) toolbarSearchInput.value = '';
|
||||
toolbarSearchBtn?.classList.toggle('active', !toolbarSearch?.hidden);
|
||||
renderStream();
|
||||
toolbarSearchInput?.focus();
|
||||
});
|
||||
|
||||
function syncToolbarFilters() {
|
||||
document.querySelectorAll('.io-tool-filter').forEach(wrap => {
|
||||
const key = wrap.dataset.filter;
|
||||
const value = state.toolbar[key] || 'all';
|
||||
const items = TOOL_FILTERS[key] || [];
|
||||
const selected = items.find(x => x.id === value) || items[0];
|
||||
wrap.querySelector('[data-filter-label]').textContent = selected?.label || '';
|
||||
wrap.querySelector('.tb-chip')?.classList.toggle('active', value !== 'all');
|
||||
const menu = wrap.querySelector('[data-filter-menu]');
|
||||
if (!menu) return;
|
||||
menu.innerHTML = items.map(it => `
|
||||
<button class="mi${it.id === value ? ' selected' : ''}" type="button" data-val="${esc(it.id)}">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg>
|
||||
<span>${esc(it.label)}</span>
|
||||
</button>
|
||||
`).join('');
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('.io-tool-filter').forEach(wrap => {
|
||||
const btn = wrap.querySelector('.tb-chip');
|
||||
const menu = wrap.querySelector('[data-filter-menu]');
|
||||
btn?.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const open = wrap.classList.contains('open');
|
||||
document.querySelectorAll('.io-tool-filter.open').forEach(x => x.classList.remove('open'));
|
||||
if (!open) wrap.classList.add('open');
|
||||
});
|
||||
menu?.addEventListener('click', e => {
|
||||
const item = e.target.closest('.mi');
|
||||
if (!item) return;
|
||||
e.stopPropagation();
|
||||
state.toolbar[wrap.dataset.filter] = item.dataset.val;
|
||||
wrap.classList.remove('open');
|
||||
syncToolbarFilters();
|
||||
renderStream();
|
||||
});
|
||||
});
|
||||
|
||||
/* ---------- 新对话 ---------- */
|
||||
newConvBtn.addEventListener('click', () => {
|
||||
// 若 default 上有内容,把它归档到「最近」(新 id + 转存 messages),然后清空 default
|
||||
@@ -1671,6 +1951,8 @@ Shell.render({
|
||||
} catch (e) {}
|
||||
buildParamMenus();
|
||||
syncParamLabels();
|
||||
syncToolbarFilters();
|
||||
try { setSideCollapsed(localStorage.getItem('fs-io-side-collapsed') === '1'); } catch (e) {}
|
||||
updateCost();
|
||||
renderRefs();
|
||||
syncSendDisabled();
|
||||
|
||||
Reference in New Issue
Block a user