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 } |
@ -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 |
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 |
||||
|
@ -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 |
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; |
@ -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…
Reference in new issue