nx-sk 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +18 -2
- package/assets/nx-sk/SKILL.md +0 -1
- package/assets/nx-sk/references/00-design.md +1 -1
- package/assets/nx-sk/references/10-sections-entries.md +1 -1
- package/assets/nx-sk/references/30-export-backup.md +16 -0
- package/package.json +1 -1
- package/src/modules/backup/index.js +41 -0
- package/src/modules/backup/service.js +188 -0
- package/src/modules/backup/view.jsx +145 -0
- package/src/runtime/registry.js +2 -1
- package/src/web/frontend/App.jsx +1 -1
- package/src/web/frontend/index.html +4 -0
- package/src/web/frontend/public/favicon-16.png +0 -0
- package/src/web/frontend/public/favicon-32.png +0 -0
- package/src/web/frontend/public/favicon-48.png +0 -0
- package/src/web/frontend/public/favicon.ico +0 -0
- package/src/web/frontend/public/logo-rounded.png +0 -0
- package/src/web/frontend/public/logo.png +0 -0
- package/src/web/frontend/registry.js +1 -0
- package/src/web/frontend/styles.css +2 -1
- package/src/web/public/assets/{index-BnVuH1lC.js → index-Bbe3iqam.js} +7 -7
- package/src/web/public/assets/index-C1bxqdjl.css +1 -0
- package/src/web/public/assets/{view-DsyplQBh.js → view-Bnwx5y_O.js} +1 -1
- package/src/web/public/assets/{view-rvADq3vX.js → view-CekTHUPO.js} +1 -1
- package/src/web/public/assets/{view-BvyRxTgX.js → view-Djo-TKw9.js} +1 -1
- package/src/web/public/assets/{view-CDje4hqZ.js → view-DuKJm1oq.js} +1 -1
- package/src/web/public/assets/view-b8TAoyhU.js +3 -0
- package/src/web/public/favicon-16.png +0 -0
- package/src/web/public/favicon-32.png +0 -0
- package/src/web/public/favicon-48.png +0 -0
- package/src/web/public/favicon.ico +0 -0
- package/src/web/public/index.html +6 -2
- package/src/web/public/logo-rounded.png +0 -0
- package/src/web/public/logo.png +0 -0
- package/assets/nx-sk/references/40-extend.md +0 -75
- package/src/web/public/assets/index-CwF5hM2v.css +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--ink: #14161a;--paper: #fff;--soft: #f5f6f7;--soft-2: #ebedf0;--mid: #8b9096;--danger: #b3261e;--border: 1px solid var(--soft-2);--row-h: 28px;--btn-h: 32px;--fs-core: 12px;--radius-btn: 8px;--shadow-relief: -1px -1px 2px rgba(255, 255, 255, .85), 2px 2px 6px rgba(20, 22, 26, .07);--shadow-relief-press: inset 1px 1px 2px rgba(20, 22, 26, .1), inset -1px -1px 2px rgba(255, 255, 255, .7);--shadow-relief-inset: inset 1px 1px 2px rgba(20, 22, 26, .06), inset -1px -1px 2px rgba(255, 255, 255, .9)}*{box-sizing:border-box}html,body,#root{height:100%}body{margin:0;background:var(--paper);color:var(--ink);font-family:system-ui,-apple-system,Segoe UI,Microsoft YaHei,sans-serif;font-size:var(--fs-core);line-height:1.5;-webkit-font-smoothing:antialiased}.mono,code,.path,.cli-cmd{font-family:ui-monospace,Consolas,Courier New,monospace}.muted{color:var(--mid)}.bad{color:var(--danger)}.nowrap{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pad{padding:10px 14px}.rowgap{display:flex;flex-wrap:wrap;gap:8px;align-items:center}.grow{flex:1;min-width:0}.right{margin-left:auto}.app{display:flex;flex-direction:column;height:100%;min-height:0}header.top{display:flex;flex-wrap:wrap;align-items:center;gap:10px;padding:6px 14px;border-bottom:var(--border);flex-shrink:0}header.top .brand{font-weight:700;letter-spacing:.3px;display:inline-flex;align-items:center;gap:7px}header.top .brand img{width:20px;height:20px;border-radius:5px;display:block}header.top .meta{color:var(--mid);font-size:11px}nav.tabs{display:flex;flex-wrap:wrap;gap:6px;padding:6px 14px;border-bottom:var(--border);flex-shrink:0}main.body{flex:1;min-height:0;overflow:auto}.tab{font:inherit;font-size:var(--fs-core);height:var(--btn-h);padding:0 12px;border:0;border-radius:var(--radius-btn);background:var(--soft);color:var(--ink);cursor:pointer;display:inline-flex;align-items:center;gap:6px}.tab .count{color:var(--mid);font-size:11px}.tab.active{background:var(--paper);box-shadow:var(--shadow-relief);font-weight:600}.btn{font:inherit;font-size:var(--fs-core);cursor:pointer;height:var(--btn-h);padding:0 12px;border:0;border-radius:var(--radius-btn);background:var(--ink);color:var(--paper);box-shadow:var(--shadow-relief);display:inline-flex;align-items:center;gap:4px;transition:box-shadow .12s,transform .08s}.btn:hover:not(:disabled){box-shadow:var(--shadow-relief),0 0 0 3px #14161a0d}.btn:active:not(:disabled){box-shadow:var(--shadow-relief-press);transform:translateY(1px)}.btn:disabled{background:var(--soft);color:var(--mid);box-shadow:var(--shadow-relief-inset);cursor:not-allowed}.btn.ghost{background:var(--soft);color:var(--ink)}.btn.danger{background:var(--danger);color:#fff}.btn.small{height:22px;padding:0 8px;font-size:11px;border-radius:6px}input,select,textarea{font:inherit;font-size:var(--fs-core);color:var(--ink);background:var(--paper);border:var(--border);border-radius:6px;padding:0 8px}input,select{height:var(--row-h)}textarea{padding:5px 8px;resize:vertical;min-height:56px;line-height:1.5}input:focus,select:focus,textarea:focus{outline:none;border-color:var(--soft-2);box-shadow:var(--shadow-relief-inset)}label.inline{display:inline-flex;align-items:center;gap:6px}.card{border-bottom:var(--border)}.card:last-child{border-bottom:0}.card>.colhead{display:flex;align-items:center;gap:8px;height:var(--row-h);padding:0 14px;border-bottom:var(--border);font-size:13px;font-weight:600}.card>.colbody{padding:0}.row{display:flex;align-items:center;gap:8px;min-height:var(--row-h);padding:0 14px;border-bottom:var(--border);font-size:var(--fs-core);transition:background .12s}.row:hover{background:var(--soft)}.row:last-child{border-bottom:0}.row .name{font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.row .desc{color:var(--mid);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.row .acts{margin-left:auto;display:flex;align-items:center;gap:6px;flex-shrink:0}.row.selectable{cursor:pointer}.row.selected{background:var(--soft);box-shadow:inset 2px 0 0 var(--ink)}.tag{display:inline-flex;align-items:center;height:22px;padding:0 7px;border-radius:999px;background:var(--soft);color:var(--mid);font-size:11px;white-space:nowrap}.tag.strong{background:var(--ink);color:var(--paper)}.tag.bad{background:#fbeae9;color:var(--danger)}.copyable{cursor:copy;border-radius:4px;padding:0 3px}.copyable:hover{background:var(--soft)}.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:0 14px}.field{border-bottom:var(--border);padding:5px 0;min-width:0}.field>.flabel{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--mid)}.field>.flabel .fkey{font-family:ui-monospace,Consolas,monospace;font-size:10px;color:var(--soft-2)}.field>.fctrl{margin-top:3px}.field input,.field select,.field textarea{width:100%}.field.wide{grid-column:1 / -1}.group-title{display:flex;align-items:center;gap:8px;height:var(--row-h);padding:0 14px;border-bottom:var(--border);background:var(--soft);font-size:11px;color:var(--mid);letter-spacing:.5px}.cli-hint{display:flex;flex-wrap:wrap;align-items:center;gap:2px;padding:6px 14px;color:var(--mid);font-size:11px;border-top:var(--border)}.cli-hint-label{margin-right:4px}.cli-hint-sep{color:var(--soft-2)}.cli-cmd{color:var(--ink)}.toasts{position:fixed;right:14px;bottom:14px;display:flex;flex-direction:column;gap:6px;z-index:40}.toast{background:var(--ink);color:var(--paper);padding:7px 12px;border-radius:8px;font-size:var(--fs-core);box-shadow:var(--shadow-relief);max-width:420px}.toast.bad{background:var(--danger)}.mask{position:fixed;top:0;right:0;bottom:0;left:0;background:#14161a47;display:flex;align-items:center;justify-content:center;z-index:50;padding:20px}.dialog{background:var(--paper);border-radius:8px;min-width:320px;max-width:min(880px,92vw);max-height:86vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 10px 40px #14161a2e}.dialog-title{display:flex;align-items:center;gap:8px;height:var(--row-h);padding:0 14px;border-bottom:var(--border);font-size:13px;font-weight:600;flex-shrink:0}.dialog-body{padding:10px 14px;overflow:auto;min-height:0}.dialog-foot{display:flex;gap:8px;justify-content:flex-end;padding:8px 14px;border-top:var(--border);flex-shrink:0}pre.block{margin:0;padding:10px 14px;background:var(--soft);border-radius:8px;font-family:ui-monospace,Consolas,monospace;font-size:11px;line-height:1.55;white-space:pre-wrap;word-break:break-word;max-height:60vh;overflow:auto}.hint{color:var(--mid);font-size:11px}.stack{display:flex;flex-direction:column;gap:10px}.toolbar{display:flex;flex-wrap:wrap;gap:8px;align-items:center}.cols{display:flex;gap:14px;align-items:flex-start;padding:0 14px 14px}.col{flex:1;min-width:0}.desc{color:var(--mid)}.cols>.col>.card{border:var(--border);border-radius:4px}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{j as e,u as O,r as m,a as N,b as q,E as F,C as L,T as _,M as V}from"./index-
|
|
1
|
+
import{j as e,u as O,r as m,a as N,b as q,E as F,C as L,T as _,M as V}from"./index-Bbe3iqam.js";function Y({def:o,value:v,onChange:u,disabled:p}){const l=o.type||"text",r=v??"";if(l==="bool"){const s=r===!0||r==="true"||r==="是";return e.jsxs("label",{className:"inline",children:[e.jsx("input",{type:"checkbox",checked:s,onChange:d=>u(d.target.checked)}),e.jsx("span",{className:"muted",children:s?"是":"否"})]})}if(l==="select"){const s=o.options||[],d=r!==""&&!s.includes(r);return e.jsxs("select",{value:d?"__custom":String(r),onChange:h=>u(h.target.value==="__custom"?"":h.target.value),children:[e.jsx("option",{value:"",children:"(未填写)"}),s.map(h=>e.jsx("option",{value:h,children:h},h)),d?e.jsxs("option",{value:"__custom",children:[String(r),"(当前值,不在候选里)"]}):null]})}if(l==="textarea")return e.jsx("textarea",{value:String(r),onChange:s=>u(s.target.value),rows:3});if(l==="tags"){const s=Array.isArray(r)?r.join("、"):String(r);return e.jsx("input",{value:s,placeholder:o.maxItems?`多个用「、」分隔,最多 ${o.maxItems} 个`:"多个用「、」分隔",onChange:d=>u(d.target.value.split(/[,,、;;]+/).map(h=>h.trim()).filter(Boolean))})}const f=l==="number"?"number":l==="date"?"date":l==="month"?"month":"text";return l==="secret"?e.jsx("input",{type:"text",value:String(r),placeholder:"密文落盘;原样保留占位则视为不修改",disabled:p,onChange:s=>u(s.target.value)}):e.jsx("input",{type:f,value:String(r),onChange:s=>u(s.target.value)})}function z({title:o,fields:v,draft:u,onPatch:p}){return e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"group-title",children:o}),e.jsx("div",{className:"pad",children:e.jsx("div",{className:"grid",children:v.map(l=>e.jsxs("div",{className:`field ${l.type==="textarea"?"wide":""}`,children:[e.jsxs("div",{className:"flabel",children:[e.jsx("span",{children:l.label}),l.sensitive?e.jsx("span",{className:"tag bad",children:"密文"}):null,l.fill&&l.fill!=="normal"?e.jsx("span",{className:"tag",children:l.fillLabel||l.fill}):null,e.jsx("span",{className:"fkey",children:l.key})]}),e.jsx("div",{className:"fctrl",children:e.jsx(Y,{def:l,value:u[l.key],onChange:r=>p(l.key,r)})})]},l.key))})})]})}function G({sectionId:o,sectionTitle:v}){const{ui:u,patchUi:p,toast:l,guard:r,dialog:f,reload:s}=O(),[d,h]=m.useState(null),[E,U]=m.useState({}),[C,i]=m.useState({name:"",value:""}),k=!!u.mask,j=m.useCallback(async()=>{try{const n=`section=${encodeURIComponent(o)}${k?"&mask=1":""}`,a=await N(`/api/keys?${n}`);h(a);const x={};for(const w of a.keys)x[w.name]=w.value??"";U(x)}catch(n){l(n.message,"bad"),h(null)}},[o,k,l]);m.useEffect(()=>{j()},[j]);const A=r(async n=>{const a=E[n.name];(n.value??"")!==a&&(await N(`/api/keys/${encodeURIComponent(n.name)}`,{method:"PUT",body:{value:a,section:o}}),l(`已保存 ${n.name}`),await j())}),$=r(async(n,a)=>{const x=String(a||"").trim();if(!(!x||x===n.name)){if(d.keys.some(w=>w.name===x)){l(`已经有叫「${x}」的键了`,"bad"),await j();return}await N(`/api/entries/${encodeURIComponent(n.id)}`,{method:"PATCH",body:{title:x}}),U(w=>{const y={};for(const[D,K]of Object.entries(w))y[D===n.name?x:D]=K;return y}),l(`已重命名为 ${x}`),await j(),await s()}}),S=r(async()=>{const n=C.name.trim();if(!n){l("先填键名,例如 OPENAI_KEY","bad");return}if(d.keys.some(a=>a.name===n)){l(`已经有叫「${n}」的键了——直接在上面那行改值即可`,"bad");return}await N(`/api/keys/${encodeURIComponent(n)}`,{method:"PUT",body:{value:C.value,section:o}}),i({name:"",value:""}),l(`已新增 ${n}`),await j(),await s()}),T=r(async n=>{await f({message:`删除「${n.name}」?删除前会自动留一份整库快照在 ~/nx-sk/backup。`,danger:!0,confirmText:"删除"})&&(await N(`/api/keys/${encodeURIComponent(n.name)}`,{method:"DELETE",body:{section:o}}),l(`已删除 ${n.name}`),await j(),await s())});return d?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"toolbar rowgap pad",children:[e.jsx("strong",{children:v}),e.jsxs("span",{className:"muted",children:[d.count," 个键 · 名称 → 值"]}),e.jsx("span",{className:"grow"}),e.jsxs("label",{className:"inline hint",children:[e.jsx("input",{type:"checkbox",checked:k,onChange:()=>p({mask:!k})}),"打码显示(投屏/截图时用)"]}),e.jsx("button",{className:"btn small ghost",onClick:j,children:"刷新"})]}),e.jsxs("div",{className:"card",children:[e.jsxs("div",{className:"colhead",children:[e.jsx("span",{style:{width:200},children:"名称"}),e.jsx("span",{className:"grow",children:"值"})]}),e.jsxs("div",{className:"colbody",children:[d.keys.length?d.keys.map(n=>e.jsxs("div",{className:"row",children:[e.jsx("input",{className:"mono",style:{width:200},defaultValue:n.name,title:"改完按回车或点开别处即重命名",onBlur:a=>$(n,a.target.value),onKeyDown:a=>{a.key==="Enter"&&a.currentTarget.blur()}}),e.jsx("input",{className:"mono grow",value:E[n.name]??"",onChange:a=>U(x=>({...x,[n.name]:a.target.value})),onBlur:()=>A(n),onKeyDown:a=>{a.key==="Enter"&&a.currentTarget.blur()}}),e.jsxs("span",{className:"acts",children:[e.jsx(q,{className:"tag",text:E[n.name]??"",title:"点击复制值",children:"复制"}),e.jsx("button",{className:"btn small ghost",onClick:()=>T(n),children:"删除"})]})]},n.id)):e.jsx(F,{children:"还没有密钥。在下面一行填「名称 / 值」按添加,或 CLI:nx-sk key set OPENAI_KEY sk-xxx"}),e.jsxs("div",{className:"row",children:[e.jsx("input",{className:"mono",style:{width:200},placeholder:"新键名,例如 OPENAI_KEY",value:C.name,onChange:n=>i(a=>({...a,name:n.target.value})),onKeyDown:n=>{n.key==="Enter"&&S()}}),e.jsx("input",{className:"mono grow",placeholder:"值(留空也行,之后再填)",value:C.value,onChange:n=>i(a=>({...a,value:n.target.value})),onKeyDown:n=>{n.key==="Enter"&&S()}}),e.jsx("span",{className:"acts",children:e.jsx("button",{className:"btn small",onClick:S,children:"添加"})})]})]})]}),e.jsx("div",{className:"pad hint",children:"改值:直接在「值」列改完按回车(或点开别处)即保存。改键名同理。 值以密文落盘,这里显示的是原文;要打码勾上方那个框。删除前自动整库快照。"}),e.jsx(L,{module:"entries"})]}):e.jsx("div",{className:"pad muted",children:"加载中…"})}const I=o=>o==null?"":Array.isArray(o)?o.join("、"):String(o);function W({sectionId:o}){var R;const{ui:v,patchUi:u,toast:p,guard:l,dialog:r,reload:f}=O(),[s,d]=m.useState(null),[h,E]=m.useState(null),[U,C]=m.useState(!1),[i,k]=m.useState(null),[j,A]=m.useState(null),[$,S]=m.useState(""),T=!!v.mask,n=m.useCallback(async t=>{C(!0),E(null);try{const c=await N(`/api/sections/${encodeURIComponent(o)}/dump${T?"?mask=1":""}`);d(c);const b=t||v.entry,g=c.entries.find(P=>P.id===b)||c.entries[0]||null;k(g?{id:g.id,title:g.title,values:{...g.values}}:null),g&&g.id!==v.entry&&u({entry:g.id})}catch(c){E(c.message),d(null),k(null)}finally{C(!1)}},[o,T]);m.useEffect(()=>{n()},[n]);const a=m.useMemo(()=>(s==null?void 0:s.entries.find(t=>t.id===(i==null?void 0:i.id)))||null,[s,i]),x=m.useMemo(()=>s?s.groups.map(t=>({...t,fields:s.fields.filter(c=>c.group===t.id)})).filter(t=>t.fields.length):[],[s]),w=(t,c)=>k(b=>b&&{...b,values:{...b.values,[t]:c}}),y=m.useMemo(()=>{if(!i||!a)return{set:{},titleChanged:!1,count:0};const t={};for(const c of s.fields)I(a.values[c.key])!==I(i.values[c.key])&&(t[c.key]=i.values[c.key]);return{set:t,titleChanged:i.title!==a.title,count:Object.keys(t).length+(i.title!==a.title?1:0)}},[i,a,s]),D=l(async()=>{if(!i||!a)return;if(!y.count){p("没有改动");return}const t={set:y.set};y.titleChanged&&(t.title=i.title),await N(`/api/entries/${encodeURIComponent(i.id)}`,{method:"PATCH",body:t}),p(`已保存 ${y.count} 处改动`),await n(i.id),await f()}),K=l(async()=>{var P;if(!i){p("先新建一条条目","bad");return}const t=$.indexOf("=");if(t<=0){p("用 字段=值 的格式,例如 微信=zhangsan","bad");return}const c=$.slice(0,t).trim(),b=$.slice(t+1),g=await N(`/api/entries/${encodeURIComponent(i.id)}`,{method:"PATCH",body:{set:{[c]:b}}});S(""),p(`已写入 ${c}${(P=g.newFields)!=null&&P.length?"(新字段已登记)":""}`),await n(i.id),await f()}),B=l(async()=>{const t=(s==null?void 0:s.section.titleLabel)||"名称",c=await r({message:`新建「${s==null?void 0:s.section.title}」条目,${t}是?`,input:{placeholder:`例如 ${(s==null?void 0:s.section.titleField)||"名称"}`}});if(!c)return;const b={section:o,title:c};s!=null&&s.section.titleField&&(b.set={[s.section.titleField]:c});const g=await N("/api/entries",{method:"POST",body:b});p(`已新建:${g.title}`),await n(g.id),await f()}),M=l(async()=>{!a||!await r({message:`删除条目「${a.title}」?删除前会自动留一份快照在 ~/nx-sk/backup。`,danger:!0,confirmText:"删除"})||(await N(`/api/entries/${encodeURIComponent(a.id)}`,{method:"DELETE"}),p("已删除"),await n(""),await f())}),H=l(async()=>{const t=await N("/api/export",{method:"POST",body:{section:o,format:"both"}});A({title:"导出结果",body:e.jsx("pre",{className:"block",children:`目录: ${t.dir}
|
|
2
2
|
格式: ${t.format}
|
|
3
3
|
条目: ${t.entries}
|
|
4
4
|
凭据: ${t.withSecrets?"含明文":"已打码"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{u as b,r as d,a as k,j as s,b as r,E as F,C as I,T as $}from"./index-
|
|
1
|
+
import{u as b,r as d,a as k,j as s,b as r,E as F,C as I,T as $}from"./index-Bbe3iqam.js";function D(){var N,v,u,g,f,w,y,S;const{boot:e,reload:t,toast:n,guard:C}=b(),[o,h]=d.useState(null),[l,i]=d.useState(null),j=typeof window>"u"?"":window.location.origin,p=typeof window>"u"?"":window.location.port||"80",m=d.useCallback(async()=>{try{const a=await k("/api/settings");h(a),i({...a.settings})}catch(a){n(a.message,"bad")}},[n]);d.useEffect(()=>{m()},[m]);const E=C(async()=>{const a={exportDir:l.exportDir,exportFormat:l.exportFormat,includeSecretsInExport:!!l.includeSecretsInExport,defaultSection:l.defaultSection},x=await k("/api/settings",{method:"PATCH",body:a});n("设置已保存"),h({...o,settings:x.set}),await t()});return!o||!l?s.jsx("div",{className:"pad muted",children:"加载中…"}):s.jsxs(s.Fragment,{children:[s.jsxs("div",{className:"toolbar rowgap pad",children:[s.jsx("strong",{children:"设置"}),s.jsx("span",{className:"muted",children:"本机配置与运行状态"}),s.jsx("span",{className:"grow"}),s.jsx("button",{className:"btn small ghost",onClick:m,children:"重新读取"})]}),s.jsxs("div",{className:"card",children:[s.jsx("div",{className:"colhead",children:"存储与运行"}),s.jsxs("div",{className:"colbody",children:[s.jsx(c,{label:"数据目录",value:e==null?void 0:e.home,copy:e==null?void 0:e.home}),s.jsx(c,{label:"store.json",value:e==null?void 0:e.storePath,copy:e==null?void 0:e.storePathRaw}),s.jsx(c,{label:"栏目 / 条目",value:e?`${e.counts.sections} / ${e.counts.entries}`:""}),s.jsx(c,{label:"命令条数",value:e?String(e.counts.commands):""}),s.jsx(c,{label:"app / node",value:e?`v${e.app.version} · node ${e.app.node} · ${e.app.platform}`:""}),s.jsx(c,{label:"环境变量覆盖",value:e?Object.entries(e.envOverrides).filter(([,a])=>a).map(([a,x])=>`${a}=${x}`).join(" ")||"(无)":""})]})]}),s.jsxs("div",{className:"card",children:[s.jsx("div",{className:"colhead",children:"密钥(密文落盘)"}),s.jsxs("div",{className:"colbody",children:[s.jsx(c,{label:"密钥来源",value:((N=e==null?void 0:e.vault)==null?void 0:N.source)==="env"?"环境变量 NX_SK_PASSPHRASE(口令不落盘)":"本机密钥文件"}),s.jsx(c,{label:"密钥文件",value:((v=e==null?void 0:e.vault)==null?void 0:v.keyFile)||((u=e==null?void 0:e.vault)==null?void 0:u.saltFile)||"",copy:((g=e==null?void 0:e.vault)==null?void 0:g.keyFile)||((f=e==null?void 0:e.vault)==null?void 0:f.saltFile)}),s.jsx("div",{className:"row",children:s.jsx("span",{className:"desc",children:(w=e==null?void 0:e.vault)==null?void 0:w.note})}),s.jsx("div",{className:"row",children:s.jsx("span",{className:"desc bad",children:"删掉密钥文件或换掉口令后,已存的密文都解不开了——导出前先确认密钥可用。"})})]})]}),s.jsxs("div",{className:"card",children:[s.jsx("div",{className:"colhead",children:"面板与 CLI"}),s.jsxs("div",{className:"colbody",children:[s.jsxs("div",{className:"row",children:[s.jsx("span",{className:"name",children:"面板地址"}),s.jsx("span",{className:"desc mono",children:j}),s.jsx("span",{className:"acts",children:s.jsx(r,{className:"mono",text:j,children:"复制"})})]}),s.jsxs("div",{className:"row",children:[s.jsx("span",{className:"name",children:"重启面板"}),s.jsx("span",{className:"desc",children:"面板本身就是 serve 提供的;改代码后重启它即可"}),s.jsx("span",{className:"acts",children:s.jsx(r,{className:"mono",text:`nx-sk serve --port ${p}`,children:`nx-sk serve --port ${p}`})})]})]})]}),s.jsxs("div",{className:"card",children:[s.jsx("div",{className:"colhead",children:"默认行为"}),s.jsxs("div",{className:"pad",children:[s.jsxs("div",{className:"grid",children:[s.jsxs("div",{className:"field",children:[s.jsx("div",{className:"flabel",children:s.jsx("span",{children:"默认导出格式"})}),s.jsx("div",{className:"fctrl",children:s.jsxs("select",{value:l.exportFormat,onChange:a=>i({...l,exportFormat:a.target.value}),children:[s.jsx("option",{value:"both",children:"JSON + Markdown"}),s.jsx("option",{value:"json",children:"仅 JSON"}),s.jsx("option",{value:"md",children:"仅 Markdown"})]})})]}),s.jsxs("div",{className:"field",children:[s.jsxs("div",{className:"flabel",children:[s.jsx("span",{children:"默认导出目录"}),s.jsx("span",{className:"fkey",children:"留空 = ~/nx-sk/export"})]}),s.jsx("div",{className:"fctrl",children:s.jsx("input",{value:l.exportDir,onChange:a=>i({...l,exportDir:a.target.value})})})]}),s.jsxs("div",{className:"field",children:[s.jsxs("div",{className:"flabel",children:[s.jsx("span",{children:"默认栏目"}),s.jsx("span",{className:"fkey",children:"CLI 未指定 --section 时的首选"})]}),s.jsx("div",{className:"fctrl",children:s.jsx("input",{value:l.defaultSection,onChange:a=>i({...l,defaultSection:a.target.value})})})]}),s.jsxs("div",{className:"field",children:[s.jsxs("div",{className:"flabel",children:[s.jsx("span",{children:"导出默认带明文密钥"}),s.jsx("span",{className:"fkey",children:"默认开;要分享出去时关掉"})]}),s.jsx("div",{className:"fctrl",children:s.jsxs("label",{className:"inline",children:[s.jsx("input",{type:"checkbox",checked:!!l.includeSecretsInExport,onChange:a=>i({...l,includeSecretsInExport:a.target.checked})}),s.jsx("span",{className:l.includeSecretsInExport?"muted":"bad",children:l.includeSecretsInExport?"开(导出文件含真实凭据,注意别外发)":"关(凭据打码后再导出)"})]})})]})]}),s.jsx("div",{className:"rowgap",style:{marginTop:8},children:s.jsx("button",{className:"btn",onClick:E,children:"保存设置"})})]})]}),s.jsxs("div",{className:"card",children:[s.jsxs("div",{className:"colhead",children:["自动快照",s.jsx("span",{className:"right muted",children:((y=e==null?void 0:e.snapshots)==null?void 0:y.length)??0})]}),s.jsxs("div",{className:"colbody",children:[s.jsx("div",{className:"row",children:s.jsx("span",{className:"desc",children:"每次删除条目 / 栏目、修改设置前,都会在 ~/nx-sk/backup 留一份可回滚的整库快照。"})}),(S=e==null?void 0:e.snapshots)!=null&&S.length?e.snapshots.map(a=>s.jsxs("div",{className:"row",children:[s.jsx("span",{className:"mono name",children:a.name}),s.jsx("span",{className:"desc",children:a.at}),s.jsx("span",{className:"acts",children:s.jsxs("span",{className:"tag",children:[Math.ceil(a.bytes/1024)," KB"]})})]},a.name)):s.jsx(F,{children:"还没有快照。"})]})]}),e!=null&&e.migration?s.jsxs("div",{className:"card",children:[s.jsx("div",{className:"colhead",children:"数据迁移"}),s.jsxs("div",{className:"colbody",children:[s.jsx("div",{className:"row",children:s.jsxs("span",{className:"desc",children:["store 已从 v",e.migration.from," 迁到 v",e.migration.to,":密钥栏目字段 ",e.migration.fieldsBefore," → ",e.migration.fieldsAfter,",",e.migration.entries," 条条目"]})}),s.jsxs("div",{className:"row",children:[s.jsx("span",{className:"desc",children:"迁移前的原始文件"}),s.jsx("span",{className:"acts",children:s.jsx(r,{className:"mono",text:e.migration.snapshotRaw||"",children:e.migration.snapshot})})]}),e.migration.legacyValuesKept?s.jsx("div",{className:"row",children:s.jsxs("span",{className:"desc",children:["另保留了 ",e.migration.legacyValuesKept," 个旧字段的值(不再显示,但没删)"]})}):null]})]}):null,e!=null&&e.corrupt?s.jsxs("div",{className:"card",children:[s.jsx("div",{className:"colhead",children:"上次读取异常"}),s.jsx("div",{className:"colbody",children:s.jsx("div",{className:"row",children:s.jsxs("span",{className:"desc bad",children:["store.json 解析失败,已挪到 ",e.corrupt.to,"(原文件没有被覆盖)"]})})})]}):null,s.jsx(I,{module:"settings"})]})}function c({label:e,value:t,copy:n}){return s.jsxs("div",{className:"row",children:[s.jsx("span",{className:"name",children:e}),s.jsx("span",{className:"desc mono",children:t||"(空)"}),n?s.jsx("span",{className:"acts",children:s.jsx(r,{className:"mono",text:n,children:"复制"})}):s.jsx($,{children:" "})]})}export{D as default};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{u as C,r as i,a as h,j as s,b as f,T as j,E as b,C as y,M as E}from"./index-
|
|
1
|
+
import{u as C,r as i,a as h,j as s,b as f,T as j,E as b,C as y,M as E}from"./index-Bbe3iqam.js";function W(){var u,N,v;const{boot:r,toast:d,guard:w}=C(),[g,S]=i.useState([]),[a,k]=i.useState(null),[c,n]=i.useState({format:"both",section:"",out:"",withSecrets:!1}),[o,m]=i.useState(null),x=i.useCallback(async()=>{try{const[e,l]=await Promise.all([h("/api/sections"),h("/api/exports")]);S(e.sections),k(l)}catch(e){d(e.message,"bad")}},[d]);i.useEffect(()=>{x()},[x]),i.useEffect(()=>{const e=r==null?void 0:r.settings;e&&n(l=>({...l,format:e.exportFormat||"both",withSecrets:!!e.includeSecretsInExport,out:e.exportDir||""}))},[r]);const $=()=>({format:c.format,out:c.out.trim()||void 0,section:c.section||void 0,...c.withSecrets?{"with-secrets":!0}:{"no-secrets":!0}}),p=w(async e=>{const l=await h("/api/export",{method:"POST",body:{...$(),"dry-run":!!e}});if(e){m({title:"试运行(没有写盘)",body:s.jsx("pre",{className:"block",children:`目录: ${l.wouldWrite.dir}
|
|
2
2
|
栏目: ${l.wouldWrite.sections} 条目: ${l.wouldWrite.entries}
|
|
3
3
|
凭据: ${l.wouldWrite.withSecrets?"含明文":"已打码"}
|
|
4
4
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{u as L,r as x,a as h,j as e,T as v,E as S,C as F,M as R}from"./index-BnVuH1lC.js";const T=["text","textarea","number","date","month","bool","select","tags","secret"];function A(){const{reload:c,toast:n,guard:m,dialog:j}=L(),[i,b]=x.useState(null),[d,p]=x.useState([]),[r,u]=x.useState(null),[o,a]=x.useState(null),t=x.useCallback(async()=>{try{const[s,l]=await Promise.all([h("/api/sections"),h("/api/sections/templates")]);b(s),p(l.templates),u(null)}catch(s){u(s.message)}},[]);x.useEffect(()=>{t()},[t]);const f=m(async s=>{const l=await h(`/api/sections/${encodeURIComponent(s.id)}/dump`);a({kind:"dump",title:`${s.title}(${s.id})的全部信息`,body:e.jsx("pre",{className:"block",children:JSON.stringify(l,null,2)})})}),g=m(async s=>{const l=await h(`/api/sections/${encodeURIComponent(s.id)}`);a({kind:"edit",id:s.id,form:{title:l.title,description:l.description,order:String(l.order),titleLabel:l.titleLabel,titleField:l.titleField||""},full:l})}),y=m(async s=>{const l={id:s.id.trim(),title:s.title.trim()||s.id.trim(),template:s.template||void 0,description:s.description,order:Number(s.order)||50};await h("/api/sections",{method:"POST",body:l}),n("栏目已创建"),a(null),await t(),await c()}),k=m(async s=>{const l={title:s.form.title,description:s.form.description,order:Number(s.form.order)||50,titleLabel:s.form.titleLabel,titleField:s.form.titleField};await h(`/api/sections/${encodeURIComponent(s.id)}`,{method:"PATCH",body:l}),n("栏目已更新");const N=await h(`/api/sections/${encodeURIComponent(s.id)}`);a({...s,full:N}),await t(),await c()}),C=m(async(s,l)=>{await h(`/api/sections/${encodeURIComponent(s.id)}`,{method:"PATCH",body:{"add-field":l}}),n(`已新增字段 ${l.key}`);const N=await h(`/api/sections/${encodeURIComponent(s.id)}`);a({...s,full:N}),await t()}),w=m(async(s,l)=>{if(!await j({message:`从字段字典里删掉「${l}」?已填的值会留在条目里(只是不再显示/校验)。`,danger:!0,confirmText:"删除字段"}))return;await h(`/api/sections/${encodeURIComponent(s.id)}`,{method:"PATCH",body:{"remove-field":l}}),n(`已删字段 ${l}`);const E=await h(`/api/sections/${encodeURIComponent(s.id)}`);a({...s,full:E}),await t()}),$=m(async s=>{await j({message:`删除栏目「${s.title}」,连带其中 ${s.entries} 条条目?删除前会自动留快照。`,danger:!0,confirmText:"连带删除"})&&(await h(`/api/sections/${encodeURIComponent(s.id)}`,{method:"DELETE",body:{force:!0}}),n("栏目已删除"),await t(),await c())});return r?e.jsxs("div",{className:"pad stack",children:[e.jsx("div",{className:"bad",children:e.jsx("strong",{children:"读不到栏目"})}),e.jsx("pre",{className:"block",children:r})]}):i?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"toolbar rowgap pad",children:[e.jsx("strong",{children:"栏目"}),e.jsxs("span",{className:"muted",children:["共 ",i.count," 个 · 条目 ",i.totalEntries," 条"]}),e.jsx("span",{className:"grow"}),e.jsx("button",{className:"btn small ghost",onClick:t,children:"刷新"}),e.jsx("button",{className:"btn small",onClick:()=>{var s;return a({kind:"create",form:{id:"",title:"",template:((s=d[0])==null?void 0:s.id)||"",description:"",order:"50"}})},children:"新建栏目"})]}),e.jsxs("div",{className:"card",children:[e.jsx("div",{className:"colhead",children:"全部栏目"}),e.jsx("div",{className:"colbody",children:i.sections.length?i.sections.map(s=>e.jsxs("div",{className:"row",children:[e.jsx("span",{className:"name",children:s.title}),e.jsx("span",{className:"mono muted",children:s.id}),s.template?e.jsx(v,{children:s.template}):e.jsx(v,{children:"自定义"}),e.jsx("span",{className:"desc",children:s.description}),e.jsxs("span",{className:"acts",children:[e.jsxs("span",{className:"tag",children:[s.fields," 字段"]}),e.jsxs("span",{className:"tag",children:[s.entries," 条目"]}),e.jsx("button",{className:"btn small ghost",onClick:()=>f(s),children:"全部信息"}),e.jsx("button",{className:"btn small ghost",onClick:()=>g(s),children:"编辑"}),e.jsx("button",{className:"btn small ghost",onClick:()=>$(s),children:"删除"})]})]},s.id)):e.jsx(S,{children:"还没有栏目。点「新建栏目」,用内置模板(求职 / 密钥)起步。"})})]}),e.jsx("div",{className:"pad hint",children:"栏目 = 一组同构条目 + 一份字段字典。字典是数据,所以这里加一个字段,CLI 与面板立刻就都能填它。"}),e.jsx(F,{module:"sections"}),o?e.jsxs(R,{title:I(o),onClose:()=>a(null),children:[o.kind==="dump"?o.body:null,o.kind==="create"?e.jsx(U,{templates:d,form:o.form,onChange:s=>a(l=>({...l,form:{...l.form,...s}})),onSubmit:()=>{if(!/^[a-z][a-z0-9_-]{0,63}$/.test(o.form.id.trim())){n("栏目 id 必须是小写字母开头,仅含 a-z 0-9 _ -","bad");return}y(o.form)}}):null,o.kind==="edit"?e.jsx(P,{modal:o,onChange:s=>a(l=>({...l,form:{...l.form,...s}})),onSave:()=>k(o),onAddField:s=>C(o,s),onRemoveField:s=>w(o,s),onClose:()=>a(null)}):null]}):null]}):e.jsx("div",{className:"pad muted",children:"加载中…"})}function I(c){return c.kind==="dump"?c.title:c.kind==="create"?"新建栏目":`编辑栏目 ${c.id}`}function U({templates:c,form:n,onChange:m,onSubmit:j}){return e.jsxs("div",{className:"stack",children:[e.jsxs("div",{className:"grid",children:[e.jsxs("div",{className:"field",children:[e.jsxs("div",{className:"flabel",children:[e.jsx("span",{children:"栏目 id"}),e.jsx("span",{className:"fkey",children:"小写字母开头,仅 a-z 0-9 _ -"})]}),e.jsx("div",{className:"fctrl",children:e.jsx("input",{value:n.id,onChange:i=>m({id:i.target.value}),placeholder:"例如 projects"})})]}),e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"flabel",children:e.jsx("span",{children:"标题"})}),e.jsx("div",{className:"fctrl",children:e.jsx("input",{value:n.title,onChange:i=>m({title:i.target.value}),placeholder:"例如 项目档案"})})]}),e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"flabel",children:e.jsx("span",{children:"从模板起步"})}),e.jsx("div",{className:"fctrl",children:e.jsxs("select",{value:n.template,onChange:i=>m({template:i.target.value}),children:[e.jsx("option",{value:"",children:"(空栏目,自定义字段)"}),c.map(i=>e.jsxs("option",{value:i.id,children:[i.label,"(",i.fields," 字段)"]},i.id))]})})]}),e.jsxs("div",{className:"field",children:[e.jsxs("div",{className:"flabel",children:[e.jsx("span",{children:"排序"}),e.jsx("span",{className:"fkey",children:"越小越靠前"})]}),e.jsx("div",{className:"fctrl",children:e.jsx("input",{type:"number",value:n.order,onChange:i=>m({order:i.target.value})})})]})]}),e.jsxs("div",{className:"field wide",children:[e.jsx("div",{className:"flabel",children:e.jsx("span",{children:"描述"})}),e.jsx("div",{className:"fctrl",children:e.jsx("input",{value:n.description,onChange:i=>m({description:i.target.value})})})]}),e.jsxs("div",{className:"rowgap",children:[e.jsx("button",{className:"btn",onClick:j,children:"创建"}),e.jsx("span",{className:"hint",children:"同 id 已存在会直接报冲突,不会静默覆盖。"})]})]})}function P({modal:c,onChange:n,onSave:m,onAddField:j,onRemoveField:i,onClose:b}){var o;const[d,p]=x.useState({key:"",label:"",type:"text",group:""}),r=c.full,u=(r==null?void 0:r.groupList)||[];return e.jsxs("div",{className:"stack",children:[e.jsxs("div",{className:"grid",children:[e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"flabel",children:e.jsx("span",{children:"标题"})}),e.jsx("div",{className:"fctrl",children:e.jsx("input",{value:c.form.title,onChange:a=>n({title:a.target.value})})})]}),e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"flabel",children:e.jsx("span",{children:"排序"})}),e.jsx("div",{className:"fctrl",children:e.jsx("input",{type:"number",value:c.form.order,onChange:a=>n({order:a.target.value})})})]}),e.jsxs("div",{className:"field",children:[e.jsxs("div",{className:"flabel",children:[e.jsx("span",{children:"条目名字段"}),e.jsx("span",{className:"fkey",children:"列表里显示哪个字段"})]}),e.jsx("div",{className:"fctrl",children:e.jsxs("select",{value:c.form.titleField,onChange:a=>n({titleField:a.target.value}),children:[e.jsx("option",{value:"",children:"(用条目 title)"}),((r==null?void 0:r.fieldList)||[]).map(a=>e.jsxs("option",{value:a.key,children:[a.label,"(",a.key,")"]},a.key))]})})]}),e.jsxs("div",{className:"field",children:[e.jsxs("div",{className:"flabel",children:[e.jsx("span",{children:"名称标签"}),e.jsx("span",{className:"fkey",children:"新建条目时问什么"})]}),e.jsx("div",{className:"fctrl",children:e.jsx("input",{value:c.form.titleLabel,onChange:a=>n({titleLabel:a.target.value})})})]})]}),e.jsxs("div",{className:"field wide",children:[e.jsx("div",{className:"flabel",children:e.jsx("span",{children:"描述"})}),e.jsx("div",{className:"fctrl",children:e.jsx("input",{value:c.form.description,onChange:a=>n({description:a.target.value})})})]}),e.jsxs("div",{className:"rowgap",children:[e.jsx("button",{className:"btn",onClick:m,children:"保存栏目属性"}),e.jsx("button",{className:"btn ghost",onClick:b,children:"关闭"})]}),e.jsxs("div",{className:"card",style:{border:"1px solid var(--soft-2)",borderRadius:4},children:[e.jsxs("div",{className:"colhead",children:["字段字典(",((o=r==null?void 0:r.fieldList)==null?void 0:o.length)||0,")"]}),e.jsxs("div",{className:"colbody",children:[u.map(a=>e.jsxs("div",{children:[e.jsxs("div",{className:"group-title",children:[a.title,"(",a.id,")"]}),(r.fieldList||[]).filter(t=>t.group===a.id).map(t=>e.jsxs("div",{className:"row",children:[e.jsx("span",{className:"name",children:t.label}),e.jsx("span",{className:"mono muted",children:t.key}),e.jsx(v,{children:t.type}),t.sensitive?e.jsx(v,{kind:"bad",children:"密文"}):null,e.jsx("span",{className:"desc",children:t.hint||""}),e.jsx("span",{className:"acts",children:e.jsx("button",{className:"btn small ghost",onClick:()=>i(t.key),children:"删除"})})]},t.key))]},a.id)),((r==null?void 0:r.fieldList)||[]).filter(a=>!u.some(t=>t.id===a.group)).map(a=>e.jsxs("div",{className:"row",children:[e.jsx("span",{className:"name",children:a.label}),e.jsx("span",{className:"mono muted",children:a.key}),e.jsx(v,{children:a.type}),e.jsxs("span",{className:"desc",children:["分组 ",a.group,"(不在 groups 列表里)"]}),e.jsx("span",{className:"acts",children:e.jsx("button",{className:"btn small ghost",onClick:()=>i(a.key),children:"删除"})})]},a.key))]})]}),e.jsxs("div",{className:"card",style:{border:"1px solid var(--soft-2)",borderRadius:4},children:[e.jsx("div",{className:"colhead",children:"新增字段"}),e.jsxs("div",{className:"pad rowgap",children:[e.jsx("input",{style:{width:150},placeholder:"key(英文字母开头)",value:d.key,onChange:a=>p({...d,key:a.target.value})}),e.jsx("input",{style:{width:150},placeholder:"标签(中文名)",value:d.label,onChange:a=>p({...d,label:a.target.value})}),e.jsx("select",{style:{width:110},value:d.type,onChange:a=>p({...d,type:a.target.value}),children:T.map(a=>e.jsx("option",{value:a,children:a},a))}),e.jsxs("select",{style:{width:150},value:d.group,onChange:a=>p({...d,group:a.target.value}),children:[e.jsx("option",{value:"",children:"(首个分组)"}),u.map(a=>e.jsx("option",{value:a.id,children:a.title},a.id))]}),e.jsx("button",{className:"btn small",onClick:()=>{const a={key:d.key.trim(),label:d.label.trim()||d.key.trim(),type:d.type};d.group&&(a.group=d.group),j(a),p({key:"",label:"",type:"text",group:d.group})},children:"加入字典"}),e.jsx("span",{className:"hint",children:"加完立刻生效:CLI 与面板都能填它。"})]})]})]})}export{A as default};
|
|
1
|
+
import{u as L,r as x,a as h,j as e,T as v,E as S,C as F,M as R}from"./index-Bbe3iqam.js";const T=["text","textarea","number","date","month","bool","select","tags","secret"];function A(){const{reload:c,toast:n,guard:m,dialog:j}=L(),[i,b]=x.useState(null),[d,p]=x.useState([]),[r,u]=x.useState(null),[o,a]=x.useState(null),t=x.useCallback(async()=>{try{const[s,l]=await Promise.all([h("/api/sections"),h("/api/sections/templates")]);b(s),p(l.templates),u(null)}catch(s){u(s.message)}},[]);x.useEffect(()=>{t()},[t]);const f=m(async s=>{const l=await h(`/api/sections/${encodeURIComponent(s.id)}/dump`);a({kind:"dump",title:`${s.title}(${s.id})的全部信息`,body:e.jsx("pre",{className:"block",children:JSON.stringify(l,null,2)})})}),g=m(async s=>{const l=await h(`/api/sections/${encodeURIComponent(s.id)}`);a({kind:"edit",id:s.id,form:{title:l.title,description:l.description,order:String(l.order),titleLabel:l.titleLabel,titleField:l.titleField||""},full:l})}),y=m(async s=>{const l={id:s.id.trim(),title:s.title.trim()||s.id.trim(),template:s.template||void 0,description:s.description,order:Number(s.order)||50};await h("/api/sections",{method:"POST",body:l}),n("栏目已创建"),a(null),await t(),await c()}),k=m(async s=>{const l={title:s.form.title,description:s.form.description,order:Number(s.form.order)||50,titleLabel:s.form.titleLabel,titleField:s.form.titleField};await h(`/api/sections/${encodeURIComponent(s.id)}`,{method:"PATCH",body:l}),n("栏目已更新");const N=await h(`/api/sections/${encodeURIComponent(s.id)}`);a({...s,full:N}),await t(),await c()}),C=m(async(s,l)=>{await h(`/api/sections/${encodeURIComponent(s.id)}`,{method:"PATCH",body:{"add-field":l}}),n(`已新增字段 ${l.key}`);const N=await h(`/api/sections/${encodeURIComponent(s.id)}`);a({...s,full:N}),await t()}),w=m(async(s,l)=>{if(!await j({message:`从字段字典里删掉「${l}」?已填的值会留在条目里(只是不再显示/校验)。`,danger:!0,confirmText:"删除字段"}))return;await h(`/api/sections/${encodeURIComponent(s.id)}`,{method:"PATCH",body:{"remove-field":l}}),n(`已删字段 ${l}`);const E=await h(`/api/sections/${encodeURIComponent(s.id)}`);a({...s,full:E}),await t()}),$=m(async s=>{await j({message:`删除栏目「${s.title}」,连带其中 ${s.entries} 条条目?删除前会自动留快照。`,danger:!0,confirmText:"连带删除"})&&(await h(`/api/sections/${encodeURIComponent(s.id)}`,{method:"DELETE",body:{force:!0}}),n("栏目已删除"),await t(),await c())});return r?e.jsxs("div",{className:"pad stack",children:[e.jsx("div",{className:"bad",children:e.jsx("strong",{children:"读不到栏目"})}),e.jsx("pre",{className:"block",children:r})]}):i?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"toolbar rowgap pad",children:[e.jsx("strong",{children:"栏目"}),e.jsxs("span",{className:"muted",children:["共 ",i.count," 个 · 条目 ",i.totalEntries," 条"]}),e.jsx("span",{className:"grow"}),e.jsx("button",{className:"btn small ghost",onClick:t,children:"刷新"}),e.jsx("button",{className:"btn small",onClick:()=>{var s;return a({kind:"create",form:{id:"",title:"",template:((s=d[0])==null?void 0:s.id)||"",description:"",order:"50"}})},children:"新建栏目"})]}),e.jsxs("div",{className:"card",children:[e.jsx("div",{className:"colhead",children:"全部栏目"}),e.jsx("div",{className:"colbody",children:i.sections.length?i.sections.map(s=>e.jsxs("div",{className:"row",children:[e.jsx("span",{className:"name",children:s.title}),e.jsx("span",{className:"mono muted",children:s.id}),s.template?e.jsx(v,{children:s.template}):e.jsx(v,{children:"自定义"}),e.jsx("span",{className:"desc",children:s.description}),e.jsxs("span",{className:"acts",children:[e.jsxs("span",{className:"tag",children:[s.fields," 字段"]}),e.jsxs("span",{className:"tag",children:[s.entries," 条目"]}),e.jsx("button",{className:"btn small ghost",onClick:()=>f(s),children:"全部信息"}),e.jsx("button",{className:"btn small ghost",onClick:()=>g(s),children:"编辑"}),e.jsx("button",{className:"btn small ghost",onClick:()=>$(s),children:"删除"})]})]},s.id)):e.jsx(S,{children:"还没有栏目。点「新建栏目」,用内置模板(求职 / 密钥)起步。"})})]}),e.jsx("div",{className:"pad hint",children:"栏目 = 一组同构条目 + 一份字段字典。字典是数据,所以这里加一个字段,CLI 与面板立刻就都能填它。"}),e.jsx(F,{module:"sections"}),o?e.jsxs(R,{title:I(o),onClose:()=>a(null),children:[o.kind==="dump"?o.body:null,o.kind==="create"?e.jsx(U,{templates:d,form:o.form,onChange:s=>a(l=>({...l,form:{...l.form,...s}})),onSubmit:()=>{if(!/^[a-z][a-z0-9_-]{0,63}$/.test(o.form.id.trim())){n("栏目 id 必须是小写字母开头,仅含 a-z 0-9 _ -","bad");return}y(o.form)}}):null,o.kind==="edit"?e.jsx(P,{modal:o,onChange:s=>a(l=>({...l,form:{...l.form,...s}})),onSave:()=>k(o),onAddField:s=>C(o,s),onRemoveField:s=>w(o,s),onClose:()=>a(null)}):null]}):null]}):e.jsx("div",{className:"pad muted",children:"加载中…"})}function I(c){return c.kind==="dump"?c.title:c.kind==="create"?"新建栏目":`编辑栏目 ${c.id}`}function U({templates:c,form:n,onChange:m,onSubmit:j}){return e.jsxs("div",{className:"stack",children:[e.jsxs("div",{className:"grid",children:[e.jsxs("div",{className:"field",children:[e.jsxs("div",{className:"flabel",children:[e.jsx("span",{children:"栏目 id"}),e.jsx("span",{className:"fkey",children:"小写字母开头,仅 a-z 0-9 _ -"})]}),e.jsx("div",{className:"fctrl",children:e.jsx("input",{value:n.id,onChange:i=>m({id:i.target.value}),placeholder:"例如 projects"})})]}),e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"flabel",children:e.jsx("span",{children:"标题"})}),e.jsx("div",{className:"fctrl",children:e.jsx("input",{value:n.title,onChange:i=>m({title:i.target.value}),placeholder:"例如 项目档案"})})]}),e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"flabel",children:e.jsx("span",{children:"从模板起步"})}),e.jsx("div",{className:"fctrl",children:e.jsxs("select",{value:n.template,onChange:i=>m({template:i.target.value}),children:[e.jsx("option",{value:"",children:"(空栏目,自定义字段)"}),c.map(i=>e.jsxs("option",{value:i.id,children:[i.label,"(",i.fields," 字段)"]},i.id))]})})]}),e.jsxs("div",{className:"field",children:[e.jsxs("div",{className:"flabel",children:[e.jsx("span",{children:"排序"}),e.jsx("span",{className:"fkey",children:"越小越靠前"})]}),e.jsx("div",{className:"fctrl",children:e.jsx("input",{type:"number",value:n.order,onChange:i=>m({order:i.target.value})})})]})]}),e.jsxs("div",{className:"field wide",children:[e.jsx("div",{className:"flabel",children:e.jsx("span",{children:"描述"})}),e.jsx("div",{className:"fctrl",children:e.jsx("input",{value:n.description,onChange:i=>m({description:i.target.value})})})]}),e.jsxs("div",{className:"rowgap",children:[e.jsx("button",{className:"btn",onClick:j,children:"创建"}),e.jsx("span",{className:"hint",children:"同 id 已存在会直接报冲突,不会静默覆盖。"})]})]})}function P({modal:c,onChange:n,onSave:m,onAddField:j,onRemoveField:i,onClose:b}){var o;const[d,p]=x.useState({key:"",label:"",type:"text",group:""}),r=c.full,u=(r==null?void 0:r.groupList)||[];return e.jsxs("div",{className:"stack",children:[e.jsxs("div",{className:"grid",children:[e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"flabel",children:e.jsx("span",{children:"标题"})}),e.jsx("div",{className:"fctrl",children:e.jsx("input",{value:c.form.title,onChange:a=>n({title:a.target.value})})})]}),e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"flabel",children:e.jsx("span",{children:"排序"})}),e.jsx("div",{className:"fctrl",children:e.jsx("input",{type:"number",value:c.form.order,onChange:a=>n({order:a.target.value})})})]}),e.jsxs("div",{className:"field",children:[e.jsxs("div",{className:"flabel",children:[e.jsx("span",{children:"条目名字段"}),e.jsx("span",{className:"fkey",children:"列表里显示哪个字段"})]}),e.jsx("div",{className:"fctrl",children:e.jsxs("select",{value:c.form.titleField,onChange:a=>n({titleField:a.target.value}),children:[e.jsx("option",{value:"",children:"(用条目 title)"}),((r==null?void 0:r.fieldList)||[]).map(a=>e.jsxs("option",{value:a.key,children:[a.label,"(",a.key,")"]},a.key))]})})]}),e.jsxs("div",{className:"field",children:[e.jsxs("div",{className:"flabel",children:[e.jsx("span",{children:"名称标签"}),e.jsx("span",{className:"fkey",children:"新建条目时问什么"})]}),e.jsx("div",{className:"fctrl",children:e.jsx("input",{value:c.form.titleLabel,onChange:a=>n({titleLabel:a.target.value})})})]})]}),e.jsxs("div",{className:"field wide",children:[e.jsx("div",{className:"flabel",children:e.jsx("span",{children:"描述"})}),e.jsx("div",{className:"fctrl",children:e.jsx("input",{value:c.form.description,onChange:a=>n({description:a.target.value})})})]}),e.jsxs("div",{className:"rowgap",children:[e.jsx("button",{className:"btn",onClick:m,children:"保存栏目属性"}),e.jsx("button",{className:"btn ghost",onClick:b,children:"关闭"})]}),e.jsxs("div",{className:"card",style:{border:"1px solid var(--soft-2)",borderRadius:4},children:[e.jsxs("div",{className:"colhead",children:["字段字典(",((o=r==null?void 0:r.fieldList)==null?void 0:o.length)||0,")"]}),e.jsxs("div",{className:"colbody",children:[u.map(a=>e.jsxs("div",{children:[e.jsxs("div",{className:"group-title",children:[a.title,"(",a.id,")"]}),(r.fieldList||[]).filter(t=>t.group===a.id).map(t=>e.jsxs("div",{className:"row",children:[e.jsx("span",{className:"name",children:t.label}),e.jsx("span",{className:"mono muted",children:t.key}),e.jsx(v,{children:t.type}),t.sensitive?e.jsx(v,{kind:"bad",children:"密文"}):null,e.jsx("span",{className:"desc",children:t.hint||""}),e.jsx("span",{className:"acts",children:e.jsx("button",{className:"btn small ghost",onClick:()=>i(t.key),children:"删除"})})]},t.key))]},a.id)),((r==null?void 0:r.fieldList)||[]).filter(a=>!u.some(t=>t.id===a.group)).map(a=>e.jsxs("div",{className:"row",children:[e.jsx("span",{className:"name",children:a.label}),e.jsx("span",{className:"mono muted",children:a.key}),e.jsx(v,{children:a.type}),e.jsxs("span",{className:"desc",children:["分组 ",a.group,"(不在 groups 列表里)"]}),e.jsx("span",{className:"acts",children:e.jsx("button",{className:"btn small ghost",onClick:()=>i(a.key),children:"删除"})})]},a.key))]})]}),e.jsxs("div",{className:"card",style:{border:"1px solid var(--soft-2)",borderRadius:4},children:[e.jsx("div",{className:"colhead",children:"新增字段"}),e.jsxs("div",{className:"pad rowgap",children:[e.jsx("input",{style:{width:150},placeholder:"key(英文字母开头)",value:d.key,onChange:a=>p({...d,key:a.target.value})}),e.jsx("input",{style:{width:150},placeholder:"标签(中文名)",value:d.label,onChange:a=>p({...d,label:a.target.value})}),e.jsx("select",{style:{width:110},value:d.type,onChange:a=>p({...d,type:a.target.value}),children:T.map(a=>e.jsx("option",{value:a,children:a},a))}),e.jsxs("select",{style:{width:150},value:d.group,onChange:a=>p({...d,group:a.target.value}),children:[e.jsx("option",{value:"",children:"(首个分组)"}),u.map(a=>e.jsx("option",{value:a.id,children:a.title},a.id))]}),e.jsx("button",{className:"btn small",onClick:()=>{const a={key:d.key.trim(),label:d.label.trim()||d.key.trim(),type:d.type};d.group&&(a.group=d.group),j(a),p({key:"",label:"",type:"text",group:d.group})},children:"加入字典"}),e.jsx("span",{className:"hint",children:"加完立刻生效:CLI 与面板都能填它。"})]})]})]})}export{A as default};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{u as C,r as i,a as b,j as s,E as S,C as R,M as $}from"./index-Bbe3iqam.js";function P(){const e=new Date,t=r=>String(r).padStart(2,"0");return`${e.getFullYear()}${t(e.getMonth()+1)}${t(e.getDate())}-${t(e.getHours())}${t(e.getMinutes())}${t(e.getSeconds())}`}function E(e,t){const r=new Blob([t],{type:"application/json;charset=utf-8"}),o=URL.createObjectURL(r),n=document.createElement("a");n.href=o,n.download=e,document.body.appendChild(n),n.click(),n.remove(),setTimeout(()=>URL.revokeObjectURL(o),1e3)}function O(){const{toast:e,guard:t,dialog:r,reload:o}=C(),[n,x]=i.useState(""),[h,m]=i.useState(""),[p,j]=i.useState(""),[c,f]=i.useState(null),[d,v]=i.useState(null),u=i.useRef(null),k=t(async()=>{if(!n){e("请先设置备份口令","bad");return}if(n!==h){e("两次输入的口令不一致","bad");return}const a=await b("/api/backup",{method:"POST",body:{password:n}}),l=`nx-sk-backup-${P()}.nxskbak`;E(l,JSON.stringify(a.archive,null,2)+`
|
|
2
|
+
`),e(`已生成加密备份:${a.counts.sections} 栏目 / ${a.counts.entries} 条目`),x(""),m("")}),g=async a=>{var N;const l=(N=a.target.files)==null?void 0:N[0];if(!l)return;const y=await l.text();f({name:l.name,bytes:l.size,text:y})},w=t(async()=>{if(!c){e("请先选择备份文件","bad");return}if(!p){e("请输入备份口令","bad");return}if(!await r({danger:!0,confirmText:"解密并整份恢复",message:"恢复会用备份内容整份替换当前数据。",detail:`备份文件:${c.name}
|
|
3
|
+
恢复前会自动给当前数据留一份安全快照(可回滚)。`}))return;const l=await b("/api/backup/restore",{method:"POST",body:{password:p,content:c.text}});v(l),j(""),f(null),u.current&&(u.current.value=""),await o()});return s.jsxs(s.Fragment,{children:[s.jsxs("div",{className:"toolbar rowgap pad",children:[s.jsx("strong",{children:"加密备份"}),s.jsx("span",{className:"muted",children:"整份数据用口令对称加密(AES-256-GCM);导出设口令、恢复输口令,口令不落盘"})]}),s.jsxs("div",{className:"card",children:[s.jsx("div",{className:"colhead",children:"加密导出"}),s.jsxs("div",{className:"pad stack",children:[s.jsxs("div",{className:"grid",children:[s.jsxs("div",{className:"field",children:[s.jsxs("div",{className:"flabel",children:[s.jsx("span",{children:"设置备份口令"}),s.jsx("span",{className:"fkey",children:"建议至少 6 位"})]}),s.jsx("div",{className:"fctrl",children:s.jsx("input",{type:"password",autoComplete:"new-password",value:n,onChange:a=>x(a.target.value),placeholder:"备份口令"})})]}),s.jsxs("div",{className:"field",children:[s.jsx("div",{className:"flabel",children:s.jsx("span",{children:"再输入一次"})}),s.jsx("div",{className:"fctrl",children:s.jsx("input",{type:"password",autoComplete:"new-password",value:h,onChange:a=>m(a.target.value),placeholder:"确认口令"})})]})]}),s.jsx("div",{className:"hint",children:"导出为单个 .nxskbak 文件并在浏览器下载;文件内含随机盐,密钥由口令经 scrypt 派生。恢复时需要输入同一口令。"}),s.jsx("div",{className:"rowgap",children:s.jsx("button",{className:"btn",onClick:k,children:"加密导出并下载"})})]})]}),s.jsxs("div",{className:"card",children:[s.jsx("div",{className:"colhead",children:"解密恢复(导入)"}),s.jsxs("div",{className:"pad stack",children:[s.jsxs("div",{className:"grid",children:[s.jsxs("div",{className:"field",children:[s.jsxs("div",{className:"flabel",children:[s.jsx("span",{children:"备份文件"}),s.jsx("span",{className:"fkey",children:".nxskbak"})]}),s.jsx("div",{className:"fctrl",children:s.jsx("input",{ref:u,type:"file",accept:".nxskbak,application/json",onChange:g})})]}),s.jsxs("div",{className:"field",children:[s.jsx("div",{className:"flabel",children:s.jsx("span",{children:"输入备份口令"})}),s.jsx("div",{className:"fctrl",children:s.jsx("input",{type:"password",autoComplete:"off",value:p,onChange:a=>j(a.target.value),placeholder:"导出时设置的口令"})})]})]}),c?s.jsxs("div",{className:"row",children:[s.jsx("span",{className:"mono name",children:c.name}),s.jsx("span",{className:"acts",children:s.jsxs("span",{className:"tag",children:[c.bytes," 字节"]})})]}):s.jsx(S,{children:"还没有选择备份文件。"}),s.jsx("div",{className:"hint",children:"口令错误或文件损坏会明确报错、不会改动当前数据;确认恢复前会先留安全快照。"}),s.jsx("div",{className:"rowgap",children:s.jsx("button",{className:"btn danger",onClick:w,children:"解密并恢复"})})]})]}),s.jsx(R,{module:"backup"}),d?s.jsx($,{title:"恢复完成",onClose:()=>v(null),children:s.jsxs("div",{className:"stack",children:[s.jsxs("div",{children:["已从加密备份恢复:",d.sections," 个栏目 / ",d.entries," 条条目"]}),s.jsxs("div",{className:"muted",children:["备份生成于 ",d.backupCreatedAt||"未知"]}),s.jsxs("pre",{className:"block",children:["恢复前的安全快照:",d.safetySnapshot]})]})}):null]})}export{O as default};
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -4,8 +4,12 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>nx-sk · 个人资源管理器</title>
|
|
7
|
-
<
|
|
8
|
-
<link rel="
|
|
7
|
+
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
|
|
8
|
+
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16.png" />
|
|
9
|
+
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
10
|
+
<link rel="apple-touch-icon" href="/logo-rounded.png" />
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-Bbe3iqam.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C1bxqdjl.css">
|
|
9
13
|
</head>
|
|
10
14
|
<body>
|
|
11
15
|
<div id="root"></div>
|
|
Binary file
|
|
Binary file
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
# 40 · 扩展:改这个项目本身
|
|
2
|
-
|
|
3
|
-
> 归属 `nx-sk` skill 主文档。**要加栏目模板、加功能域、改对外契约时读这篇。**
|
|
4
|
-
> 只填数据的话不用看——那走 `10-sections-entries` 的「扩字段字典」一节就够。
|
|
5
|
-
|
|
6
|
-
## 一、加一个栏目模板(最常见)
|
|
7
|
-
|
|
8
|
-
1. `src/core/fields.js` 的 `TEMPLATES` 里加一个 `{ id, label, description, titleField, titleLabel, groups, fields }`。
|
|
9
|
-
2. 完成。`section templates` / 面板的「新建栏目」会自动列出它。
|
|
10
|
-
|
|
11
|
-
不需要动任何别的文件——模板是**数据**。
|
|
12
|
-
|
|
13
|
-
## 二、加一个功能域(`src/modules/<域>/`)
|
|
14
|
-
|
|
15
|
-
按闭环表逐条对。**第 6、7、11 项没有任何断言会替你发现**,是真正会漏的地方:
|
|
16
|
-
|
|
17
|
-
| # | 落点 | 漏了会怎样 | 谁会发现 |
|
|
18
|
-
| --- | --- | --- | --- |
|
|
19
|
-
| 1 | `modules/<域>/service.js` | —(业务本身) | — |
|
|
20
|
-
| 2 | `modules/<域>/index.js` | 命令不存在 | 装载期自检 / 一致性测试 |
|
|
21
|
-
| 3 | `modules/<域>/view.jsx` | 声明了 view 却没文件 → **构建期报错** | `vite build` |
|
|
22
|
-
| 4 | `runtime/registry.js` 的 `MODULES` | 模块整个不生效 | 一致性测试(目录 ↔ 注册表对账) |
|
|
23
|
-
| 5 | `web/frontend/registry.js` 的 `VIEWS` | 后端有 view、前端没登记 | 一致性测试(两侧视图表对账) |
|
|
24
|
-
| 6 | `src/index.js` 的 `export * as <域>` | 库用方拿不到该 service | ❗**无任何断言,纯静默** |
|
|
25
|
-
| 7 | `eslint.config.js` 的 `SIBLING_MODULES` 加 `'../<域>/*'` | 新模块悄悄变成「谁都可以依赖」 | ❗**无任何断言,纯静默** |
|
|
26
|
-
| 8 | `core/paths.js` / `core/fields.js` 新增常量(如有) | 散落字面量 | 靠 review |
|
|
27
|
-
| 9 | `tests/unit/` 纯逻辑断言 | 算法只被端到端覆盖 | 靠自律 |
|
|
28
|
-
| 10 | `tests/smoke.mjs` 只读断言 | 端到端坏了没人知道 | 无断言 |
|
|
29
|
-
| 11 | **`assets/nx-sk/references/`** | agent 永远不知道这条命令存在 | ❗**单向断言**(只保证「文档提到的命令一定存在」,反方向不强制) |
|
|
30
|
-
| 12 | `README.md` 命令总表 / `CHANGELOG.md` | 文档漂移 | 无断言 |
|
|
31
|
-
|
|
32
|
-
第 7 项的禁列是**逐模块枚举**的,不是通配——这是刻意的:加一行就等于提醒「你在加一个可能有依赖的模块」。
|
|
33
|
-
但代价是漏补**完全静默**,且规则随模块数增加持续衰减(加 5 个漏 3 次,规则就废了一半)。
|
|
34
|
-
|
|
35
|
-
## 三、四条硬纪律
|
|
36
|
-
|
|
37
|
-
1. **一条 action 同时声明 `cli` 与 `http`**(`http` 可为 `null`)。别在别处再抄一份命令表。
|
|
38
|
-
2. **写命令的 flag 可以有 `default`;读命令的过滤 flag 一律不带。**
|
|
39
|
-
带了就会让「不传 = 全部」的分支永远走不到——不报错、不崩,只是永远返回半个结果。
|
|
40
|
-
3. **每个写命令都要 `--dry-run`;破坏性写之前 `snapshotStore()`。**
|
|
41
|
-
`remove` 类要选定幂等语义(本项目选 **`NOT_FOUND` 报错**,不静默成功)并写进 `summary`。
|
|
42
|
-
4. **集合资源才凑 CRUD 五动词**(`list/get/add/update/remove`)。
|
|
43
|
-
单例配置退化成 `get/set`;动作集合按动作命名(`export run` / `skill install`);
|
|
44
|
-
聚合模块(`system`)不必有视图。**别硬凑** —— `system add` 这种一看就知道是凑的。
|
|
45
|
-
|
|
46
|
-
## 四、验证闸门(改完必须跑)
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
pnpm test # = lint + build + 冒烟 + 单测
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
四道闸各自拦什么:
|
|
53
|
-
|
|
54
|
-
| 闸 | 拦什么 | 怎么确认它真的会触发 |
|
|
55
|
-
| --- | --- | --- |
|
|
56
|
-
| 装载期自检(`registry.js`) | 重复 action id / CLI 路径 / HTTP 路由;缺 `cli`;`http` 既不是数组也不是 `null` | 故意加一条重复命令,确认启动即抛错 |
|
|
57
|
-
| 模块目录 ↔ 注册表对账 | 写了目录忘登记(反之亦然) | 建一个空模块目录,跑单测看是否报「没有登记」 |
|
|
58
|
-
| 后端模块 ↔ 前端视图对账 | 有 view 却没登记 tab,或登记了不存在的视图 | 删掉前端 `VIEWS` 里某一行,跑单测 |
|
|
59
|
-
| 视图里的 `/api` 字面量 | 面板调了不存在的端点 | 把某个 `api('/api/x')` 改成不存在的路径,跑单测 |
|
|
60
|
-
| CLI 路径可解析回自身 | 命令匹配算法被改坏 | 靠既有断言 |
|
|
61
|
-
| lint 分层 | 越层 import / 前端 import `node:*` / 模块互依 | **必须做一次反向测试**:临时写个违例文件,确认 lint 退出码非 0 |
|
|
62
|
-
|
|
63
|
-
最后一条特别值得做:**lint 规则写错了不会报错,只会永远通过——一个从没红过的规则等于不存在。**
|
|
64
|
-
|
|
65
|
-
## 五、几个不该改的地方
|
|
66
|
-
|
|
67
|
-
| 别做 | 为什么 |
|
|
68
|
-
| --- | --- |
|
|
69
|
-
| 给 `.card` 加 `box-shadow` | 容器投影 = 一堆岛屿,用户 0.3 秒找不到按钮 |
|
|
70
|
-
| 把 `.row` 改成靠 padding 定高 | 高度不可控,行间距不一致;本项目是**行 28px / 按钮 32px** 的两级密度 |
|
|
71
|
-
| 把 `.btn` 高度改成 36px「更好按」 | 破坏 4px 差的密度二元性 |
|
|
72
|
-
| 给 `.tab.active` 再叠一层 `--shadow` | tab 浮起来抢焦点 |
|
|
73
|
-
| 用 `font-size: 14px` 让文字「看得清」 | 破坏 12px 的信息密度节奏 |
|
|
74
|
-
| 在前端 `import` 任何 `core/` 或 `runtime/` | 会把 Node 侧代码打进浏览器包 → dev 白屏 |
|
|
75
|
-
| 用变量拼 `lazy(() => import(x))` | Vite 静态分析不了,失去代码分割 |
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
:root{--ink: #14161a;--paper: #fff;--soft: #f5f6f7;--soft-2: #ebedf0;--mid: #8b9096;--danger: #b3261e;--border: 1px solid var(--soft-2);--row-h: 28px;--btn-h: 32px;--fs-core: 12px;--radius-btn: 8px;--shadow-relief: -1px -1px 2px rgba(255, 255, 255, .85), 2px 2px 6px rgba(20, 22, 26, .07);--shadow-relief-press: inset 1px 1px 2px rgba(20, 22, 26, .1), inset -1px -1px 2px rgba(255, 255, 255, .7);--shadow-relief-inset: inset 1px 1px 2px rgba(20, 22, 26, .06), inset -1px -1px 2px rgba(255, 255, 255, .9)}*{box-sizing:border-box}html,body,#root{height:100%}body{margin:0;background:var(--paper);color:var(--ink);font-family:system-ui,-apple-system,Segoe UI,Microsoft YaHei,sans-serif;font-size:var(--fs-core);line-height:1.5;-webkit-font-smoothing:antialiased}.mono,code,.path,.cli-cmd{font-family:ui-monospace,Consolas,Courier New,monospace}.muted{color:var(--mid)}.bad{color:var(--danger)}.nowrap{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pad{padding:10px 14px}.rowgap{display:flex;flex-wrap:wrap;gap:8px;align-items:center}.grow{flex:1;min-width:0}.right{margin-left:auto}.app{display:flex;flex-direction:column;height:100%;min-height:0}header.top{display:flex;flex-wrap:wrap;align-items:center;gap:10px;padding:6px 14px;border-bottom:var(--border);flex-shrink:0}header.top .brand{font-weight:700;letter-spacing:.3px}header.top .meta{color:var(--mid);font-size:11px}nav.tabs{display:flex;flex-wrap:wrap;gap:6px;padding:6px 14px;border-bottom:var(--border);flex-shrink:0}main.body{flex:1;min-height:0;overflow:auto}.tab{font:inherit;font-size:var(--fs-core);height:var(--btn-h);padding:0 12px;border:0;border-radius:var(--radius-btn);background:var(--soft);color:var(--ink);cursor:pointer;display:inline-flex;align-items:center;gap:6px}.tab .count{color:var(--mid);font-size:11px}.tab.active{background:var(--paper);box-shadow:var(--shadow-relief);font-weight:600}.btn{font:inherit;font-size:var(--fs-core);cursor:pointer;height:var(--btn-h);padding:0 12px;border:0;border-radius:var(--radius-btn);background:var(--ink);color:var(--paper);box-shadow:var(--shadow-relief);display:inline-flex;align-items:center;gap:4px;transition:box-shadow .12s,transform .08s}.btn:hover:not(:disabled){box-shadow:var(--shadow-relief),0 0 0 3px #14161a0d}.btn:active:not(:disabled){box-shadow:var(--shadow-relief-press);transform:translateY(1px)}.btn:disabled{background:var(--soft);color:var(--mid);box-shadow:var(--shadow-relief-inset);cursor:not-allowed}.btn.ghost{background:var(--soft);color:var(--ink)}.btn.danger{background:var(--danger);color:#fff}.btn.small{height:22px;padding:0 8px;font-size:11px;border-radius:6px}input,select,textarea{font:inherit;font-size:var(--fs-core);color:var(--ink);background:var(--paper);border:var(--border);border-radius:6px;padding:0 8px}input,select{height:var(--row-h)}textarea{padding:5px 8px;resize:vertical;min-height:56px;line-height:1.5}input:focus,select:focus,textarea:focus{outline:none;border-color:var(--soft-2);box-shadow:var(--shadow-relief-inset)}label.inline{display:inline-flex;align-items:center;gap:6px}.card{border-bottom:var(--border)}.card:last-child{border-bottom:0}.card>.colhead{display:flex;align-items:center;gap:8px;height:var(--row-h);padding:0 14px;border-bottom:var(--border);font-size:13px;font-weight:600}.card>.colbody{padding:0}.row{display:flex;align-items:center;gap:8px;min-height:var(--row-h);padding:0 14px;border-bottom:var(--border);font-size:var(--fs-core);transition:background .12s}.row:hover{background:var(--soft)}.row:last-child{border-bottom:0}.row .name{font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.row .desc{color:var(--mid);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.row .acts{margin-left:auto;display:flex;align-items:center;gap:6px;flex-shrink:0}.row.selectable{cursor:pointer}.row.selected{background:var(--soft);box-shadow:inset 2px 0 0 var(--ink)}.tag{display:inline-flex;align-items:center;height:22px;padding:0 7px;border-radius:999px;background:var(--soft);color:var(--mid);font-size:11px;white-space:nowrap}.tag.strong{background:var(--ink);color:var(--paper)}.tag.bad{background:#fbeae9;color:var(--danger)}.copyable{cursor:copy;border-radius:4px;padding:0 3px}.copyable:hover{background:var(--soft)}.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:0 14px}.field{border-bottom:var(--border);padding:5px 0;min-width:0}.field>.flabel{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--mid)}.field>.flabel .fkey{font-family:ui-monospace,Consolas,monospace;font-size:10px;color:var(--soft-2)}.field>.fctrl{margin-top:3px}.field input,.field select,.field textarea{width:100%}.field.wide{grid-column:1 / -1}.group-title{display:flex;align-items:center;gap:8px;height:var(--row-h);padding:0 14px;border-bottom:var(--border);background:var(--soft);font-size:11px;color:var(--mid);letter-spacing:.5px}.cli-hint{display:flex;flex-wrap:wrap;align-items:center;gap:2px;padding:6px 14px;color:var(--mid);font-size:11px;border-top:var(--border)}.cli-hint-label{margin-right:4px}.cli-hint-sep{color:var(--soft-2)}.cli-cmd{color:var(--ink)}.toasts{position:fixed;right:14px;bottom:14px;display:flex;flex-direction:column;gap:6px;z-index:40}.toast{background:var(--ink);color:var(--paper);padding:7px 12px;border-radius:8px;font-size:var(--fs-core);box-shadow:var(--shadow-relief);max-width:420px}.toast.bad{background:var(--danger)}.mask{position:fixed;top:0;right:0;bottom:0;left:0;background:#14161a47;display:flex;align-items:center;justify-content:center;z-index:50;padding:20px}.dialog{background:var(--paper);border-radius:8px;min-width:320px;max-width:min(880px,92vw);max-height:86vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 10px 40px #14161a2e}.dialog-title{display:flex;align-items:center;gap:8px;height:var(--row-h);padding:0 14px;border-bottom:var(--border);font-size:13px;font-weight:600;flex-shrink:0}.dialog-body{padding:10px 14px;overflow:auto;min-height:0}.dialog-foot{display:flex;gap:8px;justify-content:flex-end;padding:8px 14px;border-top:var(--border);flex-shrink:0}pre.block{margin:0;padding:10px 14px;background:var(--soft);border-radius:8px;font-family:ui-monospace,Consolas,monospace;font-size:11px;line-height:1.55;white-space:pre-wrap;word-break:break-word;max-height:60vh;overflow:auto}.hint{color:var(--mid);font-size:11px}.stack{display:flex;flex-direction:column;gap:10px}.toolbar{display:flex;flex-wrap:wrap;gap:8px;align-items:center}.cols{display:flex;gap:14px;align-items:flex-start;padding:0 14px 14px}.col{flex:1;min-width:0}.desc{color:var(--mid)}.cols>.col>.card{border:var(--border);border-radius:4px}
|