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
谢亚昕 2 months 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. 82
      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. 10
      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. 26
      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. 129
      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/*": [ "db/*": [
"src/db/*" "src/db/*"
], ],
"config/*": [
"src/config/*"
],
"utils/*": [ "utils/*": [
"src/utils/*" "src/utils/*"
], ],

5
package.json

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

82
public/styles.css

@ -3,6 +3,15 @@ body {
margin: 0; margin: 0;
padding: 0; padding: 0;
height: 100%; height: 100%;
background-color: #f9f9f9;
font-family: Arial, sans-serif;
color: #333;
}
body {
min-height: 100vh;
display: flex;
flex-direction: column;
} }
.navbar { .navbar {
@ -17,3 +26,76 @@ body {
margin-left: 10px; margin-left: 10px;
color: #333; 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 Router from "utils/router.js"
import UserService from "services/UserService.js" import UserService from "services/UserService.js"
import SiteConfigService from "services/SiteConfigService.js"
class PageController { class PageController {
constructor() { constructor() {
this.userService = new UserService() this.userService = new UserService()
this.siteConfigService = new SiteConfigService()
} }
async index(ctx) { async indexGet(ctx) {
const user = ctx.state.user 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) { async loginPost(ctx) {
@ -23,19 +27,29 @@ class PageController {
return ctx.redirect("/login") return ctx.redirect("/login")
} }
page(name, data) { pageGet(name, data) {
return async ctx => { return async ctx => {
return await ctx.render(name, data) return await ctx.render(name, {
...(data || {}),
user: ctx.state.user,
}, { includeSite: true })
} }
} }
static createRoutes() { static createRoutes() {
const controller = new PageController() const controller = new PageController()
const router = new Router({ auth: "try" }) const router = new Router({ auth: "try" })
router.get("/", controller.index.bind(controller)) router.get("/", controller.indexGet.bind(controller))
router.get("/login", controller.page("page/login/index", { title: "登录" }), { auth: false })
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.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 }) router.post("/register", controller.registerPost.bind(controller), { auth: false })
return router 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 }

10
src/db/seeds/20250616071157_users_seed.mjs

@ -1,10 +1,8 @@
export const seed = async knex => { export const seed = async knex => {
// 检查表是否存在 // 检查表是否存在
const tables = await knex.raw(` const hasUsersTable = await knex.schema.hasTable('users');
SELECT name FROM sqlite_master WHERE type='table' AND username='users'
`)
if (tables.length === 0) { if (!hasUsersTable) {
console.error("表 users 不存在,请先执行迁移") console.error("表 users 不存在,请先执行迁移")
return return
} }
@ -12,8 +10,8 @@ export const seed = async knex => {
await knex("users").del() await knex("users").del()
// Inserts seed entries // Inserts seed entries
await knex("users").insert([ // await knex("users").insert([
// { username: "Alice", email: "alice@example.com" }, // { username: "Alice", email: "alice@example.com" },
// { username: "Bob", email: "bob@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 getPaths from "get-paths"
// import pretty from "pretty" // import pretty from "pretty"
import { logger } from "@/logger" import { logger } from "@/logger"
import SiteConfigService from "services/SiteConfigService.js"
import assign from "lodash/assign"
import config from "config/index.js"
export default viewsMiddleware export default viewsMiddleware
function viewsMiddleware(path, { engineSource = consolidate, extension = "html", options = {}, map } = {}) { function viewsMiddleware(path, { engineSource = consolidate, extension = "html", options = {}, map } = {}) {
const siteConfigService = new SiteConfigService()
return function views(ctx, next) { return function views(ctx, next) {
if (ctx.render) return next() if (ctx.render) return next()
// 将 render 注入到 context 和 response 对象中 // 将 render 注入到 context 和 response 对象中
ctx.response.render = ctx.render = function (relPath, locals = {}) { ctx.response.render = ctx.render = function (relPath, locals = {}, renderOptions) {
return getPaths(path, relPath, extension).then(paths => { renderOptions = assign({ includeSite: false, includeUser: false }, renderOptions || {})
return getPaths(path, relPath, extension).then(async paths => {
const suffix = paths.ext 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 // deep copy partials
state.partials = Object.assign({}, options.partials || {}) state.partials = assign({}, options.partials || {})
// logger.debug("render `%s` with %j", paths.rel, state) // logger.debug("render `%s` with %j", paths.rel, state)
ctx.type = "text/html" ctx.type = "text/html"

6
src/middlewares/install.js

@ -24,7 +24,11 @@ export default app => {
{ pattern: "/", auth: false }, { pattern: "/", auth: false },
{ pattern: "/**/*", auth: false }, { pattern: "/**/*", auth: false },
], ],
blackList: [], blackList: [
"/api",
"/api/",
"/api/**/*",
],
}) })
) )
app.use(bodyParser()) 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 (auth === true) {
if (!ctx.state.user) { if (!ctx.state.user) {
if (ctx.accepts('html')) {
ctx.redirect('/no-auth')
return
}
ctx.status = 401 ctx.status = 401
ctx.body = { success: false, error: "未登录或Token无效" } ctx.body = { success: false, error: "未登录或Token无效" }
return 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 if block
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 doctype html
html(lang="zh-CN") html(lang="zh-CN")
head head
block 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(charset="utf-8")
meta(name="viewport" content="width=device-width, initial-scale=1") 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 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 content
block scripts block scripts

26
src/views/layouts/page.pug

@ -1,14 +1,32 @@
extends /layouts/base.pug extends /layouts/base.pug
block head block head
link(rel='stylesheet', href='styles.css') +css('styles.css')
block pageHead block pageHead
block content block content
+include() // 页面整体flex布局,footer吸底
include /htmx/navbar.pug .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 block pageContent
footer
+include()
- var edit = false
include /htmx/footer.pug
block scripts block scripts
block pageScripts 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 extends /layouts/page.pug
//- +include()
//- - var edit = false
//- include /htmx/login.pug
block pageContent block pageContent
div adsd .home-hero
if user h1 #{$site.site_title}
div user: #{user.username} //- 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;

129
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 .login-container
.login-card
span.back-home-text
a(href="/", title="返回首页") 返回首页
h2 登录 h2 登录
form#loginForm(action="/login" method="post") form#login-form(action="/login" method="post")
.form-group .form-group
label(for="username") 用户名 label(for="username") 用户名
input#username(type="text" name="username" required placeholder="请输入用户名") input#username(type="text" name="username" placeholder="请输入用户名" required)
.form-group .form-group
label(for="password") 密码 label(for="password") 密码
input#password(type="password" name="password" required placeholder="请输入密码") input#password(type="password" name="password" placeholder="请输入密码" required)
button(type="submit") 登录 button.login-btn(type="submit") 登录
script. if error
document.getElementById('loginForm').onsubmit = async function(e) { .login-error= error
e.preventDefault(); // 页面内联样式优化
const form = e.target; style.
const data = Object.fromEntries(new FormData(form)); .login-container {
const res = await fetch(form.action, { display: flex;
method: 'POST', justify-content: center;
headers: { 'Content-Type': 'application/json' }, align-items: center;
body: JSON.stringify(data) min-height: 80vh;
}); }
const result = await res.json(); .login-card {
if(result.success) { background: #fff;
alert('登录成功'); border-radius: 24px;
window.location.href = '/'; box-shadow: 0 4px 24px rgba(0,0,0,0.08);
} else { padding: 2.5rem 2.5rem 2rem 2.5rem;
alert(result.message || '登录失败'); 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