From ad8997a5ec7a4acb518a9bba91537d17fdaff2c4 Mon Sep 17 00:00:00 2001 From: shumengya Date: Wed, 24 Jun 2026 22:10:27 +0800 Subject: [PATCH] chore: sync local changes to Gitea --- .gitignore | 1 + README.md | 62 +- client/index.html | 32 + client/public/favicon.ico | Bin 0 -> 97613 bytes client/public/logo.png | Bin 0 -> 19781 bytes client/src/App.tsx | 25 + client/src/ConfigContext.tsx | 51 + client/src/Layout.tsx | 40 + client/src/api.ts | 123 + client/src/components/AdminGate.tsx | 78 + client/src/components/CwdComments.tsx | 134 + client/src/components/GeoFooter.tsx | 37 + client/src/components/MarkdownBody.tsx | 37 + client/src/lib/markdown.ts | 73 + client/src/lib/postToc.ts | 246 + utils.js => client/src/lib/text.ts | 47 +- client/src/main.tsx | 11 + client/src/pages/Admin.tsx | 234 + client/src/pages/Home.tsx | 95 + client/src/pages/PostPage.tsx | 85 + client/src/styles/app.css | 612 ++ .../css => client/src/styles}/markdown.css | 5 +- client/src/vite-env.d.ts | 9 + client/tsconfig.json | 24 + client/vite.config.ts | 33 + dist-desktop/assets/_baseUniq-D-yNBara.js | 1 + dist-desktop/assets/arc-CZ0PsMQW.js | 1 + .../architectureDiagram-Q4EWVU46-C8umIefJ.js | 36 + .../assets/blockDiagram-DXYQGD6D-rHreedhf.js | 132 + .../assets/c4Diagram-AHTNJAMY-Cnif2nNv.js | 10 + dist-desktop/assets/channel-Bv1ZAY8d.js | 1 + .../assets/chunk-4BX2VUAB-CI7enyPF.js | 1 + .../assets/chunk-4TB4RGXK-D8YpqJC4.js | 206 + .../assets/chunk-55IACEB6-D6svk_zs.js | 1 + .../assets/chunk-EDXVE4YY-D2oMcFeR.js | 1 + .../assets/chunk-FMBD7UC4-TatyaWoN.js | 15 + .../assets/chunk-OYMX7WX6-Bgc90t5_.js | 231 + .../assets/chunk-QZHKN3VN-BFI20CEN.js | 1 + .../assets/chunk-YZCP3GAM-Blce-T0j.js | 1 + .../assets/classDiagram-6PBFFD2Q-vWrFBNY5.js | 1 + .../classDiagram-v2-HSJHXN6E-vWrFBNY5.js | 1 + dist-desktop/assets/clone-DL0QTDot.js | 1 + .../assets/cose-bilkent-S5V4N54A-DA7lnOJ3.js | 1 + dist-desktop/assets/cytoscape.esm-D_LviqZs.js | 331 ++ .../assets/dagre-KV5264BT-BCic1E4j.js | 4 + dist-desktop/assets/defaultLocale-DX6XiGOO.js | 1 + .../assets/diagram-5BDNPKRD-DXx24IDi.js | 10 + .../assets/diagram-G4DWMVQ6-sxZR-OqR.js | 24 + .../assets/diagram-MMDJMWI5-CSpsjX4P.js | 43 + .../assets/diagram-TYMM5635-B1FX1-Vv.js | 24 + .../assets/erDiagram-SMLLAGMA-DxMlnAr7.js | 85 + .../assets/flowDiagram-DWJPFMVM-DI9fej45.js | 162 + .../assets/ganttDiagram-T4ZO3ILL-ByyPQG8e.js | 292 + .../gitGraphDiagram-UUTBAWPF-iyZ_JHIV.js | 106 + dist-desktop/assets/graph-C_fMmQpx.js | 1 + dist-desktop/assets/index-DCC3CA10.css | 1 + dist-desktop/assets/index-jkhvCyNw.js | 384 ++ .../assets/infoDiagram-42DDH7IO-69l94V-f.js | 2 + dist-desktop/assets/init-Gi6I4Gst.js | 1 + .../ishikawaDiagram-UXIWVN3A-BjC5Jacw.js | 70 + .../journeyDiagram-VCZTEJTY-CsFlB-4a.js | 139 + .../kanban-definition-6JOO6SKY-CBgE3YZo.js | 89 + dist-desktop/assets/layout-CBIttl_i.js | 1 + dist-desktop/assets/linear-BIZ8jh0J.js | 1 + dist-desktop/assets/mermaid.core-DD7RPEfx.js | 309 + dist-desktop/assets/min-CxVBSoSd.js | 1 + .../mindmap-definition-QFDTVHPH-DJZtV1tM.js | 96 + dist-desktop/assets/ordinal-Cboi1Yqb.js | 1 + .../assets/pieDiagram-DEJITSTG-BuuhnNxp.js | 30 + .../quadrantDiagram-34T5L4WZ-B6g9ZuX5.js | 7 + .../requirementDiagram-MS252O5E-CHmLf7zu.js | 84 + .../assets/sankeyDiagram-XADWPNL6-kanRK42m.js | 10 + .../sequenceDiagram-FGHM5R23-C8ahaLpL.js | 157 + .../assets/stateDiagram-FHFEXIEX-zPv0f0M3.js | 1 + .../stateDiagram-v2-QKLJ7IA2-BHZ9r_Vp.js | 1 + .../timeline-definition-GMOUNBTQ-zynpC_2V.js | 120 + .../assets/vennDiagram-DHZGUBPP-BP7e_Dyb.js | 34 + .../assets/wardley-RL74JXVD-BAks5CVw.js | 162 + .../wardleyDiagram-NUSXRM2D-CcpeuWSI.js | 20 + .../xychartDiagram-5P7HB3ND-BX6JhKI6.js | 7 + dist-desktop/favicon.ico | Bin 0 -> 97613 bytes dist-desktop/index.html | 33 + dist-desktop/logo.png | Bin 0 -> 19781 bytes package-lock.json | 5025 +++++++++++++++++ package.json | 29 +- public/css/app.css | 278 - public/js/app.js | 59 - public/js/cwd-comments.js | 78 - public/js/markdown-client.js | 97 - render.js | 131 - server.js | 504 -- src/api/router.ts | 56 + src/api/routes/admin-session.ts | 29 + src/api/routes/config.ts | 17 + src/api/routes/posts.ts | 113 + src/bindings.d.ts | 9 + src/db/posts.ts | 135 + src/lib/auth.ts | 78 + src/lib/http.ts | 63 + src/lib/slug.ts | 18 + src/lib/text.ts | 16 + src/worker/index.ts | 15 + tsconfig.worker.json | 13 + worker.js | 3 - wrangler.toml | 7 +- 105 files changed, 11146 insertions(+), 1207 deletions(-) create mode 100644 client/index.html create mode 100644 client/public/favicon.ico create mode 100644 client/public/logo.png create mode 100644 client/src/App.tsx create mode 100644 client/src/ConfigContext.tsx create mode 100644 client/src/Layout.tsx create mode 100644 client/src/api.ts create mode 100644 client/src/components/AdminGate.tsx create mode 100644 client/src/components/CwdComments.tsx create mode 100644 client/src/components/GeoFooter.tsx create mode 100644 client/src/components/MarkdownBody.tsx create mode 100644 client/src/lib/markdown.ts create mode 100644 client/src/lib/postToc.ts rename utils.js => client/src/lib/text.ts (50%) create mode 100644 client/src/main.tsx create mode 100644 client/src/pages/Admin.tsx create mode 100644 client/src/pages/Home.tsx create mode 100644 client/src/pages/PostPage.tsx create mode 100644 client/src/styles/app.css rename {public/css => client/src/styles}/markdown.css (96%) create mode 100644 client/src/vite-env.d.ts create mode 100644 client/tsconfig.json create mode 100644 client/vite.config.ts create mode 100644 dist-desktop/assets/_baseUniq-D-yNBara.js create mode 100644 dist-desktop/assets/arc-CZ0PsMQW.js create mode 100644 dist-desktop/assets/architectureDiagram-Q4EWVU46-C8umIefJ.js create mode 100644 dist-desktop/assets/blockDiagram-DXYQGD6D-rHreedhf.js create mode 100644 dist-desktop/assets/c4Diagram-AHTNJAMY-Cnif2nNv.js create mode 100644 dist-desktop/assets/channel-Bv1ZAY8d.js create mode 100644 dist-desktop/assets/chunk-4BX2VUAB-CI7enyPF.js create mode 100644 dist-desktop/assets/chunk-4TB4RGXK-D8YpqJC4.js create mode 100644 dist-desktop/assets/chunk-55IACEB6-D6svk_zs.js create mode 100644 dist-desktop/assets/chunk-EDXVE4YY-D2oMcFeR.js create mode 100644 dist-desktop/assets/chunk-FMBD7UC4-TatyaWoN.js create mode 100644 dist-desktop/assets/chunk-OYMX7WX6-Bgc90t5_.js create mode 100644 dist-desktop/assets/chunk-QZHKN3VN-BFI20CEN.js create mode 100644 dist-desktop/assets/chunk-YZCP3GAM-Blce-T0j.js create mode 100644 dist-desktop/assets/classDiagram-6PBFFD2Q-vWrFBNY5.js create mode 100644 dist-desktop/assets/classDiagram-v2-HSJHXN6E-vWrFBNY5.js create mode 100644 dist-desktop/assets/clone-DL0QTDot.js create mode 100644 dist-desktop/assets/cose-bilkent-S5V4N54A-DA7lnOJ3.js create mode 100644 dist-desktop/assets/cytoscape.esm-D_LviqZs.js create mode 100644 dist-desktop/assets/dagre-KV5264BT-BCic1E4j.js create mode 100644 dist-desktop/assets/defaultLocale-DX6XiGOO.js create mode 100644 dist-desktop/assets/diagram-5BDNPKRD-DXx24IDi.js create mode 100644 dist-desktop/assets/diagram-G4DWMVQ6-sxZR-OqR.js create mode 100644 dist-desktop/assets/diagram-MMDJMWI5-CSpsjX4P.js create mode 100644 dist-desktop/assets/diagram-TYMM5635-B1FX1-Vv.js create mode 100644 dist-desktop/assets/erDiagram-SMLLAGMA-DxMlnAr7.js create mode 100644 dist-desktop/assets/flowDiagram-DWJPFMVM-DI9fej45.js create mode 100644 dist-desktop/assets/ganttDiagram-T4ZO3ILL-ByyPQG8e.js create mode 100644 dist-desktop/assets/gitGraphDiagram-UUTBAWPF-iyZ_JHIV.js create mode 100644 dist-desktop/assets/graph-C_fMmQpx.js create mode 100644 dist-desktop/assets/index-DCC3CA10.css create mode 100644 dist-desktop/assets/index-jkhvCyNw.js create mode 100644 dist-desktop/assets/infoDiagram-42DDH7IO-69l94V-f.js create mode 100644 dist-desktop/assets/init-Gi6I4Gst.js create mode 100644 dist-desktop/assets/ishikawaDiagram-UXIWVN3A-BjC5Jacw.js create mode 100644 dist-desktop/assets/journeyDiagram-VCZTEJTY-CsFlB-4a.js create mode 100644 dist-desktop/assets/kanban-definition-6JOO6SKY-CBgE3YZo.js create mode 100644 dist-desktop/assets/layout-CBIttl_i.js create mode 100644 dist-desktop/assets/linear-BIZ8jh0J.js create mode 100644 dist-desktop/assets/mermaid.core-DD7RPEfx.js create mode 100644 dist-desktop/assets/min-CxVBSoSd.js create mode 100644 dist-desktop/assets/mindmap-definition-QFDTVHPH-DJZtV1tM.js create mode 100644 dist-desktop/assets/ordinal-Cboi1Yqb.js create mode 100644 dist-desktop/assets/pieDiagram-DEJITSTG-BuuhnNxp.js create mode 100644 dist-desktop/assets/quadrantDiagram-34T5L4WZ-B6g9ZuX5.js create mode 100644 dist-desktop/assets/requirementDiagram-MS252O5E-CHmLf7zu.js create mode 100644 dist-desktop/assets/sankeyDiagram-XADWPNL6-kanRK42m.js create mode 100644 dist-desktop/assets/sequenceDiagram-FGHM5R23-C8ahaLpL.js create mode 100644 dist-desktop/assets/stateDiagram-FHFEXIEX-zPv0f0M3.js create mode 100644 dist-desktop/assets/stateDiagram-v2-QKLJ7IA2-BHZ9r_Vp.js create mode 100644 dist-desktop/assets/timeline-definition-GMOUNBTQ-zynpC_2V.js create mode 100644 dist-desktop/assets/vennDiagram-DHZGUBPP-BP7e_Dyb.js create mode 100644 dist-desktop/assets/wardley-RL74JXVD-BAks5CVw.js create mode 100644 dist-desktop/assets/wardleyDiagram-NUSXRM2D-CcpeuWSI.js create mode 100644 dist-desktop/assets/xychartDiagram-5P7HB3ND-BX6JhKI6.js create mode 100644 dist-desktop/favicon.ico create mode 100644 dist-desktop/index.html create mode 100644 dist-desktop/logo.png create mode 100644 package-lock.json delete mode 100644 public/css/app.css delete mode 100644 public/js/app.js delete mode 100644 public/js/cwd-comments.js delete mode 100644 public/js/markdown-client.js delete mode 100644 render.js delete mode 100644 server.js create mode 100644 src/api/router.ts create mode 100644 src/api/routes/admin-session.ts create mode 100644 src/api/routes/config.ts create mode 100644 src/api/routes/posts.ts create mode 100644 src/bindings.d.ts create mode 100644 src/db/posts.ts create mode 100644 src/lib/auth.ts create mode 100644 src/lib/http.ts create mode 100644 src/lib/slug.ts create mode 100644 src/lib/text.ts create mode 100644 src/worker/index.ts create mode 100644 tsconfig.worker.json delete mode 100644 worker.js diff --git a/.gitignore b/.gitignore index 538af3f..aeed9c9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +dist/ node_modules/ .wrangler/ .dev.vars diff --git a/README.md b/README.md index 4f46c0e..d9c428e 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,59 @@ # SproutBlog -Cloudflare Worker + D1 的极简博客。 +Cloudflare Worker + D1 + 静态资源(Vite/React SPA)。Worker **仅处理 `/api/*`**,前端构建产物输出到 `dist/`,由 `[assets]` 提供;`wrangler deploy` 一次部署 API + 前端。 ## 功能 -- 公共博客页 -- Markdown 渲染 -- 简单后台 CRUD -- 手机和电脑自适应 +- 公共博客(React + React Router) +- Markdown(marked + KaTeX + highlight.js + Mermaid,按需动态加载) +- 简单后台 CRUD(Cookie / Basic Auth 与原先一致) +- 评论(cwd-widget) +- 手机与桌面适配(沿用原样式) ## 初始化 -1. 创建 D1 数据库: - `wrangler d1 create sproutblog` -2. 把生成的 `database_id` 填到 `wrangler.toml` -3. 应用迁移: - `wrangler d1 migrations apply sproutblog --remote` -4. 本地运行: - `npm run dev` +1. 创建 D1:`wrangler d1 create sproutblog`,把 `database_id` 写入 `wrangler.toml`。 +2. 应用迁移:`wrangler d1 migrations apply sproutblog --remote` +3. 安装依赖:`npm install` -## 可选后台密码 +## 开发 -如果要给 `/admin` 加密码,设置 `ADMIN_PASSWORD`: +- 一体化(会先 `vite build` 再同时拉起 Worker 与前端开发服务器): + - `npm run dev` + - Worker:默认 `http://127.0.0.1:8787`(读取 `dist` 静态资源) + - 前端:`http://127.0.0.1:5173`,`/api` 代理到 `8787` +- 仅 Worker(需已有 `dist`):`npm run dev:worker` +- 仅前端 + API 代理:`npm run dev:client` +- 修改 React 后若要在 `8787` 上看到最新静态文件,请再执行 `npm run build`。 -`wrangler secret put ADMIN_PASSWORD` +## 部署 +```bash +npm run deploy +``` + +等同 `npm run build && wrangler deploy`。 + +## 环境变量 + +- `SITE_TITLE` / `SITE_TITLE_EN` / `CWD_API_BASE`:可在 `wrangler.toml` 的 `[vars]` 中配置。 +- `ADMIN_PASSWORD`:生产环境建议使用 `wrangler secret put ADMIN_PASSWORD`,勿提交仓库。 +- 前端可选:`VITE_CWD_SITE_ID`(构建时注入;否则以 `/api/config` 中的 `CWD_SITE_ID` 为准)。 + +## API 说明(节选) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/config` | 站点公开配置 | +| POST | `/api/admin/session` | 设置后台 Cookie(body: `{ token }`) | +| GET | `/api/posts` | 列表;未登录仅返回已发布文章 | +| GET | `/api/posts/:idOrSlug?bump=1` | 详情;`bump=1` 且匿名访问已发布文章时增加浏览量 | +| POST/PUT/DELETE | `/api/posts` … | 写操作需鉴权(与原先一致) | + +## 目录结构(节选) + +- `src/worker` — Worker 入口 +- `src/api` — 路由与 HTTP 处理 +- `src/db` — D1 访问 +- `client/` — Vite + React 源码,构建至 `dist/` +- `migrations/` — D1 SQL diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..b22cf0b --- /dev/null +++ b/client/index.html @@ -0,0 +1,32 @@ + + + + + + 萌芽小窝 + + + + + + + + +
+ + + diff --git a/client/public/favicon.ico b/client/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..42c5b46e573672ac58ba25b065f28b7cc6183526 GIT binary patch literal 97613 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

