nx-sk 0.3.4 → 0.6.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 +47 -1
- package/README.md +62 -4
- package/assets/nx-sk/SKILL.md +31 -2
- package/assets/nx-sk/references/00-design.md +4 -3
- package/assets/nx-sk/references/10-sections-entries.md +10 -2
- package/assets/nx-sk/references/30-export-backup.md +16 -0
- package/assets/nx-sk/references/40-files.md +104 -0
- package/package.json +1 -1
- package/src/core/attachments.js +378 -0
- package/src/core/fields.js +58 -2
- package/src/core/paths.js +5 -0
- package/src/core/render.js +1 -0
- package/src/core/store.js +3 -0
- package/src/modules/backup/index.js +41 -0
- package/src/modules/backup/service.js +283 -0
- package/src/modules/backup/view.jsx +145 -0
- package/src/modules/entries/service.js +35 -2
- package/src/modules/files/index.js +114 -0
- package/src/modules/files/service.js +276 -0
- package/src/modules/files/view.jsx +342 -0
- package/src/modules/sections/index.js +10 -0
- package/src/modules/sections/service.js +55 -2
- package/src/modules/sections/view.jsx +30 -2
- package/src/runtime/api.js +10 -1
- package/src/runtime/registry.js +3 -1
- package/src/web/frontend/App.jsx +36 -18
- package/src/web/frontend/api/files.js +28 -0
- package/src/web/frontend/components/fieldEditor.jsx +72 -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 +2 -0
- package/src/web/frontend/styles.css +104 -6
- package/src/web/public/assets/files-BNh48qeo.js +1 -0
- package/src/web/public/assets/{index-BnVuH1lC.js → index-B7CtqgoG.js} +10 -9
- package/src/web/public/assets/index-DLtjhflt.css +1 -0
- package/src/web/public/assets/view-1kwWQYbz.js +7 -0
- package/src/web/public/assets/view-BAYyPWdU.js +1 -0
- package/src/web/public/assets/{view-BvyRxTgX.js → view-BIdVqIGe.js} +1 -1
- package/src/web/public/assets/{view-rvADq3vX.js → view-Bu70Q6GO.js} +1 -1
- package/src/web/public/assets/view-D5LfHr7W.js +1 -0
- package/src/web/public/assets/view-DXqI-4_F.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
- package/src/web/public/assets/view-CDje4hqZ.js +0 -1
- package/src/web/public/assets/view-DsyplQBh.js +0 -7
|
@@ -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:12px;padding:10px 14px;border-bottom:var(--border);flex-shrink:0}header.top .brand{font-size:15px;font-weight:700;letter-spacing:.2px;line-height:1;display:inline-flex;align-items:center;gap:8px}header.top .brand img{width:22px;height:22px;border-radius:6px;display:block}header.top .tagline{color:var(--mid);font-size:11px}header.top .ver{font-size:10px;color:var(--mid);background:var(--soft);border:var(--border);border-radius:999px;padding:1px 8px;line-height:1.7}header.top .store{display:inline-flex;align-items:center;gap:6px;min-width:0;font-size:11px;color:var(--mid)}header.top .store .store-label{color:var(--mid)}header.top .store .copyable{max-width:30ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}nav.tabs{display:flex;flex-wrap:wrap;align-items:center;gap:6px;padding:6px 14px;border-bottom:var(--border);flex-shrink:0}.tabs-sep{width:1px;height:18px;background:var(--soft-2);margin:0 4px}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:7px}.tab .count{color:var(--mid);font-size:10px;font-variant-numeric:tabular-nums}.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}.files-page{min-height:100%;position:relative}.files-page.drag-over{outline:2px dashed var(--ink);outline-offset:-8px;background:var(--soft)}.tab.small{height:22px;padding:0 8px;font-size:11px;border-radius:6px}.file-filters{padding-top:4px;padding-bottom:4px}.drop-hint{margin:0 14px;padding:18px;text-align:center;border:1px dashed var(--ink);border-radius:8px;background:var(--paper);font-weight:600}.file-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:10px}.file-card{display:flex;flex-direction:column;border:var(--border);border-radius:8px;overflow:hidden;background:var(--paper)}.file-thumb{display:flex;align-items:center;justify-content:center;height:120px;background:var(--soft);border-bottom:var(--border)}.file-thumb img{max-width:100%;max-height:100%;object-fit:contain}.ext-badge{font-size:22px;font-weight:700;letter-spacing:1px;color:var(--mid);font-family:ui-monospace,Consolas,monospace}.file-meta{padding:8px 10px 4px;display:flex;flex-direction:column;gap:4px}a.file-name{color:var(--ink);font-weight:600;text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}a.file-name:hover{text-decoration:underline}.file-acts{display:flex;flex-wrap:wrap;align-items:center;gap:4px;padding:6px 10px 8px;margin-top:auto}.btn.ghost.danger-ghost{color:var(--danger)}.file-picker{display:flex;flex-direction:column;gap:4px}.file-picker .file-pick-row{display:flex;flex-wrap:wrap;gap:6px;align-items:center}.file-picker select{flex:1;min-width:120px}.file-preview{display:flex;align-items:center;gap:8px;border:var(--border);border-radius:6px;padding:4px 8px;background:var(--soft)}.file-preview img{width:40px;height:40px;object-fit:cover;border-radius:4px;border:var(--border);background:#fff}.file-preview .file-preview-name{color:var(--ink);text-decoration:none;font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.file-preview .file-preview-name:hover{text-decoration:underline}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import{j as e,u as K,r as d,a as N,b as B,E as R,C as M,T as _,M as V}from"./index-B7CtqgoG.js";import{l as Y,f as z,r as G}from"./files-BNh48qeo.js";function Q({def:c,value:b,onChange:m}){const{toast:p,guard:i,patchUi:o}=K(),[j,s]=d.useState(null),u=d.useRef(null),h=b||"",C=d.useCallback(async()=>{try{s(await Y())}catch{}},[]);d.useEffect(()=>{C()},[C]);const x=((j==null?void 0:j.files)||[]).find(n=>n.id===h)||null,$=i(async n=>{const g=n==null?void 0:n[0];if(!g)return;const y=await z(g),S=await N("/api/files",{method:"POST",body:{content:y,name:g.name}});m(S.id),p(S.duplicated?"库中已有相同文件,已直接引用":"已上传并关联"),s(await G())});return e.jsxs("div",{className:"file-picker",children:[x?e.jsxs("div",{className:"file-preview",children:[x.isImage?e.jsx("img",{src:x.url,alt:x.name}):e.jsx("span",{className:"ext-badge",style:{fontSize:13,width:40,textAlign:"center"},children:x.ext?x.ext.slice(1).toUpperCase():"FILE"}),e.jsx("a",{className:"file-preview-name grow",href:x.url,target:"_blank",rel:"noreferrer",title:x.displayPath,children:x.name}),e.jsx("a",{className:"btn small ghost",href:x.downloadUrl,children:"下载"}),e.jsx(B,{className:"hint",text:x.path,title:"点击复制物理路径",children:"路径"}),e.jsx("button",{type:"button",className:"btn small ghost",onClick:()=>m(""),children:"清除"})]}):null,e.jsxs("div",{className:"file-pick-row",children:[e.jsxs("select",{value:h,onChange:n=>m(n.target.value),children:[e.jsx("option",{value:"",children:"(未选择:从附件库选一个)"}),((j==null?void 0:j.files)||[]).map(n=>e.jsx("option",{value:n.id,children:n.name},n.id))]}),e.jsx("button",{type:"button",className:"btn small",onClick:()=>{var n;return(n=u.current)==null?void 0:n.click()},children:"上传文件"}),e.jsx("button",{type:"button",className:"btn small ghost",onClick:()=>o({view:"files"}),children:"附件中心"}),e.jsx("input",{ref:u,type:"file",style:{display:"none"},onChange:n=>{$(n.target.files),n.target.value=""}})]}),h&&!x?e.jsxs("span",{className:"hint bad",children:["引用的附件已不存在(",h,")—— 重新上传,或清除。"]}):null,c.hint?e.jsx("span",{className:"hint",children:c.hint}):null]})}function W({def:c,value:b,onChange:m,disabled:p}){const i=c.type||"text",o=b??"";if(i==="file")return e.jsx(Q,{def:c,value:o,onChange:m});if(i==="bool"){const s=o===!0||o==="true"||o==="是";return e.jsxs("label",{className:"inline",children:[e.jsx("input",{type:"checkbox",checked:s,onChange:u=>m(u.target.checked)}),e.jsx("span",{className:"muted",children:s?"是":"否"})]})}if(i==="select"){const s=c.options||[],u=o!==""&&!s.includes(o);return e.jsxs("select",{value:u?"__custom":String(o),onChange:h=>m(h.target.value==="__custom"?"":h.target.value),children:[e.jsx("option",{value:"",children:"(未填写)"}),s.map(h=>e.jsx("option",{value:h,children:h},h)),u?e.jsxs("option",{value:"__custom",children:[String(o),"(当前值,不在候选里)"]}):null]})}if(i==="textarea")return e.jsx("textarea",{value:String(o),onChange:s=>m(s.target.value),rows:3});if(i==="tags"){const s=Array.isArray(o)?o.join("、"):String(o);return e.jsx("input",{value:s,placeholder:c.maxItems?`多个用「、」分隔,最多 ${c.maxItems} 个`:"多个用「、」分隔",onChange:u=>m(u.target.value.split(/[,,、;;]+/).map(h=>h.trim()).filter(Boolean))})}const j=i==="number"?"number":i==="date"?"date":i==="month"?"month":"text";return i==="secret"?e.jsx("input",{type:"text",value:String(o),placeholder:"密文落盘;原样保留占位则视为不修改",disabled:p,onChange:s=>m(s.target.value)}):e.jsx("input",{type:j,value:String(o),onChange:s=>m(s.target.value)})}function J({title:c,fields:b,draft:m,onPatch:p}){return e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"group-title",children:c}),e.jsx("div",{className:"pad",children:e.jsx("div",{className:"grid",children:b.map(i=>e.jsxs("div",{className:`field ${i.type==="textarea"||i.type==="file"?"wide":""}`,children:[e.jsxs("div",{className:"flabel",children:[e.jsx("span",{children:i.label}),i.sensitive?e.jsx("span",{className:"tag bad",children:"密文"}):null,i.fill&&i.fill!=="normal"?e.jsx("span",{className:"tag",children:i.fillLabel||i.fill}):null,e.jsx("span",{className:"fkey",children:i.key})]}),e.jsx("div",{className:"fctrl",children:e.jsx(W,{def:i,value:m[i.key],onChange:o=>p(i.key,o)})})]},i.key))})})]})}function X({sectionId:c,sectionTitle:b}){const{ui:m,patchUi:p,toast:i,guard:o,dialog:j,reload:s}=K(),[u,h]=d.useState(null),[C,x]=d.useState({}),[$,n]=d.useState({name:"",value:""}),g=!!m.mask,y=d.useCallback(async()=>{try{const l=`section=${encodeURIComponent(c)}${g?"&mask=1":""}`,a=await N(`/api/keys?${l}`);h(a);const v={};for(const E of a.keys)v[E.name]=E.value??"";x(v)}catch(l){i(l.message,"bad"),h(null)}},[c,g,i]);d.useEffect(()=>{y()},[y]);const S=o(async l=>{const a=C[l.name];(l.value??"")!==a&&(await N(`/api/keys/${encodeURIComponent(l.name)}`,{method:"PUT",body:{value:a,section:c}}),i(`已保存 ${l.name}`),await y())}),T=o(async(l,a)=>{const v=String(a||"").trim();if(!(!v||v===l.name)){if(u.keys.some(E=>E.name===v)){i(`已经有叫「${v}」的键了`,"bad"),await y();return}await N(`/api/entries/${encodeURIComponent(l.id)}`,{method:"PATCH",body:{title:v}}),x(E=>{const k={};for(const[D,F]of Object.entries(E))k[D===l.name?v:D]=F;return k}),i(`已重命名为 ${v}`),await y(),await s()}}),U=o(async()=>{const l=$.name.trim();if(!l){i("先填键名,例如 OPENAI_KEY","bad");return}if(u.keys.some(a=>a.name===l)){i(`已经有叫「${l}」的键了——直接在上面那行改值即可`,"bad");return}await N(`/api/keys/${encodeURIComponent(l)}`,{method:"PUT",body:{value:$.value,section:c}}),n({name:"",value:""}),i(`已新增 ${l}`),await y(),await s()}),A=o(async l=>{await j({message:`删除「${l.name}」?删除前会自动留一份整库快照在 ~/nx-sk/backup。`,danger:!0,confirmText:"删除"})&&(await N(`/api/keys/${encodeURIComponent(l.name)}`,{method:"DELETE",body:{section:c}}),i(`已删除 ${l.name}`),await y(),await s())});return u?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"toolbar rowgap pad",children:[e.jsx("strong",{children:b}),e.jsxs("span",{className:"muted",children:[u.count," 个键 · 名称 → 值"]}),e.jsx("span",{className:"grow"}),e.jsxs("label",{className:"inline hint",children:[e.jsx("input",{type:"checkbox",checked:g,onChange:()=>p({mask:!g})}),"打码显示(投屏/截图时用)"]}),e.jsx("button",{className:"btn small ghost",onClick:y,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:[u.keys.length?u.keys.map(l=>e.jsxs("div",{className:"row",children:[e.jsx("input",{className:"mono",style:{width:200},defaultValue:l.name,title:"改完按回车或点开别处即重命名",onBlur:a=>T(l,a.target.value),onKeyDown:a=>{a.key==="Enter"&&a.currentTarget.blur()}}),e.jsx("input",{className:"mono grow",value:C[l.name]??"",onChange:a=>x(v=>({...v,[l.name]:a.target.value})),onBlur:()=>S(l),onKeyDown:a=>{a.key==="Enter"&&a.currentTarget.blur()}}),e.jsxs("span",{className:"acts",children:[e.jsx(B,{className:"tag",text:C[l.name]??"",title:"点击复制值",children:"复制"}),e.jsx("button",{className:"btn small ghost",onClick:()=>A(l),children:"删除"})]})]},l.id)):e.jsx(R,{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:$.name,onChange:l=>n(a=>({...a,name:l.target.value})),onKeyDown:l=>{l.key==="Enter"&&U()}}),e.jsx("input",{className:"mono grow",placeholder:"值(留空也行,之后再填)",value:$.value,onChange:l=>n(a=>({...a,value:l.target.value})),onKeyDown:l=>{l.key==="Enter"&&U()}}),e.jsx("span",{className:"acts",children:e.jsx("button",{className:"btn small",onClick:U,children:"添加"})})]})]})]}),e.jsx("div",{className:"pad hint",children:"改值:直接在「值」列改完按回车(或点开别处)即保存。改键名同理。 值以密文落盘,这里显示的是原文;要打码勾上方那个框。删除前自动整库快照。"}),e.jsx(M,{module:"entries"})]}):e.jsx("div",{className:"pad muted",children:"加载中…"})}const O=c=>c==null?"":Array.isArray(c)?c.join("、"):String(c);function se({sectionId:c}){var I;const{ui:b,patchUi:m,toast:p,guard:i,dialog:o,reload:j}=K(),[s,u]=d.useState(null),[h,C]=d.useState(null),[x,$]=d.useState(!1),[n,g]=d.useState(null),[y,S]=d.useState(null),[T,U]=d.useState(""),A=!!b.mask,l=d.useCallback(async t=>{$(!0),C(null);try{const r=await N(`/api/sections/${encodeURIComponent(c)}/dump${A?"?mask=1":""}`);u(r);const w=t||b.entry,f=r.entries.find(P=>P.id===w)||r.entries[0]||null;g(f?{id:f.id,title:f.title,values:{...f.values}}:null),f&&f.id!==b.entry&&m({entry:f.id})}catch(r){C(r.message),u(null),g(null)}finally{$(!1)}},[c,A]);d.useEffect(()=>{l()},[l]);const a=d.useMemo(()=>(s==null?void 0:s.entries.find(t=>t.id===(n==null?void 0:n.id)))||null,[s,n]),v=d.useMemo(()=>s?s.groups.map(t=>({...t,fields:s.fields.filter(r=>r.group===t.id)})).filter(t=>t.fields.length):[],[s]),E=(t,r)=>g(w=>w&&{...w,values:{...w.values,[t]:r}}),k=d.useMemo(()=>{if(!n||!a)return{set:{},titleChanged:!1,count:0};const t={};for(const r of s.fields)O(a.values[r.key])!==O(n.values[r.key])&&(t[r.key]=n.values[r.key]);return{set:t,titleChanged:n.title!==a.title,count:Object.keys(t).length+(n.title!==a.title?1:0)}},[n,a,s]),D=i(async()=>{if(!n||!a)return;if(!k.count){p("没有改动");return}const t={set:k.set};k.titleChanged&&(t.title=n.title),await N(`/api/entries/${encodeURIComponent(n.id)}`,{method:"PATCH",body:t}),p(`已保存 ${k.count} 处改动`),await l(n.id),await j()}),F=i(async()=>{var P;if(!n){p("先新建一条条目","bad");return}const t=T.indexOf("=");if(t<=0){p("用 字段=值 的格式,例如 微信=zhangsan","bad");return}const r=T.slice(0,t).trim(),w=T.slice(t+1),f=await N(`/api/entries/${encodeURIComponent(n.id)}`,{method:"PATCH",body:{set:{[r]:w}}});U(""),p(`已写入 ${r}${(P=f.newFields)!=null&&P.length?"(新字段已登记)":""}`),await l(n.id),await j()}),H=i(async()=>{const t=(s==null?void 0:s.section.titleLabel)||"名称",r=await o({message:`新建「${s==null?void 0:s.section.title}」条目,${t}是?`,input:{placeholder:`例如 ${(s==null?void 0:s.section.titleField)||"名称"}`}});if(!r)return;const w={section:c,title:r};s!=null&&s.section.titleField&&(w.set={[s.section.titleField]:r});const f=await N("/api/entries",{method:"POST",body:w});p(`已新建:${f.title}`),await l(f.id),await j()}),L=i(async()=>{!a||!await o({message:`删除条目「${a.title}」?删除前会自动留一份快照在 ~/nx-sk/backup。`,danger:!0,confirmText:"删除"})||(await N(`/api/entries/${encodeURIComponent(a.id)}`,{method:"DELETE"}),p("已删除"),await l(""),await j())}),q=i(async()=>{const t=await N("/api/export",{method:"POST",body:{section:c,format:"both"}});S({title:"导出结果",body:e.jsx("pre",{className:"block",children:`目录: ${t.dir}
|
|
2
|
+
格式: ${t.format}
|
|
3
|
+
条目: ${t.entries}
|
|
4
|
+
凭据: ${t.withSecrets?"含明文":"已打码"}
|
|
5
|
+
|
|
6
|
+
${t.files.map(r=>`${r.path} ${r.bytes} 字节`).join(`
|
|
7
|
+
`)}`})})});return h?e.jsxs("div",{className:"pad stack",children:[e.jsx("div",{className:"bad",children:e.jsx("strong",{children:"栏目读不到"})}),e.jsx("pre",{className:"block",children:h}),e.jsx("div",{className:"hint",children:"如果这个栏目刚被删掉,切到「栏目」tab 看看现有栏目。"})]}):s?s.section.kv?e.jsx(X,{sectionId:c,sectionTitle:s.section.title}):e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"toolbar rowgap pad",children:[e.jsx("strong",{children:s.section.title}),e.jsx(_,{children:s.section.id}),e.jsxs("span",{className:"muted",children:[s.count," 条 / ",s.section.fields," 字段"]}),s.section.description?e.jsx("span",{className:"desc muted nowrap grow",children:s.section.description}):e.jsx("span",{className:"grow"}),e.jsxs("label",{className:"inline hint",children:[e.jsx("input",{type:"checkbox",checked:A,onChange:()=>m({mask:!A})}),"打码显示(投屏/截图时用)"]}),e.jsx("button",{className:"btn small ghost",onClick:()=>l(),children:"刷新"}),e.jsx("button",{className:"btn small ghost",onClick:q,children:"导出本栏目"}),e.jsx("button",{className:"btn small",onClick:H,children:"新建条目"})]}),e.jsxs("div",{className:"cols",children:[e.jsx("div",{className:"col",style:{flex:"0 0 300px"},children:e.jsxs("div",{className:"card",children:[e.jsxs("div",{className:"colhead",children:["条目列表",e.jsx("span",{className:"right muted",children:s.count})]}),e.jsx("div",{className:"colbody",children:s.entries.length?s.entries.map(t=>e.jsxs("div",{className:`row selectable ${t.id===(n==null?void 0:n.id)?"selected":""}`,onClick:()=>{m({entry:t.id}),g({id:t.id,title:t.title,values:{...t.values}})},children:[e.jsx("span",{className:"name",children:t.title||"(无名)"}),e.jsx("span",{className:"acts",children:t.missing.length?e.jsxs("span",{className:"tag",children:[t.completeness.ratio,"%"]}):e.jsx("span",{className:"tag strong",children:"已填满"})})]},t.id)):e.jsxs(R,{children:["还没有条目。点右上角「新建条目」,或用 CLI:nx-sk entry add --section ",s.section.id," --set ..."]})})]})}),e.jsx("div",{className:"col",children:n?e.jsxs("div",{className:"card",children:[e.jsxs("div",{className:"colhead",children:[e.jsx("span",{className:"grow nowrap",children:(a==null?void 0:a.title)||n.title}),e.jsx("span",{className:"muted mono",children:n.id}),a?e.jsxs(_,{kind:a.missing.length?"":"strong",children:["完整度 ",a.completeness.ratio,"%"]}):null]}),e.jsxs("div",{className:"field wide pad",style:{borderBottom:"1px solid var(--soft-2)"},children:[e.jsxs("div",{className:"flabel",children:[e.jsx("span",{children:s.section.titleLabel||"名称"}),e.jsx("span",{className:"bad",children:"改动后必须保存"})]}),e.jsx("div",{className:"fctrl",children:e.jsx("input",{value:n.title,onChange:t=>g(r=>({...r,title:t.target.value}))})})]}),e.jsxs("div",{className:"rowgap",style:{padding:"6px 14px",borderBottom:"1px solid var(--soft-2)"},children:[e.jsx("input",{className:"grow",style:{minWidth:220},placeholder:"直接加字段:字段=值 (回车写入;没见过的键会自动登记)",value:T,onChange:t=>U(t.target.value),onKeyDown:t=>{t.key==="Enter"&&F()}}),e.jsx("button",{className:"btn small",onClick:F,disabled:!T.includes("="),children:"写入"})]}),v.map(t=>e.jsx(J,{title:t.title,fields:t.fields,draft:n.values,onPatch:E},t.id)),e.jsxs("div",{className:"rowgap pad",children:[e.jsxs("button",{className:"btn",onClick:D,disabled:!k.count,children:["保存",k.count?`(${k.count} 处改动)`:""]}),e.jsx("button",{className:"btn ghost",onClick:()=>l(n.id),disabled:!k.count,children:"放弃改动"}),e.jsx("button",{className:"btn ghost",onClick:L,children:"删除条目"}),a!=null&&a.missing.length?e.jsxs("span",{className:"hint",children:["模板建议里还有 ",a.missing.length," 项没填(不影响使用):",a.missing.slice(0,5).map(t=>t.label).join("、"),a.missing.length>5?"…":""]}):null,(I=a==null?void 0:a.excluded)!=null&&I.length?e.jsxs("span",{className:"hint",children:["另有 ",a.excluded.length," 项已标不填、不计入完整度:",a.excluded.slice(0,6).map(t=>t.label).join("、"),a.excluded.length>6?"…":""]}):null]})]}):e.jsx("div",{className:"card",children:e.jsx(R,{children:"左边选一条,或新建一条"})})})]}),e.jsx(M,{module:"entries"}),y?e.jsx(V,{title:y.title,onClose:()=>S(null),children:y.body}):null]}):e.jsx("div",{className:"pad muted",children:x?"加载中…":"暂无数据"})}export{se as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{u as q,r,a as p,j as e,b as w,T as A,E as H,C as _,M as z}from"./index-B7CtqgoG.js";import{f as K,r as f}from"./files-BNh48qeo.js";function S(t){return t<1024?`${t} B`:t<1024*1024?`${(t/1024).toFixed(1)} KB`:`${(t/(1024*1024)).toFixed(2)} MB`}const Q=t=>`${t.sectionTitle} / ${t.entryTitle||"(无名)"} / ${t.fieldLabel}`;function X(){const{toast:t,guard:l,dialog:m,reload:n}=q(),[i,k]=r.useState(null),[C,$]=r.useState(null),[u,N]=r.useState(""),[g,E]=r.useState(""),[D,v]=r.useState(!1),[d,h]=r.useState(null),T=r.useRef(null),x=r.useRef(0),o=r.useCallback(async()=>{try{const a=new URLSearchParams;u&&a.set("section",u),g&&a.set("q",g);const s=a.toString(),c=await p(`/api/files${s?`?${s}`:""}`);k(c),$(null)}catch(a){$(a.message),k(null)}},[u,g]);r.useEffect(()=>{o()},[o]);const b=r.useCallback(async a=>{const s=Array.from(a||[]);if(!s.length)return;let c=0;for(const j of s)try{const y=await K(j);await p("/api/files",{method:"POST",body:{content:y,name:j.name}}),c+=1}catch(y){t(`${j.name} 上传失败:${y.message}`,"bad")}c&&t(`已导入 ${c} 个附件(到栏目条目里选它即可链接)`),await f(),await o(),await n()},[o,n,t]),P=r.useCallback(a=>{a.preventDefault(),x.current=0,v(!1),b(a.dataTransfer.files)},[b]),I=l(async()=>{h({kind:"path",form:{path:"",copy:!1}})}),R=l(async a=>{if(!a.path.trim()){t("请填本机文件的绝对路径","bad");return}const s=await p("/api/files",{method:"POST",body:{path:a.path.trim(),copy:a.copy}});h(null),t(s.duplicated?"库中已有相同文件":s.moved?"已移动进附件库(可还原)":"已复制进附件库"),await f(),await o(),await n()}),B=l(async a=>{h({kind:"edit",id:a.id,form:{name:a.name,tags:a.tags.join("、"),note:a.note}})}),F=l(async(a,s)=>{await p(`/api/files/${encodeURIComponent(a)}`,{method:"PATCH",body:{name:s.name,tags:s.tags.split(/[,,、;;]+/).map(c=>c.trim()).filter(Boolean),note:s.note}}),h(null),t("附件已更新"),await f(),await o()}),U=l(async a=>{const s=await m({message:`把「${a.name}」还原到哪?`,detail:a.sourceDisplay?`默认移回原始位置并撤销台账:${a.sourceDisplay}`:"该附件没有原始位置记录,请在下一步填写目标目录。",input:a.sourceDisplay?void 0:{placeholder:"目标目录绝对路径,例如 D:\\资料"},confirmText:"移回并撤销台账",cancelText:"取消"});if(s===null)return;const c={};s&&typeof s=="string"&&(c.to=s.trim());const j=await p(`/api/files/${encodeURIComponent(a.id)}/restore`,{method:"POST",body:c});t(`已还原到 ${j.displayPath}`),await f(),await o(),await n()}),O=l(async a=>{const s=await m({message:`导出「${a.name}」的副本到哪个目录?库中原件保留。`,input:{placeholder:"目标目录绝对路径,例如 D:\\资料"},confirmText:"导出副本"});if(!s)return;const c=await p(`/api/files/${encodeURIComponent(a.id)}/restore`,{method:"POST",body:{to:String(s).trim(),copy:!0}});t(`已导出到 ${c.displayPath}`)}),L=l(async a=>{await m({message:`删除附件「${a.name}」?物理原件会一起删除,删除前自动留台账快照。`,detail:"想保留原件、只撤销台账,请改用「还原」。",danger:!0,confirmText:"删除"})&&(await p(`/api/files/${encodeURIComponent(a.id)}`,{method:"DELETE"}),t("已删除"),await f(),await o(),await n())});if(C)return e.jsxs("div",{className:"pad stack",children:[e.jsx("div",{className:"bad",children:e.jsx("strong",{children:"附件库读不到"})}),e.jsx("pre",{className:"block",children:C})]});if(!i)return e.jsx("div",{className:"pad muted",children:"加载中…"});const M=i.sections||[];return e.jsxs("div",{className:`files-page ${D?"drag-over":""}`,onDragEnter:a=>{a.preventDefault(),x.current+=1,v(!0)},onDragOver:a=>a.preventDefault(),onDragLeave:a=>{a.preventDefault(),x.current-=1,x.current<=0&&(x.current=0,v(!1))},onDrop:P,children:[e.jsxs("div",{className:"toolbar rowgap pad",children:[e.jsx("strong",{children:"附件"}),e.jsxs("span",{className:"muted",children:[i.total," 个 · ",S(i.totalBytes)]}),e.jsxs("span",{className:"hint",title:"被栏目条目的 file 字段引用的附件数",children:["被引用 ",i.total-i.unlinked,"/",i.total]}),e.jsx(w,{className:"hint",text:i.rootRaw,title:"点击复制附件库目录",children:i.root}),e.jsx("span",{className:"grow"}),e.jsx("input",{style:{width:180},placeholder:"搜索名称 / 备注 / 标签 / 引用它的条目",value:g,onChange:a=>E(a.target.value)}),e.jsx("button",{className:"btn small ghost",onClick:o,children:"刷新"}),e.jsx("button",{className:"btn small ghost",onClick:I,children:"本机路径导入"}),e.jsx("button",{className:"btn small",onClick:()=>{var a;return(a=T.current)==null?void 0:a.click()},children:"上传文件"}),e.jsx("input",{ref:T,type:"file",multiple:!0,style:{display:"none"},onChange:a=>{b(a.target.files),a.target.value=""}})]}),e.jsxs("div",{className:"rowgap pad file-filters",children:[e.jsxs("button",{className:`tab small ${u?"":"active"}`,onClick:()=>N(""),children:["全部",e.jsx("span",{className:"count",children:i.total})]}),M.map(a=>e.jsxs("button",{className:`tab small ${u===a.id?"active":""}`,onClick:()=>N(a.id),title:`被「${a.title}」栏目引用的附件`,children:[a.title,e.jsx("span",{className:"count",children:a.count})]},a.id)),e.jsxs("button",{className:`tab small ${u==="none"?"active":""}`,onClick:()=>N("none"),children:["未引用",e.jsx("span",{className:"count",children:i.unlinked})]})]}),D?e.jsx("div",{className:"drop-hint",children:"松开鼠标,文件直接导入附件库"}):null,i.files.length?e.jsx("div",{className:"file-grid pad",children:i.files.map(a=>{var s;return e.jsxs("div",{className:"file-card",children:[e.jsx("a",{className:"file-thumb",href:a.url,target:"_blank",rel:"noreferrer",title:"在浏览器中预览",children:a.isImage?e.jsx("img",{src:a.url,alt:a.name,loading:"lazy"}):e.jsx("span",{className:"ext-badge",children:a.ext?a.ext.slice(1).toUpperCase():"FILE"})}),e.jsxs("div",{className:"file-meta",children:[e.jsx("a",{className:"file-name",href:a.url,target:"_blank",rel:"noreferrer",title:a.name,children:a.name}),e.jsxs("div",{className:"rowgap",style:{gap:4},children:[e.jsx("span",{className:"hint",children:S(a.bytes)}),a.mode==="move"?e.jsx(A,{children:"移动导入"}):null]}),(s=a.usedBy)!=null&&s.length?e.jsxs("div",{className:"hint",title:"条目的 file 字段引用了本附件",children:["被引用:",a.usedBy.map(Q).join("、")]}):e.jsx("div",{className:"hint",children:"未被任何条目引用(到栏目条目里选它即可链接)"})]}),e.jsxs("div",{className:"file-acts",children:[e.jsx("a",{className:"btn small ghost",href:a.downloadUrl,children:"下载"}),e.jsx("button",{className:"btn small ghost",onClick:()=>O(a),children:"导出副本"}),e.jsx("button",{className:"btn small ghost",onClick:()=>U(a),children:"还原"}),e.jsx("button",{className:"btn small ghost",onClick:()=>B(a),children:"编辑"}),e.jsx("button",{className:"btn small ghost danger-ghost",onClick:()=>L(a),children:"删除"}),e.jsx("span",{className:"grow"}),e.jsx(w,{className:"hint",text:a.path,title:"点击复制物理路径",children:"路径"}),e.jsx(w,{className:"hint",text:a.url,title:"点击复制链接",children:"链接"})]})]},a.id)})}):e.jsxs("div",{className:"pad",children:[e.jsx(H,{children:"还没有附件。把文件拖到这里、点「上传文件」,或用「本机路径导入」把原件移动进库;随后到栏目的条目里选它。"}),e.jsx("div",{className:"hint pad",children:'CLI:nx-sk file add --path "D:\\简历\\张三-简历.pdf"(默认移动;--copy 仅复制)'})]}),e.jsx(_,{module:"files"}),d?e.jsx(z,{title:d.kind==="edit"?"编辑附件":"从本机路径导入",onClose:()=>h(null),children:d.kind==="path"?e.jsx(V,{form:d.form,onChange:a=>h(s=>({...s,form:{...s.form,...a}})),onSubmit:()=>R(d.form)}):e.jsx(G,{form:d.form,onChange:a=>h(s=>({...s,form:{...s.form,...a}})),onSubmit:()=>F(d.id,d.form)})}):null]})}function V({form:t,onChange:l,onSubmit:m}){return e.jsxs("div",{className:"stack",children:[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:t.path,placeholder:"例如 D:\\简历\\张三-简历.pdf",onChange:n=>l({path:n.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.jsxs("label",{className:"inline",children:[e.jsx("input",{type:"checkbox",checked:t.copy,onChange:n=>l({copy:n.target.checked})}),e.jsx("span",{className:"hint",children:"仅复制、保留原件(默认把原件移动进库,可用「还原」撤销)"})]})})]}),e.jsx("div",{className:"rowgap",children:e.jsx("button",{className:"btn",onClick:m,children:"导入"})})]})}function G({form:t,onChange:l,onSubmit:m}){return e.jsxs("div",{className:"stack",children:[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:t.name,onChange:n=>l({name:n.target.value})})})]}),e.jsxs("div",{className:"field wide",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:t.tags,onChange:n=>l({tags:n.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("textarea",{rows:2,value:t.note,onChange:n=>l({note:n.target.value})})})]}),e.jsxs("div",{className:"rowgap",children:[e.jsx("button",{className:"btn",onClick:m,children:"保存"}),e.jsx("span",{className:"hint",children:"改文件名时,物理原件会随之移动。要链接到条目,请在栏目条目的 file 字段里选本附件。"})]})]})}export{X 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-B7CtqgoG.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 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-B7CtqgoG.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};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{u as R,r as j,a as h,j as e,T as N,E as T,C as I,M as U}from"./index-B7CtqgoG.js";const P=["text","textarea","number","date","month","bool","select","tags","secret","file"];function M(){const{reload:d,toast:n,guard:o,dialog:u}=R(),[t,b]=j.useState(null),[f,r]=j.useState([]),[x,i]=j.useState(null),[c,m]=j.useState(null),a=j.useCallback(async()=>{try{const[s,l]=await Promise.all([h("/api/sections"),h("/api/sections/templates")]);b(s),r(l.templates),i(null)}catch(s){i(s.message)}},[]);j.useEffect(()=>{a()},[a]);const p=o(async s=>{const l=await h(`/api/sections/${encodeURIComponent(s.id)}/dump`);m({kind:"dump",title:`${s.title}(${s.id})的全部信息`,body:e.jsx("pre",{className:"block",children:JSON.stringify(l,null,2)})})}),C=o(async s=>{const l=await h(`/api/sections/${encodeURIComponent(s.id)}`);m({kind:"edit",id:s.id,form:{title:l.title,description:l.description,order:String(l.order),titleLabel:l.titleLabel,titleField:l.titleField||""},full:l})}),w=o(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("栏目已创建"),m(null),await a(),await d()}),$=o(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 v=await h(`/api/sections/${encodeURIComponent(s.id)}`);m({...s,full:v}),await a(),await d()}),S=o(async(s,l)=>{await h(`/api/sections/${encodeURIComponent(s.id)}`,{method:"PATCH",body:{"add-field":l}}),n(`已新增字段 ${l.key}`);const v=await h(`/api/sections/${encodeURIComponent(s.id)}`);m({...s,full:v}),await a()}),F=o(async s=>{var y,k;const l=await h(`/api/sections/${encodeURIComponent(s.id)}/sync`,{method:"POST"});if(l.upToDate){n("已是最新,模板里没有需要补的字段");return}const v=[(y=l.addedGroups)!=null&&y.length?`分组 ${l.addedGroups.join("、")}`:"",(k=l.addedFields)!=null&&k.length?`字段 ${l.addedFields.join("、")}`:""].filter(Boolean).join(";");n(`已按模板补齐:${v||"无变化"}`);const g=await h(`/api/sections/${encodeURIComponent(s.id)}`);m({...s,full:g}),await a(),await d()}),E=o(async(s,l)=>{if(!await u({message:`从字段字典里删掉「${l}」?已填的值会留在条目里(只是不再显示/校验)。`,danger:!0,confirmText:"删除字段"}))return;await h(`/api/sections/${encodeURIComponent(s.id)}`,{method:"PATCH",body:{"remove-field":l}}),n(`已删字段 ${l}`);const g=await h(`/api/sections/${encodeURIComponent(s.id)}`);m({...s,full:g}),await a()}),L=o(async s=>{await u({message:`删除栏目「${s.title}」,连带其中 ${s.entries} 条条目?删除前会自动留快照。`,danger:!0,confirmText:"连带删除"})&&(await h(`/api/sections/${encodeURIComponent(s.id)}`,{method:"DELETE",body:{force:!0}}),n("栏目已删除"),await a(),await d())});return x?e.jsxs("div",{className:"pad stack",children:[e.jsx("div",{className:"bad",children:e.jsx("strong",{children:"读不到栏目"})}),e.jsx("pre",{className:"block",children:x})]}):t?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"toolbar rowgap pad",children:[e.jsx("strong",{children:"栏目"}),e.jsxs("span",{className:"muted",children:["共 ",t.count," 个 · 条目 ",t.totalEntries," 条"]}),e.jsx("span",{className:"grow"}),e.jsx("button",{className:"btn small ghost",onClick:a,children:"刷新"}),e.jsx("button",{className:"btn small",onClick:()=>{var s;return m({kind:"create",form:{id:"",title:"",template:((s=f[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:t.sections.length?t.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(N,{children:s.template}):e.jsx(N,{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:()=>p(s),children:"全部信息"}),e.jsx("button",{className:"btn small ghost",onClick:()=>C(s),children:"编辑"}),e.jsx("button",{className:"btn small ghost",onClick:()=>L(s),children:"删除"})]})]},s.id)):e.jsx(T,{children:"还没有栏目。点「新建栏目」,用内置模板(求职 / 密钥)起步。"})})]}),e.jsx("div",{className:"pad hint",children:"栏目 = 一组同构条目 + 一份字段字典。字典是数据,所以这里加一个字段,CLI 与面板立刻就都能填它。"}),e.jsx(I,{module:"sections"}),c?e.jsxs(U,{title:z(c),onClose:()=>m(null),children:[c.kind==="dump"?c.body:null,c.kind==="create"?e.jsx(A,{templates:f,form:c.form,onChange:s=>m(l=>({...l,form:{...l.form,...s}})),onSubmit:()=>{if(!/^[a-z][a-z0-9_-]{0,63}$/.test(c.form.id.trim())){n("栏目 id 必须是小写字母开头,仅含 a-z 0-9 _ -","bad");return}w(c.form)}}):null,c.kind==="edit"?e.jsx(D,{modal:c,onChange:s=>m(l=>({...l,form:{...l.form,...s}})),onSave:()=>$(c),onSync:()=>F(c),onAddField:s=>S(c,s),onRemoveField:s=>E(c,s),onClose:()=>m(null)}):null]}):null]}):e.jsx("div",{className:"pad muted",children:"加载中…"})}function z(d){return d.kind==="dump"?d.title:d.kind==="create"?"新建栏目":`编辑栏目 ${d.id}`}function A({templates:d,form:n,onChange:o,onSubmit:u}){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:t=>o({id:t.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:t=>o({title:t.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:t=>o({template:t.target.value}),children:[e.jsx("option",{value:"",children:"(空栏目,自定义字段)"}),d.map(t=>e.jsxs("option",{value:t.id,children:[t.label,"(",t.fields," 字段)"]},t.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:t=>o({order:t.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:t=>o({description:t.target.value})})})]}),e.jsxs("div",{className:"rowgap",children:[e.jsx("button",{className:"btn",onClick:u,children:"创建"}),e.jsx("span",{className:"hint",children:"同 id 已存在会直接报冲突,不会静默覆盖。"})]})]})}function D({modal:d,onChange:n,onSave:o,onSync:u,onAddField:t,onRemoveField:b,onClose:f}){var m;const[r,x]=j.useState({key:"",label:"",type:"text",group:""}),i=d.full,c=(i==null?void 0:i.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:d.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:d.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:d.form.titleField,onChange:a=>n({titleField:a.target.value}),children:[e.jsx("option",{value:"",children:"(用条目 title)"}),((i==null?void 0:i.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:d.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:d.form.description,onChange:a=>n({description:a.target.value})})})]}),e.jsxs("div",{className:"rowgap",children:[e.jsx("button",{className:"btn",onClick:o,children:"保存栏目属性"}),i!=null&&i.template?e.jsx("button",{className:"btn ghost",onClick:u,title:`按模板「${i.template}」补齐新增的分组与字段(只增不改不删)`,children:"同步模板字段"}):null,e.jsx("button",{className:"btn ghost",onClick:f,children:"关闭"})]}),i!=null&&i.template?e.jsxs("div",{className:"hint",children:["本栏目由模板「",i.template,"」创建。模板后来新增的字段**不会自动**出现在这里, 点「同步模板字段」补齐(例如求职模板新增的「附件资料」组 / 简历、头像等 file 字段)。"]}):null,e.jsxs("div",{className:"card",style:{border:"1px solid var(--soft-2)",borderRadius:4},children:[e.jsxs("div",{className:"colhead",children:["字段字典(",((m=i==null?void 0:i.fieldList)==null?void 0:m.length)||0,")"]}),e.jsxs("div",{className:"colbody",children:[c.map(a=>e.jsxs("div",{children:[e.jsxs("div",{className:"group-title",children:[a.title,"(",a.id,")"]}),(i.fieldList||[]).filter(p=>p.group===a.id).map(p=>e.jsxs("div",{className:"row",children:[e.jsx("span",{className:"name",children:p.label}),e.jsx("span",{className:"mono muted",children:p.key}),e.jsx(N,{children:p.type}),p.sensitive?e.jsx(N,{kind:"bad",children:"密文"}):null,e.jsx("span",{className:"desc",children:p.hint||""}),e.jsx("span",{className:"acts",children:e.jsx("button",{className:"btn small ghost",onClick:()=>b(p.key),children:"删除"})})]},p.key))]},a.id)),((i==null?void 0:i.fieldList)||[]).filter(a=>!c.some(p=>p.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(N,{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:()=>b(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:r.key,onChange:a=>x({...r,key:a.target.value})}),e.jsx("input",{style:{width:150},placeholder:"标签(中文名)",value:r.label,onChange:a=>x({...r,label:a.target.value})}),e.jsx("select",{style:{width:110},value:r.type,onChange:a=>x({...r,type:a.target.value}),children:P.map(a=>e.jsx("option",{value:a,children:a},a))}),e.jsxs("select",{style:{width:150},value:r.group,onChange:a=>x({...r,group:a.target.value}),children:[e.jsx("option",{value:"",children:"(首个分组)"}),c.map(a=>e.jsx("option",{value:a.id,children:a.title},a.id))]}),e.jsx("button",{className:"btn small",onClick:()=>{const a={key:r.key.trim(),label:r.label.trim()||r.key.trim(),type:r.type};r.group&&(a.group=r.group),t(a),x({key:"",label:"",type:"text",group:r.group})},children:"加入字典"}),e.jsx("span",{className:"hint",children:"加完立刻生效:CLI 与面板都能填它。"})]})]})]})}export{M 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-B7CtqgoG.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-B7CtqgoG.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DLtjhflt.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}
|
|
@@ -1 +0,0 @@
|
|
|
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,7 +0,0 @@
|
|
|
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-BnVuH1lC.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
|
-
格式: ${t.format}
|
|
3
|
-
条目: ${t.entries}
|
|
4
|
-
凭据: ${t.withSecrets?"含明文":"已打码"}
|
|
5
|
-
|
|
6
|
-
${t.files.map(c=>`${c.path} ${c.bytes} 字节`).join(`
|
|
7
|
-
`)}`})})});return h?e.jsxs("div",{className:"pad stack",children:[e.jsx("div",{className:"bad",children:e.jsx("strong",{children:"栏目读不到"})}),e.jsx("pre",{className:"block",children:h}),e.jsx("div",{className:"hint",children:"如果这个栏目刚被删掉,切到「栏目」tab 看看现有栏目。"})]}):s?s.section.kv?e.jsx(G,{sectionId:o,sectionTitle:s.section.title}):e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"toolbar rowgap pad",children:[e.jsx("strong",{children:s.section.title}),e.jsx(_,{children:s.section.id}),e.jsxs("span",{className:"muted",children:[s.count," 条 / ",s.section.fields," 字段"]}),s.section.description?e.jsx("span",{className:"desc muted nowrap grow",children:s.section.description}):e.jsx("span",{className:"grow"}),e.jsxs("label",{className:"inline hint",children:[e.jsx("input",{type:"checkbox",checked:T,onChange:()=>u({mask:!T})}),"打码显示(投屏/截图时用)"]}),e.jsx("button",{className:"btn small ghost",onClick:()=>n(),children:"刷新"}),e.jsx("button",{className:"btn small ghost",onClick:H,children:"导出本栏目"}),e.jsx("button",{className:"btn small",onClick:B,children:"新建条目"})]}),e.jsxs("div",{className:"cols",children:[e.jsx("div",{className:"col",style:{flex:"0 0 300px"},children:e.jsxs("div",{className:"card",children:[e.jsxs("div",{className:"colhead",children:["条目列表",e.jsx("span",{className:"right muted",children:s.count})]}),e.jsx("div",{className:"colbody",children:s.entries.length?s.entries.map(t=>e.jsxs("div",{className:`row selectable ${t.id===(i==null?void 0:i.id)?"selected":""}`,onClick:()=>{u({entry:t.id}),k({id:t.id,title:t.title,values:{...t.values}})},children:[e.jsx("span",{className:"name",children:t.title||"(无名)"}),e.jsx("span",{className:"acts",children:t.missing.length?e.jsxs("span",{className:"tag",children:[t.completeness.ratio,"%"]}):e.jsx("span",{className:"tag strong",children:"已填满"})})]},t.id)):e.jsxs(F,{children:["还没有条目。点右上角「新建条目」,或用 CLI:nx-sk entry add --section ",s.section.id," --set ..."]})})]})}),e.jsx("div",{className:"col",children:i?e.jsxs("div",{className:"card",children:[e.jsxs("div",{className:"colhead",children:[e.jsx("span",{className:"grow nowrap",children:(a==null?void 0:a.title)||i.title}),e.jsx("span",{className:"muted mono",children:i.id}),a?e.jsxs(_,{kind:a.missing.length?"":"strong",children:["完整度 ",a.completeness.ratio,"%"]}):null]}),e.jsxs("div",{className:"field wide pad",style:{borderBottom:"1px solid var(--soft-2)"},children:[e.jsxs("div",{className:"flabel",children:[e.jsx("span",{children:s.section.titleLabel||"名称"}),e.jsx("span",{className:"bad",children:"改动后必须保存"})]}),e.jsx("div",{className:"fctrl",children:e.jsx("input",{value:i.title,onChange:t=>k(c=>({...c,title:t.target.value}))})})]}),e.jsxs("div",{className:"rowgap",style:{padding:"6px 14px",borderBottom:"1px solid var(--soft-2)"},children:[e.jsx("input",{className:"grow",style:{minWidth:220},placeholder:"直接加字段:字段=值 (回车写入;没见过的键会自动登记)",value:$,onChange:t=>S(t.target.value),onKeyDown:t=>{t.key==="Enter"&&K()}}),e.jsx("button",{className:"btn small",onClick:K,disabled:!$.includes("="),children:"写入"})]}),x.map(t=>e.jsx(z,{title:t.title,fields:t.fields,draft:i.values,onPatch:w},t.id)),e.jsxs("div",{className:"rowgap pad",children:[e.jsxs("button",{className:"btn",onClick:D,disabled:!y.count,children:["保存",y.count?`(${y.count} 处改动)`:""]}),e.jsx("button",{className:"btn ghost",onClick:()=>n(i.id),disabled:!y.count,children:"放弃改动"}),e.jsx("button",{className:"btn ghost",onClick:M,children:"删除条目"}),a!=null&&a.missing.length?e.jsxs("span",{className:"hint",children:["模板建议里还有 ",a.missing.length," 项没填(不影响使用):",a.missing.slice(0,5).map(t=>t.label).join("、"),a.missing.length>5?"…":""]}):null,(R=a==null?void 0:a.excluded)!=null&&R.length?e.jsxs("span",{className:"hint",children:["另有 ",a.excluded.length," 项已标不填、不计入完整度:",a.excluded.slice(0,6).map(t=>t.label).join("、"),a.excluded.length>6?"…":""]}):null]})]}):e.jsx("div",{className:"card",children:e.jsx(F,{children:"左边选一条,或新建一条"})})})]}),e.jsx(L,{module:"entries"}),j?e.jsx(V,{title:j.title,onClose:()=>A(null),children:j.body}):null]}):e.jsx("div",{className:"pad muted",children:U?"加载中…":"暂无数据"})}export{W as default};
|