From 724a0016072bf5a3c666cbe4e78ad20010d5ff00 Mon Sep 17 00:00:00 2001 From: dash <1549469775@qq.com> Date: Sun, 31 Aug 2025 17:29:05 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=96=87=E7=AB=A0=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BC=98=E5=8C=96=E6=96=87?= =?UTF-8?q?=E7=AB=A0=E5=88=97=E8=A1=A8=E5=92=8C=E8=AF=A6=E6=83=85=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=EF=BC=8C=E6=94=AF=E6=8C=81=E5=88=86=E7=B1=BB=E3=80=81?= =?UTF-8?q?=E6=A0=87=E7=AD=BE=E5=92=8C=E6=90=9C=E7=B4=A2=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3=E8=A7=86=E5=9B=BE?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=EF=BC=8C=E6=B7=BB=E5=8A=A0=E5=BC=80=E5=8F=91?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bun.lockb | Bin 162124 -> 162470 bytes database/development.sqlite3-shm | Bin 32768 -> 32768 bytes database/development.sqlite3-wal | Bin 12392 -> 267832 bytes docs/dev.md | 6 + package.json | 1 + src/controllers/Page/ArticleController.js | 123 +++++++++++++++ src/controllers/Page/PageController.js | 6 +- src/db/models/ArticleModel.js | 128 ++++++++-------- src/views/layouts/empty.pug | 6 +- src/views/layouts/root.pug | 4 +- src/views/page/articles/article.pug | 70 +++++++++ src/views/page/articles/category.pug | 29 ++++ src/views/page/articles/index.pug | 240 ++++++++++++++++-------------- src/views/page/articles/search.pug | 34 +++++ src/views/page/articles/tag.pug | 32 ++++ src/views/page/auth/no-auth.pug | 2 +- src/views/page/index/index.pug | 6 +- src/views/page/login/index.pug | 2 +- src/views/page/register/index.pug | 2 +- 19 files changed, 499 insertions(+), 192 deletions(-) create mode 100644 docs/dev.md create mode 100644 src/controllers/Page/ArticleController.js create mode 100644 src/views/page/articles/article.pug create mode 100644 src/views/page/articles/category.pug create mode 100644 src/views/page/articles/search.pug create mode 100644 src/views/page/articles/tag.pug diff --git a/bun.lockb b/bun.lockb index 98b29ec432036451f1db9a97eb874c63281199ec..965abd33c955d96ae14025205955841900f3b8fb 100644 GIT binary patch delta 18487 zcmeHvd3a4%-}YWd64{8EB!Y-3f+P~*L?Tm-am+=esw4zKsKk^OiI$>d~egAY{xpV)1_xi21*IIiG z`<#8w*>lZ&^``l3SjgFk)q`KEKX~VfH-3qFKL4AHW|dmT#HFVn@8ww3 z9!<;6$(WWsu}YGio{&wCZ@N>Y84I?kVAB^S@!oWe`>fk2e z-v`_J9xwv)H-Vdh7l50B$AJUE$(q}M8CL_$6JHr*ryl^*F9HXE-vax9ADx^tbyQAX zj?rm^E$q`Ro0K+bawZ0QMo#*)%!#9o&gq6x8yP~t?8SQEy5LG$zl_eNe*|0~{1upS zo5A(KFM^qWF_>}Fz>G_ykMrdBg2Aqi(g{Ie7U07G@ZFJi0cXMV_kmf!Ct${H0`o+x zbb6jnpP>Cz8Bx{OQQs+hRc$=bece1J*i$JpCv@z@$>~P*iS~89HQv_O!{-z`55Fon zJv(hwMvh_RX4!FHfH@WYC&_J9hlra?mP!4*?ksl*uxwn#wzob*|v`It+G1K*|Ngt6Hjl?fz8)-T`wkV>0hT+ z2eWoZ!{~t0YD&?_DSF71|HKq zWKv_Ncu5u|CzxlTML`oXsiD*1A1z~g#ft}JZm$G$X|!QHfTYTDZbPRyB1@q8bTW)Y zXb!ofl~eSQxvm5=4;m&oDu80mz3{Nw@FXa(UW|kKwUBc3HH zX$B|Tm(d{w2HfND>{@e76v4C0<6yiF4_jP5@?oeY^pY{d;?3+{b{AuyV938Ci-sk5 z{tB&~+}BMF`-pp9gwIuiQH z$421l_On}xwl#N}d*HQmSHtrjJd9s7#~Zsb#}j%RRqqOq%Z(>1>p2fzI~g}PR=h7e zr6ri>b*!J&l=cJdMq{-=pU;5zkX;V0Qr~lVA*R9OIWTT|ImKpKl%8N-gT||Z z#$u|q8zf^!#fu!7J1W7mL5g9Fkh4d{dgj6!Zdr$5rCL__Pn zEX&#gYpi88K)o_7D-V{PdI6T5qt7tIm|&%Dg_UVpK@S_oD9f7nFY7EUJ4esqtX16D z&K@xAFz-j)70rM(j9JBJvS?g_D3c}Q63p({@iFK*n=#Xtz)R#j6-Q-JW`d*fNO>nS z-m`By7B5*kv9kw^Ua~YZ*7GE+-j>yNl$8i=DlA+33|4 zd)S2{oo4L`?r}`7FFbqBCnY(}CGfC7V2pDOIRGyP9;P5Bo!>UFZJWgrM3rx+VEPI2pr{ zBx?B-JUf!D-Ug2&&{~E(jVWA_xpGFsY6soEeHaI?y!5s3Tu7^K_0$b`UEL*^(b;w{ z+I^Z0kIR!ccPsN_cx)%?5$-f^>oj;+@*cn)9s9?+$1tCUhuxlw=`^SLB|PS{_E=L) zH4NOV?ifg3qqcl*jb051xjb^BvZhZg~_8b^UB zl$~ZJcs8Am<=EQuro+->1S4iQJeFhapJwG5?yJZa41&jV+q>Xv@HjH;ekg{guSc-c z5j0cA%!xN2o9XTBGbMHt&tddrDQ-0i zVD&{5Iupa`EIj7HguuWXKihrbIJmaLW14m0#64N^M1mt>j*NLSCN03QUIgL4%>vvI zAU6VdkUfE>Koy`Z!1Pdn2iXf~PX|XucF59aePzSt-u_H+@*fT|OTe`-ykw(!zIiaX zZCh$0*a4tht>e#_>1dL5kUap559=U%1N{M>WdKkK7^MBdU>+41SLxq{|EtdmqyUwq z&x*igh7SXHx`*lDAlC%2Hd#7W9;+Id=~nYF@2rE&(lAT#5JU1}hgvx|NDS zY8`*ZJS&E%6^|ijb;ca+vuBx}4;XU$eBV5JPtwCd=DFts?4pGLkBVFy{u+S!p4Yq< z%%dVReI3B^UIcjj3Flb>8<5Drml=hF%mOxQeg({01q)%prLY zpneG8L8ksQz~=k{F#R;(kjGd0$_p#Ktt<8$6do0s^(?kjo7vFw+9$JPt^ka?4)CbR zJkt$;d=ubNky+j!cuU3#xNRjHMnz`89j%k;-vfBUGVKeNj-w(|a^N4E>;}98p4j?n39+*;nt&@3Te=ySnv`(hqNc$C;Qe*rh2kLY(d#*W{ zWSVWX|9@iU3)68GnNqmc$&8Bxv!YR8CUxLH9C>b|BD01u2w)e+>p(Ki1pH%qSFQgQ zW@tB^ub1Y2V3gm{c#x?$$gJUD?N{VF(8p<=TuH8bI#BLW-fplPjVv8eky+wo=&bTo zo&G;zmNOmsS;P!o4w>dmfj1o)@R&{@)1L*dE{87;ly9wWB0pYTQ@*v>o^eZbgEqb^2fByuTXwzZ4h$muk$G zH`Og8(`=4^-g5TyzH;iaCc=_TCd2I=HVatDVwciYxoHz&p4ciR>3 zzPVsmyvycrq0ja8@4IdL?wh0L@4Id5uA9T+@4M|vc^ncr{>tq>XV{E;dJ{N{MW&p%A^x;17= zmh1f2esfOm8Fv23zK<`N+_cNrBcI#x{y$5~3d0)osrFiNkLiV*H)Jo7$B+5Rb;n$y zNERPUk*kmU$Ueti;v-pbJVo~W*+-Vb+b)xTPLa3az5TOGd@4)ey?(++4nN@%JLIMl zDRSsZAL(<_CBBfUCsU;NDId8V-j~uml_Iyo%R1!}J7p2PaliP;M!&ekZkhQ@iu60} zBlp7lM*5#lk-Ol{KJ5~F_lN9s4ef)s?wU)K$YOY_ucLj} zUE;1RxQ_PSK>Og8%H$hpAH273xcDF#-s?BfzMC!~;qRFZOB1HtP+9XocP$7Jjj}R`= zOr;9Q&DCa73uQV$EmayRSQU|4salmlAu5y9TJ0dUQT`q%XqN{Hn(cvtLe*{vB1{E& zg2GiUNVW1p@^McjN2m}l=#gqJsl7Tvic%4kK^@d0Qb%=)SLmySXE#`=vf6qDTO$dTm^ygY6B@jm5>tEfU2M*wTbkA64gLmR4S>f+Dz)EOdn8p zl}75Jiby?Gt?Hm;l}YNQc92}kzXqtcnnLQMc9Z(5pqik5Dwot>?IR6PA+Ii9&il_}5tQL`ms8ggA)u|5tovVXptgC}&q^e>HtLw6%bs-E>1$EibdTc0#;VQWv zgxeI}t_NX+DxvUteF($rLr7Db>O&an3&F=1!YGyM3&GnD!gdN7%JhSl#DIRmBum2SVr*2;p&6 z5D1}X69}agW~<~T5N=a=y9tCmRYKwQAPB>QAk0;pf*=fS3c;r-geO&MQwZM8AZ({F zUzyDyY^9LZ48j6cL}6TW2#uOUSfnzWL-1<>VK0Tn%D)AKT@+@wfUs2UrZBT5gwU1{ zo>93iA+!pHaGb(&6%q{LFomVT5TrUnVPPu>ajhV%Qj1zahz@~pk%Cg4LLi)@ur35b zfhwl3x;2D8tsy+G3R*+x*#<%>g>@>q4TRei-fjcoMO8xK^|la(w}r4lZE6c)Xeb1q zPzWy-Ox3Pf`|ch!F2qDojigvjkIBlO*Bo`CbpabI7C~4vJ>_ zsO;QF@$B-xn%hMz7G=t(s~9Cl7f$Iau8aD6M%W*_3j6dDCxm#dFtoQAKTtJ^5D`@lzsNf(+^BC<8(t8xeAn)NnSD@x6sZjG zp@YNi^Mutc#W1Gm7~Z_{jc^{>TH{-pewJbwInbDoZ)+CNz%fn7@y*0Ju&s9u-A@@R zLv%j_aX)F`fCTPL!$K>%CC_g2~38bA|(+7r;&GCq^$+p4q|K*JwD*lYCCEnWzLCVOgt z)}GdJb=dxaT3oEfx(MX^&OG?A1b_VavC&# zg3aDot~I`{XbIbg{l5YRGdBR50X$afK)%P?0$^{fg2saR7HdnbaWvu2db_oa*4AsS zG5mJ{9vie42s_MD4C5tee7a~f0r*6Q-TSf*3{rD9h&p*qAq~?Fe?=#;jt^_?RjoCL zmZG)Sw8nZ5(c0@;W4-xopI2j()`DTr1o+794Xw3;9n0a#PI*&{A+U!7?3A~l@shU& zMrds_G@g$gF;Z(=wAL1Wy4F6><%Pl@rL_;W#?HymS|Oj+LN~&J@mk!f3uK33S>b2& zjcqzJKJa7cua_7fX)O|3KEU4iSZnQJF9LXM*IE?p0)UPDL~9*zeq$EET79ZTo|MlA zczgzprW4>Z0;YYgjdp-o%f$QZw$1>TKh`Jq3ewm&{{LYPAztZEg@3d zrL|ofVc@v%rh4^d@pLU-E00r#6Z@^%fz4OVkh`1sDpXD)SZ5K5r~ct~TuezKhlt z2n7OxAfPG0MWzw3AJyCkd<%RB@BeMS@R9<74e1F0dxQY5)0WJggfolLS>Mige zz;)mTPy*Zo?f`dzQa~Wi0bGTB8z=+#+>`593!o(s473750Ip$8_^c=h#&ZZ%09QjU z*|`88R8Ir4fJ7h(=mJaxGJy_2N5Bb01AIHOF>nZV{t-9~90863$AII&1>h2J4=4o! z1ZH{(3@#*`{G9B2)Zo`dc-}u?ae3WcnIJc#7A^ocg6#gfGmJ(4%e9Jh@Szp zgB=Eh0&7sO0^kbttH3oN3jTC}pCO0=IsrF9?-+`0BDS#g!xdjLWm4P{{ zH~}~r;(&*NG@vWc4d@Q^0CoU7fnC7Yz!$)mzz4txRF2EU4B&Af7nlM>@JaP`1n`p! z-GLrJPaqkH2TmjHSKtr80lyO90d#@?GQc%uDXK;A_F*Bh=c zRgkwDP#vfZtV7HTfI=D{Th@V=#|N8yo8=)m5kM0l2tiE%?(*EEc`GpqcmUvL+X;vS zS^zD{h~+-RZGcNWSMPQJSLaB8TfqdBl>y|!o(V+3Zp(?m_mZ=L2>=Ui1%v=x5r+dK zfRR8tFbWtAWB}s;ZV?lJVE|9^9?%hJ4~)VI(tt;RIq>s=4jKvIg}~D|zae3)1PXws zfCa#lz&zjyfSG0iS->Ri&qkrtUj)_wQ2@6CF8|K~d>YKSY+e1FTswa2EItI0KvnP5><6G;j*|1vm#>0?q>$0e1r~z-9vu05^b} z0Eahw<+kPrz}~nroU@)>r@2aSwcx73rOW_61C;^yfl}Zea2HqsaH7{zNt;Dr^EkM* z0M1#Sho}pi^Obgeuv)%Z6sq-`h1oLzZUcY=f`f?L?ZZGSFcjd58w7AbaZPImGzGXC zHwT#31YnVE>$y|;3W@WaDbZ^0d!nBYmu(gn3#6!!E!b8^;+(_aa`hnSbJxlMMg!^8 zfDyoOfTcYGqyeJ9yICBC_qwQwa zX8~VW@uci<7RW;81M>i$>W_5};0P73}ALb-)^cu?3pff}aOA06gi-Kto`w zUag+Jqn3RjS{82oKwJ|3|9Y*jztQLrk3@6~iwuiUdv6NA!tI5kasb{=>9HSg(Utrl z+KZ=E=zigEOLOt{QT6qH(OZ0{d=H4{1Nz6<->XbdeQoYPetRa`A=-tt594qjruH5{ zZl%OQ+4K)H@NB1|?`xJcQ zJY8kUJE%{jU7s?PR}};I%LhID{n`v)hLor<)D>HKl6vQe$QK7x_)(nrzjqdzWCadW zqmGI`;=bC9IP}st^sMe_WA?&8tcK!usXjlUa>LYjKcQB`RHb90eSrHb<7V0N7e`J# zB^-j4is*zJM>XJ>80&EKRNIb;NJs6S>JmAir)qi}9Ntq6IWE?SL+a{robrsS`m^wF z=>A6en?8%5Sa2#O+-gd@Fzl0_y;b7R!mp$IJLv28ZF_WI^+|SP+l68Kc7IDfq~ISR z;n5;i=Z(Ph>#dgkEY<|Lzkfc-H@tJn)|(w2f>$&G5nrks5cbkjo7-UA#1=w1n`2sU()#Kv-vM< zMSwFbGBT`#A=L&1h!@mHjN61bAH@B*Iw*8~wZu`@DLSAE#)s-M>+l5vsw3c`{WacR z|KZ6`tTQ>;o%?jk(bkh^CPl3-LJBV&vYt?JzoP6*2&jpyuYCFIdurWB30Bq!RKdtq zQ+~yT4IZeD{wl_ZeJbe;s`Qhp^_%clQ_qOnxR?vhp#S3^RCCS>KXvSkh_chW;l1vZ z-!StYR`NFtqH${HZ{h=YV1WBO;e$tBD7*7)+AW;3eUn+UnmiKqmfCz4qs{%@`H$+> zuRZ*kf+svgR9F*5zt|k4)D8kCt~2& z9W%-UHX)@R#?T7^?Y8YN9`#Up$|q_&%if28`Uvpx?OOc8JJWZT2b@(`5g_iWs^_sR z_@vo&`02?p-6Nhmx}!WUC`~1v7k(Y05W$k(?4CJ1J0W;&c|8uk+zEpN-c~Me%z+$xn0eCO_C&q0?99z?Nf^X&PUyR4ab9cm7s>TIe z_anNX$Xc_9oZ2=py*%JDQtBeBjJ?oo{L(cG%Tp?)t4A(iS*w?>d-+y=;H-^@96$XhWRxgo;Y!ySc~GUB{RZs z|G0yH?b;IuHLGy|Dedh&t=e?e_?GY!pD5=o?9<)Ui?`4RYgExK(Z+iuUQ9MX&DFAN zqJAK{%W(g?!wYY$IA6cs5A#t_G-`}pd4~%6L$q%&I@ewiZ{K-*+~-4AzTyylpm1oZ z&u)tb{T9JvXN*q$;`0}3toL+?sZcm#+&>sGc>jgwk4>(bY87qmyYC}{D|keetLIyH zuj}Cu>!EO{+*LpRA^HTke_JAbO7pnHsx@$bV_jSHuv7`Rtr?Pf8}~u(-Vb<@ECbl<(J^u zua4EyGbe@HH-TZ})zwl_PmLV!@UCP1v`=`bP738c Rp-QUdXi>Pfo#P)p{|iTQKo$T1 delta 18331 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 diff --git a/database/development.sqlite3-wal b/database/development.sqlite3-wal index e5145a5f085469ae0b98f06b53512114fca00ddb..fd0f3fc3e3fde3d7e761b0f8c997783bc10248c9 100644 GIT binary patch literal 267832 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%0AQ6{s5h;b>>^rhinAPHMmAkq#iQbSSh4$5s# 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 P&BgF<& { + if (article.tags) { + article.tags.split(',').forEach(tag => { + tags.add(tag.trim()) + }) + } + }) + + return ctx.render("page/articles/index", { + articles, + categories: categories.map(c => c.category), + tags: Array.from(tags), + currentPage: parseInt(page), + totalPages, + view, + title: "文章列表", + }, { + includeUser: true, + includeSite: true, + }) + } + + async show(ctx) { + const { slug } = ctx.params + console.log(slug); + + const article = await ArticleModel.findBySlug(slug) + + if (!article) { + ctx.throw(404, "文章不存在") + } + + // 增加阅读次数 + await ArticleModel.incrementViewCount(article.id) + + // 将文章内容解析为HTML + article.content = marked(article.content || '') + + // 获取相关文章 + const relatedArticles = await ArticleModel.getRelatedArticles(article.id) + + return ctx.render("page/articles/article", { + article, + relatedArticles, + title: article.title, + }, { + includeUser: true, + }) + } + + async byCategory(ctx) { + const { category } = ctx.params + const articles = await ArticleModel.findByCategory(category) + + return ctx.render("page/articles/category", { + articles, + category, + title: `${category} - 分类文章`, + }, { + includeUser: true, + }) + } + + async byTag(ctx) { + const { tag } = ctx.params + const articles = await ArticleModel.findByTags(tag) + + return ctx.render("page/articles/tag", { + articles, + tag, + title: `${tag} - 标签文章`, + }, { + includeUser: true, + }) + } + + async search(ctx) { + const { q } = ctx.query + const articles = await ArticleModel.searchByKeyword(q) + + return ctx.render("page/articles/search", { + articles, + keyword: q, + title: `搜索:${q}`, + }) + } + + static createRoutes() { + const controller = new ArticleController() + const router = new Router({ auth: true, prefix: "/articles" }) + router.get("", controller.index, { auth: false }) // 允许未登录访问 + router.get("/", controller.index, { auth: false }) // 允许未登录访问 + router.get("/search", controller.search) + router.get("/category/:category", controller.byCategory) + router.get("/tag/:tag", controller.byTag) + router.get("/:slug", controller.show) + return router + } +} + +export default ArticleController +export { ArticleController } diff --git a/src/controllers/Page/PageController.js b/src/controllers/Page/PageController.js index 3deeda6..3c7f94f 100644 --- a/src/controllers/Page/PageController.js +++ b/src/controllers/Page/PageController.js @@ -16,7 +16,7 @@ class PageController { // 首页 async indexGet(ctx) { - const blogs = await this.articleService.getAllArticles() + const blogs = await this.articleService.getPublishedArticles() return await ctx.render( "page/index/index", { @@ -176,8 +176,8 @@ class PageController { // 未授权报错页 router.get("/no-auth", controller.indexNoAuth.bind(controller), { auth: false }) - router.get("/article/:id", controller.pageGet("page/articles/index"), { auth: false }) - router.get("/articles", controller.pageGet("page/articles/index"), { auth: false }) + // router.get("/article/:id", controller.pageGet("page/articles/index"), { auth: false }) + // router.get("/articles", controller.pageGet("page/articles/index"), { auth: false }) router.get("/about", controller.pageGet("page/about/index"), { auth: false }) router.get("/terms", controller.pageGet("page/extra/terms"), { auth: false }) diff --git a/src/db/models/ArticleModel.js b/src/db/models/ArticleModel.js index 4b83535..4bf5fa9 100644 --- a/src/db/models/ArticleModel.js +++ b/src/db/models/ArticleModel.js @@ -5,17 +5,22 @@ class ArticleModel { return db("articles").orderBy("created_at", "desc") } - static async findPublished() { - return db("articles") + static async findPublished(offset, limit) { + let query = db("articles") .where("status", "published") .whereNotNull("published_at") .orderBy("published_at", "desc") + if (typeof offset === "number") { + query = query.offset(offset) + } + if (typeof limit === "number") { + query = query.limit(limit) + } + return query } static async findDrafts() { - return db("articles") - .where("status", "draft") - .orderBy("updated_at", "desc") + return db("articles").where("status", "draft").orderBy("updated_at", "desc") } static async findById(id) { @@ -27,22 +32,16 @@ class ArticleModel { } static async findByAuthor(author) { - return db("articles") - .where("author", author) - .where("status", "published") - .orderBy("published_at", "desc") + return db("articles").where("author", author).where("status", "published").orderBy("published_at", "desc") } static async findByCategory(category) { - return db("articles") - .where("category", category) - .where("status", "published") - .orderBy("published_at", "desc") + return db("articles").where("category", category).where("status", "published").orderBy("published_at", "desc") } static async findByTags(tags) { // 支持多个标签搜索,标签以逗号分隔 - const tagArray = tags.split(',').map(tag => tag.trim()) + const tagArray = tags.split(",").map(tag => tag.trim()) return db("articles") .where("status", "published") .whereRaw("tags LIKE ?", [`%${tagArray[0]}%`]) @@ -52,7 +51,7 @@ class ArticleModel { static async searchByKeyword(keyword) { return db("articles") .where("status", "published") - .where(function() { + .where(function () { this.where("title", "like", `%${keyword}%`) .orWhere("content", "like", `%${keyword}%`) .orWhere("keywords", "like", `%${keyword}%`) @@ -71,7 +70,11 @@ class ArticleModel { // 处理标签,确保格式一致 let tags = data.tags if (tags && typeof tags === "string") { - tags = tags.split(',').map(tag => tag.trim()).filter(tag => tag).join(', ') + tags = tags + .split(",") + .map(tag => tag.trim()) + .filter(tag => tag) + .join(", ") } // 生成slug(如果未提供) @@ -92,17 +95,19 @@ class ArticleModel { excerpt = this.generateExcerpt(data.content) } - return db("articles").insert({ - ...data, - tags, - slug, - reading_time: readingTime, - excerpt, - status: data.status || "draft", - view_count: 0, - created_at: db.fn.now(), - updated_at: db.fn.now(), - }).returning("*") + return db("articles") + .insert({ + ...data, + tags, + slug, + reading_time: readingTime, + excerpt, + status: data.status || "draft", + view_count: 0, + created_at: db.fn.now(), + updated_at: db.fn.now(), + }) + .returning("*") } static async update(id, data) { @@ -114,7 +119,11 @@ class ArticleModel { // 处理标签,确保格式一致 let tags = data.tags if (tags && typeof tags === "string") { - tags = tags.split(',').map(tag => tag.trim()).filter(tag => tag).join(', ') + tags = tags + .split(",") + .map(tag => tag.trim()) + .filter(tag => tag) + .join(", ") } // 生成slug(如果标题改变且未提供slug) @@ -141,15 +150,18 @@ class ArticleModel { publishedAt = db.fn.now() } - return db("articles").where("id", id).update({ - ...data, - tags: tags || current.tags, - slug: slug || current.slug, - reading_time: readingTime || current.reading_time, - excerpt: excerpt || current.excerpt, - published_at: publishedAt || current.published_at, - updated_at: db.fn.now(), - }).returning("*") + return db("articles") + .where("id", id) + .update({ + ...data, + tags: tags || current.tags, + slug: slug || current.slug, + reading_time: readingTime || current.reading_time, + excerpt: excerpt || current.excerpt, + published_at: publishedAt || current.published_at, + updated_at: db.fn.now(), + }) + .returning("*") } static async delete(id) { @@ -183,10 +195,7 @@ class ArticleModel { } static async incrementViewCount(id) { - return db("articles") - .where("id", id) - .increment("view_count", 1) - .returning("*") + return db("articles").where("id", id).increment("view_count", 1).returning("*") } static async findByDateRange(startDate, endDate) { @@ -202,10 +211,7 @@ class ArticleModel { } static async getPublishedArticleCount() { - const result = await db("articles") - .where("status", "published") - .count("id as count") - .first() + const result = await db("articles").where("status", "published").count("id as count").first() return result ? result.count : 0 } @@ -219,33 +225,19 @@ class ArticleModel { } static async getArticleCountByStatus() { - return db("articles") - .select("status") - .count("id as count") - .groupBy("status") - .orderBy("count", "desc") + return db("articles").select("status").count("id as count").groupBy("status").orderBy("count", "desc") } static async getRecentArticles(limit = 10) { - return db("articles") - .where("status", "published") - .orderBy("published_at", "desc") - .limit(limit) + return db("articles").where("status", "published").orderBy("published_at", "desc").limit(limit) } static async getPopularArticles(limit = 10) { - return db("articles") - .where("status", "published") - .orderBy("view_count", "desc") - .limit(limit) + return db("articles").where("status", "published").orderBy("view_count", "desc").limit(limit) } static async getFeaturedArticles(limit = 5) { - return db("articles") - .where("status", "published") - .whereNotNull("featured_image") - .orderBy("published_at", "desc") - .limit(limit) + return db("articles").where("status", "published").whereNotNull("featured_image").orderBy("published_at", "desc").limit(limit) } static async getRelatedArticles(articleId, limit = 5) { @@ -255,12 +247,12 @@ class ArticleModel { return db("articles") .where("status", "published") .where("id", "!=", articleId) - .where(function() { + .where(function () { if (current.category) { this.orWhere("category", current.category) } if (current.tags) { - const tags = current.tags.split(',').map(tag => tag.trim()) + const tags = current.tags.split(",").map(tag => tag.trim()) tags.forEach(tag => { this.orWhereRaw("tags LIKE ?", [`%${tag}%`]) }) @@ -274,9 +266,9 @@ class ArticleModel { static generateSlug(title) { return title .toLowerCase() - .replace(/[^\w\s-]/g, '') - .replace(/\s+/g, '-') - .replace(/-+/g, '-') + .replace(/[^\w\s-]/g, "") + .replace(/\s+/g, "-") + .replace(/-+/g, "-") .trim() } diff --git a/src/views/layouts/empty.pug b/src/views/layouts/empty.pug index 78d79f6..5011437 100644 --- a/src/views/layouts/empty.pug +++ b/src/views/layouts/empty.pug @@ -15,8 +15,7 @@ block $$content #{$site.site_title} // 桌面端菜单 .left.menu.desktop-only - a.menu-item(href="/about") 明月照佳人 - a.menu-item(href="/about") 岁月催人老 + a.menu-item(href="/articles") 所有文章 if !isLogin .right.menu.desktop-only a.menu-item(href="/login") 登录 @@ -35,8 +34,7 @@ block $$content // 移动端菜单内容(与桌面端一致) .mobile-menu.container .left.menu - a.menu-item(href="/about") 明月照佳人 - a.menu-item(href="/about") 岁月催人老 + a.menu-item(href="/articles") 所有文章 if !isLogin .right.menu a.menu-item(href="/login") 登录 diff --git a/src/views/layouts/root.pug b/src/views/layouts/root.pug index ba4c10e..479f568 100644 --- a/src/views/layouts/root.pug +++ b/src/views/layouts/root.pug @@ -12,12 +12,12 @@ html(lang="zh-CN") meta(charset="utf-8") meta(name="viewport" content="width=device-width, initial-scale=1") +css('lib/reset.css') - +css('lib/simplebar.css', true) + +css('lib/simplebar.css') +css('lib/simplebar-shim.css') +css('css/layouts/root.css') +js('lib/htmx.min.js') +js('lib/tailwindcss.3.4.17.js') - +js('lib/simplebar.min.js', true) + +js('lib/simplebar.min.js') body noscript style. diff --git a/src/views/page/articles/article.pug b/src/views/page/articles/article.pug new file mode 100644 index 0000000..a92df10 --- /dev/null +++ b/src/views/page/articles/article.pug @@ -0,0 +1,70 @@ +extends /layouts/empty.pug + +block pageContent + .container.mx-auto.px-4.py-8 + article.max-w-4xl.mx-auto + header.mb-8 + h1.text-4xl.font-bold.mb-4= article.title + .flex.flex-wrap.items-center.text-gray-600.mb-4 + span.mr-4 + i.fas.fa-calendar-alt.mr-1 + = new Date(article.published_at).toLocaleDateString() + span.mr-4 + i.fas.fa-eye.mr-1 + = article.view_count + " 阅读" + if article.reading_time + span.mr-4 + i.fas.fa-clock.mr-1 + = article.reading_time + " 分钟阅读" + if article.category + a.text-blue-600.mr-4(href=`/articles/category/${article.category}` class="hover:text-blue-800") + i.fas.fa-folder.mr-1 + = article.category + if article.status === "draft" + span(class="ml-2 px-2 py-0.5 text-xs bg-yellow-200 text-yellow-800 rounded align-middle") 未发布 + + if article.tags + .flex.flex-wrap.gap-2.mb-4 + each tag in article.tags.split(',') + a.bg-gray-100.text-gray-700.px-3.py-1.rounded-full.text-sm(href=`/articles/tag/${tag.trim()}` class="hover:bg-gray-200") + i.fas.fa-tag.mr-1 + = tag.trim() + + if article.featured_image + .mb-8 + img.w-full.rounded-lg.shadow-lg(src=article.featured_image alt=article.title) + + .prose.prose-lg.max-w-none.mb-8.markdown-content(class="prose-pre:bg-gray-100 prose-pre:p-4 prose-pre:rounded-lg prose-code:text-blue-600 prose-blockquote:border-l-4 prose-blockquote:border-gray-300 prose-blockquote:pl-4 prose-blockquote:italic prose-img:rounded-lg prose-img:shadow-md") + != article.content + + if article.keywords || article.description + .bg-gray-50.rounded-lg.p-6.mb-8 + if article.keywords + .mb-4 + h3.text-lg.font-semibold.mb-2 关键词 + .flex.flex-wrap.gap-2 + each keyword in article.keywords.split(',') + span.bg-white.px-3.py-1.rounded-full.text-sm= keyword.trim() + if article.description + h3.text-lg.font-semibold.mb-2 描述 + p.text-gray-600= article.description + + if relatedArticles && relatedArticles.length + section.border-t.pt-8.mt-8 + h2.text-2xl.font-bold.mb-6 相关文章 + .grid.grid-cols-1.gap-6(class="md:grid-cols-2") + each related in relatedArticles + .bg-white.shadow-md.rounded-lg.overflow-hidden + if related.featured_image + img.w-full.h-48.object-cover(src=related.featured_image alt=related.title) + .p-6 + h3.text-xl.font-semibold.mb-2 + a(href=`/articles/${related.slug}` class="hover:text-blue-600")= related.title + if related.excerpt + p.text-gray-600.text-sm.mb-4= related.excerpt + .flex.justify-between.items-center.text-sm.text-gray-500 + span + i.fas.fa-calendar-alt.mr-1 + = new Date(related.published_at).toLocaleDateString() + if related.category + a.text-blue-600(href=`/articles/category/${related.category}` class="hover:text-blue-800")= related.category diff --git a/src/views/page/articles/category.pug b/src/views/page/articles/category.pug new file mode 100644 index 0000000..fde2be6 --- /dev/null +++ b/src/views/page/articles/category.pug @@ -0,0 +1,29 @@ +extends /layouts/empty.pug + +block pageContent + .container.mx-auto.px-4.py-8 + h1.text-3xl.font-bold.mb-8 + span.text-gray-600 分类: + = category + + .grid.grid-cols-1.gap-6(class="md:grid-cols-2 lg:grid-cols-3") + each article in articles + .bg-white.shadow-md.rounded-lg.overflow-hidden + if article.featured_image + img.w-full.h-48.object-cover(src=article.featured_image alt=article.title) + .p-6 + h2.text-xl.font-semibold.mb-2 + a(href=`/articles/${article.slug}` class="hover:text-blue-600")= article.title + if article.excerpt + p.text-gray-600.mb-4= article.excerpt + .flex.justify-between.items-center.text-sm.text-gray-500 + span + i.fas.fa-calendar-alt.mr-1 + = new Date(article.published_at).toLocaleDateString() + span + i.fas.fa-eye.mr-1 + = article.view_count + " 阅读" + + if !articles.length + .text-center.py-8 + p.text-gray-500 该分类下暂无文章 diff --git a/src/views/page/articles/index.pug b/src/views/page/articles/index.pug index 0c1d940..1f16fe0 100644 --- a/src/views/page/articles/index.pug +++ b/src/views/page/articles/index.pug @@ -1,113 +1,133 @@ -extends /layouts/page.pug +extends /layouts/empty.pug block pageContent - .article-list-container-full - - const articles = [] - - articles.push({ id: 1, title: '文章标题1', author: '作者1', created_at: '2023-08-01', summary: '这是文章摘要...' }) - - articles.push({ id: 2, title: '文章标题2', author: '作者2', created_at: '2023-08-02', summary: '这是另一篇文章摘要...' }) - //- 文章列表 - if articles && articles.length - each article in articles - .article-item-full - h2.article-title-full - a(href=`/articles/${article.id}`) #{article.title} - .article-meta-full - span 作者:#{article.author} | 发布时间:#{article.created_at} - p.article-summary-full #{article.summary} - else - p.no-articles 暂无文章 + .flex.flex-col + .flex-1.p-8.bg-gray-50 + .container.mx-auto + // 页头 + .flex.justify-between.items-center.mb-8 + h1.text-2xl.font-bold 文章列表 + .flex.gap-4 + // 搜索框 + .relative + input#searchInput.w-64.pl-10.pr-4.py-2.border.rounded-lg( + type="text" + placeholder="搜索文章..." + hx-get="/articles/search" + hx-trigger="keyup changed delay:500ms" + hx-target="#articleList" + name="q" + class="focus:outline-none focus:ring-blue-500 focus:ring-2" + ) + i.fas.fa-search.absolute.left-3.top-3.text-gray-400 - //- 分页控件 - if totalPages > 1 - .pagination-full - if page > 1 - a.page-btn-full(href=`?page=${page-1}`) 上一页 - else - span.page-btn-full.disabled 上一页 - span.page-info-full 第 #{page} / #{totalPages} 页 - if page < totalPages - a.page-btn-full(href=`?page=${page+1}`) 下一页 - else - span.page-btn-full.disabled 下一页 - style. - .article-list-container-full { - width: 100%; - max-width: 100%; - margin: 40px 0 0 0; - background: transparent; - border-radius: 0; - box-shadow: none; - padding: 0; - display: flex; - flex-direction: column; - align-items: center; - } - .article-item-full { - width: 90vw; - max-width: 1200px; - background: #fff; - border-radius: 14px; - box-shadow: 0 2px 16px #e0e7ef; - margin-bottom: 28px; - padding: 28px 36px 18px 36px; - border-left: 6px solid #6dd5fa; - transition: box-shadow 0.2s, border-color 0.2s; - } - .article-item-full:hover { - box-shadow: 0 4px 32px #cbe7ff; - border-left: 6px solid #ff6a88; - } - .article-title-full { - margin: 0 0 8px 0; - font-size: 1.6em; - } - .article-title-full a { - color: #2b7cff; - text-decoration: none; - transition: color 0.2s; - } - .article-title-full a:hover { - color: #ff6a88; - } - .article-meta-full { - color: #888; - font-size: 1em; - margin-bottom: 8px; - } - .article-summary-full { - color: #444; - font-size: 1.13em; - } - .no-articles { - text-align: center; - color: #aaa; - margin: 40px 0; - } - .pagination-full { - display: flex; - justify-content: center; - align-items: center; - margin: 32px 0 0 0; - gap: 18px; - } - .page-btn-full { - padding: 7px 22px; - border-radius: 22px; - background: linear-gradient(90deg, #6dd5fa, #ff6a88); - color: #fff; - text-decoration: none; - font-weight: bold; - transition: background 0.2s; - cursor: pointer; - font-size: 1.08em; - } - .page-btn-full.disabled { - background: #eee; - color: #bbb; - cursor: not-allowed; - pointer-events: none; - } - .page-info-full { - color: #666; - font-size: 1.12em; - } \ No newline at end of file + // 视图切换按钮 + .flex.items-center.gap-2.bg-white.p-1.rounded-lg.border + button.p-2.rounded( + class="hover:bg-gray-100" + hx-get="/articles?view=grid" + hx-target="#articleList" + ) + i.fas.fa-th-large + button.p-2.rounded( + class="hover:bg-gray-100" + hx-get="/articles?view=list" + hx-target="#articleList" + ) + i.fas.fa-list + + // 筛选栏 + .bg-white.rounded-lg.shadow-sm.p-4.mb-6 + .flex.flex-wrap.gap-4 + if categories && categories.length + .flex.items-center.gap-2 + span.text-gray-600 分类: + each cat in categories + a.px-3.py-1.rounded-full( + class="hover:bg-blue-50 hover:text-blue-600" + (cat === currentCategory ? " bg-blue-100 text-blue-600" : "") + href=`/articles/category/${cat}` + )= cat + + if tags && tags.length + .flex.items-center.gap-2 + span.text-gray-600 标签: + each tag in tags + a.px-3.py-1.rounded-full( + class="hover:bg-blue-50 hover:text-blue-600" + (tag === currentTag ? " bg-blue-100 text-blue-600" : "") + href=`/articles/tag/${tag}` + )= tag + + // 文章列表 + #articleList.grid.grid-cols-1.gap-6(class="md:grid-cols-2 lg:grid-cols-3") + each article in articles + .bg-white.rounded-lg.shadow-sm.overflow-hidden.transition.duration-300.transform(class="hover:-translate-y-1 hover:shadow-md") + if article.featured_image + .relative.h-48 + img.w-full.h-full.object-cover(src=article.featured_image alt=article.title) + if article.category + a.absolute.top-3.right-3.px-3.py-1.bg-blue-600.text-white.text-sm.rounded-full.opacity-90( + href=`/articles/category/${article.category}` + class="hover:opacity-100" + )= article.category + .p-6 + h2.text-xl.font-bold.mb-3 + a(href=`/articles/${article.slug}` class="hover:text-blue-600")= article.title + if article.excerpt + p.text-gray-600.text-sm.mb-4.line-clamp-2= article.excerpt + + .flex.flex-wrap.gap-2.mb-4 + if article.tags + each tag in article.tags.split(',') + a.text-sm.text-gray-500( + href=`/articles/tag/${tag.trim()}` + class="hover:text-blue-600" + ) + i.fas.fa-tag.mr-1 + = tag.trim() + + .flex.justify-between.items-center.text-sm.text-gray-500 + .flex.items-center.gap-4 + span + i.far.fa-calendar.mr-1 + = new Date(article.published_at).toLocaleDateString() + if article.reading_time + span + i.far.fa-clock.mr-1 + = article.reading_time + "分钟" + span + i.far.fa-eye.mr-1 + = article.view_count + " 阅读" + + if !articles.length + .col-span-full.py-16.text-center + .text-gray-400.mb-4 + i.fas.fa-inbox.text-6xl + p.text-gray-500 暂无文章 + + // 分页 + if totalPages > 1 + .flex.justify-center.mt-8 + nav.flex.items-center.gap-1(aria-label="Pagination") + // 上一页 + if currentPage > 1 + a.px-3.py-1.rounded-md.bg-white.border( + href=`/articles?page=${currentPage - 1}` + class="text-gray-500 hover:text-gray-700 hover:bg-gray-50" + ) 上一页 + + // 页码 + each page in Array.from({length: totalPages}, (_, i) => i + 1) + if page === currentPage + span.px-3.py-1.rounded-md.bg-blue-50.text-blue-600.border.border-blue-200= page + else + a.px-3.py-1.rounded-md.bg-white.border( + href=`/articles?page=${page}` + class="text-gray-500 hover:text-gray-700 hover:bg-gray-50" + )= page + + // 下一页 + if currentPage < totalPages + a.px-3.py-1.rounded-md.bg-white.border( + href=`/articles?page=${currentPage + 1}` + class="text-gray-500 hover:text-gray-700 hover:bg-gray-50" + ) 下一页 diff --git a/src/views/page/articles/search.pug b/src/views/page/articles/search.pug new file mode 100644 index 0000000..555c2e7 --- /dev/null +++ b/src/views/page/articles/search.pug @@ -0,0 +1,34 @@ +extends /layouts/empty.pug + +block pageContent + .container.mx-auto.px-4.py-8 + .mb-8 + h1.text-3xl.font-bold.mb-4 + span.text-gray-600 搜索结果: + = keyword + p.text-gray-500 找到 #{articles.length} 篇相关文章 + + .grid.grid-cols-1.md:grid-cols-2.lg:grid-cols-3.gap-6 + each article in articles + .bg-white.shadow-md.rounded-lg.overflow-hidden + if article.featured_image + img.w-full.h-48.object-cover(src=article.featured_image alt=article.title) + .p-6 + h2.text-xl.font-semibold.mb-2 + a.hover:text-blue-600(href=`/articles/${article.slug}`)= article.title + if article.excerpt + p.text-gray-600.mb-4= article.excerpt + .flex.justify-between.items-center + .text-sm.text-gray-500 + span.mr-4 + i.fas.fa-calendar-alt.mr-1 + = new Date(article.published_at).toLocaleDateString() + span + i.fas.fa-eye.mr-1 + = article.view_count + " 阅读" + if article.category + a.text-sm.bg-blue-100.text-blue-600.px-3.py-1.rounded-full.hover:bg-blue-200(href=`/articles/category/${article.category}`)= article.category + + if !articles.length + .text-center.py-8 + p.text-gray-500 未找到相关文章 diff --git a/src/views/page/articles/tag.pug b/src/views/page/articles/tag.pug new file mode 100644 index 0000000..d52241b --- /dev/null +++ b/src/views/page/articles/tag.pug @@ -0,0 +1,32 @@ +extends /layouts/empty.pug + +block pageContent + .container.mx-auto.px-4.py-8 + h1.text-3xl.font-bold.mb-8 + span.text-gray-600 标签: + = tag + + .grid.grid-cols-1.gap-6(class="md:grid-cols-2 lg:grid-cols-3") + each article in articles + .bg-white.shadow-md.rounded-lg.overflow-hidden + if article.featured_image + img.w-full.h-48.object-cover(src=article.featured_image alt=article.title) + .p-6 + h2.text-xl.font-semibold.mb-2 + a(href=`/articles/${article.slug}` class="hover:text-blue-600")= article.title + if article.excerpt + p.text-gray-600.mb-4= article.excerpt + .flex.justify-between.items-center + .text-sm.text-gray-500 + span.mr-4 + i.fas.fa-calendar-alt.mr-1 + = new Date(article.published_at).toLocaleDateString() + span + i.fas.fa-eye.mr-1 + = article.view_count + " 阅读" + if article.category + a.text-sm.bg-blue-100.text-blue-600.px-3.py-1.rounded-full(href=`/articles/category/${article.category}` class="hover:bg-blue-200")= article.category + + if !articles.length + .text-center.py-8 + p.text-gray-500 该标签下暂无文章 diff --git a/src/views/page/auth/no-auth.pug b/src/views/page/auth/no-auth.pug index 5db22eb..d578636 100644 --- a/src/views/page/auth/no-auth.pug +++ b/src/views/page/auth/no-auth.pug @@ -1,4 +1,4 @@ -extends /layouts/page.pug +extends /layouts/empty.pug block pageContent .no-auth-container diff --git a/src/views/page/index/index.pug b/src/views/page/index/index.pug index 4224ea3..69232cb 100644 --- a/src/views/page/index/index.pug +++ b/src/views/page/index/index.pug @@ -14,7 +14,9 @@ mixin item(url, desc) mixin card(blog) .article-card(class="bg-white rounded-[12px] shadow p-6 transition hover:shadow-lg border border-gray-100") h3.article-title(class="text-lg font-semibold text-gray-900 mb-2") - a(href="/article/1" class="hover:text-blue-600 transition-colors duration-200") #{blog.title} + a(href="/articles/"+blog.slug class="hover:text-blue-600 transition-colors duration-200") #{blog.title} + if blog.status === "draft" + span(class="ml-2 px-2 py-0.5 text-xs bg-yellow-200 text-yellow-800 rounded align-middle") 未发布 p.article-meta(class="text-sm text-gray-400 mb-3 flex") span(class="mr-2 line-clamp-1" title=blog.author) span 作者: @@ -30,7 +32,7 @@ mixin card(blog) style="height: 2.8em; overflow: hidden;" ) | #{blog.description} - a(href="/article/1" class="inline-block text-sm text-blue-600 hover:underline transition-colors duration-200") 阅读全文 → + a(href="/articles/"+blog.slug class="inline-block text-sm text-blue-600 hover:underline transition-colors duration-200") 阅读全文 → mixin empty() .div-placeholder(class="h-[100px] w-full bg-gray-100 text-center flex items-center justify-center text-[16px]") diff --git a/src/views/page/login/index.pug b/src/views/page/login/index.pug index 765ae21..e1ee926 100644 --- a/src/views/page/login/index.pug +++ b/src/views/page/login/index.pug @@ -1,4 +1,4 @@ -extends /layouts/pure.pug +extends /layouts/empty.pug block pageScripts script(src="js/login.js") diff --git a/src/views/page/register/index.pug b/src/views/page/register/index.pug index 96809e3..72b3d67 100644 --- a/src/views/page/register/index.pug +++ b/src/views/page/register/index.pug @@ -1,4 +1,4 @@ -extends /layouts/pure.pug +extends /layouts/empty.pug block pageHead style.