CZWg9ekj9!B@7X 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 literal 0 HcmV?d00001 diff --git a/client/public/logo.png b/client/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..2e8e9e1e448e9af8a9d8105e87041e81b92dabe6 GIT binary patch literal 19781 zcmV)RK(oJzP)<|gfV`sb}LQ*p7c>sc#gX$!x)jY~hTJHvnD^VoCQV|Usr zo|CnSi~W9r|4v%+udCw9Un@(i`g`gUKdWc?-y^T9pC5`V7XR)}^m|e7LA_7$efGSR zJ)d9s@ALPPKM(H#KDUS6lOztR>>ec%-m9qkC_ZjUqskn1FWEh{?){J6e3Re%9}oK9 z{!a*g_fP1*vs(3-q}Fl)-pJ?ExTO^Z-QU;Og+J=XD~3j2C-fdZ0EfHV^Bcx)LMSeI zW7BGWvs?I0OY<9@;?GjNdHgZIf$aBFej}yAXd$YfB^A8s74f_3lCPOx`S;TNJ#@?W z5R>S)<@9qx%1#jMlq*mF3AGW{Iz&G89q32M$ z5JDXGCsEjokG*Lv=*QP_6A><2cxkAGQW4 z^u8mV4JL4W!+lKHT*z_Q$gG>08A8XK6yc4CgIw4Fzx2%(|4#BpEhOi6X7sxg6w z`Bl+3K9W~{0^=eflIIeEOF{AXA_d2vhu^D-M{&uYmG$e?$oIJLOZZsQ!dFTV@V~F_ zOJd$DrWmnMj^4PlsV|9|(NeTE8vPxRbT!rRd)sR$6KRc^tp2A@{!heX)0fB^Z9g zBkOrYAX14;VVMFIS4sa)%@Tb6J3<*tN&&=sHGiIXKXWBl^jzulzZZ8kL)6vWSL@u@ z*PR3nK+Ay7jiZj{%b1L)%>D<2|K>li$0!PpkE8PE01*>Bn)U!SF=%9lz(~MDgP%kz zfikm}$rpasFL3?Z)Sr@@q!b_^SFQ|^{JHo#HiRCK(`gHk3R0fjivFE0CWw*}qO?+m zBIU{v8>c^TPQO%6^?8-j%Ae`y3q@+itBETDzK2HCRqCs!)qbW3acAlC=wNU>mEGSV zd|^sz9i@f!?$&zWk%M>x(G1nfMDT&*za1az_+0UO+Aa9LA9`X0{T##B8aJJsx=1L2 zH2+EL6L4UjWEDHuKQC(q;Xf;pzh%ZEriGxw({4#BsB}5%b1_A#neKxrTCDzF2_klJ z;WsNK!k;(rdA)r-2cVv|)N3947IGQ>cin;XBdF<{`+GZBdeo19q(9AXj^WF5ftc37 z6u44~s6qryg`K+y;{UQje_b(Li@51_a79j+I#p$liMC99l)pFm8ExgV6e!MP4=H^4 zIqcUILJE`>QNlOel)5Mt^nruqt533ZkB=JVMWWZnac8S_WAf`HbbMv z1lNaera^_B8iKa{dDZLBXt0;YWxUB{r7+@msX_#WQFdm3KdBHOS=ItF0CJzSeop=SrQ8JD@rG1@qR%P=ReJgZGp>-qU=--MKw73QJ9rF;TPho$PbfjDfv?jC zeI2L-O`HFwm_R-eKj|6AMlt89=QAg#3!}a!zp<;1<^0Qb6K=x_OJpSKn!sQ98N#PRV zliy2g`s5tV1sFFu^7v~{Fvo+pt|CPtrz-Zl3!$JnE?}o{u}@j0qKB46(WjnXc7R%Z zm8?M1bUEk_8tk4T$dq+-G@U?Y>;-w)W6LL>{Pa-0eGfyUc#4-$^h*0G+Y}4`og-kz zX|5QWuRfSe$7%tI3v6Jg;sJyz=1rrnB3H1KM)ZzlDCyv-=tL2aM*cp3pS6lo%uL}J z6$_j)vzCmW{zwOUhC*4`M_Q-=KI_oz!4^mW8I}9`5KTmxe%#X1@>42nujxl^xs2ub z_#%C7FlPfgGU%!5(0sf>>pi-rzCT?a%lq(}j;8=oM(Txn8 z=f=j*^4F|Mw`FE2+5%LL5(I}!X8Q3BHH;6I*kBG-2#TABMpSPGK@~TxGH!O&3a0FThaUe=ToEx6$0y|7byU(u2qeolM6rvWqJ0XPHui9^Ve`(P*f(qHgL?SadK;> z7047G5||c`Dx+OvP>?fKI;yn#y{oj?u>eOCwu}oHP}IZ`!yJBYFxeuY*HN!mf2L$F z{lVdS>qdV1F@Mwo75Tb0OVS^TE6e?`7Jk)GfRvp6%*p~1;?rFP@O(cn5$gg|{x_}Q zDkV3Rp2W<^6wLSFOQ~p~Dj}aciz=RYC?wcLKr$v1(UVG%*nzCFG9{go#si&x%UZ?d zJYCho7Yfj9Uzb2^=hI?IC$Bl3f@vM(9M(X{=AIG@}Lv5eF8wZX$3>S*`M~j0;Rx7Z3>COnYSt zNnf8@!qmV!sEeTrEUkhEx1iNhg;&U+GQXT4vE0vOeE3k?2L?LW({tchBx&^W8y@j` zy*90Oek@C)Q67y;#fT~%Y~K?H)O=8CVENOEQUY3C^uK1=Ow9%f|*DOd=qsEc6DW6r)I^vc+Wxt(rhtJtwt`MaI3b;+4ogc!BkODU*_^ z;KdcmJ|=4>bD0JF^P#M*iqJ1ZjT%V-1e=g_D7Ay90bpB`@l!G3Pm05nlbczQbY{V- zslk>hYkWcZT7p1Ep|TXJ(35`_Z|%W2SwR8r5IQ)&&n=irzywn=6~f5Z^Y^VROyJ2X z^0+Ie9DyH*38*S!(C9g|pJ4)3Ci_8*af<%Gk7+gMq{T82va5UniI6KMEG*MH#fs5E zQ(^-r@~TPmO;4O%4J>O;pK>4iM3^vZ;FMHN0h+-dyZKo}T?Per{5Xf1om89v&@OEo zC2OKs8L>WTWYrctuVeoGV1VLRGa;Q6Cc7Adr5C0${HG^73uaEwrM1K3@Df=d`Xwu?%BNI5gCpdE+VHGz__G^7-s(fcjZ^?vBDK_AL~ z(BZ=eE(bWoUaZ%ob%iPr>n1+QC4*Gl+BxCJIVWF!I9gz#Xqa`A6DumuRB9LaO>b23 z5-SX4tC^N1i^?8!&V-h2u7k;1a$#`uIibsZvhHP8V0nDx)znkqvSR?lrMZgL3vifG z&@Uf^T=boce~urlSCKk%1?WyKiZspp4JeihtU}7D1m{-@P$hUkU_q*(bBb&QOO4b^ z3db~+Uf4A<=~i|cmRi*}gAo*03g~O02}~se!Q>U=B>ReLW~y0KQm2@Li~iB-pa?q4 zgma-_^j;=&CVS-e?|+R&g~A1x>5l4RvHhHpm%EXFqD*QojM$9!eHlS&6`WiPV{`Q> z$YU3U3i4}ZbprD5vO0Y#pSDoMv`~;fPK0a5?ayR#C#tGbNHoWUq#4Hc{hBf4Rtg#F zhp=x~vyl#F7X2>gQ1C6Ov2L6HK~Hz%KJN-r#%cXB+`kfGU9OFw<1|FvJVk>g;p?SW*)Tcn++5Y%T_Y$ zH1l>e{+zNwkKeiH3c&jwS<72o#H9!ZWC$4zC%F|? z?;ks}dhqYCRFDxsVuk^rup8c*eiC|2bFmq8KoE)v)T}(+LecbVRy|6jU5DYjE$qNY zSEtX0HC*vSPWuO3x!@PAlbOoeSF`I!}a~l0>l^twD;f_c^7Ty z_<1APiz+I;NbU5{V{Q!dO+gvwH2VW1lZ705Ttuaiv1GP<<-Xqvh75B`(Ke%i<@P#c zvDu_5@@m~#EWY0YM-Xsq<2gAgNe6 ztVz0bSusS&13|&6QN*Qw8EL2N6wYh^X+eOMS)E0+x4^AsSRhZ{@vDN+n%$ddF`<%B zRXJ%lvuXp+yONJ!V=Ob}3|r~yEWwAL{sA6-b{kxL%W7D7(PWspek4p%-Fp{pWSyvnD-ce=RGv(J9b?StFN4c0v^kSJ^Q>dFmKZYSa9JaSajhO zF7UYg(zI;|;AZ9ghrN>zcr~qyGP=>0d zZJ~?7le8i+gkhev#1MX{Ok#K5%daB({CnN@um%-?R;|-+mP2hY#$^kIMNzQZjg;8D z$IxLpsS_`UPT*r-FE&Wyi4VfRz>_cB4d-3A2+lfxG|X5(0);vb7U8L1f+u?+3Vs1Q z<(nqM{7v{ZJN5a+1V0BAfLVh%8^^=G(x(iiQ}Kj-2(*9+j9eq^WxFs(<1NX*NQC8L zmB4(uMb!W!%s{N7B1x4>6?XlUO)3|@?jb(8qlNXXp@}>I{TSdhj&woa;Z7VFaX=`b zp(ridwODD$1+V&`U0RGWAm99y9O>zTw?29l zo_u)=y!zg~@cw5{!C$}sBUIZygEXz+Kna|RfhVaSufvB93YTOAStcHlS_~vwp7jAZ zsR83!&(C9yZaOxGDiZPY>|%m}Mh0#BzlHmrx)J7W7!T9d4nu)X;3s|2MN?pb6gr-I ze)99vzd)XT_Vq?7{7Kvb%*AW!+7WQ&_Vf6G;ge4cBCJ&gRzBXQl$n%pr}HZRtn9O3 z8i+0+Ya#yMp3o7f(qw7`6*eupqVjs35s1^q{!m|U1h2evFWma*Lb&UxRj~c>Ww7Ik zm2lV7YvIA?Ho$YQ-T-g!c?`bZ`v!D&elMcu-WpGv4%gcZlPeV=RV-Yy^}Lv+Lu&eQ z)k$@`DI!Nszr0!O#ISKKeMf5W%G*!D$}48Uv~z~TJT%=@*z+B5^Val43@W%Vk;pgoV^#^IGXw{F<#x#{A@@VZW6Yfm60lWx%9uyegYrhlo>ZiUg8EZrDGJT&%b&LR$p;8Oh@4_+&noq^OLymUwFY3 ze(@j=zdsNC`SkUpU@Auc>~|h|=5~1c<%i(>t;@KdpT|%A1b*_R0?ayh9RA)^XleZl z7-7R0JQW9O9F1(Mm8>E)FoFwzpH=`1VNHq4l#0z2fSFhZU^uA{iI9^51yF=bPr#-5 za}~^qV|ZuJ6L9m+g(&=U;La!4!nVhj;k6X*cybjAel1rDrU2|`?7HhorVRMJM_1zS zEr#v8R>3pBzY)Ip_7!~1GLIR0xO=Gb;IdA~v*i{|{yR(Dc6DE=mc|_2IVR8Y@wY#E z2~X~Lp3-q2&jfve5dH$5(oI2Sn2rLRjBa5RkLIt~aXx(T=_}BQLVDr#Ct>-eXJI-x zk_-Lp^Tovkf7W>z`Qz!Iyk-Pke*1Y`_}v-pfd~nsNSAsk1`y}NBA61eOSYRi7CO=H zjw=8Q0(+zaB%#M=R$LR};S8pcZ>p9?&Y&-W4?lkvZhmMH3ili|^{e2;x3T1YEWn*luH(;R3UKEW>-e?f@zrq4BTM0yhnK?6=Prhi|MDBOE){N? z4@*VJT1c7}8dyT=DK%##j$rS(n=t>wO=i;v9i}!Dn#cjjLenvQsfo9I1pZA7`WR&pMC&dm{S(A@I!a z9^!w~t&CFIcX@zj5VKoIr|o8uPZatXa@o!-zdog_a;7qvr!7Jd~eIKC!55bWm z2|W7T4w$%N7)(IH3PFo&wh}&`el+{D*zcb+mb-u^9Kj^=f0s$*1%Zqqf6Q$!+alaBK0@@2)|MfP#K_F+Q&1f@d*-xYogTR0j5XH$Jc!9(&;` zD1{$$^sI*!Ianhk2e{hI37*u%rxJ43Bp4-pjKaU(`ym%F3*MN2=fdA4qIV|zxf>^= z*&Ye!p=E?}+%Ty`~fk}Tk}QM1eP{I>mH z!EKK)Q@;wXe_#>3_{JSPvKNAC5dz6X0fm7?EZ?qF_rUA#?}U4w-T*f}w3x>OcRbEq z2*w7x)}Ugnh1++nL)U^q_fla0SUeaYqx6jcf#xMK8Gh81-fD=7w$}S3;bG-0+&~= z05jNy|DJg+K7ZM8cGz^G1{ zvwA}B#IVCC2xjWpaK z&Kj{%NJcd3s3zYxW8u4XMBo>cHahPE*pwgVN@=BJo#wa z+36RTPyqJGel~+$>&L^yl_O#8l}qscpw*IysNGXwZ~;qJ4In5e374Gqb4}-~k{Cb= zAPEbGHwF&M=W^dehdHZEP9B^w^24s*UISO(vjA>*U@6@6;0ic+@LPW3yD9mEu#uQj zvfu6HuxkPlra*7+c?x5J^{5Dox$tkp>()nBVt}{`zg~mNwFIridC*+>h(oo4T+voI z%(>3XxS}4xn6i8R@;U}|qXkH_K@@AG@F%Vw2J5#jg))|Pnecm&NyRFuTkp99Ml3oF z1HG}i;Dvyt@Mq-T6Wv7<+f(jZxQr~Dh zBPh%*9^Mm<1Y)PCK)S3#O6I_*WcF@UG1{&TdY5--RLDQzD}QZ3^3M72 zV>ckHXDv!dM>wG(<%;wC>pQqLxapy#r~sHD>}2U8D#OET;YM@`x9nU2 zU+sO9TabF1PxH=1-M(A?J8k6wFjEp_MsfGC`R27eVxOyWbrJAQLg8PrV%=vU3x5p$jWb~!`u;UnF3t%fZ8_*< zjo6`#C2x1u024w~+8AiMyF6S{0q7q#yL4SLDD7vXjTNe6sAe~K>f#DR^FIIjRlJGw zVe7ri;gZ{C!SCLDfcrg_epxuYJgsc_G@n(N3yagJ@N|E(_btpS&V_65osR+GYE%FW z8XrPyuyYM6!Ey``mtoD}b?y$jRbbdkkTBsQG7k12R`JoxX!=JO0L@%X^(L+!4%ggu z5p>rN@Ketq;V%5F=U%%KMl3uXg+30Ztr^P|fL%sOvGXg6cbikB? znc)ZrCqMK1o8jua=c7effv#m0zixhLHU4`!+;IO=jOgDImVo2mZA1Y?MeGos{3Cdx zFTd@4?)&F#5T#j`pHEyh9N+7CSo7DyXR``6Y+VJzQTWr>qHtG_5!V`F+SRp2TvOJZ zDK31@8|ICCh8f1dU3@4m*2k6gEpS4^L%{*PxIDXxez*#+0&1f>M*8yJ?Mrm+qMCQFF>Eq zgt}@pUSqg$={1>ty=pAKCaoNUY@jjd_lM%^Pl49<@AwV{U6M9;6wq#?%$hZ%6``aD zNO$V-v2mB28EEo0bSLNUY4TJ zm*UA^fmy+FxE>#`z8fLvCpNL>SDuneoPfjN9PZ8Fk>~Eje0+#F{g|V(U}})4cmMz( z07*naRBiTo6XAO#ZSo0Q?0IN$ZhLqu44r>6H{+96je;pFN9Rf~8YZKFCvzc>&MzTw zCisafM)Paj@=-8q@erhny~^3ZOvrUwj7tJ%j&Hevb=a1tGz!1Eb-*}3@ zCWnR5{XGBbQ`~ehf6#TgF#R)UDux6i&-nn#0?+|X#)!4@Q3Y(LjhQhzhNgBQ zhdS?j>Ka&k^;Fn&<6KyI`2@tC)odLmkY`?g6n;7BKVa;FGvK$s z-^FhxcL}U?40VkD--WC0T7=g^4BnO^19vH0d-rlQ-E-ih&t4D~Cs%?7x(~+IJqrOG zhUYVEo>_ux@4lGl_;sdu4Hj_z8HGLybNyi`$Wi=jA#kAxlkj9uWLGZeN%@Hv*C@R5CK-evp=ZnQ|~C zc;ZZ3wa?p(sJD=_%H;l9;y z0Mkg`8IzZK-~RArJf*{V$~F~&oU_lLg!PH9xS*JOVL{@$E0@7=cKT7+6LMis$S*12 z3CllI2yz2+dwiX55r<3# z>y#8~kV4cT2U1tIw8%!P{PO~>F={!&$IR{x_ilh?mrj6luAYT(>uA{h(%s4mNMusv zs|32uQ9EJoW4L|iRycjuiEzfOli>1OHbBOjFIZ8CF^bS+zxwVj9&lcH$6|B=i%=Ps z!o|1FhF|}FyGjk2vON9bgP6;o$|2e@OHPHyU$~$DzA*hM?E1|;FnIP!T+ri}j}U@g z#sn>{3Cl(*fsb2?KVQneW{NQeMlBqIZUfCez9!>B8m`Up@m1I7(;fwKFzMt{bM~+~ z!@+KfG2#GC2+22ZDgRH0Hk1h)H*f|c$XLOrxkU^emivG5<*Tsx;&HHQ%XC_vfCFxQ}79%@Gg$y?|9^T49rf$$bC2xI%hy922s5nYv_V< zn382;w6qUYO5A{ zDC(iN$<)ZINJDFq&j4`S#RJdmVw)0P%~$HRNlI9Ye~^Jel`iU-_4VoJ=AZPao0`|c z&FdYzu0Wt=G_2e*19A6paQ?OP5G&ZnbK|<|v?2WgM!lX<%?|PuZQl8_V8pz^XbpY^ z*WIz1r))ik+c~wW{lIr<{+Hp&p9`07TL@cjUkn%AI14!iTREn{T)^!+uY*yD=8ai$ zIz0B|>^UP~=-ks_=)BX> z@@$4`>o+|6kD!ryUFTKe3GquijndkMHey9;t+1)KKRqD&VJh*K_xw@P7%T77T&G7#F~`_~SuAf$E@seuq?NnbXHDkL!Hy=(4Rh1UkU3l}-j(JWR!zGsq`88-w5 zL6O}oW-9O4wH0shsc44JgtM@OI$`M$%nqi(yJ$*T-p*3QRF;>C0!S2%p=y8r@*Nm* z_9^HNhQWzbj)Pn7yP7Kyr;K61cl*Paz^W@Jp*xt3DCAk#g)<&redkGF|2=`3z~0hl zJT_qp@zNW+;ncHE6vEFH;B-tmSzx!|g0tc7$8SO8@f{vKv-ihb7JF~}RO`LKo>BVd zY=YFM466JtSitT);gU!2{&DffIKCRm%2I)b=0K1fMi83hLElawC;tzf@}<93+#1Lr z{drR5h#Z4lHXQys33UUin(Kj<;^4LI;K1DEih#-O2-FZ2j zH1(Gl6b^ymXP*vVfBPW{sFPcPk3RoBp7e>Z{@NL^{+gMn02APr2RHGQu&XQJK@l6A zz@B^gwhQ5uStoEG&wcxxQ_*s)!-)Q6;lA)WtPa6$qPYjy6u1eQ6O$gvLEI&M?~LiC zCBv+z&Mbh^58Q=hkIfVJzs~PlyY3ClQny2wqLW^@e)-W4xWab&K~ zs(yWVN*{V=2cEdoF|Zl})3A^-X~jqwKJOG%i1AoRx($Q0zw(>UPJHj-{VJl9Ql@lYTwq`V_8K*PqRmlcnFlRQz0*4bqjy01O13_p z#RPA@{{qZLuH4YMC!!S?fq4697>g!&2%6uitImWQ?zsZq-}48gQB`@L9y|H{7`cno zErA>Fy^05fBjyi*5pxH_cYF8X>xz``$rtXxtYZvT`DP&%3qh!h&V)bwDaZUX(FgF+ zr*B}v=QK3;C&6tGZ57||7r>G9?R96Rd^o_Ypg@mqbATDLYtX~{qeCwA8o4IE3&3^* zCVi~z42(!(H7779!>}N*F5AOpMNkYOpty8GQaN)zuFDVfrZ90(0CJEvEK&w#r>v#z z8`ygHB`^|e?n5!P8i$cMgI!saf7*-_Fwi?0=A1hnZn^&&c6d?k>+jiuf*#LPuv2IJ3Wm)&9WBC87&V`{htn|f|0Sk@C&3lB zUj*0PwFOR``YRZ6)){a*D#4#WN5#P9$_)O!bJsOk@EHZmE}6z#x|d?R*LSR`1Fb|? zXBi%T`cABs3&KfvVzr#5Rc|9{W>|%)HASs#GM&6&kK6g{Cw}@ig;_gA6<}|i z4`D(fG8$MrB6zR8^8(!bz_l>vycwtjC&RBM9gD@GQ_(sM#?wC-hR!|%734VBc3DkKA!LXQH3M;#q& z41w&*O?$0bS%6F-t`(&KVi0u2nhrkjB1s3eO$Z_6aLxxXTanCp^jTz<6=}+mT6^!! zoZnio=mbD^F{xm>wrt$YO|9&m7ZbT^yx79N|MBN~Dbr4Z zsVgS%z5xhTmu9*UO%5V)i}*VHxyZpz?qXA*l=`Bbs^TX&@gM$XtN6sngFhr(3Xm%Nwi- z*hi@=9L*)jzW6LM$HC|^wxNPm87+(fI_wl5#0dG3XYWNkU<6tOB#q2Eja!57n?Avx z*N}Wt#`5zl-lsQj!z9EQMq%K$GEYU@Jml1nVBff3xSF zm&`_xNRNn9BjF4B0b*khlFlTuyq`k{fm{LF$lM%r*InvecWQSHhyJmxNzsVeq6uHb zMyi{{IfFJa}SOX0W)$6(NS3cUTnOT0kDqWbk$Ek?nQLDw;n zKd!uVHdhSh7wy*;mY`rqqU9Qlc);kmE^e{cJpN|S7}(~5wyzQ)?Gz1T5f5T+x>}cYMq*g$Z(j^tYqSKRa zSPmK(RyF!bhE2PZ)2R{^HGh-}e1Gv`NoQ<+Ua#O;03WW9*VZ*mSwKnlxCVP&ocEMC zt>l3-Q9CPGuwV~&u(|SfnOQ4>=yqaw!VzpwpL_mUaL<$5`QQfj!7sXT4U##L1-xzo zD#B>%6!A{@6#ejY8nN&c}vWCz8`W`&_;!Y0kFt>N%^=lAW9ERq9974?_VJ5b2u{r}|Eo&AM8B?FwT~i?$ zhv!*nNm|eKB279jmu?2DScMzUDmyyxNxF=#64?+c9wQXIm=)B_n?p=Dpx>{E0c2-T z){>hk-QLhFzd>cB5)>3kTZ(Atck7osNbXv84@+TeU z7?N4Sp*KSq5LBY=WwJ&65F6QkfLFriZ^48Y34k{?0m8dE~2 z?>(s@*w(mPWliW5QhM(q7fKfu!iskXvZ(+P7A~n}(PB!NR0Faht68}wgD{a(~#aRm@CzDba zn<*>nZbwaxy~kvOsI`LE4H}u?oAS$_DJWzy5i=J&vYw7IKZN&vVR$) z)A<@6Wgk(+Pz=RIsFe!nXO{a)G$orAe17};$9O{tCwL;mmyt-Xx_vVrLMgcs2GZvf zc2aS=p&e_M_}A;CKC>SM);wmi*eWU;!prEQ%r1HI^T=N!ipmm5;WsIyk-ukE4n_q~ z{rPe>y$!x3?&eI*f~gvd8)>UjYSf{F0ji5Wf`o+;BxViXT^v>MZ}Uua)XRw-SPQu6 z-m5ubL@<3vz{%5(hX$>xEVkZ4?!8 zKwOhqfcF0~7ND6zlCQ7TP_R^h%p4c%)~xAO)g#tPRPNmhnlF}4qT;~K+}T?(M!RPg z-%bnXhpl##=AXGbh6ykwSh(?Q7>ry3UP{J`@e1B}_jx_1=cpae)H+h0W7Pq-sHIFj zo6yY?I-LVpB0NA}l}|#xO!!U4oB$aZs@cJvO?j4(&l=|MRSe7EIKbh2OI2OX46dj^ zEAupjssc!2^5NV7$KOzzdEjHYXD}UzV3L@VJ+ zqxd*}@o4N2{)!K*QiUMzA+QM^fu#&p3&ZdghCJIy21Hy-hH$xnohjjEH8NHRyrkv^ zNF`v{Ktg8-Cpjrp0zq{v@JT5%D6&jzVs6o}BlWbfsU$SPA6pTkig(Sd0T4NRd#$P8 zK6EgLx|xZ;^O0NNmlKacuHX<(89R0Qi8yOuC8vGSDPrCCIh&cl!ot^aoT#O-o23``nki#T~LEAit7)N{Bf# zxY>vW5;^ZdR|DHYPYLmz+JJyNfnx#E*j4~La@N#j*XPUXly7&S$qZ5?4EZFG_X>N<;x^a16)@^q??@ z_w6uNa5M!<@-e7%3<_^V#naz`r++e%LQa~>7{$mmo;{dT#V)ydBX8%*lF-_n=xE7KfWDrD5Dp^rES)zgFsMwqj;1gm(*(f%1hXNyX z;bLTS0djxo!3xY#re!Da@&}O>&-Sp2RhDg0bN86V^Vv(0PeNnnkz6tBu7wZ$b1n`=oDi%#ym zj#~Q@=0RYaRDf1(l9amk$<>xn$Hx-qOy~MYX4e% z!n%!Z7y{j7rvE)`A~|X5iAWzmg`0iG4mx)He_=)NT;7Nxp<)C4Nvw$8Zt;?cUm+QW zyjRceL3wY@9>Ks`c3C!By4-%k(4c=8PY^8`qy5arkZv(5&9(rv>`Qn4CLTqg@T6C8 zkv;dmQs}?rN)$58t6F41h^}~z>==QpyQS!$r?BI*rirmdT;E$}rFl6$ z6PXbkjjI<&s`hZg3nc7dbAxC_9{Fq*ZT>)18K9=aQG> zS2XISfE?pF;r47L8XLJhL9-_yB{9`NeFVA?B-lWQG4)kTqs+xhw9pfH@a)joNk=U} zS{uO6Mjv0R}H~XhdI~o1{Y4D4& z{{<^AS5b%XgE7N(!(PQd}vxTv^Aj&dYZ&nZs{E{bOo#N@}ytDSaat1Jlf*qNjc?2 zQKWVEBii@U8_!^O@G$iAtZnR6IC;v6{PFk+$06!B22sDSIq^d&jqzlL+dBx&M{8K>Hpki{`QzFD-pKfjdcSoSFb;pE=RN5tz3?f zXkJfTM52mZWow&xQHth5;t9E5lJDmexa-?Xd7($l1vcD)Q8c26p|q}L?P~OpC=&%t zti?~>%pez5LaKfCDVBfKrHutn&g}S6M?o~UZeag;kR}u$ffxVq6#NoT`l)#0+39D( z|M}RTBjsx>&LDhW%;(LgV;GDdgLG20NUrYg1e5b%25_k-UP+yu%%%dk^M3>D6bD{} zPZoP(+9^zV0faAMwCN=x24DjMuae+^*qD;p=<1gvV76 zQ9~=h^RGMs$BsLO2Ybg)JPw8bAK3c67AaugakMXqE7o8KH)W#%uK&H*efMQ^B8ray z;}Ae)z*e*YtYV~yh6}I@KRF51t_m8H!xWDocQ?5Lba%J+GX?08tzq`6b)UdYyxY^^ zVv#glY~0VZb)WC@2lU!^t5K6HK!juDpMw84>gVwD(LYDR=Vs$rK&>USrk3oy=t74eNFNHCb91kq>80oJixaVBs;r{EdMUVZji| ziq2atpeZXPl-k_D&|B41?v?Yb}py&h$7MPV#s z48rswd_|*Qi7%&_!+;yX%a)IC$_bq%y$-IcLJpjPmzie>RzfCjW==Y+kwG7qQiEoJHOg(E3SL=t{YonkTQ0&kFq(cws$iM2 zOx1oibc@M;P?ohEhoC?oL&fk(4S&B`GXWH=0)$fm)bnmr&t6-b?yandO^1%mj#hh& zS`o&b#{L;Y>h#v^MNEOSB}KUif@#<@k&4>p)y!X8*tLMrxl3xYhxf46^mMS|8G+3@ zM(EV-;lAKd_X+a%)W}IQeN6U)FlYK_92HV!^l<#;h7pK#=@(BFyx2@elhFo-;0Nxr zFFTz6G_raMZSx+HRxqoqoJPTbE#fvc&2Q3!!h%&6*)%V*-;cJ`)3$oaS9I7uWlN|f zau1tTt6QdK3wrqfsvEehw}s~W0^?3i3)d~<&OBjE0o&N7az>Z>=Rxd0td0McWdis@ zeI}n=zAzh2y3Y?b6rD=%uKTh@FePJBsK1N?&sGi3I=Yy)wTA^1?5dyX98-L}T-c~F zP*KV$8b$Wj^$M8^PB{+ty`ft_O3vOT6A`2>;WSPV)*#A(A&C<5ujRoJuBeX<%3?Af zB>zKVz$ONz3I!16Miv?5i~Hx(mc7dg%#&O)J~$m`nt29IYL<*mOLxbsXyx%?4`IRB znE~3!6trCZ0i*G0k;lJhnfiI998ut}@Jat2m&ls9iq#8fA3AfyoI3GbUer?PAY3Rw z!pI<-lmakcfay+e?w=a$4@6F-srjg2g|jChs(5J1WtTZagG&`tkQl)pL7z}^MO9B8 zDdgmZUXp57$T%*hZ9A!=L7OWZ*$h9uv9EflPX8fF3)6_VajJUsB0 zqHR#dn6?#%0}N!g_ZYg9M(hdb-fY10Z!cs^g22Qo1|^>0n2zMvc#AulFEJF%9yHk` zM#*;)@W>qV*WZsMSTz41Fqw1VV<7Mo;RaeaGBa$@--g2zglS_(Gv+<^imBCel zlq@nx$%joNPun*cqGSeLv|q6sQsXg*$-NRU$Y;vvvg-uL+RIED9m6(#Q8QqO6td@qa(!|EPEV46)%3w%HS3 zup!5a$Y3z)Y9f8-R?ca#SppOIiOiu!29QIV6*3PW(aSW7%AUwS^o*i)+s&Mw#KHy5 zR+o(d+*qGMe$8E1sc10lsr&5`Ku!0mBY$H|Opd)U^M6Pz25ums;y<)Zn;h8YjY@C&;2G&H%1wZO<^e52b9bku0*EPJMV6`X40gqPf(=V>_O z0GdirA!B3B(y-hn5-W(fOiq8qPX9J4^IQS8{FpsPQRUxI2|C$WHH`_#=@M?Ef%IK| zR*QdvrP_k!%&svfi`W$|`Wp1~>P=se;JX4B@pIgXOx;4weLM=l z<>2x)|KIhJJd}XTD%X#@3WQ0i!OFDS1+TowiST;gO-+0eu}8s@aAW<8ItQvwc6)~RpO(6NLi4Pe4&1xf`=kTfo@ z;)=+>z*>z^HOj57ur#kfFr$BG*Fn+#UhDl+mq9ZePrzqqK}If5OT>9c4?8?6>*~@Z z&{(2;+)b3k_dCQTVt^9LuDQX=+341@C~7)^%DC`9X&&$e7WVeEGoRguFW7J?$xT8N z$xtGhf@lrRD_D6noA{#SA_a)bPHVJ5nKJynJx{+}oSPTyK=Q{ACZOdmXebpfXh>>h zYsixaBZvdD?aSE+N>6ta$7Jw8LhT^Cn(*r;=)$kN4(^36tTnSLa8U`cPh&HwPiM#gYk!Z7#iwaX!Y(8D?T#4-3qcm!Z zGjFA1?(4ZrmQsgoqNj5x?xR({H4!*niPJNC%AS2b&0gRvt=X$!Jxd`;)sNjvCiss1 z-^0Q7Z{a{2UajB3e)fp-jSsYb3;S_R{r=Xk`Oom{M#uhreT?kewSVvCze)HM9pvty zWAA2$5MsHwAC;hAe(*@W!6tn}I*DS)<%UMlq9|zcZNSRE7gK=*eiu>%#1xKSH;EWZ zk*xGCrEssM0I>y)n{DqUZPxQekJA06s@umZ0(Ip88y)i8B`hs7>v*E!0QNVuqu|@B z-$F~}E2suYf@N3vOa3*#%3t;$X#1`|!d19o@Yl^$mVfhqGFL?HTZj!6b>za0wpMli2f?a7To0sWgTDG)2+ylR(7851&E3b}=l)q&=h*LQMKOEjKWB@W zu}TLE* z#3~h77i-a?6=BoKn&wmull!3O@YxHV{FXR0legcYapMXt(A$luqc*0OitlT<>!a*iOEW(8GQ z1x(wT8>j#nU$K&WsTVC@FIoG`{r>irZ}+rT|GMFuZ{GbEDI0d` z3qhX$VMX{cEhaFf$hyK`;D@*XKi~^^!&>nJ_xJa9@^!4N@1TJT8+$RE=&55{GT-b) zrjuLseKko-QM~DvaXQYmY7TNr=<}+c#m?1&h0oS&@%HhS{54v1FJr;qvR|t^|9w5K zwnZ6`%)iNYIryArb`M&5P$^=5Z<6Yl$YXp1?+xCw>K1ViTaIgM kYx^hL8t3=^?-Km~2TjZ9BA#2KSO5S307*qoM6N<$g2^n67XSbN literal 0 HcmV?d00001 diff --git a/client/src/App.tsx b/client/src/App.tsx new file mode 100644 index 0000000..bb79698 --- /dev/null +++ b/client/src/App.tsx @@ -0,0 +1,25 @@ +import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; +import { ConfigProvider } from "./ConfigContext"; +import { Layout } from "./Layout"; +import { Home } from "./pages/Home"; +import { PostPage } from "./pages/PostPage"; +import { AdminDashboard, AdminEditor } from "./pages/Admin"; + +export function App() { + return ( + + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + ); +} diff --git a/client/src/ConfigContext.tsx b/client/src/ConfigContext.tsx new file mode 100644 index 0000000..afb32ef --- /dev/null +++ b/client/src/ConfigContext.tsx @@ -0,0 +1,51 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, + type ReactNode +} from "react"; +import type { SiteConfig } from "./api"; +import { fetchConfig } from "./api"; + +const Ctx = createContext(null); + +export function ConfigProvider({ children }: { children: ReactNode }) { + const [cfg, setCfg] = useState(null); + + const load = useCallback(async () => { + try { + setCfg(await fetchConfig()); + } catch { + setCfg({ + siteTitle: "萌芽小窝", + siteTitleEn: "SproutBlog", + cwdApiBase: "https://cwd.api.smyhub.com" + }); + } + }, []); + + useEffect(() => { + load(); + }, [load]); + + const value = useMemo(() => cfg, [cfg]); + + if (!value) { + return ( +

+

加载站点配置…

+
+ ); + } + + return {children}; +} + +export function useSiteConfig(): SiteConfig { + const v = useContext(Ctx); + if (!v) throw new Error("useSiteConfig"); + return v; +} diff --git a/client/src/Layout.tsx b/client/src/Layout.tsx new file mode 100644 index 0000000..0b40aff --- /dev/null +++ b/client/src/Layout.tsx @@ -0,0 +1,40 @@ +import { Link, Outlet } from "react-router-dom"; +import { useSiteConfig } from "./ConfigContext"; +import { AdminGate } from "./components/AdminGate"; +import { GeoFooter } from "./components/GeoFooter"; + +export function Layout() { + const { siteTitle, siteTitleEn } = useSiteConfig(); + + return ( + <> +
+
+
+ + + +
+ + {siteTitle} + + {siteTitleEn} +
+
+
+ +
+
+

萌芽小窝-一个安静的小地方 @2026-至今

+ +
+ + ); +} diff --git a/client/src/api.ts b/client/src/api.ts new file mode 100644 index 0000000..e60ab1c --- /dev/null +++ b/client/src/api.ts @@ -0,0 +1,123 @@ +const API_BASE = (import.meta.env.VITE_API_BASE as string | undefined) ?? ""; + +function apiUrl(path: string): string { + return API_BASE ? `${API_BASE}${path}` : path; +} + +export type SiteConfig = { + siteTitle: string; + siteTitleEn: string; + cwdApiBase: string; + cwdSiteId?: string; +}; + +export type Post = { + id: number; + title: string; + slug: string; + excerpt: string; + content: string; + published: number; + created_at: string; + updated_at: string; + view_count?: number; +}; + +export async function fetchConfig(): Promise { + const r = await fetch(apiUrl("/api/config")); + const j = (await r.json()) as { + ok?: boolean; + config?: SiteConfig; + }; + if (!j.ok || !j.config) throw new Error("config"); + const viteId = import.meta.env.VITE_CWD_SITE_ID; + if (viteId && !j.config.cwdSiteId) { + j.config.cwdSiteId = viteId; + } + return j.config; +} + +export async function fetchPosts( + q?: string, + opts?: { credentials?: RequestCredentials } +): Promise { + const u = new URL(apiUrl("/api/posts"), window.location.origin); + if (q) u.searchParams.set("q", q); + const r = await fetch(u.toString(), { + credentials: opts?.credentials ?? "same-origin" + }); + const j = (await r.json()) as { ok?: boolean; posts?: Post[] }; + if (!r.ok || !j.ok) throw new Error("posts"); + return j.posts || []; +} + +export async function fetchPost( + idOrSlug: string, + bump: boolean, + opts?: { credentials?: RequestCredentials } +): Promise { + const u = new URL(apiUrl(`/api/posts/${encodeURIComponent(idOrSlug)}`), window.location.origin); + if (bump) u.searchParams.set("bump", "1"); + const r = await fetch(u.toString(), { + credentials: opts?.credentials ?? "same-origin" + }); + const j = (await r.json()) as { ok?: boolean; post?: Post; error?: string }; + if (!r.ok || !j.ok || !j.post) throw new Error(j.error || "post"); + return j.post; +} + +export async function postJson( + path: string, + body: unknown, + opts?: { method?: string } +): Promise { + const r = await fetch(apiUrl(path), { + method: opts?.method || "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify(body) + }); + return (await r.json()) as T; +} + +export async function adminLogin(token: string): Promise { + const j = await postJson<{ ok?: boolean }>("/api/admin/session", { token }); + return !!j.ok; +} + +export async function savePost( + data: Record, + id?: number | null +): Promise { + if (id) { + const r = await fetch(apiUrl(`/api/posts/${id}`), { + method: "PUT", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify(data) + }); + const j = (await r.json()) as { ok?: boolean; post?: Post; error?: string }; + if (!r.ok || !j.ok || !j.post) throw new Error(j.error || "save"); + return j.post; + } + const { id: _omit, ...body } = data; + void _omit; + const r = await fetch(apiUrl("/api/posts"), { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify(body) + }); + const j = (await r.json()) as { ok?: boolean; post?: Post; error?: string }; + if (!r.ok || !j.ok || !j.post) throw new Error(j.error || "save"); + return j.post; +} + +export async function deletePostApi(id: number): Promise { + const r = await fetch(apiUrl(`/api/posts/${id}`), { + method: "DELETE", + credentials: "include" + }); + const j = (await r.json()) as { ok?: boolean }; + if (!r.ok || !j.ok) throw new Error("delete"); +} diff --git a/client/src/components/AdminGate.tsx b/client/src/components/AdminGate.tsx new file mode 100644 index 0000000..546a3c2 --- /dev/null +++ b/client/src/components/AdminGate.tsx @@ -0,0 +1,78 @@ +import { useCallback, useState, type ReactNode } from "react"; +import { useNavigate } from "react-router-dom"; +import { adminLogin } from "../api"; + +export function AdminGate({ children }: { children: ReactNode }) { + const [n, setN] = useState(0); + const [open, setOpen] = useState(false); + const [token, setToken] = useState(""); + const navigate = useNavigate(); + + const onLogoClick = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + const next = n + 1; + if (next >= 5) { + setN(0); + setOpen(true); + return; + } + setN(next); + window.setTimeout(() => setN(0), 4000); + }, + [n] + ); + + const submit = useCallback(async () => { + const ok = await adminLogin(token); + if (ok) { + setOpen(false); + setToken(""); + navigate("/admin"); + return; + } + alert("Token 无效"); + }, [token, navigate]); + + return ( + <> + + + + ); +} diff --git a/client/src/components/CwdComments.tsx b/client/src/components/CwdComments.tsx new file mode 100644 index 0000000..0abf8eb --- /dev/null +++ b/client/src/components/CwdComments.tsx @@ -0,0 +1,134 @@ +import { useEffect } from "react"; +import { useSiteConfig } from "../ConfigContext"; + +declare global { + interface Window { + CWDComments?: new (opts: { + el: string; + apiBaseUrl: string; + postSlug: string; + lang?: string; + siteId?: string; + }) => { mount: () => void }; + __cwdLikeStatusQueryPatched?: boolean; + } +} + +/** 修正 cwd-widget 点赞 GET 使用完整 URL 导致计数为 0 的问题(沿用旧脚本逻辑) */ +function patchCwdLikeGetQuery(apiBaseNorm: string, postSlug: string): void { + if (window.__cwdLikeStatusQueryPatched) return; + window.__cwdLikeStatusQueryPatched = true; + + const base = apiBaseNorm; + const orig = window.fetch.bind(window); + window.fetch = function fetchPatched(input: RequestInfo | URL, init?: RequestInit) { + let url = + typeof input === "string" + ? input + : input instanceof URL + ? input.toString() + : input.url; + let method = "GET"; + if (init?.method) method = String(init.method).toUpperCase(); + else if (typeof Request !== "undefined" && input instanceof Request) { + method = String(input.method || "GET").toUpperCase(); + } + if ( + url && + url.indexOf(base) === 0 && + url.indexOf("/api/like?") !== -1 && + method === "GET" + ) { + try { + const u = new URL(url); + const ps = u.searchParams.get("post_slug") || ""; + if (ps.indexOf("://") !== -1) { + u.searchParams.set("post_slug", postSlug); + if (typeof input === "string") { + input = u.toString(); + } else if (typeof Request !== "undefined" && input instanceof Request) { + input = new Request(u.toString(), input); + } else if (input instanceof URL) { + input = u; + } + } + } catch { + /* ignore */ + } + } + return orig(input as RequestInfo, init); + }; +} + +export function CwdComments({ postSlug }: { postSlug: string }) { + const { cwdApiBase, cwdSiteId } = useSiteConfig(); + + useEffect(() => { + const apiNorm = (cwdApiBase || "").replace(/\/$/, ""); + patchCwdLikeGetQuery(apiNorm, postSlug); + + function loadScript(src: string): Promise { + return new Promise((resolve, reject) => { + const s = document.createElement("script"); + s.src = src; + s.async = true; + s.onload = () => resolve(); + s.onerror = () => reject(new Error(src)); + document.head.appendChild(s); + }); + } + + let cancelled = false; + const el = document.getElementById("comments"); + if (el) el.innerHTML = ""; + + (async () => { + try { + if (!window.CWDComments) { + await loadScript( + "https://cdn.jsdelivr.net/npm/cwd-widget@0.1.11/dist/cwd.umd.js" + ); + } + if (cancelled || typeof window.CWDComments === "undefined") return; + const opts: { + el: string; + apiBaseUrl: string; + postSlug: string; + lang?: string; + siteId?: string; + } = { + el: "#comments", + apiBaseUrl: cwdApiBase.replace(/\/$/, ""), + postSlug, + lang: "zh-CN" + }; + if (cwdSiteId) opts.siteId = cwdSiteId; + new window.CWDComments(opts).mount(); + } catch { + const node = document.getElementById("comments"); + if (node) node.textContent = "评论插件加载失败。"; + } + })(); + + return () => { + cancelled = true; + const node = document.getElementById("comments"); + if (node) node.innerHTML = ""; + }; + }, [postSlug, cwdApiBase, cwdSiteId]); + + return ( +
+

+ 评论 +

+
+
+ ); +} diff --git a/client/src/components/GeoFooter.tsx b/client/src/components/GeoFooter.tsx new file mode 100644 index 0000000..4e51556 --- /dev/null +++ b/client/src/components/GeoFooter.tsx @@ -0,0 +1,37 @@ +import { useEffect, useState } from "react"; + +const GEO_API = "https://geoip.api.smyhub.com/api"; + +function formatGeo(g: Record | null): string { + if (!g || typeof g !== "object") return ""; + const parts: string[] = []; + if (g.countryName) parts.push(g.countryName); + if (g.regionName) parts.push(g.regionName); + if (g.cityName) parts.push(g.cityName); + return parts.join(" "); +} + +export function GeoFooter() { + const [text, setText] = useState("正在获取访问信息…"); + + useEffect(() => { + fetch(GEO_API, { credentials: "omit", cache: "no-store" }) + .then((r) => { + if (!r.ok) throw new Error("http " + r.status); + return r.json() as Promise<{ ip?: string; geo?: Record }>; + }) + .then((data) => { + const ip = data && data.ip ? String(data.ip) : "—"; + let loc = formatGeo(data.geo || null); + if (!loc) loc = "未知位置"; + setText(`您的访问 IP:${ip} · ${loc}`); + }) + .catch(() => setText("无法获取访问位置信息")); + }, []); + + return ( + + ); +} diff --git a/client/src/components/MarkdownBody.tsx b/client/src/components/MarkdownBody.tsx new file mode 100644 index 0000000..63d94e6 --- /dev/null +++ b/client/src/components/MarkdownBody.tsx @@ -0,0 +1,37 @@ +import { useEffect, useRef } from "react"; +import { renderMarkdownInto } from "../lib/markdown"; +import { initPostToc, teardownPostTocUi } from "../lib/postToc"; + +export function MarkdownBody({ markdown }: { markdown: string }) { + const ref = useRef(null); + + useEffect(() => { + const el = ref.current; + if (!el) return; + teardownPostTocUi(); + if (!markdown.trim()) { + el.innerHTML = ""; + return; + } + el.classList.add("markdown-body", "markdown-pending"); + el.innerHTML = '

加载中…

'; + let cancelled = false; + (async () => { + try { + await renderMarkdownInto(el, markdown); + if (!cancelled) initPostToc(el); + } catch { + if (!cancelled) { + el.innerHTML = "

渲染失败。

"; + el.classList.remove("markdown-pending"); + } + } + })(); + return () => { + cancelled = true; + teardownPostTocUi(); + }; + }, [markdown]); + + return
; +} diff --git a/client/src/lib/markdown.ts b/client/src/lib/markdown.ts new file mode 100644 index 0000000..afd0762 --- /dev/null +++ b/client/src/lib/markdown.ts @@ -0,0 +1,73 @@ +import { marked } from "marked"; +import markedKatex from "marked-katex-extension"; +import hljs from "highlight.js"; + +marked.use( + markedKatex({ + throwOnError: false, + nonStandard: true + }) +); +marked.setOptions({ gfm: true, breaks: true }); + +function decodeHtmlEntities(str: string): string { + return String(str) + .replace(/ /g, "\u00a0") + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/&#(\d+);/g, (_, n) => { + const code = Number(n); + return code >= 0 && code <= 0x10ffff ? String.fromCharCode(code) : ""; + }) + .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => { + const code = parseInt(h, 16); + return code >= 0 && code <= 0x10ffff ? String.fromCharCode(code) : ""; + }); +} + +function escapeHtml(s: string): string { + return String(s) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function mermaidFencedToDiv(html: string): string { + return html.replace( + /
([\s\S]*?)<\/code><\/pre>/gi,
+    (_, inner: string) => {
+      const raw = decodeHtmlEntities(inner);
+      return `
${escapeHtml(raw)}
`; + } + ); +} + +/** 将 Markdown 渲染进元素(含 KaTeX、高亮、Mermaid) */ +export async function renderMarkdownInto( + el: HTMLElement, + markdown: string +): Promise { + let html = marked.parse(markdown, { async: false }) as string; + if (typeof html !== "string") html = ""; + html = mermaidFencedToDiv(html); + el.innerHTML = html; + el.classList.remove("markdown-pending"); + el.querySelectorAll("pre code").forEach((block) => { + try { + hljs.highlightElement(block as HTMLElement); + } catch { + /* ignore */ + } + }); + const mNodes = el.querySelectorAll(".mermaid"); + if (mNodes.length) { + const mermaid = (await import("mermaid")).default; + mermaid.initialize({ startOnLoad: false, theme: "neutral" }); + await mermaid.run({ nodes: [...mNodes] as HTMLElement[] }); + } +} diff --git a/client/src/lib/postToc.ts b/client/src/lib/postToc.ts new file mode 100644 index 0000000..fc1845c --- /dev/null +++ b/client/src/lib/postToc.ts @@ -0,0 +1,246 @@ +/** + * 文章目录(由旧 post-toc.js 迁移) + */ +const HEADING_SEL = "h2, h3, h4"; + +function headingText(h: Element): string { + return (h.textContent || "").replace(/\s+/g, " ").trim(); +} + +function makeHeadingIds(headings: Element[]): void { + const used = new Set(); + headings.forEach((h) => { + if (h.id && !used.has(h.id)) { + used.add(h.id); + return; + } + const raw = headingText(h); + let base = + raw + .toLowerCase() + .replace(/\s+/g, "-") + .replace(/[^a-z0-9\u2e80-\u9fff-]/gi, "-") + .replace(/-+/g, "-") + .replace(/^-|-$/g, "") || "section"; + let id = base; + let n = 0; + while (used.has(id) || document.getElementById(id)) id = `${base}-${++n}`; + h.id = id; + used.add(id); + }); +} + +function buildTocOl(headings: Element[]) { + const ol = document.createElement("ol"); + ol.className = "toc-tree"; + const links: HTMLAnchorElement[] = []; + const path: { level: number; li: HTMLLIElement }[] = []; + + for (const h of headings) { + const level = parseInt(h.tagName[1], 10); + if (level < 2 || level > 6) continue; + + const li = document.createElement("li"); + li.className = "toc-item"; + const a = document.createElement("a"); + a.className = "toc-link"; + a.href = "#" + h.id; + a.textContent = headingText(h); + a.dataset.targetId = h.id; + li.appendChild(a); + links.push(a); + + while (path.length && path[path.length - 1].level >= level) path.pop(); + + if (path.length === 0) { + ol.appendChild(li); + } else { + const parentLi = path[path.length - 1].li; + let sub = parentLi.querySelector(":scope > ol.toc-sub"); + if (!sub) { + sub = document.createElement("ol"); + sub.className = "toc-sub"; + parentLi.appendChild(sub); + } + sub.appendChild(li); + } + path.push({ level, li }); + } + + return { ol, links }; +} + +function setActive( + linkGroups: HTMLAnchorElement[][], + activeId: string | null +): void { + for (const links of linkGroups) { + for (const a of links) { + const on = a.dataset.targetId === activeId; + a.classList.toggle("toc-link--active", !!on); + } + } +} + +function bindScrollSpy( + headings: Element[], + linkGroups: HTMLAnchorElement[][], + desktopAside: HTMLElement | null, + mobileFab: HTMLElement | null, + scope: Element | null | undefined +): void { + const OFFSET = 100; + + function update() { + const inArticle = scope ? scope.getBoundingClientRect().bottom > 120 : true; + if (desktopAside) + desktopAside.classList.toggle("post-toc--hidden", !inArticle); + if (mobileFab) mobileFab.classList.toggle("toc-fab--hidden", !inArticle); + + const y = window.scrollY + OFFSET; + let current: string | null = headings[0]?.id || null; + for (const h of headings) { + if (h.getBoundingClientRect().top + window.scrollY <= y) + current = h.id || null; + } + if (current) setActive(linkGroups, current); + } + + let ticking = false; + function onScroll() { + if (ticking) return; + ticking = true; + requestAnimationFrame(() => { + update(); + ticking = false; + }); + } + + window.addEventListener("scroll", onScroll, { passive: true }); + window.addEventListener("resize", onScroll, { passive: true }); + update(); +} + +function bindTocLinks( + container: HTMLElement, + onNavigate?: (() => void) | null +): void { + container.addEventListener("click", (e) => { + const t = e.target as HTMLElement | null; + const a = t?.closest("a.toc-link") as HTMLAnchorElement | null; + if (!a) return; + const id = a.getAttribute("href")?.slice(1); + if (!id) return; + const target = document.getElementById(id); + if (target) { + e.preventDefault(); + target.scrollIntoView({ behavior: "smooth", block: "start" }); + history.replaceState(null, "", "#" + id); + onNavigate?.(); + } + }); +} + +function initDesktop( + aside: HTMLElement, + nav: HTMLElement, + headings: Element[] +): HTMLAnchorElement[] { + const { ol, links } = buildTocOl(headings); + nav.innerHTML = ""; + nav.appendChild(ol); + aside.hidden = false; + bindTocLinks(nav, null); + return links; +} + +function initMobile(headings: Element[]) { + if (document.getElementById("toc-fab")) return { links: [] as HTMLAnchorElement[], fab: null as HTMLElement | null }; + + const { ol, links } = buildTocOl(headings); + + const panel = document.createElement("div"); + panel.id = "toc-panel"; + panel.setAttribute("aria-label", "文章目录"); + panel.setAttribute("role", "navigation"); + const panelTitle = document.createElement("p"); + panelTitle.className = "toc-panel__title"; + panelTitle.textContent = "目录"; + panel.appendChild(panelTitle); + panel.appendChild(ol); + document.body.appendChild(panel); + + const fab = document.createElement("button"); + fab.id = "toc-fab"; + fab.type = "button"; + fab.setAttribute("aria-label", "打开文章目录"); + fab.setAttribute("aria-controls", "toc-panel"); + fab.setAttribute("aria-expanded", "false"); + fab.innerHTML = ` + `; + document.body.appendChild(fab); + + function openPanel() { + panel.classList.add("toc-panel--open"); + fab.setAttribute("aria-expanded", "true"); + fab.setAttribute("aria-label", "关闭文章目录"); + fab.classList.add("toc-fab--open"); + } + function closePanel() { + panel.classList.remove("toc-panel--open"); + fab.setAttribute("aria-expanded", "false"); + fab.setAttribute("aria-label", "打开文章目录"); + fab.classList.remove("toc-fab--open"); + } + + fab.addEventListener("click", () => { + panel.classList.contains("toc-panel--open") ? closePanel() : openPanel(); + }); + + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") closePanel(); + }); + + bindTocLinks(panel, closePanel); + + return { links, fab }; +} + +export function teardownPostTocUi(): void { + document.getElementById("toc-fab")?.remove(); + document.getElementById("toc-panel")?.remove(); +} + +export function initPostToc(markdownBody: HTMLElement | null): void { + const aside = document.getElementById("post-toc"); + const nav = document.getElementById("post-toc-nav"); + if (!markdownBody) return; + + const headings = [...markdownBody.querySelectorAll(HEADING_SEL)]; + if (headings.length === 0) { + if (aside) aside.hidden = true; + return; + } + + makeHeadingIds(headings); + const scope = markdownBody.closest(".post-with-toc"); + + let desktopLinks: HTMLAnchorElement[] = []; + if (aside && nav) { + desktopLinks = initDesktop(aside, nav, headings); + } + + const { links: mobileLinks, fab: mobileElem } = initMobile(headings); + + const fab = mobileElem || document.getElementById("toc-fab"); + bindScrollSpy(headings, [desktopLinks, mobileLinks], aside, fab, scope); + + if (location.hash.length > 1) { + const id = decodeURIComponent(location.hash.slice(1)); + if (headings.some((h) => h.id === id)) { + setActive([desktopLinks, mobileLinks], id); + } + } +} diff --git a/utils.js b/client/src/lib/text.ts similarity index 50% rename from utils.js rename to client/src/lib/text.ts index d7fd3d0..d1ea2c7 100644 --- a/utils.js +++ b/client/src/lib/text.ts @@ -1,37 +1,4 @@ -export function cleanText(value) { - return String(value ?? "").trim(); -} - -export function isTruthy(value) { - return value === true || value === "true" || value === "1" || value === 1 || value === "on" || value === "yes"; -} - -export function escapeHtml(value) { - return String(value ?? "") - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); -} - -export function escapeAttr(value) { - return escapeHtml(value).replace(/`/g, "`"); -} - -export function formatDateTime(value) { - if (!value) return ""; - const date = new Date(value); - if (Number.isNaN(date.getTime())) return escapeHtml(value); - const yyyy = date.getFullYear(); - const mm = String(date.getMonth() + 1).padStart(2, "0"); - const dd = String(date.getDate()).padStart(2, "0"); - const hh = String(date.getHours()).padStart(2, "0"); - const mi = String(date.getMinutes()).padStart(2, "0"); - return `${yyyy}-${mm}-${dd} ${hh}:${mi}`; -} - -export function stripMarkdown(text) { +export function stripMarkdown(text: unknown): string { return String(text ?? "") .replace(/```[\s\S]*?```/g, " ") .replace(/\$\$[\s\S]*?\$\$/g, " ") @@ -47,6 +14,14 @@ export function stripMarkdown(text) { .trim(); } -export function b64EncodeUtf8(str) { - return btoa(unescape(encodeURIComponent(str))); +export function formatDateTime(value: unknown): string { + if (!value) return ""; + const date = new Date(String(value)); + if (Number.isNaN(date.getTime())) return String(value); + const yyyy = date.getFullYear(); + const mm = String(date.getMonth() + 1).padStart(2, "0"); + const dd = String(date.getDate()).padStart(2, "0"); + const hh = String(date.getHours()).padStart(2, "0"); + const mi = String(date.getMinutes()).padStart(2, "0"); + return `${yyyy}-${mm}-${dd} ${hh}:${mi}`; } diff --git a/client/src/main.tsx b/client/src/main.tsx new file mode 100644 index 0000000..4835fb3 --- /dev/null +++ b/client/src/main.tsx @@ -0,0 +1,11 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { App } from "./App"; +import "./styles/app.css"; +import "./styles/markdown.css"; + +createRoot(document.getElementById("root")!).render( + + + +); diff --git a/client/src/pages/Admin.tsx b/client/src/pages/Admin.tsx new file mode 100644 index 0000000..3045d13 --- /dev/null +++ b/client/src/pages/Admin.tsx @@ -0,0 +1,234 @@ +import { FormEvent, useEffect, useState } from "react"; +import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom"; +import { + deletePostApi, + fetchPost, + fetchPosts, + savePost, + type Post +} from "../api"; +import { useSiteConfig } from "../ConfigContext"; + +export function AdminDashboard() { + const { siteTitle } = useSiteConfig(); + const [q, setQ] = useState(""); + const [inputQ, setInputQ] = useState(""); + const [posts, setPosts] = useState([]); + + useEffect(() => { + document.title = `后台 - ${siteTitle}`; + }, [siteTitle]); + + useEffect(() => { + const id = window.setTimeout(() => setQ(inputQ.trim()), 300); + return () => clearTimeout(id); + }, [inputQ]); + + useEffect(() => { + let cancelled = false; + (async () => { + try { + const list = await fetchPosts(q || undefined, { + credentials: "include" + }); + if (!cancelled) setPosts(list); + } catch { + if (!cancelled) setPosts([]); + } + })(); + return () => { + cancelled = true; + }; + }, [q]); + + return ( + <> +

+ 返回 +

+

后台

+

+ 新建文章 +

+
+

文章列表

+
e.preventDefault()} + > +

+ +

+
+ {posts.length ? ( + posts.map((post) => ( +
+

{post.title}

+

+ ID: {post.id} | Slug: {post.slug} |{" "} + {post.published ? "已发布" : "草稿"} +

+
+ 编辑 + setPosts((p) => p.filter((x) => x.id !== post.id))} /> +
+
+ )) + ) : ( +

没有找到文章。

+ )} +
+ + ); +} + +function DeleteButton({ + postId, + onDone +}: { + postId: number; + onDone: () => void; +}) { + return ( +
{ + e.preventDefault(); + if (!confirm("确定删除?")) return; + try { + await deletePostApi(postId); + onDone(); + } catch { + alert("删除失败"); + } + }} + > + +
+ ); +} + +export function AdminEditor() { + const { id: idParam } = useParams(); + const [search] = useSearchParams(); + const navigate = useNavigate(); + const { siteTitle } = useSiteConfig(); + const id = idParam ? Number(idParam) : Number(search.get("id") || "") || null; + + const [title, setTitle] = useState(""); + const [slug, setSlug] = useState(""); + const [excerpt, setExcerpt] = useState(""); + const [content, setContent] = useState(""); + const [published, setPublished] = useState(true); + + useEffect(() => { + document.title = `后台 - ${siteTitle}`; + }, [siteTitle]); + + useEffect(() => { + if (!id) { + setTitle(""); + setSlug(""); + setExcerpt(""); + setContent(""); + setPublished(true); + return; + } + let cancelled = false; + (async () => { + try { + const post = await fetchPost(String(id), false, { + credentials: "include" + }); + if (cancelled) return; + setTitle(post.title); + setSlug(post.slug); + setExcerpt(post.excerpt || ""); + setContent(post.content || ""); + setPublished(!!post.published); + } catch { + if (!cancelled) alert("加载文章失败"); + } + })(); + return () => { + cancelled = true; + }; + }, [id]); + + async function onSubmit(e: FormEvent) { + e.preventDefault(); + try { + const payload = { + title, + slug, + excerpt, + content, + published: published ? 1 : 0, + id: id || undefined + }; + const saved = await savePost(payload, id); + navigate(`/admin/edit/${saved.id}?saved=1`, { replace: true }); + } catch (er) { + alert(er instanceof Error ? er.message : "保存失败"); + } + } + + return ( + <> +

+ 返回后台 +

+

{id ? `编辑 #${id}` : "新建文章"}

+
+

+ +

+

+ +

+

+ +

+

+ -

-

- -

-

-
- - -
- -
-

文章列表

-
-

- -

-

-
- ${posts.length ? posts.map(renderAdminCard).join("") : "

没有找到文章。

"} -
- `, env)); -} - -async function handleAdminSave(request, env, url) { - const auth = await requireAdmin(request, env); - if (auth) return auth; - - const data = await readBody(request); - const rawId = data.id ? Number(data.id) : null; - const id = Number.isFinite(rawId) && rawId > 0 ? rawId : null; - const title = cleanText(data.title); - const content = cleanText(data.content); - const excerpt = cleanText(data.excerpt); - const slugBase = cleanText(data.slug) || title; - const published = isTruthy(data.published) ? 1 : 0; - - if (!title || !content) { - return htmlResponse(renderLayout("表单错误", ` -

标题和内容不能为空

-

返回后台

- `, env), 400); - } - - const slug = await uniqueSlug(env.DB, slugify(slugBase), id); - const now = new Date().toISOString(); - - if (id) { - const exists = await getPostById(env.DB, id); - if (!exists) return notFoundPage(env); - await env.DB.prepare(` - UPDATE posts - SET title = ?, slug = ?, excerpt = ?, content = ?, published = ?, updated_at = ? - WHERE id = ? - `).bind(title, slug, excerpt, content, published, now, id).run(); - } else { - const result = await env.DB.prepare(` - INSERT INTO posts (title, slug, excerpt, content, published, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?) - `).bind(title, slug, excerpt, content, published, now, now).run(); - return Response.redirect(new URL(`/admin?id=${result.meta.last_row_id}&saved=1`, url), 303); - } - - return Response.redirect(new URL(`/admin?id=${id}&saved=1`, url), 303); -} - -async function handleAdminDelete(request, env, url) { - const auth = await requireAdmin(request, env); - if (auth) return auth; - - const data = await readBody(request); - const id = Number(data.id); - if (!Number.isFinite(id) || id <= 0) { - return htmlResponse(renderLayout("表单错误", ` -

无效的文章 ID

-

返回后台

- `, env), 400); - } - - await env.DB.prepare("DELETE FROM posts WHERE id = ?").bind(id).run(); - return Response.redirect(new URL(`/admin?deleted=1`, url), 303); -} - -async function handleAdminSession(request, env) { - const password = cleanText(env.ADMIN_PASSWORD); - if (!password) { - return jsonResponse({ ok: false, error: "not_configured" }, 503); - } - - const data = await readBody(request); - const token = cleanText(data.token); - if (token !== password) { - return jsonResponse({ ok: false, error: "invalid" }, 401); - } - - const value = await adminSessionCookieValue(env); - const maxAge = 60 * 60 * 24 * 7; - const secure = new URL(request.url).protocol === "https:"; - const parts = [`sb_admin=${value}`, "Path=/", "HttpOnly", "SameSite=Lax", `Max-Age=${maxAge}`]; - if (secure) parts.push("Secure"); - - return jsonResponse({ ok: true }, 200, { - "Set-Cookie": parts.join("; ") - }); -} - -async function handleApiList(env, url) { - const q = (url.searchParams.get("q") || "").trim(); - const posts = await listPosts(env.DB, { publishedOnly: false, q }); - return jsonResponse({ ok: true, posts }); -} - -async function handleApiRead(env, idOrSlug) { - const post = await readPost(env.DB, idOrSlug); - if (!post) return jsonResponse({ ok: false, error: "Not found" }, 404); - return jsonResponse({ ok: true, post }); -} - -async function handleApiCreate(request, env) { - const auth = await requireAdmin(request, env); - if (auth) return auth; - - const data = await readBody(request); - const created = await createOrUpdatePost(env.DB, data, null); - return jsonResponse({ ok: true, post: created }, 201); -} - -async function handleApiUpdate(request, env, idOrSlug) { - const auth = await requireAdmin(request, env); - if (auth) return auth; - - const data = await readBody(request); - const existing = await readPost(env.DB, idOrSlug); - if (!existing) return jsonResponse({ ok: false, error: "Not found" }, 404); - const updated = await createOrUpdatePost(env.DB, data, existing.id); - return jsonResponse({ ok: true, post: updated }); -} - -async function handleApiDelete(request, env, idOrSlug) { - const auth = await requireAdmin(request, env); - if (auth) return auth; - - const post = await readPost(env.DB, idOrSlug); - if (!post) return jsonResponse({ ok: false, error: "Not found" }, 404); - await env.DB.prepare("DELETE FROM posts WHERE id = ?").bind(post.id).run(); - return jsonResponse({ ok: true, deleted: post.id }); -} - -async function createOrUpdatePost(db, data, currentId) { - const rawId = currentId ? Number(currentId) : null; - const id = Number.isFinite(rawId) && rawId > 0 ? rawId : null; - const title = cleanText(data.title); - const content = cleanText(data.content); - const excerpt = cleanText(data.excerpt); - const slugBase = cleanText(data.slug) || title; - const published = isTruthy(data.published) ? 1 : 0; - - if (!title || !content) { - throw new Error("title 和 content 不能为空"); - } - - const slug = await uniqueSlug(db, slugify(slugBase), id); - const now = new Date().toISOString(); - - if (id) { - await db.prepare(` - UPDATE posts - SET title = ?, slug = ?, excerpt = ?, content = ?, published = ?, updated_at = ? - WHERE id = ? - `).bind(title, slug, excerpt, content, published, now, id).run(); - return await getPostById(db, id); - } - - const result = await db.prepare(` - INSERT INTO posts (title, slug, excerpt, content, published, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?) - `).bind(title, slug, excerpt, content, published, now, now).run(); - return await getPostById(db, result.meta.last_row_id); -} - -async function readPost(db, idOrSlug) { - if (!idOrSlug) return null; - const trimmed = decodeURIComponent(String(idOrSlug)).trim(); - if (/^\d+$/.test(trimmed)) { - return await getPostById(db, Number(trimmed)); - } - return await db.prepare("SELECT * FROM posts WHERE slug = ? LIMIT 1").bind(trimmed).first(); -} - -async function getPostById(db, id) { - return await db.prepare("SELECT * FROM posts WHERE id = ? LIMIT 1").bind(Number(id)).first(); -} - -async function listPosts(db, { publishedOnly = true, q = "" } = {}) { - const params = []; - const where = []; - if (publishedOnly) where.push("published = 1"); - if (q) { - where.push("(title LIKE ? OR slug LIKE ? OR excerpt LIKE ? OR content LIKE ?)"); - const like = `%${q}%`; - params.push(like, like, like, like); - } - const sql = ` - SELECT * FROM posts - ${where.length ? `WHERE ${where.join(" AND ")}` : ""} - ORDER BY published DESC, updated_at DESC, id DESC - `; - const statement = db.prepare(sql); - const { results } = params.length ? await statement.bind(...params).all() : await statement.all(); - return results || []; -} - -async function uniqueSlug(db, baseSlug, currentId) { - let slug = baseSlug || "post"; - let suffix = 2; - - while (true) { - const existing = await db.prepare("SELECT id FROM posts WHERE slug = ? LIMIT 1").bind(slug).first(); - if (!existing || Number(existing.id) === Number(currentId)) return slug; - slug = `${baseSlug || "post"}-${suffix++}`; - } -} - -function slugify(value) { - const raw = cleanText(value); - const slug = raw - .normalize("NFKD") - .replace(/[\u0300-\u036f]/g, "") - .replace(/[^\p{L}\p{N}]+/gu, "-") - .replace(/^-+|-+$/g, "") - .replace(/-+/g, "-") - .toLowerCase(); - return slug || "post"; -} -function normalizePath(pathname) { - if (pathname === "/") return "/"; - return pathname.replace(/\/+$/, ""); -} - -function htmlResponse(html, status = 200, headers = {}) { - return new Response(html, { - status, - headers: { - "content-type": "text/html; charset=UTF-8", - ...headers - } - }); -} - -function jsonResponse(data, status = 200, extraHeaders = {}) { - return new Response(JSON.stringify(data, null, 2), { - status, - headers: { - "content-type": "application/json; charset=UTF-8", - ...corsHeaders(), - ...extraHeaders - } - }); -} - -function notFoundPage(env) { - return htmlResponse(renderLayout("404", ` -

404

-

页面不存在。

-

返回

- `, env), 404); -} - -function corsHeaders() { - return { - "access-control-allow-origin": "*", - "access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS", - "access-control-allow-headers": "*" - }; -} - -async function requireAdmin(request, env) { - const password = cleanText(env.ADMIN_PASSWORD); - if (!password) return null; - - const cookies = parseCookie(request.headers.get("Cookie") || ""); - const expected = await adminSessionCookieValue(env); - if (cookies.sb_admin && cookies.sb_admin === expected) { - return null; - } - - const header = request.headers.get("Authorization") || ""; - if (header.startsWith("Basic ")) { - try { - const decoded = atob(header.slice(6)); - const split = decoded.indexOf(":"); - const inputPassword = split >= 0 ? decoded.slice(split + 1) : ""; - if (inputPassword === password) return null; - } catch { - // ignore - } - } - - return unauthorized(); -} - -function parseCookie(header) { - const out = {}; - if (!header) return out; - for (const part of header.split(";")) { - const idx = part.indexOf("="); - if (idx === -1) continue; - const k = part.slice(0, idx).trim(); - const v = part.slice(idx + 1).trim(); - try { - out[k] = decodeURIComponent(v); - } catch { - out[k] = v; - } - } - return out; -} - -async function adminSessionCookieValue(env) { - const password = cleanText(env.ADMIN_PASSWORD); - const enc = new TextEncoder(); - const data = enc.encode(`${password}\0sproutblog_admin_v1`); - const hash = await crypto.subtle.digest("SHA-256", data); - return [...new Uint8Array(hash)].map((b) => b.toString(16).padStart(2, "0")).join(""); -} - -function unauthorized() { - return new Response("Authentication required", { - status: 401, - headers: { - "content-type": "text/plain; charset=UTF-8" - } - }); -} - -async function readBody(request) { - const type = request.headers.get("content-type") || ""; - if (type.includes("application/json")) { - return await request.json(); - } - - if (type.includes("multipart/form-data") || type.includes("application/x-www-form-urlencoded")) { - const form = await request.formData(); - const data = {}; - for (const [key, value] of form.entries()) { - data[key] = value; - } - return data; - } - - return {}; -} -function blankPost() { - return { - id: "", - title: "", - slug: "", - excerpt: "", - content: "", - published: 1 - }; -} - diff --git a/src/api/router.ts b/src/api/router.ts new file mode 100644 index 0000000..f2e1c06 --- /dev/null +++ b/src/api/router.ts @@ -0,0 +1,56 @@ +import { jsonResponse, corsHeaders } from "../lib/http"; +import { normalizePath } from "../lib/slug"; +import { handleConfigGet } from "./routes/config"; +import { handleAdminSessionPost } from "./routes/admin-session"; +import { + handlePostsCollection, + handlePostItem +} from "./routes/posts"; + +export async function handleApi(request: Request, env: Env): Promise { + if (!env.DB) { + return jsonResponse({ ok: false, error: "db_not_configured" }, 500); + } + + if (request.method === "OPTIONS") { + return new Response(null, { headers: corsHeaders() }); + } + + const url = new URL(request.url); + const path = normalizePath(url.pathname); + const segments = path.split("/").filter(Boolean); + + if (segments[0] !== "api") { + return jsonResponse({ ok: false, error: "Not found" }, 404); + } + + const parts = segments.slice(1); + + if ( + parts[0] === "config" && + parts.length === 1 && + request.method === "GET" + ) { + return handleConfigGet(env); + } + + if ( + parts[0] === "admin" && + parts[1] === "session" && + parts.length === 2 && + request.method === "POST" + ) { + return handleAdminSessionPost(request, env); + } + + if (parts[0] === "posts") { + if (parts.length === 1) { + return handlePostsCollection(request, env, url); + } + if (parts.length === 2) { + return handlePostItem(request, env, url, parts[1]); + } + } + + return jsonResponse({ ok: false, error: "Not found" }, 404); +} diff --git a/src/api/routes/admin-session.ts b/src/api/routes/admin-session.ts new file mode 100644 index 0000000..b3ef30d --- /dev/null +++ b/src/api/routes/admin-session.ts @@ -0,0 +1,29 @@ +import { jsonResponse, readBody, getAdminPassword } from "../../lib/http"; +import { cleanText } from "../../lib/text"; +import { adminSessionCookieValue } from "../../lib/auth"; + +export async function handleAdminSessionPost( + request: Request, + env: Env +): Promise { + const password = getAdminPassword(env); + if (!password) { + return jsonResponse({ ok: false, error: "not_configured" }, 503); + } + + const data = await readBody(request); + const token = cleanText(data.token); + if (token !== password) { + return jsonResponse({ ok: false, error: "invalid" }, 401); + } + + const value = await adminSessionCookieValue(env); + const maxAge = 60 * 60 * 24 * 7; + const secure = new URL(request.url).protocol === "https:"; + const parts = [`sb_admin=${value}`, "Path=/", "HttpOnly", "SameSite=Lax", `Max-Age=${maxAge}`]; + if (secure) parts.push("Secure"); + + return jsonResponse({ ok: true }, 200, { + "Set-Cookie": parts.join("; ") + }); +} diff --git a/src/api/routes/config.ts b/src/api/routes/config.ts new file mode 100644 index 0000000..66c4ae2 --- /dev/null +++ b/src/api/routes/config.ts @@ -0,0 +1,17 @@ +import { jsonResponse } from "../../lib/http"; +import { cleanText } from "../../lib/text"; + +export function handleConfigGet(env: Env): Response { + const cwdApiBase = + cleanText(env.CWD_API_BASE) || "https://cwd.api.smyhub.com"; + const cwdSiteId = cleanText(env.CWD_SITE_ID) || ""; + return jsonResponse({ + ok: true, + config: { + siteTitle: cleanText(env.SITE_TITLE) || "萌芽小窝", + siteTitleEn: cleanText(env.SITE_TITLE_EN) || "SproutBlog", + cwdApiBase, + cwdSiteId: cwdSiteId || undefined + } + }); +} diff --git a/src/api/routes/posts.ts b/src/api/routes/posts.ts new file mode 100644 index 0000000..654d7ed --- /dev/null +++ b/src/api/routes/posts.ts @@ -0,0 +1,113 @@ +import { jsonResponse, readBody } from "../../lib/http"; +import { requireAdmin, canAccessDrafts, hasValidAdminSession } from "../../lib/auth"; +import { + listPosts, + readPost, + createOrUpdatePost, + bumpViewCount, + deletePost, + type PostRow +} from "../../db/posts"; + +function numPublished(p: PostRow): number { + return Number(p.published) ? 1 : 0; +} + +async function listForRequest( + request: Request, + env: Env, + url: URL +): Promise { + const q = (url.searchParams.get("q") || "").trim(); + const seeAll = await canAccessDrafts(request, env); + const publishedOnly = !seeAll; + const posts = await listPosts(env.DB, { publishedOnly, q }); + return jsonResponse({ ok: true, posts }); +} + +export async function handlePostsCollection( + request: Request, + env: Env, + url: URL +): Promise { + if (request.method === "GET") { + return listForRequest(request, env, url); + } + + if (request.method === "POST") { + const denied = await requireAdmin(request, env); + if (denied) return denied; + const data = await readBody(request); + try { + const created = await createOrUpdatePost(env.DB, data, null); + return jsonResponse({ ok: true, post: created }, 201); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + return jsonResponse({ ok: false, error: msg }, 400); + } + } + + return jsonResponse({ ok: false, error: "Method not allowed" }, 405); +} + +export async function handlePostItem( + request: Request, + env: Env, + url: URL, + idOrSlug: string +): Promise { + const bump = url.searchParams.get("bump") === "1"; + + if (request.method === "GET") { + const post = await readPost(env.DB, idOrSlug); + if (!post) return jsonResponse({ ok: false, error: "Not found" }, 404); + + const draftsOk = await canAccessDrafts(request, env); + if (!draftsOk && numPublished(post) !== 1) { + return jsonResponse({ ok: false, error: "Not found" }, 404); + } + + let row = post; + const shouldBump = + bump && + numPublished(post) === 1 && + !(await hasValidAdminSession(request, env)); + if (shouldBump) { + await bumpViewCount(env.DB, Number(post.id)); + const refreshed = await readPost(env.DB, idOrSlug); + if (refreshed) row = refreshed; + } + + return jsonResponse({ ok: true, post: row }); + } + + if (request.method === "PUT") { + const denied = await requireAdmin(request, env); + if (denied) return denied; + const data = await readBody(request); + const existing = await readPost(env.DB, idOrSlug); + if (!existing) return jsonResponse({ ok: false, error: "Not found" }, 404); + try { + const updated = await createOrUpdatePost( + env.DB, + data, + Number(existing.id) + ); + return jsonResponse({ ok: true, post: updated }); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + return jsonResponse({ ok: false, error: msg }, 400); + } + } + + if (request.method === "DELETE") { + const denied = await requireAdmin(request, env); + if (denied) return denied; + const post = await readPost(env.DB, idOrSlug); + if (!post) return jsonResponse({ ok: false, error: "Not found" }, 404); + await deletePost(env.DB, Number(post.id)); + return jsonResponse({ ok: true, deleted: post.id }); + } + + return jsonResponse({ ok: false, error: "Method not allowed" }, 405); +} diff --git a/src/bindings.d.ts b/src/bindings.d.ts new file mode 100644 index 0000000..005d59a --- /dev/null +++ b/src/bindings.d.ts @@ -0,0 +1,9 @@ +interface Env { + DB: D1Database; + ASSETS: Fetcher; + SITE_TITLE: string; + SITE_TITLE_EN: string; + ADMIN_PASSWORD?: string; + CWD_API_BASE?: string; + CWD_SITE_ID?: string; +} diff --git a/src/db/posts.ts b/src/db/posts.ts new file mode 100644 index 0000000..279676f --- /dev/null +++ b/src/db/posts.ts @@ -0,0 +1,135 @@ +import { cleanText, isTruthy } from "../lib/text"; +import { slugify } from "../lib/slug"; + +type Db = Env["DB"]; + +export type PostRow = Record; + +export function blankPost(): PostRow { + return { + id: "", + title: "", + slug: "", + excerpt: "", + content: "", + published: 1 + }; +} + +export async function getPostById(db: Db, id: number): Promise { + return (await db + .prepare("SELECT * FROM posts WHERE id = ? LIMIT 1") + .bind(Number(id)) + .first()) as PostRow | null; +} + +export async function readPost(db: Db, idOrSlug: string): Promise { + if (!idOrSlug) return null; + const trimmed = decodeURIComponent(String(idOrSlug)).trim(); + if (/^\d+$/.test(trimmed)) { + return getPostById(db, Number(trimmed)); + } + return (await db + .prepare("SELECT * FROM posts WHERE slug = ? LIMIT 1") + .bind(trimmed) + .first()) as PostRow | null; +} + +export async function listPosts( + db: Db, + { publishedOnly = true, q = "" } = {} +): Promise { + const params: string[] = []; + const where: string[] = []; + if (publishedOnly) where.push("published = 1"); + if (q) { + where.push("(title LIKE ? OR slug LIKE ? OR excerpt LIKE ? OR content LIKE ?)"); + const like = `%${q}%`; + params.push(like, like, like, like); + } + const sql = ` + SELECT * FROM posts + ${where.length ? `WHERE ${where.join(" AND ")}` : ""} + ORDER BY published DESC, updated_at DESC, id DESC + `; + const statement = db.prepare(sql); + const { results } = params.length + ? await statement.bind(...params).all() + : await statement.all(); + return (results || []) as PostRow[]; +} + +export async function uniqueSlug( + db: Db, + baseSlug: string, + currentId: number | null +): Promise { + let slug = baseSlug || "post"; + let suffix = 2; + + while (true) { + const existing = (await db + .prepare("SELECT id FROM posts WHERE slug = ? LIMIT 1") + .bind(slug) + .first()) as { id: number } | null; + if (!existing || Number(existing.id) === Number(currentId)) return slug; + slug = `${baseSlug || "post"}-${suffix++}`; + } +} + +export async function createOrUpdatePost( + db: Db, + data: Record, + currentId: number | null +): Promise { + const rawId = currentId != null ? Number(currentId) : NaN; + const id = Number.isFinite(rawId) && rawId > 0 ? rawId : null; + const title = cleanText(data.title); + const content = cleanText(data.content); + const excerpt = cleanText(data.excerpt); + const slugBase = cleanText(data.slug) || title; + const published = isTruthy(data.published) ? 1 : 0; + + if (!title || !content) { + throw new Error("title 和 content 不能为空"); + } + + const slug = await uniqueSlug(db, slugify(slugBase), id); + const now = new Date().toISOString(); + + if (id) { + await db + .prepare(` + UPDATE posts + SET title = ?, slug = ?, excerpt = ?, content = ?, published = ?, updated_at = ? + WHERE id = ? + `) + .bind(title, slug, excerpt, content, published, now, id) + .run(); + return getPostById(db, id); + } + + const result = await db + .prepare(` + INSERT INTO posts (title, slug, excerpt, content, published, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + `) + .bind(title, slug, excerpt, content, published, now, now) + .run(); + const newId = result.meta?.last_row_id; + if (newId == null) return null; + return getPostById(db, Number(newId)); +} + +export async function bumpViewCount(db: Db, postId: number): Promise { + await db + .prepare( + "UPDATE posts SET view_count = COALESCE(view_count, 0) + 1 WHERE id = ?" + ) + .bind(postId) + .run(); +} + +export async function deletePost(db: Db, id: number): Promise { + await db.prepare("DELETE FROM posts WHERE id = ?").bind(id).run(); +} diff --git a/src/lib/auth.ts b/src/lib/auth.ts new file mode 100644 index 0000000..57e4c75 --- /dev/null +++ b/src/lib/auth.ts @@ -0,0 +1,78 @@ +import { cleanText } from "./text"; +import { getAdminPassword, unauthorized } from "./http"; + +export function parseCookie(header: string): Record { + const out: Record = {}; + if (!header) return out; + for (const part of header.split(";")) { + const idx = part.indexOf("="); + if (idx === -1) continue; + const k = part.slice(0, idx).trim(); + const v = part.slice(idx + 1).trim(); + try { + out[k] = decodeURIComponent(v); + } catch { + out[k] = v; + } + } + return out; +} + +export async function adminSessionCookieValue(env: Env): Promise { + const password = getAdminPassword(env); + const enc = new TextEncoder(); + const data = enc.encode(`${password}\0sproutblog_admin_v1`); + const hash = await crypto.subtle.digest("SHA-256", data); + return [...new Uint8Array(hash)] + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); +} + +/** + * 需要后台权限的接口:未配置密码则放行;否则校验 Cookie 或 Basic Auth。 + * 返回 null 表示已通过;返回 Response 为 401。 + */ +export async function requireAdmin( + request: Request, + env: Env +): Promise { + const password = getAdminPassword(env); + if (!password) return null; + + const cookies = parseCookie(request.headers.get("Cookie") || ""); + const expected = await adminSessionCookieValue(env); + if (cookies.sb_admin && cookies.sb_admin === expected) { + return null; + } + + const header = request.headers.get("Authorization") || ""; + if (header.startsWith("Basic ")) { + try { + const decoded = atob(header.slice(6)); + const split = decoded.indexOf(":"); + const inputPassword = split >= 0 ? decoded.slice(split + 1) : ""; + if (inputPassword === password) return null; + } catch { + /* ignore */ + } + } + + return unauthorized(); +} + +/** 已配置密码且当前请求具备后台凭证(Cookie / Basic) */ +export async function hasValidAdminSession( + request: Request, + env: Env +): Promise { + const password = getAdminPassword(env); + if (!password) return false; + return (await requireAdmin(request, env)) === null; +} + +/** 是否可列出/阅读草稿(未配置密码时保持原先「全量可见」行为) */ +export async function canAccessDrafts(request: Request, env: Env): Promise { + const password = getAdminPassword(env); + if (!password) return true; + return (await requireAdmin(request, env)) === null; +} diff --git a/src/lib/http.ts b/src/lib/http.ts new file mode 100644 index 0000000..89c4c87 --- /dev/null +++ b/src/lib/http.ts @@ -0,0 +1,63 @@ +import { cleanText } from "./text"; + +export function corsHeaders(): Record { + return { + "access-control-allow-origin": "*", + "access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS", + "access-control-allow-headers": "*" + }; +} + +export function jsonResponse( + data: unknown, + status = 200, + extraHeaders: Record = {} +): Response { + return new Response(JSON.stringify(data, null, 2), { + status, + headers: { + "content-type": "application/json; charset=UTF-8", + ...corsHeaders(), + ...extraHeaders + } + }); +} + +export async function readBody(request: Request): Promise> { + const type = request.headers.get("content-type") || ""; + if (type.includes("application/json")) { + try { + return (await request.json()) as Record; + } catch { + return {}; + } + } + + if ( + type.includes("multipart/form-data") || + type.includes("application/x-www-form-urlencoded") + ) { + const form = await request.formData(); + const data: Record = {}; + for (const [key, value] of form.entries()) { + data[key] = value; + } + return data; + } + + return {}; +} + +export function unauthorized(): Response { + return new Response("Authentication required", { + status: 401, + headers: { + "content-type": "text/plain; charset=UTF-8" + } + }); +} + +/** Basic Auth / Cookie 中的密码校验用原始字符串 */ +export function getAdminPassword(env: Pick): string { + return cleanText(env.ADMIN_PASSWORD); +} diff --git a/src/lib/slug.ts b/src/lib/slug.ts new file mode 100644 index 0000000..3c08f1c --- /dev/null +++ b/src/lib/slug.ts @@ -0,0 +1,18 @@ +import { cleanText } from "./text"; + +export function normalizePath(pathname: string): string { + if (pathname === "/") return "/"; + return pathname.replace(/\/+$/, ""); +} + +export function slugify(value: unknown): string { + const raw = cleanText(value); + const slug = raw + .normalize("NFKD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/[^\p{L}\p{N}]+/gu, "-") + .replace(/^-+|-+$/g, "") + .replace(/-+/g, "-") + .toLowerCase(); + return slug || "post"; +} diff --git a/src/lib/text.ts b/src/lib/text.ts new file mode 100644 index 0000000..762de8d --- /dev/null +++ b/src/lib/text.ts @@ -0,0 +1,16 @@ +/** 文本与表单常用工具(Worker 侧) */ + +export function cleanText(value: unknown): string { + return String(value ?? "").trim(); +} + +export function isTruthy(value: unknown): boolean { + return ( + value === true || + value === "true" || + value === "1" || + value === 1 || + value === "on" || + value === "yes" + ); +} diff --git a/src/worker/index.ts b/src/worker/index.ts new file mode 100644 index 0000000..b3049ba --- /dev/null +++ b/src/worker/index.ts @@ -0,0 +1,15 @@ +import { handleApi } from "../api/router"; + +export default { + async fetch( + request: Request, + env: Env, + _ctx: ExecutionContext + ): Promise { + const url = new URL(request.url); + if (url.pathname.startsWith("/api/")) { + return handleApi(request, env); + } + return env.ASSETS.fetch(request); + } +}; diff --git a/tsconfig.worker.json b/tsconfig.worker.json new file mode 100644 index 0000000..263a559 --- /dev/null +++ b/tsconfig.worker.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "types": ["@cloudflare/workers-types"], + "strict": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src/**/*.ts", "src/**/*.d.ts"] +} diff --git a/worker.js b/worker.js deleted file mode 100644 index c3a2e14..0000000 --- a/worker.js +++ /dev/null @@ -1,3 +0,0 @@ -import app from "./server.js"; - -export default app; diff --git a/wrangler.toml b/wrangler.toml index 3b2cb98..6823be7 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -1,5 +1,5 @@ name = "sproutblog" -main = "worker.js" +main = "src/worker/index.ts" compatibility_date = "2026-04-13" routes = [ @@ -7,7 +7,10 @@ routes = [ ] [assets] -directory = "./public/" +directory = "./dist" +binding = "ASSETS" +not_found_handling = "single-page-application" +run_worker_first = [ "/api/*" ] [vars] SITE_TITLE = "萌芽小窝"