feat(editor): 在线编辑文章(Worker 前端 + 轻量 docker 后端)
后端(新增 editor-api/,零 npm 依赖,只用 node 内置模块): - 只做文章相关:列表/读取/新建/保存/删除/图片上传/git 状态·发布·同步 - front matter 往返保真:未改动的块按字节照抄,CRLF/块标量/引号写法都不动 - 列表用目录名当 id,slug 撞名不再静默改错文件(返回 409 列候选) - 鉴权只有一条路:X-Editor-Token(只存在 Worker 侧,浏览器拿不到) - 端口只绑 127.0.0.1,由宿主机 nginx 反代出去 部署(新增 deploy/editor-api/bootstrap.sh): - 一条命令在新机器上完成 克隆仓库→写 .env→起容器→健康检查 - 状态全在两个目录(/srv/blog 仓库工作区 + /srv/editor-api 配置), 迁移 = 复制目录或在新机重跑本脚本,容器本身无状态 Cloudflare Worker 侧(blog-admin): - src/routes/editor.ts:鉴权 + 反代,浏览器只跟 Worker 说话 - 管理面板新增「文章编辑」:两栏布局 + 快捷插入面板(13 项短代码, 与 write-server 的 ShortcutPanel 一致)+ 底部 草稿/保存/发布 - 系统设置改为 schema 驱动的表单,且**以运行时实际生效的配置为准** (frontend_conf/captcha/moderator/ip_region/site_default + KV human_check), 修复「表单显示一套、评论系统跑另一套」的脱节问题 验证:tsc 0 错;front matter 往返 130/130;保存往返 130/130; API e2e 44/44;无头 Chrome UI e2e 14/14
This commit is contained in:
1 parent
48a2fd697f
commit
c217d20c30
24 files changed
+3597
-221
No files matched your search
@@ -504,13 +504,149 @@ textarea.code:focus{outline:none;border-color:var(--accent)}
|
||||
.modal-head h3{flex:1;font-size:14px;font-weight:700}
|
||||
.modal-body{padding:18px;overflow-y:auto}
|
||||
|
||||
/* ---------------------------------------------------------- 响应式 */
|
||||
/* ---------------------------------------------------------- 文章编辑 */
|
||||
|
||||
/* 列表行里的标题:可点,悬停才显色 */
|
||||
.po-t{color:var(--text);font-weight:500;cursor:pointer}
|
||||
.po-t:hover{color:var(--accent);text-decoration:underline;text-underline-offset:2px}
|
||||
.po-meta{display:flex;gap:5px;flex-wrap:wrap;align-items:center;margin-top:4px}
|
||||
.po-meta:empty{display:none}
|
||||
|
||||
/* 编辑器头部的线上地址 + 真实文件路径 */
|
||||
.po-url{
|
||||
display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;
|
||||
margin:-6px 0 14px;font-size:12px;color:var(--muted);
|
||||
}
|
||||
.po-url .lk{color:var(--accent)}
|
||||
.po-file{
|
||||
margin-left:auto;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
||||
font-size:11px;color:var(--muted);word-break:break-all;
|
||||
}
|
||||
|
||||
/* 两列字段 */
|
||||
.po-grid{display:grid;grid-template-columns:1fr 1fr;gap:0 16px}
|
||||
.po-grid .field:last-child{margin-bottom:14px}
|
||||
|
||||
.po-ck{
|
||||
display:flex;align-items:center;gap:8px;
|
||||
margin:2px 0 4px;font-size:13px;color:var(--text);cursor:pointer;
|
||||
}
|
||||
|
||||
/* 只读的「其他字段」—— 平时收着,别喧宾夺主 */
|
||||
.po-ro{
|
||||
margin-top:14px;padding:11px 13px;
|
||||
background:var(--surface);border:1px solid var(--line);border-radius:6px;
|
||||
font-size:12px;
|
||||
}
|
||||
.po-ro-k{display:block;margin-bottom:8px;color:var(--muted)}
|
||||
.po-ro-i{display:flex;align-items:baseline;gap:10px;padding:3px 0}
|
||||
.po-ro-i code{
|
||||
flex:none;min-width:118px;
|
||||
font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
||||
font-size:11px;color:var(--accent);
|
||||
}
|
||||
.po-ro-i .t{flex:none;font-size:11px;color:var(--muted)}
|
||||
.po-ro-i .v{
|
||||
flex:1;min-width:0;color:var(--muted);
|
||||
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
|
||||
}
|
||||
|
||||
/* 正文编辑区:等宽 + 够高,写 Markdown 才不憋屈(14px/1.75 对齐 write-server 的阅读密度) */
|
||||
.po-body{
|
||||
min-height:52vh;
|
||||
font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,"Courier New",monospace;
|
||||
font-size:14px;line-height:1.75;
|
||||
tab-size:2;
|
||||
}
|
||||
.po-hint{margin-left:auto;font-size:11px;color:var(--muted);font-weight:400}
|
||||
.po-foot{
|
||||
display:flex;align-items:center;gap:12px;flex-wrap:wrap;
|
||||
margin-top:10px;font-size:12px;color:var(--muted);
|
||||
}
|
||||
.po-kbd{margin-left:auto;font-size:11px;color:var(--muted)}
|
||||
|
||||
/* 文章列表:write-server 式卡片行(标题可点 + hover 出操作) */
|
||||
.tag.ok{color:var(--success);background:rgba(57,184,111,.13)}
|
||||
.po-list{display:flex;flex-direction:column;gap:8px}
|
||||
.po-row{
|
||||
display:flex;align-items:center;gap:14px;
|
||||
padding:12px 16px;background:var(--card);
|
||||
border:1px solid var(--line);border-radius:8px;
|
||||
transition:border-color .15s;
|
||||
}
|
||||
.po-row:hover{border-color:var(--accent)}
|
||||
.po-main{flex:1;min-width:0}
|
||||
.po-line{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
||||
.po-sub{margin-top:3px;font-size:12px;color:var(--muted)}
|
||||
.po-acts{display:flex;gap:2px;flex:none}
|
||||
.po-ic{
|
||||
display:inline-flex;align-items:center;justify-content:center;
|
||||
width:30px;height:30px;
|
||||
border:none;border-radius:6px;cursor:pointer;
|
||||
background:transparent;color:var(--muted);text-decoration:none;
|
||||
transition:background .15s,color .15s;
|
||||
}
|
||||
.po-ic:hover{background:var(--surface);color:var(--accent)}
|
||||
.po-ic.danger:hover{color:var(--danger)}
|
||||
@media (min-width:861px){
|
||||
.po-acts{opacity:0;transition:opacity .15s}
|
||||
.po-row:hover .po-acts,.po-row:focus-within .po-acts{opacity:1}
|
||||
}
|
||||
|
||||
/* 编辑器:紧凑表单(对齐 write-server 的行式布局) */
|
||||
.po-frow{display:grid;grid-template-columns:1fr 240px;gap:0 14px}
|
||||
.po-frow4{display:grid;grid-template-columns:180px 150px 110px 1fr;gap:0 14px}
|
||||
.po-frow .field,.po-frow4 .field{margin-bottom:12px}
|
||||
.po-folder{margin:-4px 0 12px;font-size:11px;color:var(--muted);word-break:break-all}
|
||||
|
||||
/* 编辑器两栏:左主编辑 + 右快捷插入(对齐 write-server 的撰写页) */
|
||||
.po-grid{display:grid;grid-template-columns:minmax(0,1fr) 196px;gap:16px;align-items:start}
|
||||
.po-side-t{margin:2px 0 8px;font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
|
||||
.po-sc{
|
||||
display:flex;flex-direction:column;gap:2px;padding:6px;
|
||||
background:var(--card);border:1px solid var(--line);border-radius:8px;
|
||||
}
|
||||
.po-sc-b{
|
||||
display:flex;align-items:center;gap:9px;width:100%;
|
||||
padding:7px 9px;border:none;border-radius:6px;
|
||||
background:transparent;cursor:pointer;text-align:left;
|
||||
font-size:12px;color:var(--text);opacity:.75;
|
||||
transition:background .15s,color .15s,opacity .15s;
|
||||
}
|
||||
.po-sc-b:hover{background:var(--surface);color:var(--accent);opacity:1}
|
||||
.po-sc-b .g{
|
||||
flex:none;width:18px;text-align:center;
|
||||
font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
||||
font-size:11px;color:var(--muted);
|
||||
}
|
||||
.po-sc-b:hover .g{color:var(--accent)}
|
||||
.po-note{
|
||||
margin-top:14px;padding:10px 12px;
|
||||
background:var(--surface);border:1px solid var(--line);border-radius:8px;
|
||||
font-size:11px;color:var(--muted);line-height:1.7;
|
||||
}
|
||||
.po-note h4{margin:0 0 6px;font-size:10px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
|
||||
.po-note ul{margin:0;padding-left:14px}
|
||||
|
||||
/* 设置页:schema 驱动的表单 */
|
||||
.sf-grid{display:grid;grid-template-columns:1fr 1fr;gap:0 18px}
|
||||
.sf-item{margin-bottom:12px;min-width:0}
|
||||
.sf-hint{display:block;margin-top:4px;font-size:11px;color:var(--muted)}
|
||||
.sf-ck{margin:0}
|
||||
.sf-em{font-style:normal;font-size:11px;color:var(--muted)}
|
||||
.sf-adv{font-size:13px;color:var(--muted)}
|
||||
.sf-adv summary{cursor:pointer;user-select:none}
|
||||
.sf-adv summary:hover{color:var(--text)}
|
||||
|
||||
@media (max-width:1000px){
|
||||
.stats{grid-template-columns:repeat(2,1fr)}
|
||||
.grid2{grid-template-columns:1fr}
|
||||
.main{padding:0 20px}
|
||||
.topbar{margin:0 -20px;padding:13px 20px}
|
||||
.po-grid{grid-template-columns:1fr}
|
||||
.po-frow,.po-frow4{grid-template-columns:1fr}
|
||||
.sf-grid{grid-template-columns:1fr}
|
||||
.po-ro-i code{min-width:90px}
|
||||
}
|
||||
|
||||
@media (max-width:760px){
|
||||
|
||||
Reference in new issue
Block a user