Browse Source

feat: 修复问题

route
谢亚昕 1 week ago
parent
commit
76d66cc38f
  1. 3
      .vscode/settings.json
  2. BIN
      bun.lockb
  3. BIN
      database/development.sqlite3-shm
  4. BIN
      database/development.sqlite3-wal
  5. 5
      package.json
  6. 424
      public/lib/bg-change.js
  7. BIN
      public/static/bg2.webp
  8. 9
      public/styles.css
  9. 43
      src/controllers/Page/HtmxController.js
  10. 68
      src/controllers/Page/PageController.js
  11. 30
      src/db/index.js
  12. 2
      src/db/migrations/20250616065041_create_users_table.mjs
  13. 4
      src/db/seeds/20250621013324_site_config_seed.mjs
  14. 1
      src/middlewares/Views/index.js
  15. 90
      src/middlewares/errorHandler/index.js
  16. 1
      src/middlewares/install.js
  17. 9
      src/services/userService.js
  18. 7
      src/utils/error/CommonError.js
  19. 8
      src/views/error/index.pug
  20. 3
      src/views/htmx/footer.pug
  21. 4
      src/views/htmx/fuck.pug
  22. 2
      src/views/htmx/navbar.pug
  23. 44
      src/views/htmx/timeline.pug
  24. 124
      src/views/layouts/base.pug
  25. 8
      src/views/page/auth/no-auth.pug
  26. 17
      src/views/page/index/index copy.pug
  27. 12
      src/views/page/index/index.pug
  28. 16
      src/views/page/register/index.pug

3
.vscode/settings.json

@ -0,0 +1,3 @@
{
"CodeFree.index": true
}

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.

5
package.json

@ -42,5 +42,8 @@
"db": "./src/db", "db": "./src/db",
"utils": "./src/utils", "utils": "./src/utils",
"services": "./src/services" "services": "./src/services"
},
"peerDependencies": {
"typescript": "^5.0.0"
} }
} }

424
public/lib/bg-change.js

