Browse Source
- 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
30 changed files with 1016 additions and 67 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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 || "登录失败") |
|||
} |
|||
} |
File diff suppressed because one or more lines are too long
@ -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 |
|||
} |
@ -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; |
|||
} |
@ -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(); |
|||
}); |
@ -0,0 +1,3 @@ |
|||
export default { |
|||
base: "/", |
|||
} |
@ -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") // 回滚时删除表
|
|||
} |
@ -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 } |
@ -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" },
|
|||
]) |
|||
// ])
|
|||
} |
|||
|
@ -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: '/' } |
|||
]); |
|||
}; |
@ -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 |
@ -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; |
|||
} |
@ -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 |
|||
|
@ -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 |
@ -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; |
|||
} |
|||
} |
@ -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; |
|||
} |
@ -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; |
|||
} |
@ -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; |
@ -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…
Reference in new issue