docs: 固化管理端分组导航原型 (#59)
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
(()=>{var u={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":2,"stroke-linecap":"round","stroke-linejoin":"round"};var L=([e,a,t])=>{let r=document.createElementNS("http://www.w3.org/2000/svg",e);return Object.keys(a).forEach(o=>{r.setAttribute(o,String(a[o]))}),t?.length&&t.forEach(o=>{let s=L(o);r.appendChild(s)}),r},y=(e,a={})=>{let r={...u,...a};return L(["svg",r,e])};var R=e=>{for(let a in e)if(a.startsWith("aria-")||a==="role"||a==="title")return!0;return!1};var T=(...e)=>e.filter((a,t,r)=>!!a&&a.trim()!==""&&r.indexOf(a)===t).join(" ").trim();var q=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(a,t,r)=>r?r.toUpperCase():t.toLowerCase());var b=e=>{let a=q(e);return a.charAt(0).toUpperCase()+a.slice(1)};var G=e=>Array.from(e.attributes).reduce((a,t)=>(a[t.name]=t.value,a),{}),U=e=>typeof e=="string"?e:!e||!e.class?"":e.class&&typeof e.class=="string"?e.class.split(" "):e.class&&Array.isArray(e.class)?e.class:"",p=(e,{nameAttr:a,icons:t,attrs:r})=>{let o=e.getAttribute(a);if(o==null)return;let s=b(o),f=t[s];if(!f)return console.warn(`${e.outerHTML} icon name was not found in the provided icons object.`);let l=G(e),O=R(l)?{}:{"aria-hidden":"true"},F={...u,"data-lucide":o,...O,...r,...l},v=U(l),H=U(r),D=T("lucide",`lucide-${o}`,...v,...H);D&&Object.assign(F,{class:D});let E=y(f,F);return e.parentNode?.replaceChild(E,e)};var m=[["path",{d:"M20 6 9 17l-5-5"}]];var d=[["circle",{cx:"12",cy:"12",r:"10"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16"}]];var x=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M12 6v6h4"}]];var i=[["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"}]];var n=[["path",{d:"M12 15V3"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"}],["path",{d:"m7 10 5 5 5-5"}]];var c=[["path",{d:"M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"}],["path",{d:"M14.084 14.158a3 3 0 0 1-4.242-4.242"}],["path",{d:"M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"}],["path",{d:"m2 2 20 20"}]];var C=[["path",{d:"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"}],["circle",{cx:"12",cy:"12",r:"3"}]];var h=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M10 9H8"}],["path",{d:"M16 13H8"}],["path",{d:"M16 17H8"}]];var S=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["circle",{cx:"9",cy:"9",r:"2"}],["path",{d:"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"}]];var g=[["path",{d:"m16 17 5-5-5-5"}],["path",{d:"M21 12H9"}],["path",{d:"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"}]];var w=[["path",{d:"M5 12h14"}],["path",{d:"M12 5v14"}]];var k=[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"}],["path",{d:"M21 3v5h-5"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"}],["path",{d:"M8 16H3v5"}]];var P=[["path",{d:"m21 21-4.34-4.34"}],["circle",{cx:"11",cy:"11",r:"8"}]];var A=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"m9 12 2 2 4-4"}]];var M=[["path",{d:"M18 6 6 18"}],["path",{d:"m6 6 12 12"}]];var B=({icons:e={},nameAttr:a="data-lucide",attrs:t={},root:r=document,inTemplates:o}={})=>{if(!Object.values(e).length)throw new Error(`Please provide an icons object.
|
||||
If you want to use all the icons you can import it like:
|
||||
\`import { createIcons, icons } from 'lucide';
|
||||
lucide.createIcons({icons});\``);if(typeof r>"u")throw new Error("`createIcons()` only works in a browser environment.");if(Array.from(r.querySelectorAll(`[${a}]`)).forEach(f=>p(f,{nameAttr:a,icons:e,attrs:t})),o&&Array.from(r.querySelectorAll("template")).forEach(l=>B({icons:e,nameAttr:a,attrs:t,root:l.content,inTemplates:o})),a==="data-lucide"){let f=r.querySelectorAll("[icon-name]");f.length>0&&(console.warn("[Lucide] Some icons were found with the now deprecated icon-name attribute. These will still be replaced for backwards compatibility, but will no longer be supported in v1.0 and you should switch to data-lucide"),Array.from(f).forEach(l=>p(l,{nameAttr:"icon-name",icons:e,attrs:t})))}};var V={AlertCircle:d,Check:m,Clock3:x,Copy:i,Download:n,Eye:C,EyeOff:c,FileText:h,Image:S,LogOut:g,Plus:w,RefreshCw:k,Search:P,ShieldCheck:A,X:M};window.renderChorusIcons=(e=document)=>B({icons:V,attrs:{"aria-hidden":"true"},root:e});document.addEventListener("DOMContentLoaded",()=>window.renderChorusIcons());})();
|
||||
/*! Bundled license information:
|
||||
|
||||
lucide/dist/esm/defaultAttributes.mjs:
|
||||
lucide/dist/esm/createElement.mjs:
|
||||
lucide/dist/esm/shared/src/utils/hasA11yProp.mjs:
|
||||
lucide/dist/esm/shared/src/utils/mergeClasses.mjs:
|
||||
lucide/dist/esm/shared/src/utils/toCamelCase.mjs:
|
||||
lucide/dist/esm/shared/src/utils/toPascalCase.mjs:
|
||||
lucide/dist/esm/replaceElement.mjs:
|
||||
lucide/dist/esm/icons/check.mjs:
|
||||
lucide/dist/esm/icons/circle-alert.mjs:
|
||||
lucide/dist/esm/icons/clock-3.mjs:
|
||||
lucide/dist/esm/icons/copy.mjs:
|
||||
lucide/dist/esm/icons/download.mjs:
|
||||
lucide/dist/esm/icons/eye-off.mjs:
|
||||
lucide/dist/esm/icons/eye.mjs:
|
||||
lucide/dist/esm/icons/file-text.mjs:
|
||||
lucide/dist/esm/icons/image.mjs:
|
||||
lucide/dist/esm/icons/log-out.mjs:
|
||||
lucide/dist/esm/icons/plus.mjs:
|
||||
lucide/dist/esm/icons/refresh-cw.mjs:
|
||||
lucide/dist/esm/icons/search.mjs:
|
||||
lucide/dist/esm/icons/shield-check.mjs:
|
||||
lucide/dist/esm/icons/x.mjs:
|
||||
lucide/dist/esm/lucide.mjs:
|
||||
(**
|
||||
* @license lucide v1.33.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*)
|
||||
*/
|
||||
@@ -0,0 +1,293 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light">
|
||||
<title>Chorus 管理端分组导航原型 v1</title>
|
||||
<link rel="icon" href="./favicon.ico">
|
||||
<script defer src="./icons.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--sidebar: #001529;
|
||||
--sidebar-deep: #000c17;
|
||||
--sidebar-text: #c7d2df;
|
||||
--sidebar-muted: #8393a7;
|
||||
--primary: #1677ff;
|
||||
--primary-hover: #0958d9;
|
||||
--primary-soft: #e6f4ff;
|
||||
--surface: #ffffff;
|
||||
--surface-alt: #f5f7fa;
|
||||
--surface-soft: #fafbfc;
|
||||
--text: #1f2937;
|
||||
--muted: #5d6b7a;
|
||||
--border: #dfe4ea;
|
||||
--success: #237804;
|
||||
--success-soft: #f0f9eb;
|
||||
--warning: #874d00;
|
||||
--warning-soft: #fff7e6;
|
||||
--danger: #b42318;
|
||||
--danger-soft: #fff1f0;
|
||||
--focus: #69b1ff;
|
||||
--sidebar-width: 236px;
|
||||
font-family: "Segoe UI", "Microsoft YaHei", Arial, sans-serif;
|
||||
color: var(--text);
|
||||
background: var(--surface-alt);
|
||||
}
|
||||
* { box-sizing: border-box; letter-spacing: 0; }
|
||||
html { min-width: 320px; }
|
||||
body { min-width: 320px; min-height: 100dvh; margin: 0; overflow-x: hidden; background: var(--surface-alt); font-size: 14px; line-height: 1.5; }
|
||||
button, input, select { font: inherit; letter-spacing: 0; }
|
||||
button { cursor: pointer; }
|
||||
button:disabled { cursor: not-allowed; }
|
||||
svg { width: 18px; height: 18px; flex: 0 0 18px; pointer-events: none; stroke-width: 2; }
|
||||
:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
|
||||
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
||||
.prototype-bar { position: fixed; inset: 0 0 auto 0; z-index: 70; display: flex; align-items: center; justify-content: space-between; min-height: 34px; padding: 5px 14px; color: #29445e; background: #eaf4ff; border-bottom: 1px solid #b7d8f7; font-size: 12px; }
|
||||
.prototype-bar strong { color: #12395e; }
|
||||
.prototype-bar span { color: #53697d; }
|
||||
.app-shell { min-height: 100dvh; padding-top: 34px; }
|
||||
.sidebar { position: fixed; z-index: 50; inset: 34px auto 0 0; width: var(--sidebar-width); overflow-y: auto; color: var(--sidebar-text); background: var(--sidebar); transition: transform .2s ease-out; }
|
||||
.brand { display: flex; align-items: center; gap: 10px; min-height: 58px; padding: 0 17px; color: #fff; background: #002140; border-bottom: 1px solid #15354f; }
|
||||
.brand-mark { display: grid; place-items: center; width: 30px; height: 30px; color: #fff; background: var(--primary); border-radius: 4px; font-size: 15px; font-weight: 800; }
|
||||
.brand-copy { min-width: 0; }
|
||||
.brand-copy strong { display: block; font-size: 16px; line-height: 1.2; }
|
||||
.brand-copy span { color: #91a6ba; font-size: 11px; }
|
||||
.nav { padding: 10px 0 18px; }
|
||||
.nav-group { border-bottom: 1px solid #102b42; }
|
||||
.group-button, .nav-item { width: 100%; color: inherit; border: 0; text-align: left; }
|
||||
.group-button { display: flex; align-items: center; gap: 10px; min-height: 48px; padding: 0 15px; color: #f4f7fa; background: transparent; font-weight: 650; }
|
||||
.group-button:hover { background: #061d30; }
|
||||
.group-button .chevron { width: 16px; height: 16px; margin-left: auto; transition: transform .18s ease-out; }
|
||||
.group-button[aria-expanded="true"] .chevron { transform: rotate(90deg); }
|
||||
.group-items { margin: 0; padding: 3px 0 7px; list-style: none; background: var(--sidebar-deep); }
|
||||
.group-items[hidden] { display: none; }
|
||||
.nav-item { position: relative; display: flex; align-items: center; gap: 9px; min-height: 44px; padding: 8px 12px 8px 42px; color: var(--sidebar-text); background: transparent; }
|
||||
.nav-item:hover { color: #fff; background: #08243a; }
|
||||
.nav-item.active { color: #fff; background: var(--primary); }
|
||||
.nav-item.active::before { position: absolute; inset: 0 auto 0 0; width: 3px; content: ""; background: #91caff; }
|
||||
.nav-item .label { min-width: 0; overflow-wrap: anywhere; }
|
||||
.nav-item .readonly { margin-left: auto; padding: 1px 5px; color: #c9d9e8; border: 1px solid #425e75; border-radius: 3px; font-size: 10px; }
|
||||
.nav-item.active .readonly { color: #fff; border-color: #91caff; }
|
||||
.main { min-width: 0; margin-left: var(--sidebar-width); transition: margin-left .2s ease-out; }
|
||||
.topbar { position: sticky; top: 34px; z-index: 30; display: flex; align-items: center; gap: 12px; min-height: 58px; padding: 0 18px; background: #fff; border-bottom: 1px solid var(--border); box-shadow: 0 1px 4px #1f29370d; }
|
||||
.icon-button { display: inline-grid; place-items: center; width: 44px; height: 44px; padding: 0; color: #526170; background: transparent; border: 1px solid transparent; border-radius: 4px; }
|
||||
.icon-button:hover { color: var(--primary); background: var(--primary-soft); border-color: #bae0ff; }
|
||||
.breadcrumb { display: flex; align-items: center; gap: 7px; min-width: 0; color: var(--muted); }
|
||||
.breadcrumb strong { color: var(--text); font-weight: 650; }
|
||||
.top-actions { display: flex; align-items: center; gap: 4px; margin-left: auto; }
|
||||
.account { display: flex; align-items: center; gap: 8px; margin-left: 5px; color: #44505d; white-space: nowrap; }
|
||||
.avatar { display: grid; place-items: center; width: 30px; height: 30px; color: #0050b3; background: #bae0ff; border-radius: 50%; font-weight: 750; }
|
||||
.tagbar { display: flex; align-items: center; min-height: 36px; padding: 0 14px; background: #fff; border-bottom: 1px solid var(--border); }
|
||||
.page-tag { display: inline-flex; align-items: center; gap: 7px; min-height: 28px; padding: 4px 10px; color: var(--primary); background: var(--primary-soft); border: 1px solid #91caff; border-radius: 3px; font-size: 12px; }
|
||||
.content { padding: 18px; }
|
||||
.page-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; margin-bottom: 14px; }
|
||||
.page-heading h1 { display: flex; align-items: center; gap: 9px; margin: 0; font-size: 22px; line-height: 1.3; }
|
||||
.page-heading p { max-width: 760px; margin: 5px 0 0; color: var(--muted); }
|
||||
.badge { display: inline-flex; align-items: center; gap: 4px; min-height: 24px; padding: 2px 7px; border-radius: 3px; font-size: 11px; font-weight: 650; white-space: nowrap; }
|
||||
.badge.readonly { color: #245b78; background: #e6f7ff; border: 1px solid #91d5ff; }
|
||||
.badge.success { color: var(--success); background: var(--success-soft); border: 1px solid #b7eb8f; }
|
||||
.badge.warning { color: var(--warning); background: var(--warning-soft); border: 1px solid #ffd591; }
|
||||
.badge.neutral { color: #4a5563; background: #f0f2f5; border: 1px solid #d9d9d9; }
|
||||
.button { display: inline-flex; align-items: center; justify-content: center; gap: 7px; min-width: 44px; min-height: 40px; padding: 7px 13px; color: #334155; background: #fff; border: 1px solid #b9c2cc; border-radius: 4px; font-weight: 600; }
|
||||
.button:hover { color: var(--primary); border-color: var(--primary); background: #f5faff; }
|
||||
.button.primary { color: #fff; background: var(--primary); border-color: var(--primary); }
|
||||
.button.primary:hover { background: var(--primary-hover); }
|
||||
.button:disabled { color: #9aa4af; background: #f5f5f5; border-color: #d9d9d9; }
|
||||
.panel { overflow: hidden; background: #fff; border: 1px solid var(--border); border-radius: 6px; box-shadow: 0 1px 3px #1f29370a; }
|
||||
.panel-head { display: flex; align-items: center; justify-content: space-between; gap: 14px; min-height: 50px; padding: 10px 15px; border-bottom: 1px solid var(--border); }
|
||||
.panel-head h2 { margin: 0; font-size: 15px; }
|
||||
.panel-head span { color: var(--muted); font-size: 12px; }
|
||||
.toolbar { display: flex; align-items: center; gap: 8px; padding: 10px 12px; background: var(--surface-soft); border-bottom: 1px solid var(--border); }
|
||||
.search { width: min(280px, 100%); min-height: 40px; padding: 7px 10px; color: var(--text); background: #fff; border: 1px solid #b9c2cc; border-radius: 4px; }
|
||||
.toolbar .spacer { flex: 1; }
|
||||
.result-count { color: var(--muted); white-space: nowrap; }
|
||||
.notice { display: flex; align-items: flex-start; gap: 9px; margin-bottom: 12px; padding: 10px 12px; color: #245b78; background: #e6f7ff; border: 1px solid #91d5ff; border-radius: 4px; }
|
||||
.notice.warning { color: var(--warning); background: var(--warning-soft); border-color: #ffd591; }
|
||||
.notice p { margin: 0; }
|
||||
.data-list { width: 100%; }
|
||||
.list-row { display: grid; grid-template-columns: minmax(160px, 1.3fr) minmax(140px, 1fr) minmax(110px, .8fr) minmax(150px, 1fr) 120px; align-items: center; gap: 14px; min-height: 52px; padding: 10px 14px; border-bottom: 1px solid #edf0f2; }
|
||||
.list-row:last-child { border-bottom: 0; }
|
||||
.list-row.header { min-height: 40px; color: var(--muted); background: var(--surface-soft); font-size: 12px; font-weight: 650; }
|
||||
.list-row:not(.header):hover { background: #fafcff; }
|
||||
.cell { min-width: 0; overflow-wrap: anywhere; }
|
||||
.cell strong { display: block; font-weight: 650; }
|
||||
.secondary { display: block; margin-top: 2px; color: var(--muted); font-size: 12px; }
|
||||
.mono { font-family: "Cascadia Code", Consolas, monospace; font-variant-numeric: tabular-nums; }
|
||||
.row-actions { display: flex; align-items: center; justify-content: flex-end; gap: 4px; }
|
||||
.link-button { min-height: 36px; padding: 5px 7px; color: var(--primary); background: transparent; border: 0; border-radius: 3px; font-weight: 600; }
|
||||
.link-button:hover { background: var(--primary-soft); }
|
||||
.link-button:disabled { color: #9aa4af; background: transparent; }
|
||||
.stat-strip { display: grid; grid-template-columns: repeat(4, minmax(130px, 1fr)); gap: 12px; margin-bottom: 12px; }
|
||||
.stat { padding: 14px; background: #fff; border: 1px solid var(--border); border-radius: 6px; border-top: 3px solid var(--primary); }
|
||||
.stat:nth-child(2) { border-top-color: #13a8a8; }
|
||||
.stat:nth-child(3) { border-top-color: #52c41a; }
|
||||
.stat:nth-child(4) { border-top-color: #faad14; }
|
||||
.stat span { color: var(--muted); font-size: 12px; }
|
||||
.stat strong { display: block; margin-top: 4px; font-size: 23px; font-variant-numeric: tabular-nums; }
|
||||
.mobile-scrim { display: none; position: fixed; z-index: 45; inset: 34px 0 0; background: #0f172a80; }
|
||||
@media (max-width: 920px) {
|
||||
.stat-strip { grid-template-columns: repeat(2, minmax(130px, 1fr)); }
|
||||
.list-row { grid-template-columns: minmax(150px, 1.3fr) minmax(120px, 1fr) minmax(100px, .8fr) 110px; }
|
||||
.list-row > :nth-child(4) { display: none; }
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.prototype-bar span { display: none; }
|
||||
.sidebar { transform: translateX(-100%); box-shadow: 10px 0 28px #0015294d; }
|
||||
body.sidebar-open .sidebar { transform: translateX(0); }
|
||||
body.sidebar-open .mobile-scrim { display: block; }
|
||||
.main { margin-left: 0; }
|
||||
.content { padding: 12px; }
|
||||
.topbar { padding: 0 10px; }
|
||||
.breadcrumb { font-size: 13px; }
|
||||
.breadcrumb > span:first-child, .account > span { display: none; }
|
||||
.page-heading { flex-direction: column; }
|
||||
.page-heading h1 { font-size: 20px; }
|
||||
.toolbar { flex-wrap: wrap; }
|
||||
.search { flex: 1 1 200px; width: auto; }
|
||||
.toolbar .spacer { display: none; }
|
||||
.result-count { width: 100%; }
|
||||
.list-row, .list-row.header { grid-template-columns: 1fr; gap: 7px; }
|
||||
.list-row.header { display: none; }
|
||||
.list-row { align-items: start; padding: 13px 14px; }
|
||||
.list-row > :nth-child(4) { display: block; }
|
||||
.cell::before { display: block; margin-bottom: 1px; color: #7a8794; content: attr(data-label); font-size: 11px; font-weight: 600; }
|
||||
.row-actions { justify-content: flex-start; }
|
||||
.stat-strip { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
@media (max-width: 390px) {
|
||||
.stat-strip { grid-template-columns: 1fr; }
|
||||
.prototype-bar { padding-inline: 10px; }
|
||||
.top-actions .icon-button:first-child { display: none; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after { scroll-behavior: auto !important; transition: none !important; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="prototype-bar"><strong>#59 设计证据 v1</strong><span>管理端分组导航与适用系统模块</span></div>
|
||||
<div class="app-shell">
|
||||
<aside class="sidebar" id="sidebar" aria-label="管理端主导航">
|
||||
<div class="brand"><span class="brand-mark">C</span><span class="brand-copy"><strong>Chorus Admin</strong><span>运营与系统管理</span></span></div>
|
||||
<nav class="nav" id="primary-nav"></nav>
|
||||
</aside>
|
||||
<div class="mobile-scrim" id="mobile-scrim" aria-hidden="true"></div>
|
||||
<div class="main">
|
||||
<header class="topbar">
|
||||
<button class="icon-button" id="sidebar-toggle" type="button" aria-label="打开或关闭侧边栏" aria-controls="sidebar" aria-expanded="false"><i data-lucide="file-text"></i></button>
|
||||
<div class="breadcrumb" aria-label="面包屑"><span>首页</span><span>/</span><strong id="breadcrumb-group">生成配置</strong><span>/</span><strong id="breadcrumb-page">上游服务商</strong></div>
|
||||
<div class="top-actions"><button class="icon-button" type="button" aria-label="搜索"><i data-lucide="search"></i></button><button class="icon-button" type="button" aria-label="全屏"><i data-lucide="eye"></i></button><div class="account"><span class="avatar">管</span><span>当前管理员</span></div></div>
|
||||
</header>
|
||||
<div class="tagbar"><span class="page-tag"><i data-lucide="check"></i><span id="tag-title">上游服务商</span></span></div>
|
||||
<main class="content" id="page-content" tabindex="-1"></main>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const groups = [
|
||||
{ id: 'configuration', title: '生成配置', icon: 'image', items: [
|
||||
{ id: 'providers', title: '上游服务商', icon: 'image' },
|
||||
{ id: 'models', title: '模型配置', icon: 'image' },
|
||||
{ id: 'routes', title: '路由策略', icon: 'refresh-cw' },
|
||||
{ id: 'templates', title: '提示词模板', icon: 'file-text' }
|
||||
]},
|
||||
{ id: 'monitoring', title: '运行监控', icon: 'clock-3', items: [
|
||||
{ id: 'health', title: '上游健康', icon: 'shield-check' },
|
||||
{ id: 'generations', title: '生成记录', icon: 'clock-3' }
|
||||
]},
|
||||
{ id: 'access', title: '用户与访问', icon: 'eye', items: [
|
||||
{ id: 'end-users', title: '终端用户', icon: 'eye' },
|
||||
{ id: 'api-keys', title: 'API 密钥', icon: 'shield-check' }
|
||||
]},
|
||||
{ id: 'system', title: '系统管理', icon: 'refresh-cw', items: [
|
||||
{ id: 'admins', title: '管理员账号', icon: 'shield-check' },
|
||||
{ id: 'roles', title: '角色权限', icon: 'shield-check' },
|
||||
{ id: 'menus', title: '菜单结构', icon: 'file-text', readonly: true },
|
||||
{ id: 'apis', title: '接口清单', icon: 'file-text', readonly: true },
|
||||
{ id: 'login-logs', title: '登录日志', icon: 'file-text', readonly: true }
|
||||
]}
|
||||
];
|
||||
const pageMeta = {
|
||||
providers: ['上游服务商', '维护生成服务的连接、启停与健康状态。凭据不会出现在列表中。'],
|
||||
models: ['模型配置', '按上游服务商维护可用于文本与图片生成的模型。'],
|
||||
routes: ['路由策略', '配置生成类型、模型优先级和失败切换顺序。'],
|
||||
templates: ['提示词模板', '维护可复用的系统提示词模板和启用状态。'],
|
||||
health: ['上游健康', '查看连接状态、最近探测结果和熔断状态。'],
|
||||
generations: ['生成记录', '按任务查看状态、耗时和上游尝试记录。'],
|
||||
'end-users': ['终端用户', '查看使用 Portal 和 OpenAPI 的受控用户,与管理员账号分开管理。'],
|
||||
'api-keys': ['API 密钥', '查看 API Key 元数据和状态;完整 Key 不在管理端列表回显。'],
|
||||
admins: ['管理员账号', '维护管理端登录账号;当前账号和最后一个管理员不能被删除。'],
|
||||
roles: ['角色权限', '维护管理端角色和菜单授权;内置运营角色受到保护。'],
|
||||
menus: ['菜单结构', '查看当前生效的版本化菜单结构。'],
|
||||
apis: ['接口清单', '查看管理端 API 与权限映射。'],
|
||||
'login-logs': ['登录日志', '查看管理员登录结果和安全元数据。']
|
||||
};
|
||||
const list = (headers, rows) => `<div class="data-list"><div class="list-row header">${headers.map(h => `<div>${h}</div>`).join('')}</div>${rows.map(row => `<div class="list-row">${row.map((value, index) => `<div class="cell${index === row.length - 1 ? ' row-actions' : ''}" data-label="${headers[index]}">${value}</div>`).join('')}</div>`).join('')}</div>`;
|
||||
const badge = (text, type = 'neutral') => `<span class="badge ${type}">${text}</span>`;
|
||||
const action = (text, disabled = false) => `<button class="link-button" type="button"${disabled ? ' disabled' : ''}>${text}</button>`;
|
||||
function representativeContent(id) {
|
||||
if (id === 'providers') return list(['名称', '协议', '状态', '最近检查', '操作'], [
|
||||
['<strong>示例图像服务</strong><span class="secondary">凭据已配置</span>', 'OpenAI 兼容', badge('已启用', 'success'), '刚刚 · 正常', action('查看') + action('编辑')],
|
||||
['<strong>备用文本服务</strong><span class="secondary">凭据已配置</span>', 'OpenAI 兼容', badge('已停用'), '未检查', action('查看') + action('编辑')]
|
||||
]);
|
||||
if (id === 'models') return list(['模型名称', '用途', 'Provider', '状态', '操作'], [['<strong>示例图片模型</strong>', '文生图 / 图片编辑', '示例图像服务', badge('已启用', 'success'), action('编辑')], ['<strong>示例文本模型</strong>', '文本生成', '备用文本服务', badge('已停用'), action('编辑')]]);
|
||||
if (id === 'routes') return list(['路由名称', '生成类型', '模型数', '状态', '操作'], [['<strong>图片生成主路由</strong>', '文生图', '2', badge('已启用', 'success'), action('编辑')], ['<strong>文本生成主路由</strong>', '文本', '1', badge('已启用', 'success'), action('编辑')]]);
|
||||
if (id === 'templates') return list(['模板名称', '用途', '版本', '状态', '操作'], [['<strong>商品图增强</strong>', '文生图', 'v3', badge('已启用', 'success'), action('编辑')], ['<strong>文本润色</strong>', '文本', 'v2', badge('草稿', 'warning'), action('编辑')]]);
|
||||
if (id === 'health') return list(['上游服务商', '连接状态', '熔断状态', '最近探测', '操作'], [['<strong>示例图像服务</strong>', badge('正常', 'success'), '关闭', '刚刚', action('查看')], ['<strong>备用文本服务</strong>', badge('未探测', 'warning'), '关闭', '-', action('探测')]]);
|
||||
if (id === 'generations') return list(['任务编号', '终端用户', '类型', '状态', '操作'], [['<strong class="mono">#DEMO-20</strong>', '示例用户 A', '图片', badge('成功', 'success'), action('查看')], ['<strong class="mono">#DEMO-19</strong>', '示例用户 B', '文本', badge('失败', 'warning'), action('查看')]]);
|
||||
if (id === 'end-users') return list(['账号', '显示名称', '状态', '创建时间', '操作'], [['<strong>示例用户 A</strong>', '受控用户', badge('正常', 'success'), '示例时间', action('查看')], ['<strong>示例用户 B</strong>', '受控用户', badge('停用'), '示例时间', action('查看')]]);
|
||||
if (id === 'api-keys') return list(['所属用户', '名称 / 前缀', '状态', '最近使用', '操作'], [['<strong>示例用户 A</strong>', '自动化脚本<span class="secondary mono">chorus_demo</span>', badge('有效', 'success'), '示例时间', action('查看')], ['<strong>示例用户 B</strong>', '旧版接入<span class="secondary mono">chorus_old</span>', badge('已撤销'), '示例时间', action('查看')]]);
|
||||
if (id === 'admins') return `<div class="notice warning"><i data-lucide="alert-circle"></i><p>当前登录账号不能删除;系统至少保留一个可用管理员。</p></div>${list(['管理员', '角色', '状态', '保护', '操作'], [['<strong>当前管理员</strong><span class="secondary">当前登录账号</span>', '超级管理员', badge('正常', 'success'), badge('禁止删除', 'warning'), action('编辑') + action('删除', true)], ['<strong>备用管理员</strong>', '运营管理员', badge('正常', 'success'), '最后一名时禁止删除', action('编辑') + action('删除')]])}`;
|
||||
if (id === 'roles') return `<div class="notice warning"><i data-lucide="shield-check"></i><p>内置 <span class="mono">chorus_operator</span> 角色可调整授权,但不能改名或删除。</p></div>${list(['角色', '角色标识', '用户数', '保护', '操作'], [['<strong>超级管理员</strong>', '<span class="mono">admin</span>', '1', badge('内置', 'warning'), action('授权')], ['<strong>运营管理员</strong>', '<span class="mono">chorus_operator</span>', '1', badge('内置', 'warning'), action('授权') + action('删除', true)]])}`;
|
||||
if (id === 'menus') return `<div class="notice"><i data-lucide="shield-check"></i><p>菜单由版本化 migration 管理。本页面仅用于核对生效结构,不提供新增、编辑、删除或拖拽排序。</p></div>${list(['分组 / 模块', '层级', '状态', '来源', '操作'], [['<strong>生成配置</strong><span class="secondary">4 个模块</span>', '一级分组', badge('显示', 'success'), '版本化 migration', action('查看')], ['<strong>系统管理</strong><span class="secondary">5 个模块</span>', '一级分组', badge('显示', 'success'), '版本化 migration', action('查看')]])}`;
|
||||
if (id === 'apis') return `<div class="notice"><i data-lucide="shield-check"></i><p>接口与权限映射仅供核对。变更必须通过生产实现工单和版本化 migration。</p></div>${list(['接口名称', '方法', '路径', '权限状态', '操作'], [['<strong>Provider 列表</strong>', '<span class="mono">GET</span>', '<span class="mono">/api/v1/chorus/providers</span>', badge('已映射', 'success'), action('查看')], ['<strong>生成记录</strong>', '<span class="mono">GET</span>', '<span class="mono">/api/v1/chorus/generations</span>', badge('已映射', 'success'), action('查看')]])}`;
|
||||
return `<div class="notice"><i data-lucide="shield-check"></i><p>登录审计记录只读保存,页面不提供删除和清空操作;示例不包含真实账号、IP 或设备信息。</p></div>${list(['时间', '管理员', '结果', '来源', '操作'], [['<strong>示例时间</strong>', '示例管理员', badge('成功', 'success'), '已脱敏', action('查看')], ['<strong>示例时间</strong>', '示例管理员', badge('失败', 'warning'), '已脱敏', action('查看')]])}`;
|
||||
}
|
||||
function renderNav() {
|
||||
const nav = document.getElementById('primary-nav');
|
||||
nav.innerHTML = groups.map(group => `<section class="nav-group"><button class="group-button" type="button" data-group="${group.id}" aria-expanded="true" aria-controls="group-${group.id}"><i data-lucide="${group.icon}"></i><span>${group.title}</span><i class="chevron" data-lucide="plus"></i></button><ul class="group-items" id="group-${group.id}">${group.items.map(item => `<li><button class="nav-item${item.id === 'providers' ? ' active' : ''}" type="button" data-page="${item.id}" aria-current="${item.id === 'providers' ? 'page' : 'false'}"><i data-lucide="${item.icon}"></i><span class="label">${item.title}</span>${item.readonly ? '<span class="readonly">只读</span>' : ''}</button></li>`).join('')}</ul></section>`).join('');
|
||||
nav.querySelectorAll('[data-group]').forEach(button => button.addEventListener('click', () => {
|
||||
const expanded = button.getAttribute('aria-expanded') === 'true';
|
||||
button.setAttribute('aria-expanded', String(!expanded));
|
||||
document.getElementById(button.getAttribute('aria-controls')).hidden = expanded;
|
||||
}));
|
||||
nav.querySelectorAll('[data-page]').forEach(button => button.addEventListener('click', () => selectPage(button.dataset.page)));
|
||||
icons();
|
||||
}
|
||||
function selectPage(id) {
|
||||
const group = groups.find(entry => entry.items.some(item => item.id === id));
|
||||
const item = group.items.find(entry => entry.id === id);
|
||||
document.querySelectorAll('[data-page]').forEach(button => {
|
||||
const active = button.dataset.page === id;
|
||||
button.classList.toggle('active', active);
|
||||
button.setAttribute('aria-current', active ? 'page' : 'false');
|
||||
});
|
||||
document.getElementById('breadcrumb-group').textContent = group.title;
|
||||
document.getElementById('breadcrumb-page').textContent = item.title;
|
||||
document.getElementById('tag-title').textContent = item.title;
|
||||
renderPage(id, item.readonly);
|
||||
if (window.matchMedia('(max-width: 720px)').matches) closeSidebar();
|
||||
}
|
||||
function renderPage(id, readonly = false) {
|
||||
const [title, description] = pageMeta[id];
|
||||
const content = document.getElementById('page-content');
|
||||
const stats = id === 'providers' ? `<section class="stat-strip" aria-label="生成配置摘要"><div class="stat"><span>上游服务商</span><strong>2</strong></div><div class="stat"><span>启用模型</span><strong>3</strong></div><div class="stat"><span>启用路由</span><strong>2</strong></div><div class="stat"><span>健康异常</span><strong>0</strong></div></section>` : '';
|
||||
content.innerHTML = `<header class="page-heading"><div><h1>${title}${readonly ? '<span class="badge readonly"><i data-lucide="shield-check"></i>只读</span>' : ''}</h1><p>${description}</p></div>${readonly ? '' : '<button class="button primary" type="button"><i data-lucide="plus"></i>新增</button>'}</header>${stats}<section class="panel"><div class="panel-head"><div><h2>${title}列表</h2><span>原型使用构造数据</span></div></div><div class="toolbar"><label class="sr-only" for="page-search">搜索</label><input class="search" id="page-search" type="search" placeholder="输入关键词"><button class="button" type="button"><i data-lucide="refresh-cw"></i>重置</button><span class="spacer"></span><span class="result-count">共 2 条</span></div>${representativeContent(id)}</section>`;
|
||||
content.focus();
|
||||
icons();
|
||||
}
|
||||
function icons() { window.renderChorusIcons?.(); }
|
||||
function openSidebar() { document.body.classList.add('sidebar-open'); document.getElementById('sidebar-toggle').setAttribute('aria-expanded', 'true'); }
|
||||
function closeSidebar() { document.body.classList.remove('sidebar-open'); document.getElementById('sidebar-toggle').setAttribute('aria-expanded', 'false'); }
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
renderNav();
|
||||
renderPage('providers');
|
||||
document.getElementById('sidebar-toggle').addEventListener('click', () => document.body.classList.contains('sidebar-open') ? closeSidebar() : openSidebar());
|
||||
document.getElementById('mobile-scrim').addEventListener('click', closeSidebar);
|
||||
document.addEventListener('keydown', event => { if (event.key === 'Escape') closeSidebar(); });
|
||||
window.addEventListener('resize', () => { if (!window.matchMedia('(max-width: 720px)').matches) closeSidebar(); });
|
||||
icons();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user