16 changed files with 229 additions and 117 deletions
Binary file not shown.
@ -1,101 +0,0 @@ |
|||||
<!DOCTYPE html> |
|
||||
<html lang="zh-cn"> |
|
||||
<head> |
|
||||
<meta charset="UTF-8"> |
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
||||
<title>登录 / 注册</title> |
|
||||
<style> |
|
||||
body { background: #f5f6fa; font-family: 'Segoe UI', Arial, sans-serif; } |
|
||||
.container { max-width: 350px; margin: 60px auto; background: #fff; border-radius: 8px; box-shadow: 0 2px 12px #0001; padding: 32px 28px; } |
|
||||
h2 { text-align: center; margin-bottom: 24px; color: #333; } |
|
||||
.tabs { display: flex; margin-bottom: 24px; } |
|
||||
.tab { flex: 1; text-align: center; padding: 10px 0; cursor: pointer; border-bottom: 2px solid #eee; color: #888; font-weight: 500; } |
|
||||
.tab.active { color: #1976d2; border-bottom: 2px solid #1976d2; } |
|
||||
.form-group { margin-bottom: 18px; } |
|
||||
label { display: block; margin-bottom: 6px; color: #555; } |
|
||||
input { width: 100%; padding: 8px 10px; border: 1px solid #ccc; border-radius: 4px; font-size: 15px; } |
|
||||
button { width: 100%; padding: 10px; background: #1976d2; color: #fff; border: none; border-radius: 4px; font-size: 16px; cursor: pointer; margin-top: 8px; } |
|
||||
button:active { background: #145ea8; } |
|
||||
.msg { text-align: center; color: #e53935; margin-bottom: 10px; min-height: 22px; } |
|
||||
</style> |
|
||||
</head> |
|
||||
<body> |
|
||||
<div class="container"> |
|
||||
<div class="tabs"> |
|
||||
<div class="tab active" id="loginTab">登录</div> |
|
||||
<div class="tab" id="registerTab">注册</div> |
|
||||
</div> |
|
||||
<div class="msg" id="msg"></div> |
|
||||
<form id="loginForm"> |
|
||||
<div class="form-group"> |
|
||||
<label for="login-username">用户名</label> |
|
||||
<input type="text" id="login-username" required autocomplete="username"> |
|
||||
</div> |
|
||||
<div class="form-group"> |
|
||||
<label for="login-password">密码</label> |
|
||||
<input type="password" id="login-password" required autocomplete="current-password"> |
|
||||
</div> |
|
||||
<button type="submit">登录</button> |
|
||||
</form> |
|
||||
<form id="registerForm" style="display:none;"> |
|
||||
<div class="form-group"> |
|
||||
<label for="register-username">用户名</label> |
|
||||
<input type="text" id="register-username" required autocomplete="username"> |
|
||||
</div> |
|
||||
<div class="form-group"> |
|
||||
<label for="register-email">邮箱</label> |
|
||||
<input type="email" id="register-email" required autocomplete="email"> |
|
||||
</div> |
|
||||
<div class="form-group"> |
|
||||
<label for="register-password">密码</label> |
|
||||
<input type="password" id="register-password" required autocomplete="new-password"> |
|
||||
</div> |
|
||||
<button type="submit">注册</button> |
|
||||
</form> |
|
||||
</div> |
|
||||
<script> |
|
||||
const loginTab = document.getElementById('loginTab'); |
|
||||
const registerTab = document.getElementById('registerTab'); |
|
||||
const loginForm = document.getElementById('loginForm'); |
|
||||
const registerForm = document.getElementById('registerForm'); |
|
||||
const msg = document.getElementById('msg'); |
|
||||
|
|
||||
loginTab.onclick = () => { |
|
||||
loginTab.classList.add('active'); |
|
||||
registerTab.classList.remove('active'); |
|
||||
loginForm.style.display = ''; |
|
||||
registerForm.style.display = 'none'; |
|
||||
msg.textContent = ''; |
|
||||
}; |
|
||||
registerTab.onclick = () => { |
|
||||
registerTab.classList.add('active'); |
|
||||
loginTab.classList.remove('active'); |
|
||||
registerForm.style.display = ''; |
|
||||
loginForm.style.display = 'none'; |
|
||||
msg.textContent = ''; |
|
||||
}; |
|
||||
|
|
||||
loginForm.onsubmit = async e => { |
|
||||
e.preventDefault(); |
|
||||
msg.textContent = ''; |
|
||||
const username = document.getElementById('login-username').value.trim(); |
|
||||
const password = document.getElementById('login-password').value; |
|
||||
if (!username || !password) { msg.textContent = '请填写完整信息'; return; } |
|
||||
// TODO: 替换为实际API |
|
||||
msg.style.color = '#1976d2'; |
|
||||
msg.textContent = '登录成功(示例)'; |
|
||||
}; |
|
||||
registerForm.onsubmit = async e => { |
|
||||
e.preventDefault(); |
|
||||
msg.textContent = ''; |
|
||||
const username = document.getElementById('register-username').value.trim(); |
|
||||
const email = document.getElementById('register-email').value.trim(); |
|
||||
const password = document.getElementById('register-password').value; |
|
||||
if (!username || !email || !password) { msg.textContent = '请填写完整信息'; return; } |
|
||||
// TODO: 替换为实际API |
|
||||
msg.style.color = '#1976d2'; |
|
||||
msg.textContent = '注册成功(示例)'; |
|
||||
}; |
|
||||
</script> |
|
||||
</body> |
|
||||
</html> |
|
@ -0,0 +1 @@ |
|||||
|
asd |
@ -0,0 +1,23 @@ |
|||||
|
export const Index = async ctx => { |
||||
|
return await ctx.render("index", { name: "bluescurry" }) |
||||
|
} |
||||
|
|
||||
|
export const Page = (name, data) => async ctx => { |
||||
|
return await ctx.render(name, data) |
||||
|
} |
||||
|
|
||||
|
import Router from "utils/router.js" |
||||
|
export function createRoutes() { |
||||
|
const router = new Router() |
||||
|
router.post("/clicked", async ctx => { |
||||
|
ctx.cookies.set("token", "sadas", { |
||||
|
httpOnly: true, |
||||
|
// Setting httpOnly to false allows JavaScript to access the cookie
|
||||
|
// This enables browsers to automatically include the cookie in requests
|
||||
|
sameSite: "lax", |
||||
|
// maxAge: 86400000, // Optional: cookie expiration in milliseconds (e.g., 24 hours)
|
||||
|
}) |
||||
|
return await ctx.render("htmx/fuck", { title: "HTMX Clicked" }) |
||||
|
}) |
||||
|
return router |
||||
|
} |
@ -0,0 +1,14 @@ |
|||||
|
export const Index = async ctx => { |
||||
|
return await ctx.render("index", { name: "bluescurry" }) |
||||
|
} |
||||
|
|
||||
|
export const Page = (name, data) => async ctx => { |
||||
|
return await ctx.render(name, data) |
||||
|
} |
||||
|
|
||||
|
import Router from "utils/router.js" |
||||
|
export function createRoutes() { |
||||
|
const router = new Router() |
||||
|
router.get("/", Index) |
||||
|
return router |
||||
|
} |
@ -0,0 +1,78 @@ |
|||||
|
import { resolve } from "path" |
||||
|
import consolidate from "consolidate" |
||||
|
import send from "../Send" |
||||
|
import getPaths from "get-paths" |
||||
|
// import pretty from "pretty"
|
||||
|
import { logger } from "@/logger" |
||||
|
|
||||
|
export default viewsMiddleware |
||||
|
|
||||
|
function viewsMiddleware(path, { engineSource = consolidate, extension = "html", options = {}, map } = {}) { |
||||
|
return function views(ctx, next) { |
||||
|
if (ctx.render) return next() |
||||
|
|
||||
|
ctx.getRender = function (relPath, locals = {}) { |
||||
|
return getPaths(path, relPath, extension).then(paths => { |
||||
|
const suffix = paths.ext |
||||
|
const state = Object.assign(locals, options, ctx.state || {}) |
||||
|
state.partials = Object.assign({}, options.partials || {}) |
||||
|
|
||||
|
if (isHtml(suffix) && !map) { |
||||
|
return send.getBody(ctx, paths.rel, { root: path }) |
||||
|
} |
||||
|
|
||||
|
const engineName = map && map[suffix] ? map[suffix] : suffix |
||||
|
const render = engineSource[engineName] |
||||
|
|
||||
|
if (!engineName || !render) { |
||||
|
return Promise.reject(new Error(`Engine not found for the ".${suffix}" file extension`)) |
||||
|
} |
||||
|
|
||||
|
return render(resolve(path, paths.rel), state) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// 将 render 注入到 context 和 response 对象中
|
||||
|
ctx.response.render = ctx.render = function (relPath, locals = {}) { |
||||
|
return getPaths(path, relPath, extension).then(paths => { |
||||
|
const suffix = paths.ext |
||||
|
const state = Object.assign(locals, options, ctx.state || {}) |
||||
|
// deep copy partials
|
||||
|
state.partials = Object.assign({}, options.partials || {}) |
||||
|
logger.debug("render `%s` with %j", paths.rel, state) |
||||
|
ctx.type = "text/html" |
||||
|
|
||||
|
// 如果是 html 文件,不编译直接 send 静态文件
|
||||
|
if (isHtml(suffix) && !map) { |
||||
|
return send(ctx, paths.rel, { |
||||
|
root: path, |
||||
|
}) |
||||
|
} else { |
||||
|
const engineName = map && map[suffix] ? map[suffix] : suffix |
||||
|
|
||||
|
// 使用 engineSource 配置的渲染引擎 render
|
||||
|
const render = engineSource[engineName] |
||||
|
|
||||
|
if (!engineName || !render) return Promise.reject(new Error(`Engine not found for the ".${suffix}" file extension`)) |
||||
|
|
||||
|
return render(resolve(path, paths.rel), state).then(html => { |
||||
|
// since pug has deprecated `pretty` option
|
||||
|
// we'll use the `pretty` package in the meanwhile
|
||||
|
// if (locals.pretty) {
|
||||
|
// debug("using `pretty` package to beautify HTML")
|
||||
|
// html = pretty(html)
|
||||
|
// }
|
||||
|
ctx.body = html |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// 中间件执行结束
|
||||
|
return next() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function isHtml(ext) { |
||||
|
return ext === "html" |
||||
|
} |
@ -0,0 +1 @@ |
|||||
|
<a href="/page/htmx">#{title || '默认标题'}</a> |
@ -0,0 +1,26 @@ |
|||||
|
extends ./layouts/page.pug |
||||
|
|
||||
|
|
||||
|
block pageRoot |
||||
|
- var title = '示例页面标题' |
||||
|
|
||||
|
block pageContent |
||||
|
.container.mt-5 |
||||
|
.row.justify-content-center |
||||
|
.col-md-8.text-center |
||||
|
img.rounded-circle.shadow.mb-4(src='https://avatars.githubusercontent.com/u/9919?s=200&v=4', alt='Avatar', width='120', height='120') |
||||
|
h1.mt-3.mb-1 你的姓名 |
||||
|
h4.text-muted 你的职位 / 头衔 |
||||
|
p.lead.mt-3 这里是一段简短的自我介绍,突出你的专业技能、兴趣或座右铭。 |
||||
|
button(hx-post="/clicked" hx-swap="outerHTML") Click Me |
||||
|
hr.my-4 |
||||
|
.d-flex.justify-content-center.gap-4 |
||||
|
a(href='mailto:your@email.com', target='_blank') |
||||
|
i.fas.fa-envelope.me-2 |
||||
|
| 邮箱 |
||||
|
a(href='https://github.com/your-github', target='_blank') |
||||
|
i.fab.fa-github.me-2 |
||||
|
| GitHub |
||||
|
a(href='https://your-website.com', target='_blank') |
||||
|
i.fas.fa-globe.me-2 |
||||
|
| 个人网站 |
@ -0,0 +1,12 @@ |
|||||
|
block root |
||||
|
doctype html |
||||
|
html(lang="zh-CN") |
||||
|
head |
||||
|
title #{title || '默认标题'} |
||||
|
meta(charset="utf-8") |
||||
|
meta(name="viewport" content="width=device-width, initial-scale=1") |
||||
|
script(src="https://unpkg.com/htmx.org@2.0.4") |
||||
|
block head |
||||
|
body |
||||
|
block content |
||||
|
block scripts |
@ -0,0 +1,17 @@ |
|||||
|
|
||||
|
extends ./base.pug |
||||
|
|
||||
|
block root |
||||
|
block pageRoot |
||||
|
|
||||
|
block head |
||||
|
link(href='https://cdn.bootcss.com/twitter-bootstrap/4.1.3/css/bootstrap.min.css' rel='stylesheet') |
||||
|
script(src='https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js') |
||||
|
script(src='https://cdn.bootcss.com/twitter-bootstrap/4.1.3/js/bootstrap.bundle.min.js') |
||||
|
block pageHead |
||||
|
|
||||
|
block content |
||||
|
block pageContent |
||||
|
|
||||
|
block scripts |
||||
|
block pageScripts |
Loading…
Reference in new issue