Browse Source

feat: add site configuration management with database migration and seeding

- Implemented a new `site_config` table with migration script.
- Created a model `SiteConfigModel` for CRUD operations on site configurations.
- Added a service layer `SiteConfigService` to interact with the model.
- Developed seed data for initial site configurations.
- Introduced a script to run migrations and optionally seed the database.
- Added a footer component in Pug templates with styling.
- Created an about page and articles listing page with responsive design.
- Implemented a no-auth page for restricted access with user-friendly messaging.
- Added reset CSS and SimpleBar styles for consistent UI.
route
谢亚昕 1 month ago
parent
commit
b391dcc998
  1. BIN
      bun.lockb
  2. BIN
      database/development.sqlite3-shm
  3. BIN
      database/development.sqlite3-wal
  4. 3
      jsconfig.json
  5. 5
      package.json
  6. 58
      public/js/login.js
  7. 1
      public/lib/htmx.min.js
  8. 52
      public/reset.css
  9. 21
      public/simplebar-shim.css
  10. 84
      public/styles.css
  11. 23
      scripts/init.js
  12. 3
      src/config/index.js
  13. 28
      src/controllers/Page/PageController.js
  14. 21
      src/db/migrations/20250621013128_site_config.mjs
  15. 42
      src/db/models/SiteConfigModel.js
  16. 18
      src/db/seeds/20250616071157_users_seed.mjs
  17. 13
      src/db/seeds/20250621013324_site_config_seed.mjs
  18. 25
      src/middlewares/Views/index.js
  19. 6
      src/middlewares/install.js
  20. 25
      src/services/SiteConfigService.js
  21. 4
      src/utils/router/RouteAuth.js
  22. 51
      src/views/htmx/footer.pug
  23. 44
      src/views/layouts/base.pug
  24. 28
      src/views/layouts/page.pug
  25. 19
      src/views/layouts/pure.pug
  26. 100
      src/views/page/about/index.pug
  27. 113
      src/views/page/articles/index.pug
  28. 49
      src/views/page/auth/no-auth.pug
  29. 104
      src/views/page/index/index.pug
  30. 143
      src/views/page/login/index.pug

BIN
bun.lockb

Binary file not shown.

BIN
database/development.sqlite3-shm

Binary file not shown.

BIN
database/development.sqlite3-wal

Binary file not shown.

3
jsconfig.json

@ -8,6 +8,9 @@
"db/*": [
"src/db/*"
],
"config/*": [
"src/config/*"
],
"utils/*": [
"src/utils/*"
],

5
package.json

@ -8,7 +8,8 @@
"migrate:make": "npx knex migrate:make ",
"migrate": "npx knex migrate:latest",
"seed:make": "npx knex seed:make ",
"seed": "npx knex seed:run "
"seed": "npx knex seed:run ",
"init": "bun run scripts/init.js"
},
"devDependencies": {
"@types/bun": "latest",
@ -23,6 +24,7 @@
"koa": "^3.0.0",
"koa-bodyparser": "^4.4.1",
"koa-session": "^7.0.2",
"lodash": "^4.17.21",
"log4js": "^6.9.1",
"minimatch": "^9.0.0",
"module-alias": "^2.2.3",
@ -33,6 +35,7 @@
},
"_moduleAliases": {
"@": "./src",
"config": "./src/config",
"db": "./src/db",
"utils": "./src/utils",
"services": "./src/services"

58
public/js/login.js

@ -0,0 +1,58 @@
let loginToastTimer = null;
function showLoginToast(msg, isSuccess = false) {
let toast = document.getElementById("login-toast");
if (!toast) {
toast = document.createElement("div");
toast.id = "login-toast";
toast.style.position = "fixed";
toast.style.top = "20px";
toast.style.right = "20px";
toast.style.left = "auto";
toast.style.transform = "none";
toast.style.minWidth = "220px";
toast.style.maxWidth = "80vw";
toast.style.background = isSuccess ? "linear-gradient(90deg,#7ec6f7,#b2f7ef)" : "#fff";
toast.style.color = isSuccess ? "#1976d2" : "#ff4d4f";
toast.style.fontSize = "1.08rem";
toast.style.fontWeight = "600";
toast.style.padding = "1.1em 2.2em";
toast.style.borderRadius = "18px";
toast.style.boxShadow = "0 4px 24px rgba(30,136,229,0.13),0 1.5px 8px rgba(79,209,255,0.08)";
toast.style.zIndex = 9999;
toast.style.textAlign = "center";
toast.style.opacity = "0";
toast.style.transition = "opacity 0.2s";
document.body.appendChild(toast);
}
toast.textContent = msg;
toast.style.opacity = "1";
if (loginToastTimer) clearTimeout(loginToastTimer);
loginToastTimer = setTimeout(() => {
toast.style.opacity = "0";
setTimeout(() => {
toast.remove();
}, 300);
loginToastTimer = null;
}, 2000);
}
const loginForm = document.getElementById("login-form")
loginForm.onsubmit = async function (e) {
e.preventDefault()
const form = e.target
const data = Object.fromEntries(new FormData(form))
const res = await fetch(form.action, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
})
const result = await res.json()
if (result.success) {
showLoginToast("登录成功,2秒后跳转到首页", true)
setTimeout(() => {
window.location.href = "/"
}, 2000)
} else {
showLoginToast(result.message || "登录失败")
}
}

