Polish static UI flows

This commit is contained in:
iye
2026-05-28 12:29:12 +08:00
parent 5edfa05369
commit bbe29622c2
45 changed files with 2187 additions and 813 deletions
+253 -5
View File
@@ -1482,9 +1482,9 @@ select.duration-select:focus,
/* ─── Table ─── */
table.t {
width: 100%; border-collapse: collapse;
width: 100%; border-collapse: separate; border-spacing: 0;
background: var(--surface);
border: 1px solid var(--border-faint);
border: 1px solid var(--border-muted);
border-radius: var(--r-md);
overflow: hidden;
}
@@ -1495,19 +1495,18 @@ table.t thead th {
color: var(--black-alpha-48);
padding: 14px 16px;
background: var(--black-alpha-3);
border-bottom: 1px solid var(--border-faint);
border-bottom: 1px solid var(--border-muted);
letter-spacing: .04em;
text-transform: uppercase;
font-family: var(--font-mono);
}
table.t tbody td {
padding: 16px;
border-bottom: 1px solid var(--border-faint);
border-bottom: 0;
font-size: 13px;
vertical-align: middle;
color: var(--accent-black);
}
table.t tbody tr:last-child td { border-bottom: 0; }
table.t tbody tr { cursor: pointer; transition: background var(--t-base); }
table.t tbody tr:hover { background: var(--black-alpha-4); }
@@ -1589,6 +1588,255 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
letter-spacing: .04em; font-weight: 400;
}
/* ─── Global command palette + account menu ─── */
.shell-command-bg {
position: fixed;
inset: 0;
z-index: 1800;
display: none;
align-items: flex-start;
justify-content: center;
padding: 12vh 16px 24px;
background: var(--black-alpha-48);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.shell-command-bg.show { display: flex; }
.shell-command {
width: min(640px, 100%);
max-height: min(680px, 76vh);
display: flex;
flex-direction: column;
background: var(--surface-raised);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
box-shadow: var(--shadow-floating);
overflow: hidden;
}
.shell-command-h {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
border-bottom: 1px solid var(--border-faint);
}
.shell-command-h .ic {
width: 30px;
height: 30px;
display: grid;
place-items: center;
background: var(--heat-12);
color: var(--heat);
border: 1px solid var(--heat-20);
border-radius: var(--r-md);
flex-shrink: 0;
}
.shell-command-h .ic svg { width: 14px; height: 14px; }
.shell-command-h input {
flex: 1;
min-width: 0;
height: 34px;
border: 0;
background: transparent;
color: var(--accent-black);
font-family: inherit;
font-size: 15px;
outline: none;
}
.shell-command-h input::placeholder { color: var(--black-alpha-48); }
.shell-command-close {
height: 26px;
padding: 0 8px;
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
background: var(--background-lighter);
color: var(--black-alpha-48);
font-family: var(--font-inter);
font-size: 11px;
font-weight: 700;
letter-spacing: .02em;
cursor: pointer;
}
.shell-command-close:hover { color: var(--accent-black); border-color: var(--black-alpha-24); }
.shell-command-list {
padding: 8px;
overflow-y: auto;
}
.shell-command-section {
padding: 8px 10px 6px;
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--black-alpha-48);
letter-spacing: .06em;
}
.shell-command-item {
width: 100%;
min-height: 48px;
display: flex;
align-items: center;
gap: 12px;
padding: 8px 10px;
border: 0;
border-radius: var(--r-md);
background: transparent;
color: var(--accent-black);
font-family: inherit;
text-align: left;
cursor: pointer;
}
.shell-command-item:hover,
.shell-command-item.active {
background: var(--heat-12);
color: var(--heat);
}
.shell-command-item .cmd-ic {
width: 30px;
height: 30px;
display: grid;
place-items: center;
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
background: var(--surface);
color: currentColor;
flex-shrink: 0;
}
.shell-command-item .cmd-ic svg { width: 14px; height: 14px; }
.shell-command-item .cmd-main {
flex: 1;
min-width: 0;
}
.shell-command-item .cmd-title {
display: block;
font-size: 13px;
font-weight: 600;
color: currentColor;
}
.shell-command-item .cmd-sub {
display: block;
margin-top: 2px;
font-size: 12px;
line-height: 1.45;
color: var(--black-alpha-56);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.shell-command-item:hover .cmd-sub,
.shell-command-item.active .cmd-sub { color: var(--black-alpha-72); }
.shell-command-item .cmd-key {
min-width: 26px;
height: 22px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 7px;
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
background: var(--surface);
color: var(--black-alpha-48);
font-family: var(--font-inter);
font-size: 10px;
font-weight: 700;
letter-spacing: .02em;
}
.shell-command-empty {
padding: 48px 20px;
display: grid;
place-items: center;
gap: 8px;
color: var(--black-alpha-48);
font-size: 13px;
}
.shell-command-foot {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 16px;
border-top: 1px solid var(--border-faint);
color: var(--black-alpha-48);
font-family: var(--font-mono);
font-size: 10.5px;
letter-spacing: .04em;
}
.shell-command-foot .spacer { flex: 1; }
.shell-account-menu {
position: fixed;
z-index: 1700;
min-width: 232px;
display: none;
padding: 8px;
background: var(--surface-raised);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
box-shadow: var(--shadow-floating);
}
.shell-account-menu.show { display: block; }
.shell-account-head {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 8px 10px;
border-bottom: 1px solid var(--border-faint);
margin-bottom: 6px;
}
.shell-account-head .av {
width: 30px;
height: 30px;
display: grid;
place-items: center;
border-radius: 6px;
background: var(--accent-black);
color: var(--accent-white);
font-size: 12px;
font-weight: 600;
}
.shell-account-head .nm {
display: block;
font-size: 13px;
font-weight: 600;
color: var(--accent-black);
}
.shell-account-head .mail {
display: block;
margin-top: 2px;
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--black-alpha-48);
letter-spacing: .02em;
}
.shell-account-menu button {
width: 100%;
min-height: 34px;
display: flex;
align-items: center;
gap: 9px;
padding: 0 9px;
border: 0;
border-radius: var(--r-sm);
background: transparent;
color: var(--accent-black);
font-family: inherit;
font-size: 13px;
text-align: left;
cursor: pointer;
}
.shell-account-menu button svg {
width: 14px;
height: 14px;
color: var(--black-alpha-56);
}
.shell-account-menu button:hover {
background: var(--black-alpha-4);
color: var(--heat);
}
.shell-account-menu button:hover svg { color: var(--heat); }
.shell-account-menu .sep {
height: 1px;
background: var(--border-faint);
margin: 6px 4px;
}
/* ─── Modal ─── */
.modal-bg {
position: fixed; inset: 0;
+247 -5
View File
@@ -44,6 +44,23 @@ const NAV = [
}
];
const SHELL_COMMANDS = [
{ id: 'dashboard', group: '导航', label: '工作台', sub: '任务队列、今日消耗、项目进度', href: 'index.html', icon: 'dashboard', key: 'D' },
{ id: 'products', group: '导航', label: '商品库', sub: '管理 SKU、商品图册、卖点信息', href: 'products.html', icon: 'package', key: 'P' },
{ id: 'projects', group: '导航', label: '视频项目', sub: '查看五阶段短视频流水线', href: 'projects.html', icon: 'clapperboard', key: 'V' },
{ id: 'asset-factory', group: '导航', label: '图片生成', sub: '模特上身图、平台套图、图片创作', href: 'asset-factory.html', icon: 'sparkles', key: 'I' },
{ id: 'library', group: '导航', label: '资产库', sub: '素材、人物、场景、成片统一管理', href: 'library.html', icon: 'folder', key: 'A' },
{ id: 'team', group: '导航', label: '团队', sub: '成员、权限、额度、协作记录', href: 'team.html', icon: 'users' },
{ id: 'account', group: '导航', label: '消费', sub: '余额、充值、账单流水', href: 'account.html', icon: 'creditCard' },
{ id: 'settings', group: '导航', label: '设置', sub: '个人信息、通知、安全、偏好', href: 'settings.html', icon: 'settings' },
{ id: 'messages', group: '常用动作', label: '消息中心', sub: '任务提醒、协作评论、系统通知', href: 'messages.html', icon: 'bell', key: 'M' },
{ id: 'new-product', group: '常用动作', label: '新建商品', sub: '从商品信息开始生成素材与视频', href: 'product-create.html', icon: 'productPlus' },
{ id: 'new-project', group: '常用动作', label: '新建视频项目', sub: '选择商品并进入脚本配置', href: 'projects-new.html', icon: 'plus' },
{ id: 'model-photo', group: '常用动作', label: '生成模特上身图', sub: '快速生成 3:4 商品展示素材', href: 'model-photo.html', icon: 'users' },
{ id: 'platform-cover', group: '常用动作', label: '生成平台套图', sub: '适配电商平台封面与详情图', href: 'platform-cover.html', icon: 'images' },
{ id: 'image-optimize', group: '常用动作', label: '图片创作', sub: '对话式生成、编辑、加入资产库', href: 'image-optimize.html', icon: 'image' },
];
window.Shell = {
render({ active = '', crumbs = [], balance = '¥327.40', topActions = '' } = {}) {
const navHtml = NAV.map(n => `
@@ -59,15 +76,15 @@ window.Shell = {
<div class="brand">
<img class="brand-logo" src="assets/logo.png" alt="Airshelf">
</div>
<div class="search-box" onclick="document.getElementById('global-search').focus()">
<div class="search-box" onclick="Shell.openCommandPalette()">
${ShellIcon('search')}
<input id="global-search" placeholder="搜索"/>
<input id="global-search" placeholder="搜索" readonly aria-label="打开全局搜索"/>
<span class="kbd">Ctrl K</span>
</div>
<div class="nav-section">主要</div>
<nav>${navHtml}</nav>
<div class="aside-foot">
<div class="user" onclick="Shell.toast('账户菜单', 'li@shop.com')">
<div class="user" onclick="Shell.toggleAccountMenu(event)">
<div class="av">李</div>
<div class="em">小李的店</div>
</div>
@@ -103,7 +120,7 @@ window.Shell = {
${ShellIcon('bell')}
<span class="count-noti">12</span>
</button>
<div class="topbar-avatar" onclick="Shell.toast('账户菜单', '李 · li@shop.com')" title="账户">
<div class="topbar-avatar" onclick="Shell.toggleAccountMenu(event)" title="账户">
<span>李</span>
</div>
${topActions}
@@ -126,6 +143,43 @@ window.Shell = {
</div>
`;
const commandHtml = `
<div class="shell-command-bg" id="shell-command-bg" aria-hidden="true">
<div class="shell-command" role="dialog" aria-modal="true" aria-labelledby="shell-command-title">
<div class="shell-command-h">
<span class="ic">${ShellIcon('search')}</span>
<input id="shell-command-input" autocomplete="off" placeholder="搜索页面、动作、项目入口" aria-label="搜索命令">
<button class="shell-command-close" type="button" id="shell-command-close">ESC</button>
</div>
<div class="shell-command-list" id="shell-command-list"></div>
<div class="shell-command-foot">
<span id="shell-command-title">COMMAND</span>
<span>// Enter 打开 · Ctrl K 唤起</span>
<span class="spacer"></span>
<span id="shell-command-count">0 项</span>
</div>
</div>
</div>
`;
const accountMenuHtml = `
<div class="shell-account-menu" id="shell-account-menu" aria-hidden="true">
<div class="shell-account-head">
<span class="av">李</span>
<span>
<span class="nm">小李的店</span>
<span class="mail">li@shop.com</span>
</span>
</div>
<button type="button" data-account-act="settings">${ShellIcon('settings')}个人设置</button>
<button type="button" data-account-act="messages">${ShellIcon('bell')}消息中心</button>
<button type="button" data-account-act="team">${ShellIcon('users')}团队管理</button>
<button type="button" data-account-act="account">${ShellIcon('creditCard')}消费与余额</button>
<div class="sep"></div>
<button type="button" data-account-act="logout">${ShellIcon('arrowRight')}退出登录</button>
</div>
`;
// ─── 全局 Lightbox · 任意页面可用 Shell._openLightbox(src, name) ──
const lightboxHtml = `
<div class="np-lightbox" id="np-lightbox" onclick="Shell._closeLightbox()">
@@ -284,15 +338,203 @@ window.Shell = {
}
document.body.insertAdjacentHTML('beforeend', toastHtml);
document.body.insertAdjacentHTML('beforeend', lightboxHtml);
document.body.insertAdjacentHTML('beforeend', commandHtml);
document.body.insertAdjacentHTML('beforeend', accountMenuHtml);
document.addEventListener('keydown', e => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
document.getElementById('global-search')?.focus();
Shell.openCommandPalette();
}
});
this.enhanceSelects(document);
this._bindGlobalChrome();
},
_esc(s) {
return String(s ?? '').replace(/[&<>"']/g, c => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
})[c]);
},
_bindGlobalChrome() {
const input = document.getElementById('global-search');
if (input && !input.dataset.shellBound) {
input.dataset.shellBound = '1';
input.addEventListener('focus', () => {
input.blur();
Shell.openCommandPalette();
});
input.addEventListener('keydown', e => {
e.preventDefault();
Shell.openCommandPalette();
});
}
const bg = document.getElementById('shell-command-bg');
const closeBtn = document.getElementById('shell-command-close');
const cmdInput = document.getElementById('shell-command-input');
if (bg && !bg.dataset.shellBound) {
bg.dataset.shellBound = '1';
bg.addEventListener('click', e => {
if (e.target === bg) Shell.closeCommandPalette();
});
closeBtn?.addEventListener('click', () => Shell.closeCommandPalette());
cmdInput?.addEventListener('input', () => Shell.renderCommandList(cmdInput.value));
cmdInput?.addEventListener('keydown', e => {
if (e.key === 'Escape') {
e.preventDefault();
Shell.closeCommandPalette();
return;
}
if (e.key !== 'Enter') return;
const first = document.querySelector('#shell-command-list .shell-command-item');
if (first) {
e.preventDefault();
Shell.runCommand(first.dataset.command);
}
});
}
const accountMenu = document.getElementById('shell-account-menu');
if (accountMenu && !accountMenu.dataset.shellBound) {
accountMenu.dataset.shellBound = '1';
accountMenu.addEventListener('click', e => {
const item = e.target.closest('[data-account-act]');
if (!item) return;
Shell.closeAccountMenu();
const act = item.dataset.accountAct;
const hrefs = {
settings: 'settings.html',
messages: 'messages.html',
team: 'team.html',
account: 'account.html',
logout: 'login.html',
};
if (act === 'logout') {
Shell.toast('已退出登录', '返回登录页');
setTimeout(() => { location.href = hrefs.logout; }, 220);
return;
}
if (hrefs[act]) location.href = hrefs[act];
});
}
if (!this._globalChromeDocBound) {
this._globalChromeDocBound = true;
document.addEventListener('click', e => {
if (!e.target.closest('.shell-account-menu') && !e.target.closest('.topbar-avatar') && !e.target.closest('.user')) {
Shell.closeAccountMenu();
}
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
Shell.closeCommandPalette();
Shell.closeAccountMenu();
}
});
}
this.renderCommandList('');
},
openCommandPalette(query = '') {
const bg = document.getElementById('shell-command-bg');
const input = document.getElementById('shell-command-input');
if (!bg || !input) return;
this.closeAccountMenu();
const wasOpen = bg.classList.contains('show');
bg.classList.add('show');
bg.setAttribute('aria-hidden', 'false');
input.value = query;
this.renderCommandList(query);
if (!wasOpen) this.lockScroll();
requestAnimationFrame(() => input.focus());
},
closeCommandPalette() {
const bg = document.getElementById('shell-command-bg');
if (!bg || !bg.classList.contains('show')) return;
bg.classList.remove('show');
bg.setAttribute('aria-hidden', 'true');
this.unlockScroll();
},
renderCommandList(query = '') {
const list = document.getElementById('shell-command-list');
const count = document.getElementById('shell-command-count');
if (!list) return;
const q = String(query || '').trim().toLowerCase();
const items = SHELL_COMMANDS.filter(cmd => {
if (!q) return true;
return [cmd.label, cmd.sub, cmd.group, cmd.id].join(' ').toLowerCase().includes(q);
});
if (count) count.textContent = items.length + ' 项';
if (!items.length) {
list.innerHTML = `<div class="shell-command-empty">${ShellIcon('search')}<span>没有匹配的入口</span><span class="shell-command-section">// 换个关键词试试</span></div>`;
return;
}
let lastGroup = '';
list.innerHTML = items.map((cmd, i) => {
const section = cmd.group !== lastGroup ? `<div class="shell-command-section">${this._esc(cmd.group)}</div>` : '';
lastGroup = cmd.group;
return section + `
<button class="shell-command-item${i === 0 ? ' active' : ''}" type="button" data-command="${this._esc(cmd.id)}">
<span class="cmd-ic">${ShellIcon(cmd.icon)}</span>
<span class="cmd-main">
<span class="cmd-title">${this._esc(cmd.label)}</span>
<span class="cmd-sub">${this._esc(cmd.sub)}</span>
</span>
${cmd.key ? `<span class="cmd-key">${this._esc(cmd.key)}</span>` : ''}
</button>
`;
}).join('');
list.querySelectorAll('.shell-command-item').forEach(btn => {
btn.addEventListener('click', () => Shell.runCommand(btn.dataset.command));
});
},
runCommand(id) {
const cmd = SHELL_COMMANDS.find(item => item.id === id);
if (!cmd) return;
this.closeCommandPalette();
if (cmd.href) {
location.href = cmd.href;
return;
}
Shell.toast('已执行', cmd.label);
},
toggleAccountMenu(event) {
event?.stopPropagation?.();
const menu = document.getElementById('shell-account-menu');
const anchor = event?.currentTarget;
if (!menu || !anchor) return;
const shouldOpen = !menu.classList.contains('show');
this.closeCommandPalette();
if (!shouldOpen) {
this.closeAccountMenu();
return;
}
const rect = anchor.getBoundingClientRect();
menu.classList.add('show');
menu.setAttribute('aria-hidden', 'false');
const width = menu.offsetWidth || 232;
const height = menu.offsetHeight || 260;
let left = rect.right - width;
if (left < 12) left = rect.left;
left = Math.min(Math.max(12, left), window.innerWidth - width - 12);
let top = rect.bottom + 8;
if (top + height > window.innerHeight - 12) top = Math.max(12, rect.top - height - 8);
menu.style.left = left + 'px';
menu.style.top = top + 'px';
},
closeAccountMenu() {
const menu = document.getElementById('shell-account-menu');
if (!menu) return;
menu.classList.remove('show');
menu.setAttribute('aria-hidden', 'true');
},
enhanceSelects(root = document) {