- 10 个页面 (工作台/项目/商品/流水线/资产/账户/创建向导) - V2.1 Restraint 设计规范 (冷灰底 + #FA5D19 + 8px 圆角) - 完整 design-system.html 组件库参考 - SVG line icon · stroke 1.5 全合规 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
196 lines
9.1 KiB
JavaScript
196 lines
9.1 KiB
JavaScript
/* ============================================================
|
|
流·Studio · Shell renderer V2.1
|
|
渲染 sidebar / topbar / 网格背景装饰 / Toast / Modal helpers
|
|
每个页面调用 Shell.render({ active, crumbs, balance, topActions })
|
|
|
|
V2.1 变化:
|
|
- sidebar 搜索 ⌘K → "Ctrl K" Inter Bold 平铺(无 kbd 边框)
|
|
============================================================ */
|
|
|
|
const NAV = [
|
|
{
|
|
id: 'dashboard', label: '工作台', href: 'index.html',
|
|
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 12 12 3l9 9"/><path d="M5 10v10h14V10"/></svg>'
|
|
},
|
|
{
|
|
id: 'products', label: '商品库', href: 'products.html', badge: '12',
|
|
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="m3 7 9-4 9 4-9 4-9-4z"/><path d="m3 12 9 4 9-4M3 17l9 4 9-4"/></svg>'
|
|
},
|
|
{
|
|
id: 'projects', label: '视频项目', href: 'projects.html',
|
|
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="4" width="18" height="16"/><path d="M7 4v16M16 4v16M3 9h18M3 15h18"/></svg>'
|
|
},
|
|
{
|
|
id: 'library', label: '资产库', href: 'library.html',
|
|
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="4" width="6" height="16"/><rect x="11" y="4" width="4" height="16"/><rect x="17" y="6" width="4" height="14"/></svg>'
|
|
},
|
|
{
|
|
id: 'account', label: '账户', href: 'account.html',
|
|
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="6" width="18" height="13" rx="2"/><path d="M3 10h18M16 14h2"/></svg>'
|
|
},
|
|
{
|
|
id: 'settings', label: '设置', href: '#',
|
|
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8 2 2 0 0 1-2.8 2.8 1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5 2 2 0 0 1-4 0 1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3 2 2 0 0 1-2.8-2.8 1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1 2 2 0 0 1 0-4 1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8 2 2 0 0 1 2.8-2.8 1.7 1.7 0 0 0 1.8.3 1.7 1.7 0 0 0 1-1.5 2 2 0 0 1 4 0 1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3 2 2 0 0 1 2.8 2.8 1.7 1.7 0 0 0-.3 1.8 1.7 1.7 0 0 0 1.5 1 2 2 0 0 1 0 4 1.7 1.7 0 0 0-1.5 1.1Z"/></svg>'
|
|
}
|
|
];
|
|
|
|
const TEAM_NAV = {
|
|
label: '团队', icon:
|
|
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="9" cy="8" r="3"/><circle cx="17" cy="9" r="2.5"/><path d="M3 19c0-3 2.7-5 6-5s6 2 6 5M14 19c.5-2.4 2.4-4 5-4 .8 0 1.5.2 2 .5"/></svg>',
|
|
badge: 'V1.5'
|
|
};
|
|
|
|
window.Shell = {
|
|
render({ active = '', crumbs = [], balance = '¥327.40', topActions = '' } = {}) {
|
|
const navHtml = NAV.map(n => `
|
|
<a href="${n.href}" class="${active === n.id ? 'active' : ''}">
|
|
${n.icon}
|
|
<span>${n.label}</span>
|
|
${n.badge ? `<span class="pill-mini">${n.badge}</span>` : ''}
|
|
</a>
|
|
`).join('');
|
|
|
|
const sidebar = `
|
|
<aside class="sidebar">
|
|
<div class="brand">
|
|
<div class="flame"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2c1 3 4 5 4 9a4 4 0 0 1-4 4 4 4 0 0 1-4-4 5 5 0 0 1 1.5-3.5C10.5 6 11.5 4 12 2zm-1 13c0 2 1 3 1 5 1-1 3-2 3-5 0-1.5-1-2-2-3-1 1-2 2-2 3z"/></svg></div>
|
|
<div><div class="name">流·Studio</div></div>
|
|
</div>
|
|
<div class="search-box" onclick="document.getElementById('global-search').focus()">
|
|
<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="global-search" placeholder="搜索"/>
|
|
<span class="kbd">Ctrl K</span>
|
|
</div>
|
|
<div class="nav-section">主要</div>
|
|
<nav>${navHtml}</nav>
|
|
<div class="nav-section">协作</div>
|
|
<nav>
|
|
<a class="disabled" title="V1.5 上线,敬请期待">
|
|
${TEAM_NAV.icon}
|
|
<span>${TEAM_NAV.label}</span>
|
|
<span class="pill-mini">${TEAM_NAV.badge}</span>
|
|
</a>
|
|
</nav>
|
|
<div class="aside-foot">
|
|
<div class="user" onclick="Shell.toast('账户菜单', 'li@shop.com')">
|
|
<div class="av">李</div>
|
|
<div class="em">小李的店</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
`;
|
|
|
|
const crumbHtml = crumbs.length ? `
|
|
<div class="crumbs">
|
|
${crumbs.map((c, i) => {
|
|
const last = i === crumbs.length - 1;
|
|
const sep = i > 0 ? '<span class="sep">/</span>' : '';
|
|
if (last) return `${sep}<span class="here">${c.label}</span>`;
|
|
return `${sep}<a href="${c.href || '#'}">${c.label}</a>`;
|
|
}).join('')}
|
|
</div>
|
|
` : '';
|
|
|
|
const topbar = `
|
|
<header class="topbar">
|
|
${crumbHtml}
|
|
<div class="right">
|
|
<span class="balance-chip" onclick="location.href='account.html'">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="9"/><path d="M12 7v10M9 10h4a1.5 1.5 0 0 1 0 3h-3a1.5 1.5 0 0 0 0 3h4"/></svg>
|
|
余额 <strong>${balance}</strong>
|
|
</span>
|
|
<button class="icon-btn" onclick="Shell.toast('通知中心', '3 条未读')">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10 21a2 2 0 0 0 4 0"/></svg>
|
|
<span class="dot-noti"></span>
|
|
</button>
|
|
${topActions}
|
|
</div>
|
|
</header>
|
|
`;
|
|
|
|
const decorations = `
|
|
<div class="grid-bg"></div>
|
|
<pre class="scatter" style="top:96px;left:280px"> · · +
|
|
· +XX+
|
|
+XXXX·
|
|
+X· </pre>
|
|
<pre class="scatter" style="top:340px;right:96px">+ · ·
|
|
XX· ·
|
|
·XXXX·+
|
|
·++· </pre>
|
|
<pre class="scatter" style="bottom:160px;left:42%"> · +
|
|
+·XX·
|
|
·X+ ·
|
|
· </pre>
|
|
<pre class="scatter" style="top:580px;left:60px"> +X·
|
|
·XX·
|
|
+·X·+</pre>
|
|
<span class="sq-mark" style="top:238px;left:478px"></span>
|
|
<span class="sq-mark" style="top:478px;left:1198px"></span>
|
|
<span class="sq-mark" style="bottom:300px;left:238px"></span>
|
|
<span class="sq-mark" style="top:718px;right:240px"></span>
|
|
<span class="tag-corner" style="top:158px;left:34px">[ 200 OK ]</span>
|
|
<span class="tag-corner" style="top:158px;right:34px">[ /v2 ]</span>
|
|
<span class="tag-corner" style="bottom:36px;left:34px">[ .MP4 · 9:16 ]</span>
|
|
<span class="tag-corner" style="bottom:36px;right:34px">[ STUDIO ]</span>
|
|
`;
|
|
|
|
const toastHtml = `
|
|
<div class="toast" id="__toast">
|
|
<div class="ic-t"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg></div>
|
|
<div class="txt" id="__toast-txt">操作成功<span class="mono">[ 200 OK ]</span></div>
|
|
</div>
|
|
`;
|
|
|
|
// 装订线 SVG 准星 · V2.1 签名元素(圆弧内凹的"+")
|
|
const cornerSvg = `<path d="M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z" fill="currentColor"/>`;
|
|
const cornerMarks = `
|
|
<span class="corner-mark tl"><svg viewBox="0 0 22 21" fill="none">${cornerSvg}</svg></span>
|
|
<span class="corner-mark tr"><svg viewBox="0 0 22 21" fill="none">${cornerSvg}</svg></span>
|
|
<span class="corner-mark bl"><svg viewBox="0 0 22 21" fill="none">${cornerSvg}</svg></span>
|
|
<span class="corner-mark br"><svg viewBox="0 0 22 21" fill="none">${cornerSvg}</svg></span>
|
|
`;
|
|
|
|
const app = document.createElement('div');
|
|
app.className = 'app';
|
|
app.innerHTML = sidebar + `<main>${decorations}${topbar}<div class="content" id="page-content">${cornerMarks}</div></main>`;
|
|
|
|
const src = document.getElementById('page');
|
|
document.body.prepend(app);
|
|
if (src) {
|
|
// 把页面 body 内容追加到 .content,保留 4 个 corner-mark SVG
|
|
document.getElementById('page-content').insertAdjacentHTML('beforeend', src.innerHTML);
|
|
src.remove();
|
|
}
|
|
document.body.insertAdjacentHTML('beforeend', toastHtml);
|
|
|
|
document.addEventListener('keydown', e => {
|
|
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
e.preventDefault();
|
|
document.getElementById('global-search')?.focus();
|
|
}
|
|
});
|
|
},
|
|
|
|
toast(text, mono) {
|
|
const t = document.getElementById('__toast');
|
|
const txt = document.getElementById('__toast-txt');
|
|
if (!t || !txt) return;
|
|
txt.innerHTML = text + (mono ? `<span class="mono">[ ${mono} ]</span>` : '');
|
|
t.classList.add('show');
|
|
clearTimeout(this._tt);
|
|
this._tt = setTimeout(() => t.classList.remove('show'), 2400);
|
|
},
|
|
|
|
openModal(id) { document.getElementById(id)?.classList.add('show'); },
|
|
closeModal(id) { document.getElementById(id)?.classList.remove('show'); },
|
|
openDrawer(id) {
|
|
document.getElementById(id)?.classList.add('show');
|
|
document.getElementById(id + '-bg')?.classList.add('show');
|
|
},
|
|
closeDrawer(id) {
|
|
document.getElementById(id)?.classList.remove('show');
|
|
document.getElementById(id + '-bg')?.classList.remove('show');
|
|
}
|
|
};
|