@ -0,0 +1,424 @@
class BgSwitcher {
constructor(images, options = {}) {
this.images = images;
// 从localStorage中读取保存的索引
const savedIndex = localStorage.getItem('bgSwitcherIndex');
if (savedIndex !== null && !isNaN(savedIndex)) {
this.index = parseInt(savedIndex);
} else {
this.index = 0;
}
this.container = options.container || document.body;
this.interval = options.interval || 3000;
this.effect = options.effect || BgSwitcher.fadeEffect;
this.timer = null;
this.apiTimer = null;
this.apiUrl = null;
this.apiInterval = 30000;
this.startTime = 0;
// 从localStorage中读取保存的剩余时间
const savedRemainingTime = localStorage.getItem('bgSwitcherRemainingTime');
this.remainingTime = savedRemainingTime !== null && !isNaN(savedRemainingTime) && savedRemainingTime >= 0 ? parseInt(savedRemainingTime) : this.interval;
this.bgLayer = document.createElement('div');
this.isInitialLoad = true;
// 从localStorage中读取API模式状态
const isApiMode = localStorage.getItem('bgSwitcherIsApiMode') === 'true';
if (isApiMode) {
this.apiUrl = localStorage.getItem('bgSwitcherApiUrl') || null;
const savedApiInterval = localStorage.getItem('bgSwitcherApiInterval');
this.apiInterval = savedApiInterval !== null && !isNaN(savedApiInterval) ? parseInt(savedApiInterval) : 30000;
}
// 监听页面可见性变化
this.handleVisibilityChange = this.handleVisibilityChange.bind(this);
document.addEventListener('visibilitychange', this.handleVisibilityChange);
// 监听页面卸载事件,确保保存状态
this.handleBeforeUnload = this.handleBeforeUnload.bind(this);
window.addEventListener('beforeunload', this.handleBeforeUnload);
this.bgLayer.style.position = 'fixed';
this.bgLayer.style.top = 0;
this.bgLayer.style.left = 0;
this.bgLayer.style.width = '100vw';
this.bgLayer.style.height = '100vh';
this.bgLayer.style.zIndex = '-1';
this.bgLayer.style.transition = 'opacity 1s';
this.bgLayer.style.opacity = 1;
this.bgLayer.style.backgroundSize = 'cover';
this.bgLayer.style.backgroundColor = '#000000';
this.bgLayer.style.backgroundPosition = 'center';
this.bgLayer.style.filter = 'brightness(0.68)'
this.container.style.backgroundColor = '#000000';
}
setBg(url) {
// 切换时先预加载目标图片,加载完成后再切换显示
const img = new Image();
img.onload = () => {
if (this.isInitialLoad) {
// 初始加载时,先设置背景图再添加到页面
this.bgLayer.style.backgroundImage = `url(${url})`;
this.container.appendChild(this.bgLayer);
this.isInitialLoad = false;
} else {
this.effect(this.bgLayer, url);
}
if(!this.isApiMode) {
this.scheduleNext();
}
};
img.onerror = () => {
// 加载失败时处理
console.warn('背景图片加载失败:', url);
if (this.isInitialLoad) {
// 初始加载失败时,也添加背景层到页面
this.container.appendChild(this.bgLayer);
this.isInitialLoad = false;
}
};
img.src = url;
}
next() {
const nextIndex = (this.index + 1) % this.images.length;
const nextUrl = this.images[nextIndex];
// 切换前先预加载
const img = new Image();
img.onload = () => {
this.index = nextIndex;
// 保存索引到localStorage
localStorage.setItem('bgSwitcherIndex', this.index);
this.effect(this.bgLayer, nextUrl);
this.scheduleNext();
};
img.onerror = () => {
// 加载失败时跳过
console.warn('背景图片加载失败:', nextUrl);
this.scheduleNext();
};
img.src = nextUrl;
}
prev() {
const prevIndex = (this.index - 1 + this.images.length) % this.images.length;
const prevUrl = this.images[prevIndex];
// 切换前先预加载
const img = new Image();
img.onload = () => {
this.index = prevIndex;
// 保存索引到localStorage
localStorage.setItem('bgSwitcherIndex', this.index);
this.effect(this.bgLayer, prevUrl);
this.scheduleNext();
};
img.onerror = () => {
// 加载失败时跳过
console.warn('背景图片加载失败:', prevUrl);
this.scheduleNext();
};
img.src = prevUrl;
}
start() {
if (this.timer || this.apiTimer) return;
// 如果处于API模式,启动API请求
if (this.apiUrl) {
this.fetchRandomWallpaper();
} else {
// 否则使用默认轮播
this.setBg(this.images[this.index]);
}
}
/**
* 安排下一次背景切换
*/
scheduleNext() {
if (this.timer) {
clearTimeout(this.timer);
}
// 记录开始时间
this.startTime = Date.now();
// 使用剩余时间或默认间隔
const timeToWait = this.remainingTime > 0 ? this.remainingTime : this.interval;
this.timer = setTimeout(() => {
this.remainingTime = this.interval; // 重置剩余时间
// 保存剩余时间到localStorage
localStorage.setItem('bgSwitcherRemainingTime', this.remainingTime);
this.next();
}, timeToWait);
}
/**
* 处理页面可见性变化
*/
handleVisibilityChange() {
if (document.hidden) {
// 页面不可见时,暂停计时器并计算剩余时间
if (this.timer) {
const elapsedTime = Date.now() - this.startTime;
this.remainingTime = Math.max(0, this.remainingTime - elapsedTime);
// 保存剩余时间到localStorage
localStorage.setItem('bgSwitcherRemainingTime', this.remainingTime);
clearTimeout(this.timer);
this.timer = null;
}
// 暂停API计时器
if (this.apiTimer) {
const elapsedTime = Date.now() - this.startTime;
this.remainingTime = Math.max(0, this.remainingTime - elapsedTime);
// 保存剩余时间到localStorage
localStorage.setItem('bgSwitcherRemainingTime', this.remainingTime);
clearTimeout(this.apiTimer);
this.apiTimer = null;
}
} else {
// 页面可见时,恢复计时器
if (!this.timer && !this.apiTimer && !this.apiUrl) {
// 如果没有活跃的计时器,使用默认的轮播
this.scheduleNext();
} else if (this.apiTimer === null && this.apiUrl) {
// 如果处于API模式但计时器未运行,恢复API请求
this.scheduleNextApiRequest();
}
}
}
/**
* 处理页面卸载事件确保保存状态
*/
handleBeforeUnload() {
// 保存当前索引
localStorage.setItem('bgSwitcherIndex', this.index);
// 保存API模式状态
localStorage.setItem('bgSwitcherIsApiMode', !!this.apiUrl);
if (this.apiUrl) {
localStorage.setItem('bgSwitcherApiUrl', this.apiUrl);
localStorage.setItem('bgSwitcherApiInterval', this.apiInterval);
}
// 如果计时器在运行,计算并保存剩余时间
if (this.timer || this.apiTimer) {
const elapsedTime = Date.now() - this.startTime;
this.remainingTime = Math.max(0, this.remainingTime - elapsedTime);
localStorage.setItem('bgSwitcherRemainingTime', this.remainingTime);
}
}
stop() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
// 重置剩余时间
this.remainingTime = this.interval;
// 保存剩余时间到localStorage
localStorage.setItem('bgSwitcherRemainingTime', this.remainingTime);
}
/**
* 从API获取随机壁纸并定期更新
* @param {string} apiUrl - 获取随机壁纸的API地址
* @param {number} interval - 请求间隔时间(毫秒)
*/
startRandomApiSwitch(apiUrl, interval = 30000) {
this.stop(); // 停止当前的轮播
this.apiInterval = interval;
this.apiUrl = apiUrl;
// 创建专用的API计时器
this.apiTimer = null;
// 立即请求一次
this.fetchRandomWallpaper();
}
/**
* 从API获取随机壁纸
*/
fetchRandomWallpaper() {
// 记录开始时间,用于计算剩余时间
this.startTime = Date.now();
this.remainingTime = this.apiInterval;
fetch(this.apiUrl)
.then(response => {
console.log(response);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
// 假设API返回的数据格式为 { wallpaperUrl: '图片地址' }
const wallpaperUrl = data.wallpaperUrl || data.url || data.image;
if (wallpaperUrl) {
// 预加载图片
const img = new Image();
img.onload = () => {
if (this.isInitialLoad) {
// 初始加载时,先设置背景图再添加到页面
this.container.appendChild(this.bgLayer);
this.isInitialLoad = false;
}
// 保存当前索引(使用-1标记这是API获取的图片)
this.index = -1;
localStorage.setItem('bgSwitcherIndex', -1);
this.effect(this.bgLayer, wallpaperUrl);
this.scheduleNextApiRequest();
};
img.onerror = () => {
console.warn('API返回的壁纸加载失败:', wallpaperUrl);
this.scheduleNextApiRequest();
};
img.src = wallpaperUrl;
} else {
console.warn('背景图片加载失败:', url);
if (this.isInitialLoad) {
console.warn('API返回的数据格式不正确,未找到壁纸地址');
// 初始加载失败时,也添加背景层到页面
this.container.appendChild(this.bgLayer);
this.isInitialLoad = false;
}
this.scheduleNextApiRequest();
}
})
.catch(error => {
console.error('获取随机壁纸失败:', error);
this.scheduleNextApiRequest();
});
}
/**
* 安排下一次API请求
*/
scheduleNextApiRequest() {
if (this.apiTimer) {
clearTimeout(this.apiTimer);
}
// 使用剩余时间或默认间隔
const timeToWait = this.remainingTime > 0 ? this.remainingTime : this.apiInterval;
this.apiTimer = setTimeout(() => {
this.remainingTime = this.apiInterval; // 重置剩余时间
localStorage.setItem('bgSwitcherRemainingTime', this.remainingTime);
this.fetchRandomWallpaper();
}, timeToWait);
}
/**
* 停止API随机壁纸请求
*/
stopRandomApiSwitch() {
if (this.apiTimer) {
clearTimeout(this.apiTimer);
this.apiTimer = null;
}
this.apiUrl = null
// 重置剩余时间
this.remainingTime = this.interval;
localStorage.setItem('bgSwitcherRemainingTime', this.remainingTime);
}
static fadeEffect(layer, url) {
layer.style.transition = 'opacity 1s';
layer.style.opacity = 0;
setTimeout(() => {
layer.style.backgroundImage = `url(${url})`;
layer.style.opacity = 1;
}, 500);
}
}
// 使用示例
// 1. 默认本地图片轮播
// const images = [
// '/static/bg2.webp',
// '/static/bg.jpg',
// ];
// const bgSwitcher = new BgSwitcher(images, { interval: 5000 });
// 启动轮播
// bgSwitcher.start();
// 2. 随机API壁纸示例
// 创建一个新的BgSwitcher实例用于API模式
const apiBgSwitcher = new BgSwitcher([], { interval: 5000 }); // API模式不需要本地图片列表
// 模拟API函数,实际使用时替换为真实API地址
function createMockWallpaperApi() {
// 模拟壁纸地址库
const mockWallpapers = [
'/static/bg2.webp',
'/static/bg.jpg',
];
// 创建一个简单的服务器端点
if (window.mockWallpaperServer) {
clearInterval(window.mockWallpaperServer);
}
// 模拟API响应
window.fetchRandomWallpaper = function () {
return new Promise((resolve) => {
setTimeout(() => {
const randomIndex = Math.floor(Math.random() * mockWallpapers.length);
resolve({
ok: true,
json() {
return {
wallpaperUrl: mockWallpapers[randomIndex]
}
}
});
}, 500);
});
};
// 替换原生fetch以模拟API调用
window.originalFetch = window.fetch;
window.fetch = function (url) {
if (url === '/api/random-wallpaper') {
return window.fetchRandomWallpaper();
}
return window.originalFetch(url);
};
console.log('模拟壁纸API已启动');
}
// 初始化模拟API
createMockWallpaperApi();
// 启动API模式的随机壁纸切换(每10秒请求一次)
apiBgSwitcher.startRandomApiSwitch('/api/random-wallpaper', 10000);
fetch("https://pic.xieyaxin.top/random.php")
.then(response => {
if(response.body instanceof ReadableStream) {
return response.blob()
}
return response.json()
})
.then(data => {
console.log(URL.createObjectURL(data));
})
// 要停止API模式,使用
// apiBgSwitcher.stopRandomApiSwitch();
// 要切换回本地图片轮播,使用
// apiBgSwitcher.stopRandomApiSwitch();
// apiBgSwitcher.start();
// 启动默认轮播
// bgSwitcher.start();