1
public/lib/htmx.min.js

File diff suppressed because one or more lines are too long

52
public/reset.css

@ -0,0 +1,52 @@
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
* {
box-sizing: border-box
}

21
public/simplebar-shim.css

@ -0,0 +1,21 @@
.simplebar-content {
height: 100%;
}
.simplebar-scrollbar::before {
background-color: #bdbdbd;
border-radius: 0;
left: 0;
right: 0;
bottom: 0;
top: 0;
}
.simplebar-hover .simplebar-scrollbar::before {
background-color: #909090;
}
.simplebar-wrapper:hover ~ .simplebar-track > .simplebar-scrollbar:before {
opacity: 0.5 !important;
}

84
public/styles.css

@ -3,6 +3,15 @@ body {
margin: 0;
padding: 0;
height: 100%;
background-color: #f9f9f9;
font-family: Arial, sans-serif;
color: #333;
}
body {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.navbar {
@ -12,8 +21,81 @@ body {
box-shadow: 1px 1px 3px #e4e4e4;
}
.title{
.title {
font-size: 1.5em;
margin-left: 10px;
color: #333;
}
.page-layout {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.page {
width: 100%;
display: flex;
flex-direction: row;
flex: 1;
position: relative;
max-width: 1400px;
margin: 0 auto;
}
.content {
flex: 1;
width: 0;
}
.footer {}
.nav {
padding: 20px;
}
.flota-nav {
position: sticky;
left: 0;
top: 20px;
width: 80px;
padding: 40px 0;
border-radius: 80px;
background: linear-gradient(135deg, #4fd1ff 0%, #ff6a6a 100%);
/* 更明亮的渐变色 */
box-shadow: 0 8px 32px 0 rgba(80, 80, 200, 0.18), 0 2px 16px 0 rgba(255, 106, 106, 0.12);
/* 更明显的阴影 */
transition: background 0.3s, box-shadow 0.3s;
}
.flota-nav .item {
display: block;
width: 100%;
height: 40px;
line-height: 40px;
color: #fff;
text-align: center;
cursor: pointer;
text-decoration: none;
font-weight: bold;
font-size: 1.1em;
letter-spacing: 1px;
margin-bottom: 8px;
transition: background 0.2s, color 0.2s, box-shadow 0.2s;
box-shadow: 0 2px 8px 0 rgba(79, 209, 255, 0.10);
/* item 也加轻微阴影 */
}
.flota-nav .item:hover {
background: linear-gradient(90deg, #ffb86c 0%, #4fd1ff 100%);
color: #fff200;
box-shadow: 0 4px 16px 0 rgba(255, 184, 108, 0.18);
}
.flota-nav .item.active {
background: #fff200;
color: #ff6a6a;
box-shadow: 0 4px 20px 0 rgba(255, 242, 0, 0.22);
}

23
scripts/init.js

@ -0,0 +1,23 @@
const { execSync } = require('child_process');
const readline = require('readline');
// 写一个执行npm run migrate && npm run seed的脚本,当执行npm run seed时会谈提示是否重置数据
function run(command) {
execSync(command, { stdio: 'inherit' });
}
run('npx knex migrate:latest');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question('是否重置数据?(y/N): ', (answer) => {
if (answer.trim().toLowerCase() === 'y') {
run('npx knex seed:run');
} else {
console.log('已取消数据重置。');
}
rl.close();
});

3
src/config/index.js

@ -0,0 +1,3 @@
export default {
base: "/",
}

28
src/controllers/Page/PageController.js

@ -1,13 +1,17 @@
import Router from "utils/router.js"
import UserService from "services/UserService.js"
import SiteConfigService from "services/SiteConfigService.js"
class PageController {
constructor() {
this.userService = new UserService()
this.siteConfigService = new SiteConfigService()
}
async index(ctx) {
async indexGet(ctx) {
const user = ctx.state.user
return await ctx.render("page/index/index", { title: "沧源一场", user: user })
return await ctx.render("page/index/index", {
user: user,
}, { includeSite: true })
}
async loginPost(ctx) {
@ -23,19 +27,29 @@ class PageController {
return ctx.redirect("/login")
}
page(name, data) {
pageGet(name, data) {
return async ctx => {
return await ctx.render(name, data)
return await ctx.render(name, {
...(data || {}),
user: ctx.state.user,
}, { includeSite: true })
}
}
static createRoutes() {
const controller = new PageController()
const router = new Router({ auth: "try" })
router.get("/", controller.index.bind(controller))
router.get("/login", controller.page("page/login/index", { title: "登录" }), { auth: false })
router.get("/", controller.indexGet.bind(controller))
router.get("/no-auth", controller.pageGet("page/auth/no-auth"), { 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("/login", controller.pageGet("page/login/index"), { auth: false })
router.post("/login", controller.loginPost.bind(controller), { auth: false })
router.get("/register", controller.page("page/register/index", { title: "注册" }), { auth: false })
router.get("/register", controller.pageGet("page/register/index", { title: "注册" }), { auth: false })
router.post("/register", controller.registerPost.bind(controller), { auth: false })
return router
}

21
src/db/migrations/20250621013128_site_config.mjs

@ -0,0 +1,21 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
export const up = async knex => {
return knex.schema.createTable("site_config", function (table) {
table.increments("id").primary() // 自增主键
table.string("key", 100).notNullable().unique() // 配置项key,唯一
table.text("value").notNullable() // 配置项value
table.timestamp("created_at").defaultTo(knex.fn.now()) // 创建时间
table.timestamp("updated_at").defaultTo(knex.fn.now()) // 更新时间
})
}
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
export const down = async knex => {
return knex.schema.dropTable("site_config") // 回滚时删除表
}

42
src/db/models/SiteConfigModel.js

@ -0,0 +1,42 @@
import db from "../index.js"
class SiteConfigModel {
// 获取指定key的配置
static async get(key) {
const row = await db("site_config").where({ key }).first()
return row ? row.value : null
}
// 设置指定key的配置(有则更新,无则插入)
static async set(key, value) {
const exists = await db("site_config").where({ key }).first()
if (exists) {
await db("site_config").where({ key }).update({ value, updated_at: db.fn.now() })
} else {
await db("site_config").insert({ key, value })
}
}
// 批量获取多个key的配置
static async getMany(keys) {
const rows = await db("site_config").whereIn("key", keys)
const result = {}
rows.forEach(row => {
result[row.key] = row.value
})
return result
}
// 获取所有配置
static async getAll() {
const rows = await db("site_config").select("key", "value")
const result = {}
rows.forEach(row => {
result[row.key] = row.value
})
return result
}
}
export default SiteConfigModel
export { SiteConfigModel }

18
src/db/seeds/20250616071157_users_seed.mjs

@ -1,19 +1,17 @@
export const seed = async knex => {
// 检查表是否存在
const tables = await knex.raw(`
SELECT name FROM sqlite_master WHERE type='table' AND username='users'
`)
// 检查表是否存在
const hasUsersTable = await knex.schema.hasTable('users');
if (tables.length === 0) {
console.error("表 users 不存在,请先执行迁移")
return
}
if (!hasUsersTable) {
console.error("表 users 不存在,请先执行迁移")
return
}
// Deletes ALL existing entries
await knex("users").del()
// Inserts seed entries
await knex("users").insert([
// await knex("users").insert([
// { username: "Alice", email: "alice@example.com" },
// { username: "Bob", email: "bob@example.com" },
])
// ])
}

13
src/db/seeds/20250621013324_site_config_seed.mjs

@ -0,0 +1,13 @@
export const seed = async (knex) => {
// 删除所有已有配置
await knex('site_config').del();
// 插入常用站点配置项
await knex('site_config').insert([
{ key: 'site_title', value: '🥔未野明的小屋' },
{ key: 'site_description', value: '一屋很小,却也很大' },
{ key: 'site_logo', value: '/static/logo.png' },
{ key: 'keywords', value: 'blog' },
{ key: 'base', value: '/' }
]);
};

25
src/middlewares/Views/index.js

@ -4,20 +4,37 @@ import send from "../Send"
import getPaths from "get-paths"
// import pretty from "pretty"
import { logger } from "@/logger"
import SiteConfigService from "services/SiteConfigService.js"
import assign from "lodash/assign"
import config from "config/index.js"
export default viewsMiddleware
function viewsMiddleware(path, { engineSource = consolidate, extension = "html", options = {}, map } = {}) {
const siteConfigService = new SiteConfigService()
return function views(ctx, next) {
if (ctx.render) return next()
// 将 render 注入到 context 和 response 对象中
ctx.response.render = ctx.render = function (relPath, locals = {}) {
return getPaths(path, relPath, extension).then(paths => {
ctx.response.render = ctx.render = function (relPath, locals = {}, renderOptions) {
renderOptions = assign({ includeSite: false, includeUser: false }, renderOptions || {})
return getPaths(path, relPath, extension).then(async paths => {
const suffix = paths.ext
const state = Object.assign(locals, options, ctx.state || {})
const site = await siteConfigService.getAll()
const otherData = {
currentPath: ctx.path,
$config: config,
}
if (renderOptions.includeSite) {
otherData.$site = site
}
if (renderOptions.includeUser && ctx.state && ctx.state.user) {
otherData.$user = ctx.state.user
}
const state = assign({}, otherData, locals, options, ctx.state || {})
// deep copy partials
state.partials = Object.assign({}, options.partials || {})
state.partials = assign({}, options.partials || {})
// logger.debug("render `%s` with %j", paths.rel, state)
ctx.type = "text/html"

6
src/middlewares/install.js

@ -24,7 +24,11 @@ export default app => {
{ pattern: "/", auth: false },
{ pattern: "/**/*", auth: false },
],
blackList: [],
blackList: [
"/api",
"/api/",
"/api/**/*",
],
})
)
app.use(bodyParser())

25
src/services/SiteConfigService.js

@ -0,0 +1,25 @@
import SiteConfigModel from "../db/models/SiteConfigModel.js"
class SiteConfigService {
// 获取单个配置
async get(key) {
return await SiteConfigModel.get(key)
}
// 设置单个配置
async set(key, value) {
return await SiteConfigModel.set(key, value)
}
// 批量获取
async getMany(keys) {
return await SiteConfigModel.getMany(keys)
}
// 获取全部配置
async getAll() {
return await SiteConfigModel.getAll()
}
}
export default SiteConfigService

4
src/utils/router/RouteAuth.js

@ -27,6 +27,10 @@ export default function RouteAuth(options = {}) {
if (auth === true) {
if (!ctx.state.user) {
if (ctx.accepts('html')) {
ctx.redirect('/no-auth')
return
}
ctx.status = 401
ctx.body = { success: false, error: "未登录或Token无效" }
return

51
src/views/htmx/footer.pug

@ -0,0 +1,51 @@
.footer-panel
.footer-content
p © 2023-2025 My Website. 保留所有权利。
ul.footer-links
li
a(href="/about") 关于我们
li
a(href="/contact") 联系方式
li
a(href="/privacy") 隐私
style.
.footer-panel {
background: #222;
color: #eee;
padding: 40px 0 24px 0;
font-size: 15px;
margin-top: 40px;
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.footer-content {
max-width: 900px;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
}
.footer-content p {
margin: 0 0 10px 0;
letter-spacing: 1px;
}
.footer-links {
list-style: none;
padding: 0;
display: flex;
gap: 24px;
}
.footer-links li {
display: inline;
}
.footer-links a {
color: #eee;
text-decoration: none;
transition: color 0.2s;
}
.footer-links a:hover {
color: #4fc3f7;
text-decoration: underline;
}

44
src/views/layouts/base.pug

@ -2,14 +2,54 @@ mixin include()
if block
block
mixin css(url, extranl = false)
if extranl || url.startsWith('http') || url.startsWith('//')
style(type="text/css" href=url)
else
link(rel="stylesheet", href=($config && $config.base || "") + url)
mixin js(url, extranl = false)
if extranl || url.startsWith('http') || url.startsWith('//')
script(type="text/javascript" src=url)
else
script(src=($config && $config.base || "") + url)
mixin link(href, name)
//- attributes == {class: "btn"}
a(href=href)&attributes(attributes)= name
doctype html
html(lang="zh-CN")
head
block head
title #{title || ''}
title #{$site && $site.site_title || ''}
meta(name="description" content=$site && $site.site_description || '')
meta(name="keywords" content=$site && $site.keywords || '')
if $site && $site.site_favicon
+link($site.site_favicon, '')
meta(charset="utf-8")
meta(name="viewport" content="width=device-width, initial-scale=1")
script(src="https://unpkg.com/htmx.org@2.0.4")
+css('reset.css')
+js('lib/htmx.min.js')
//- +css('https://unpkg.com/simplebar@latest/dist/simplebar.css', true)
//- +css('simplebar-shim.css')
//- +js('https://unpkg.com/simplebar@latest/dist/simplebar.min.js', true)
body
noscript
style.
.simplebar-content-wrapper {
scrollbar-width: auto;
-ms-overflow-style: auto;
}
.simplebar-content-wrapper::-webkit-scrollbar,
.simplebar-hide-scrollbar::-webkit-scrollbar {
display: initial;
width: initial;
height: initial;
}
//- div(data-simplebar style="height: 100%")
//- block content
//- block scripts
block content
block scripts

28
src/views/layouts/page.pug

@ -1,14 +1,32 @@
extends /layouts/base.pug
block head
link(rel='stylesheet', href='styles.css')
+css('styles.css')
block pageHead
block content
+include()
include /htmx/navbar.pug
block pageContent
// 页面整体flex布局,footer吸底
.page-layout
.page
- const navs = [];
- navs.push({ href: '/', label: '首页' });
- navs.push({ href: '/articles', label: '文章' });
- navs.push({ href: '/article', label: '留言板' });
- navs.push({ href: '/about', label: '关于' });
nav.nav
ul.flota-nav
each nav in navs
li
a.item(
href=nav.href,
class=currentPath === nav.href ? 'active' : ''
) #{nav.label}
.content
block pageContent
footer
+include()
- var edit = false
include /htmx/footer.pug
block scripts
block pageScripts

19
src/views/layouts/pure.pug

@ -0,0 +1,19 @@
extends /layouts/base.pug
block head
+css('styles.css')
block pageHead
block content
// 页面整体flex布局,footer吸底
.page-layout
.page
.content
block pageContent
footer
+include()
- var edit = false
include /htmx/footer.pug
block scripts
block pageScripts

100
src/views/page/about/index.pug

@ -0,0 +1,100 @@
extends /layouts/page.pug
block pageContent
.about-container
h1 关于我们
p 我们致力于打造一个基于 Koa3 的现代 Web 示例项目,帮助开发者快速上手高效、可扩展的 Web 应用开发。
.about-section
h2 我们的愿景
p 推动 Node.js 生态下的现代 Web 技术发展,降低开发门槛,提升开发体验。
.about-section
h2 技术栈
ul
li Koa3
li Pug 模板引擎
li 现代前端技术(如 ES6+、CSS3)
.about-section
h2 联系我们
p 如有建议或合作意向,欢迎通过
a(href="mailto:1549469775@qq.com") 联系方式
| 与我们取得联系。
style.
.about-container {
margin: 32px auto 0 auto;
padding: 56px 40px 44px 40px;
background: linear-gradient(120deg, #e3f3ff 0%, #fafdff 100%);
border-radius: 28px;
box-shadow: 0 8px 36px rgba(126,198,247,0.13), 0 2px 12px rgba(255,140,168,0.08);
border: 1.5px solid #b2ebf2;
//- max-width: 900px;
//- min-width: 340px;
}
.about-container h1 {
font-size: 2.7em;
color: #2196f3;
margin-bottom: 28px;
text-align: center;
font-weight: 900;
letter-spacing: 2.5px;
text-shadow: 0 2px 16px rgba(33,150,243,0.10);
background: linear-gradient(90deg, #2196f3 30%, #7ec6f7 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.about-container p {
font-size: 1.18em;
color: #333;
margin-bottom: 26px;
line-height: 1.85;
}
.about-section {
margin-bottom: 38px;
padding-bottom: 14px;
border-bottom: 1px dashed #b2ebf2;
}
.about-section:last-child {
border-bottom: none;
}
.about-section h2 {
font-size: 1.22em;
color: #1976d2;
margin-bottom: 10px;
font-weight: 700;
letter-spacing: 1.2px;
background: linear-gradient(90deg, #1976d2 60%, #7ec6f7 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.about-section ul {
padding-left: 28px;
margin-bottom: 0;
}
.about-section li {
font-size: 1.08em;
color: #444;
margin-bottom: 8px;
line-height: 1.7;
}
.about-container a {
color: #2196f3;
text-decoration: underline;
font-weight: 600;
transition: color 0.2s, background 0.2s;
border-radius: 8px;
padding: 1px 4px;
}
.about-container a:hover {
color: #fff;
background: linear-gradient(90deg, #7ec6f7 0%, #ff8ca8 100%);
text-decoration: none;
}
@media (max-width: 900px) {
.about-container {
max-width: 98vw;
padding: 18px 4vw 18px 4vw;
}
.about-container h1 {
font-size: 1.5em;
}
}

113
src/views/page/articles/index.pug

@ -0,0 +1,113 @@
extends /layouts/page.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 暂无文章
//- 分页控件
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;
}

49
src/views/page/auth/no-auth.pug

@ -0,0 +1,49 @@
extends /layouts/page.pug
block pageContent
.no-auth-container
.no-auth-icon
i.fa.fa-lock
h2 访问受限
p 您没有权限访问此页面,请先登录或联系管理员。
a.btn(href='/login') 去登录
block pageHead
style.
.no-auth-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 60vh;
text-align: center;
}
.no-auth-icon {
font-size: 4em;
color: #ff6a6a;
margin-bottom: 20px;
}
.no-auth-container h2 {
margin: 0 0 10px 0;
color: #333;
}
.no-auth-container p {
color: #888;
margin-bottom: 24px;
}
.no-auth-container .btn {
display: inline-block;
padding: 10px 32px;
background: linear-gradient(90deg, #4fd1ff 0%, #ff6a6a 100%);
color: #fff;
border: none;
border-radius: 24px;
font-size: 1.1em;
text-decoration: none;
transition: background 0.2s;
cursor: pointer;
}
.no-auth-container .btn:hover {
background: linear-gradient(90deg, #ffb86c 0%, #4fd1ff 100%);
color: #fff200;
}

104
src/views/page/index/index.pug

@ -1,10 +1,100 @@
extends /layouts/page.pug
//- +include()
//- - var edit = false
//- include /htmx/login.pug
block pageContent
div adsd
if user
div user: #{user.username}
.home-hero
h1 #{$site.site_title}
//- p.subtitle #{$site.site_description}
.actions
a.btn-primary(href="/about") 了解更多
a.btn-secondary(href="/contact") 联系我们
.features
.feature
h2 🚀 极速开发
p 使用 Koa3 和现代前端技术,快速搭建高效网站。
.feature
h2 🔒 安全可靠
p 内置多项安全机制,保障数据与用户安全。
.feature
h2 🌈 易于扩展
p 结构清晰,方便二次开发和功能拓展。
style.
.home-hero {
text-align: center;
padding: 60px 0 40px 0;
margin: 20px 20px;
background: linear-gradient(90deg, #4fc3f7 0%, #1976d2 100%);
color: #fff;
border-radius: 12px;
margin-bottom: 40px;
}
.home-hero h1 {
font-size: 2.8em;
margin-bottom: 42px;
letter-spacing: 2px;
}
.home-hero .subtitle {
font-size: 1.3em;
margin-bottom: 28px;
color: #e3f2fd;
}
.home-hero .actions {
margin-top: 18px;
}
.btn-primary, .btn-secondary {
display: inline-block;
padding: 10px 28px;
border-radius: 24px;
font-size: 1em;
margin: 0 10px;
text-decoration: none;
transition: background 0.2s, color 0.2s;
}
.btn-primary {
background: #fff;
color: #1976d2;
font-weight: bold;
border: none;
}
.btn-primary:hover {
background: #e3f2fd;
color: #1565c0;
}
.btn-secondary {
background: transparent;
color: #fff;
border: 1px solid #fff;
}
.btn-secondary:hover {
background: #1976d2;
color: #fff;
border-color: #e3f2fd;
}
.features {
display: flex;
justify-content: space-around;
margin-top: 40px;
gap: 24px;
flex-wrap: wrap;
}
.feature {
background: #fafbfc;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(30, 136, 229, 0.08);
padding: 28px 24px;
flex: 1 1 220px;
min-width: 220px;
max-width: 320px;
text-align: center;
margin: 0 8px;
}
.feature h2 {
font-size: 1.3em;
margin-bottom: 10px;
color: #1976d2;
}
.feature p {
color: #333;
font-size: 1em;
margin: 0;

143
src/views/page/login/index.pug

@ -1,31 +1,116 @@
extends /layouts/page.pug
extends /layouts/pure.pug
block content
block pageScripts
script(src="js/login.js")
block pageContent
.login-container
h2 登录
form#loginForm(action="/login" method="post")
.form-group
label(for="username") 用户名
input#username(type="text" name="username" required placeholder="请输入用户名")
.form-group
label(for="password") 密码
input#password(type="password" name="password" required placeholder="请输入密码")
button(type="submit") 登录
script.
document.getElementById('loginForm').onsubmit = async function(e) {
e.preventDefault();
const form = e.target;
const data = Object.fromEntries(new FormData(form));
const res = await fetch(form.action, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await res.json();
if(result.success) {
alert('登录成功');
window.location.href = '/';
} else {
alert(result.message || '登录失败');
}
}
.login-card
span.back-home-text
a(href="/", title="返回首页") 返回首页
h2 登录
form#login-form(action="/login" method="post")
.form-group
label(for="username") 用户名
input#username(type="text" name="username" placeholder="请输入用户名" required)
.form-group
label(for="password") 密码
input#password(type="password" name="password" placeholder="请输入密码" required)
button.login-btn(type="submit") 登录
if error
.login-error= error
// 页面内联样式优化
style.
.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 80vh;
}
.login-card {
background: #fff;
border-radius: 24px;
box-shadow: 0 4px 24px rgba(0,0,0,0.08);
padding: 2.5rem 2.5rem 2rem 2.5rem;
min-width: 380px;
max-width: 96vw;
width: 420px;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.back-home-text {
position: absolute;
left: 24px;
top: 18px;
font-size: 0.98rem;
color: #bbb;
font-weight: 500;
letter-spacing: 1px;
}
.back-home-text a {
color: #bbb;
text-decoration: none;
transition: color 0.2s;
}
.back-home-text a:hover {
color: #7ec6f7;
text-decoration: underline;
}
.login-card h2 {
margin-bottom: 1.5rem;
font-weight: 600;
color: #333;
letter-spacing: 2px;
}
.form-group {
width: 100%;
margin-bottom: 1.4rem;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.form-group label {
margin-bottom: 0.5rem;
color: #444;
font-size: 1.13rem;
font-weight: 600;
letter-spacing: 1px;
}
.form-group input {
width: 100%;
padding: 0.7rem 1.1rem;
border: 1px solid #e0e0e0;
border-radius: 8px;
font-size: 1.08rem;
outline: none;
transition: border 0.2s;
}
.form-group input:focus {
border-color: #7ec6f7;
}
.login-btn {
width: 100%;
padding: 0.8rem 0;
border: none;
border-radius: 20px;
background: linear-gradient(135deg, #7ec6f7 0%, #ff8ca8 100%);
color: #fff;
font-size: 1.13rem;
font-weight: 600;
cursor: pointer;
box-shadow: 0 2px 8px rgba(126,198,247,0.12);
transition: background 0.2s, box-shadow 0.2s;
margin-top: 0.7rem;
}
.login-btn:hover {
background: linear-gradient(135deg, #5bb0e6 0%, #ff6f91 100%);
box-shadow: 0 4px 16px rgba(255,140,168,0.12);
}
.login-error {
color: #ff4d4f;
margin-top: 1rem;
font-size: 0.98rem;
text-align: center;
}

Loading…
Cancel
Save