BIN
public/static/bg2.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

9
public/styles.css

@ -12,7 +12,7 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
/*
body::after { body::after {
content: ""; content: "";
position: fixed; position: fixed;
@ -24,10 +24,11 @@ body::after {
background-image: var(--bg); background-image: var(--bg);
background-size: cover; background-size: cover;
background-repeat: no-repeat; background-repeat: no-repeat;
background-color: #000000;
background-color: #f9f9f9; background-color: #f9f9f9;
filter: brightness(.55); filter: brightness(.75);
z-index: -1; z-index: -2;
} } */
.navbar { .navbar {
height: 49px; height: 49px;

43
src/controllers/Page/HtmxController.js

@ -14,8 +14,47 @@ class HtmxController {
static createRoutes() { static createRoutes() {
const controller = new HtmxController() const controller = new HtmxController()
const router = new Router({ auth: "try" }) const router = new Router({ auth: "try" })
router.post("/clicked", async ctx => { router.get("/htmx/timeline", async ctx => {
return await ctx.render("htmx/fuck", { title: "HTMX Clicked" }) return await ctx.render("htmx/timeline", {
timeLine: [
{
icon: "第一份工作",
title: "???",
desc: `做游戏的。`,
},
{
icon: "大学毕业",
title: "2014年09月",
desc: `我从<a href="https://www.jxnu.edu.cn/" target="_blank">江西师范大学</a>毕业,
获得了软件工程虚拟现实与技术专业的学士学位`,
},
{
icon: "高中",
title: "???",
desc: `宜春中学`,
},
{
icon: "初中",
title: "???",
desc: `宜春实验中学`,
},
{
icon: "小学(4-6年级)",
title: "???",
desc: `宜春二小`,
},
{
icon: "小学(1-3年级)",
title: "???",
desc: `丰城市泉港镇小学`,
},
{
icon: "出生",
title: "1996年06月",
desc: `我出生于江西省丰城市泉港镇`,
},
],
})
}) })
return router return router
} }

68
src/controllers/Page/PageController.js

@ -14,6 +14,18 @@ class PageController {
return await ctx.render("page/auth/no-auth", {}) return await ctx.render("page/auth/no-auth", {})
} }
async loginGet(ctx) {
if (ctx.state.user) {
ctx.cookies.set("toast", JSON.stringify({ type: "error", message: encodeURIComponent("用户已登录") }), {
maxAge: 1,
httpOnly: false,
path: "/",
})
return ctx.redirect("/?msg=用户已登录")
}
return await ctx.render("page/login/index", { site_title: "登录" })
}
async loginPost(ctx) { async loginPost(ctx) {
const { username, email, password } = ctx.request.body const { username, email, password } = ctx.request.body
const result = await this.userService.login({ username, email, password }) const result = await this.userService.login({ username, email, password })
@ -21,18 +33,53 @@ class PageController {
ctx.body = { success: true, message: "登录成功" } ctx.body = { success: true, message: "登录成功" }
} }
async registerGet(ctx) {
if (ctx.state.user) {
ctx.cookies.set("toast", JSON.stringify({ type: "error", message: encodeURIComponent("用户已登录") }), {
maxAge: 1,
httpOnly: false,
path: "/",
})
return ctx.redirect("/?msg=用户已登录")
}
return await ctx.render("page/register/index", { site_title: "注册" })
}
async registerPost(ctx) { async registerPost(ctx) {
const { username, email, password } = ctx.request.body const { username, password } = ctx.request.body
await this.userService.register({ username, email, password })
// try {
await this.userService.register({ username, password, role: "user" })
// ctx.cookies.set("toast", JSON.stringify({ type: "success", message: "注册成功" }), {
// maxAge: 1,
// httpOnly: false,
// path: "/",
// })
// } catch (error) {
// ctx.cookies.set('toast', JSON.stringify({type:"error",message:encodeURIComponent(error.message || error)}), {
// maxAge: 1,
// httpOnly: false,
// path: '/',
// })
// }
return ctx.redirect("/login") return ctx.redirect("/login")
} }
logout(ctx) {
delete ctx.session.user
return ctx.redirect("/?msg=用户已退出")
}
pageGet(name, data) { pageGet(name, data) {
return async ctx => { return async ctx => {
return await ctx.render(name, { return await ctx.render(
...(data || {}), name,
user: ctx.state.user, {
}, { includeSite: true }) ...(data || {}),
},
{ includeSite: true, includeUser: true }
)
} }
} }
@ -40,18 +87,19 @@ class PageController {
const controller = new PageController() const controller = new PageController()
const router = new Router({ auth: "try" }) const router = new Router({ auth: "try" })
// 首页 // 首页
router.get("/", controller.indexGet.bind(controller), { auth: false }) router.get("/", controller.indexGet.bind(controller), { auth: false })
// 未授权报错页 // 未授权报错页
router.get("/no-auth", controller.indexNoAuth.bind(controller), { auth: false }) router.get("/no-auth", controller.indexNoAuth.bind(controller), { auth: false })
router.get("/article/:id", controller.pageGet("page/articles/index"), { 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("/articles", controller.pageGet("page/articles/index"), { auth: false })
router.get("/about", controller.pageGet("page/about/index"), { auth: false }) router.get("/about", controller.pageGet("page/about/index"), { auth: false })
router.get("/login", controller.pageGet("page/login/index"), { auth: false }) router.get("/login", controller.loginGet.bind(controller), { auth: "try" })
router.post("/login", controller.loginPost.bind(controller), { auth: false }) router.post("/login", controller.loginPost.bind(controller), { auth: false })
router.get("/register", controller.pageGet("page/register/index", { title: "注册" }), { auth: false }) router.get("/register", controller.registerGet.bind(controller), { auth: "try" })
router.post("/register", controller.registerPost.bind(controller), { auth: false }) router.post("/register", controller.registerPost.bind(controller), { auth: false })
router.post("/logout", controller.logout.bind(controller), { auth: true })
return router return router
} }
} }

30
src/db/index.js

@ -1,21 +1,21 @@
import buildKnex from "knex" import buildKnex from "knex"
import knexConfig from "../../knexfile.mjs" import knexConfig from "../../knexfile.mjs"
const cache = {} // const cache = {}
buildKnex.QueryBuilder.extend("cache", async function () { // buildKnex.QueryBuilder.extend("cache", async function () {
try { // try {
const cacheKey = this.toString() // const cacheKey = this.toString()
if (cache[cacheKey]) { // if (cache[cacheKey]) {
return cache[cacheKey] // return cache[cacheKey]
} // }
const data = await this // const data = await this
cache[cacheKey] = data // cache[cacheKey] = data
return data // return data
} catch (e) { // } catch (e) {
throw new Error(e) // throw new Error(e)
} // }
}) // })
const environment = process.env.NODE_ENV || "development" const environment = process.env.NODE_ENV || "development"
const db = buildKnex(knexConfig[environment]) const db = buildKnex(knexConfig[environment])

2
src/db/migrations/20250616065041_create_users_table.mjs

@ -6,7 +6,7 @@ export const up = async knex => {
return knex.schema.createTable("users", function (table) { return knex.schema.createTable("users", function (table) {
table.increments("id").primary() // 自增主键 table.increments("id").primary() // 自增主键
table.string("username", 100).notNullable() // 字符串字段(最大长度100) table.string("username", 100).notNullable() // 字符串字段(最大长度100)
table.string("email", 100).unique().notNullable() // 唯一邮箱 table.string("email", 100).unique() // 唯一邮箱
table.string("password", 100).notNullable() // 密码 table.string("password", 100).notNullable() // 密码
table.string("role", 100).notNullable() table.string("role", 100).notNullable()
table.string("phone", 100) table.string("phone", 100)

4
src/db/seeds/20250621013324_site_config_seed.mjs

@ -6,10 +6,10 @@ export const seed = async (knex) => {
await knex('site_config').insert([ await knex('site_config').insert([
{ key: 'site_title', value: '罗非鱼的秘密' }, { key: 'site_title', value: '罗非鱼的秘密' },
{ key: 'site_author', value: '罗非鱼' }, { key: 'site_author', value: '罗非鱼' },
{ key: 'site_author_avatar', value: 'https://alist.xieyaxin.top/p/%E6%B8%B8%E5%AE%A2%E6%96%87%E4%BB%B6/%E5%85%AC%E5%85%B1%E4%BF%A1%E6%81%AF/avatar.jpg' },
{ key: 'site_description', value: '一屋很小,却也很大' }, { key: 'site_description', value: '一屋很小,却也很大' },
{ key: 'site_logo', value: '/static/logo.png' }, { key: 'site_logo', value: '/static/logo.png' },
{ key: 'site_bg', value: '/static/bg.jpg' }, { key: 'site_bg', value: '/static/bg.jpg' },
{ key: 'keywords', value: 'blog' }, { key: 'keywords', value: 'blog' }
{ key: 'base', value: '/' }
]); ]);
}; };

1
src/middlewares/Views/index.js

@ -25,6 +25,7 @@ function viewsMiddleware(path, { engineSource = consolidate, extension = "html",
const otherData = { const otherData = {
currentPath: ctx.path, currentPath: ctx.path,
$config: config, $config: config,
isLogin: !!ctx.state && !!ctx.state.user,
} }
if (renderOptions.includeSite) { if (renderOptions.includeSite) {
otherData.$site = site otherData.$site = site

90
src/middlewares/errorHandler/index.js

@ -1,54 +1,52 @@
import CommonError from "utils/error/CommonError"
// src/plugins/errorHandler.js // src/plugins/errorHandler.js
// 错误处理中间件插件 // 错误处理中间件插件
function formatError(ctx, status, message, stack) { async function formatError(ctx, status, message, stack) {
const accept = ctx.accepts('json', 'html', 'text'); const accept = ctx.accepts("json", "html", "text")
const isDev = process.env.NODE_ENV === 'development'; const isDev = process.env.NODE_ENV === "development"
if (accept === 'json') { if (accept === "json") {
ctx.type = 'application/json'; ctx.type = "application/json"
ctx.body = isDev && stack ctx.body = isDev && stack ? { success: false, error: message, stack } : { success: false, error: message }
? { success: false, error: message, stack } } else if (accept === "html") {
: { success: false, error: message }; ctx.type = "html"
} else if (accept === 'html') { await ctx.render("error/index", { status, message, stack, isDev })
ctx.type = 'html'; } else {
ctx.body = ` ctx.type = "text"
<html> ctx.body = isDev && stack ? `${status} - ${message}\n${stack}` : `${status} - ${message}`
<head><title>${status} Error</title></head> }
<body> ctx.status = status
<h1>${status} Error</h1>
<p>${message}</p>
${isDev && stack ? `<pre style='color:red;'>${stack}</pre>` : ''}
</body>
</html>
`;
} else {
ctx.type = 'text';
ctx.body = isDev && stack
? `${status} - ${message}\n${stack}`
: `${status} - ${message}`;
}
ctx.status = status;
} }
export default function errorHandler() { export default function errorHandler() {
return async (ctx, next) => { return async (ctx, next) => {
// 拦截 Chrome DevTools 探测请求,直接返回 204 // 拦截 Chrome DevTools 探测请求,直接返回 204
if (ctx.path === '/.well-known/appspecific/com.chrome.devtools.json') { if (ctx.path === "/.well-known/appspecific/com.chrome.devtools.json") {
ctx.status = 204; ctx.status = 204
ctx.body = ''; ctx.body = ""
return; return
} }
try { try {
await next(); await next()
if (ctx.status === 404) { if (ctx.status === 404) {
formatError(ctx, 404, 'Resource not found'); await formatError(ctx, 404, "Resource not found")
} }
} catch (err) { } catch (err) {
const isDev = process.env.NODE_ENV === 'development'; console.error(err);
if (isDev && err.stack) { const isDev = process.env.NODE_ENV === "development"
console.error(err.stack); if (isDev && err.stack) {
} console.error(err.stack)
formatError(ctx, err.statusCode || 500, err.message || err || 'Internal server error', isDev ? err.stack : undefined); }
if (err instanceof CommonError) {
ctx.cookies.set("toast", JSON.stringify({ type: "error", message: encodeURIComponent(err.message) }), {
maxAge: 1,
httpOnly: false,
path: "/",
})
ctx.redirect(ctx.path)
return
}
await formatError(ctx, err.statusCode || 500, err.message || err || "Internal server error", isDev ? err.stack : undefined)
}
} }
};
} }

1
src/middlewares/install.js

@ -25,6 +25,7 @@ export default app => {
{ pattern: "/**/*", auth: false }, { pattern: "/**/*", auth: false },
], ],
blackList: [ blackList: [
// 禁用api请求
"/api", "/api",
"/api/", "/api/",
"/api/**/*", "/api/**/*",

9
src/services/userService.js

@ -1,5 +1,6 @@
import UserModel from "db/models/UserModel.js" import UserModel from "db/models/UserModel.js"
import { hashPassword, comparePassword } from "utils/bcrypt.js" import { hashPassword, comparePassword } from "utils/bcrypt.js"
import CommonError from "utils/error/CommonError"
import { JWT_SECRET } from "@/middlewares/Auth/auth.js" import { JWT_SECRET } from "@/middlewares/Auth/auth.js"
import jwt from "@/middlewares/Auth/jwt.js" import jwt from "@/middlewares/Auth/jwt.js"
@ -37,14 +38,12 @@ class UserService {
// 注册新用户 // 注册新用户
async register(data) { async register(data) {
if (!data.username || !data.email || !data.password) throw new Error("用户名、邮箱和密码不能为空") if (!data.username || !data.password) throw new CommonError("用户名、邮箱和密码不能为空")
const existUser = await UserModel.findByUsername(data.username) const existUser = await UserModel.findByUsername(data.username)
if (existUser) throw new Error("用户名已存在") if (existUser) throw new CommonError(`用户名${data.username}已存在`)
const existEmail = await UserModel.findByEmail(data.email)
if (existEmail) throw new Error("邮箱已被注册")
// 密码加密 // 密码加密
const hashed = await hashPassword(data.password) const hashed = await hashPassword(data.password)
const user = await UserModel.create({ ...data, password: hashed }) const user = await UserModel.create({ ...data, password: hashed })
// 返回脱敏信息 // 返回脱敏信息
const { password, ...userInfo } = Array.isArray(user) ? user[0] : user const { password, ...userInfo } = Array.isArray(user) ? user[0] : user

7
src/utils/error/CommonError.js

@ -0,0 +1,7 @@
export default class CommonError extends Error {
constructor(message, redirect) {
super(message)
this.name = "CommonError"
this.status = 500
}
}

8
src/views/error/index.pug

@ -0,0 +1,8 @@
html
head
title #{status} Error
body
h1 #{status} Error
p #{message}
if isDev && stack
pre(style="color:red;") #{stack}

3
src/views/htmx/footer.pug

@ -1,6 +1,7 @@
.footer-panel .footer-panel
.footer-content .footer-content
p © 2023-#{new Date().getFullYear()} #{$site.site_title}. 保留所有权利。 p © 2023-#{new Date().getFullYear()} #{$site.site_author}. 保留所有权利。
ul.footer-links ul.footer-links
li li
a(href="/") 首页 a(href="/") 首页

4
src/views/htmx/fuck.pug

@ -1,4 +0,0 @@
if title
h1 <a href="/page/htmx">#{title}</a>
else
h1 默认标题

2
src/views/htmx/navbar.pug

@ -1,2 +0,0 @@
nav.navbar
.title 首页

44
src/views/htmx/timeline.pug

@ -3,48 +3,10 @@ ul.time-line
each item in _dataList each item in _dataList
li.time-line-item li.time-line-item
.timeline-icon .timeline-icon
div #{item.icon} div !{item.icon}
.time-line-item-content .time-line-item-content
.time-line-item-title #{item.title} .time-line-item-title !{item.title}
.time-line-item-desc #{item.desc} .time-line-item-desc !{item.desc}
li.time-line-item
.timeline-icon
div 第一份工作
.time-line-item-content
.time-line-item-title ???
.time-line-item-desc 做游戏的。
li.time-line-item
.timeline-icon
div 大学毕业
.time-line-item-content
.time-line-item-title 2014年09月
.time-line-item-desc 我从
a(href="https://www.jxnu.edu.cn/" target="_blank") 江西师范大学
span 毕业,获得了软件工程(虚拟现实与技术)专业的学士学位。
li.time-line-item
.timeline-icon
div 高中
.time-line-item-content
.time-line-item-title ???
.time-line-item-desc 宜春中学
li.time-line-item
.timeline-icon
div 初中
.time-line-item-content
.time-line-item-title ???
.time-line-item-desc 宜春实验中学
li.time-line-item
.timeline-icon
div 小学
.time-line-item-content
.time-line-item-title ???
.time-line-item-desc 宜春二小
li.time-line-item
.timeline-icon
div 出生
.time-line-item-content
.time-line-item-title 1996年06月
.time-line-item-desc 我出生于江西省丰城市泉港镇。
style. style.
.time-line { .time-line {
display: flex; display: flex;

124
src/views/layouts/base.pug

@ -22,19 +22,22 @@ doctype html
html(lang="zh-CN") html(lang="zh-CN")
head head
block head block head
title #{$site && $site.site_title || ''} title #{site_title || $site && $site.site_title || ''}
meta(name="description" content=$site && $site.site_description || '') meta(name="description" content=site_description || $site && $site.site_description || '')
meta(name="keywords" content=$site && $site.keywords || '') meta(name="keywords" content=keywords || $site && $site.keywords || '')
if $site && $site.site_favicon if $site && $site.site_favicon
+link($site.site_favicon, '') link(rel="shortcut icon", href=$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")
+css('reset.css') +css('reset.css')
+js('lib/htmx.min.js') +js('lib/htmx.min.js')
//- +css('https://unpkg.com/simplebar@latest/dist/simplebar.css', true) +js('https://cdn.tailwindcss.com')
//- +css('simplebar-shim.css') +css('https://unpkg.com/simplebar@latest/dist/simplebar.css', true)
//- +js('https://unpkg.com/simplebar@latest/dist/simplebar.min.js', true) +css('simplebar-shim.css')
body(style="--bg:url("+($site && $site.site_bg || '#fff')+")") +js('https://unpkg.com/simplebar@latest/dist/simplebar.min.js', true)
//- body(style="--bg:url("+($site && $site.site_bg || '#fff')+")")
//- body(style="--bg:url(./static/bg2.webp)")
body
noscript noscript
style. style.
.simplebar-content-wrapper { .simplebar-content-wrapper {
@ -48,8 +51,103 @@ html(lang="zh-CN")
width: initial; width: initial;
height: initial; height: initial;
} }
//- div(data-simplebar style="height: 100%") div(data-simplebar style="height: 100%")
//- block content div(style="height: 100%; display: flex; flex-direction: column" id="aaaa")
//- block scripts block content
block content block scripts
block scripts +js('lib/bg-change.js')
script.
const el = document.querySelector('.simplebar-content-wrapper')
const scrollTop = sessionStorage.getItem('scrollTop-'+location.pathname)
el.scrollTop = scrollTop
el.addEventListener("scroll", function(e) {
sessionStorage.setItem('scrollTop-'+location.pathname, e.target.scrollTop)
})
function getCookie(cname) {
var name = cname + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
for(var i = 0; i <ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
let have = false
const queryMsg = (new URLSearchParams(location.search)).get('msg')
let msg = getCookie('toast')
if(!msg) {
msg = JSON.stringify({type:'success',message: queryMsg})
have = !!queryMsg
} else {
have = true
}
if (have) {
function showToast(message, type = 'info') {
const containerId = 'toast-container';
let container = document.getElementById(containerId);
if (!container) {
container = document.createElement('div');
container.id = containerId;
container.style.position = 'fixed';
container.style.top = '24px';
container.style.right = '24px';
container.style.zIndex = '9999';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '12px';
document.body.appendChild(container);
}
const toast = document.createElement('div');
toast.className = 'toast-message';
toast.textContent = message;
// Set style based on type
let bg = '#409eff';
if (type === 'error') bg = '#f56c6c';
else if (type === 'warning') bg = '#e6a23c';
else if (type === 'success') bg = '#67c23a';
toast.style.background = bg;
toast.style.color = '#fff';
toast.style.padding = '14px 28px';
toast.style.borderRadius = '6px';
toast.style.boxShadow = '0 2px 8px rgba(0,0,0,0.12)';
toast.style.fontSize = '1rem';
toast.style.cursor = 'pointer';
toast.style.transition = 'opacity 0.3s';
toast.style.opacity = '1';
let timer;
const removeToast = () => {
toast.style.opacity = '0';
setTimeout(() => {
if (toast.parentNode) toast.parentNode.removeChild(toast);
}, 300);
};
const resetTimer = () => {
clearTimeout(timer);
timer = setTimeout(removeToast, 5000);
};
toast.addEventListener('mouseenter', resetTimer);
toast.addEventListener('mouseleave', resetTimer);
container.appendChild(toast);
resetTimer();
}
try {
const toastObj = JSON.parse(msg);
showToast(toastObj.message, toastObj.type);
} catch (e) {
showToast(msg);
}
}

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

@ -48,7 +48,7 @@ block pageHead
color: #fff200; color: #fff200;
} }
block pageScripts //- block pageScripts
script. //- script.
const curUrl = URL.parse(location.href).searchParams.get("from") //- const curUrl = URL.parse(location.href).searchParams.get("from")
fetch(curUrl,{redirect: 'error'}).then(res=>location.href=curUrl).catch(e=>console.log(e)) //- fetch(curUrl,{redirect: 'error'}).then(res=>location.href=curUrl).catch(e=>console.log(e))

17
src/views/page/index/index copy.pug

@ -0,0 +1,17 @@
extends /layouts/pure.pug
block pageHead
+css("css/page/index.css")
block pageContent
.home-hero
.avatar-container
.author #{$site.site_author}
img.avatar(src=$site.site_author_avatar, alt="")
.card
div 人生轨迹
+include()
- var timeLine = [{icon: "第一份工作",title: "???", desc: `做游戏的。`, } ]
include /htmx/timeline.pug
//- div(hx-get="/htmx/timeline" hx-trigger="load")
//- div(style="text-align:center;color:white") Loading

12
src/views/page/index/index.pug

@ -4,12 +4,6 @@ block pageHead
+css("css/page/index.css") +css("css/page/index.css")
block pageContent block pageContent
.home-hero div(class="mt-[50px]")
.avatar-container +include()
.author #{$site.site_author} include /htmx/navbar.pug
img.avatar(src="https://alist.xieyaxin.top/p/%E6%B8%B8%E5%AE%A2%E6%96%87%E4%BB%B6/%E5%85%AC%E5%85%B1%E4%BF%A1%E6%81%AF/avatar.jpg", alt="")
.card
div 人生轨迹
+include()
- let timeLine = [{icon: '11',title: "aaaa",desc:"asd"}]
include /htmx/timeline.pug

16
src/views/page/register/index.pug

@ -1,9 +1,6 @@
doctype html extends /layouts/pure.pug
html
head block pageHead
meta(charset="UTF-8")
meta(name="viewport", content="width=device-width, initial-scale=1.0")
title 注册
style. style.
body { body {
background: #f5f7fa; background: #f5f7fa;
@ -43,6 +40,7 @@ html
font-size: 1rem; font-size: 1rem;
background: #f9fafb; background: #f9fafb;
transition: border 0.2s; transition: border 0.2s;
box-sizing: border-box;
} }
input:focus { input:focus {
border-color: #409eff; border-color: #409eff;
@ -74,7 +72,8 @@ html
} }
.login-link:hover { .login-link:hover {
text-decoration: underline; text-decoration: underline;
body
block pageContent
.register-container .register-container
.register-title 注册账号 .register-title 注册账号
form(action="/register" method="post") form(action="/register" method="post")
@ -82,9 +81,6 @@ html
label(for="username") 用户名 label(for="username") 用户名
input(type="text" id="username" name="username" required placeholder="请输入用户名") input(type="text" id="username" name="username" required placeholder="请输入用户名")
.form-group .form-group
label(for="email") 邮箱
input(type="email" id="email" name="email" required placeholder="请输入邮箱")
.form-group
label(for="password") 密码 label(for="password") 密码
input(type="password" id="password" name="password" required placeholder="请输入密码") input(type="password" id="password" name="password" required placeholder="请输入密码")
.form-group .form-group

Loading…
Cancel
Save