diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f4a7f34 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "CodeFree.index": true +} \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index f91fe08..cc49194 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/database/development.sqlite3-shm b/database/development.sqlite3-shm index 25096a7..1ea89d8 100644 Binary files a/database/development.sqlite3-shm and b/database/development.sqlite3-shm differ diff --git a/database/development.sqlite3-wal b/database/development.sqlite3-wal index 805e8c0..7ebc8c5 100644 Binary files a/database/development.sqlite3-wal and b/database/development.sqlite3-wal differ diff --git a/package.json b/package.json index 8ff90bb..2e71fbb 100644 --- a/package.json +++ b/package.json @@ -42,5 +42,8 @@ "db": "./src/db", "utils": "./src/utils", "services": "./src/services" + }, + "peerDependencies": { + "typescript": "^5.0.0" } -} +} \ No newline at end of file diff --git a/public/lib/bg-change.js b/public/lib/bg-change.js new file mode 100644 index 0000000..db5de67 --- /dev/null +++ b/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(); diff --git a/public/static/bg2.webp b/public/static/bg2.webp new file mode 100644 index 0000000..6e1a18c Binary files /dev/null and b/public/static/bg2.webp differ diff --git a/public/styles.css b/public/styles.css index c9e5e11..d59f079 100644 --- a/public/styles.css +++ b/public/styles.css @@ -12,7 +12,7 @@ body { display: flex; flex-direction: column; } - +/* body::after { content: ""; position: fixed; @@ -24,10 +24,11 @@ body::after { background-image: var(--bg); background-size: cover; background-repeat: no-repeat; + background-color: #000000; background-color: #f9f9f9; - filter: brightness(.55); - z-index: -1; -} + filter: brightness(.75); + z-index: -2; +} */ .navbar { height: 49px; diff --git a/src/controllers/Page/HtmxController.js b/src/controllers/Page/HtmxController.js index 3df03f3..9908a22 100644 --- a/src/controllers/Page/HtmxController.js +++ b/src/controllers/Page/HtmxController.js @@ -14,8 +14,47 @@ class HtmxController { static createRoutes() { const controller = new HtmxController() const router = new Router({ auth: "try" }) - router.post("/clicked", async ctx => { - return await ctx.render("htmx/fuck", { title: "HTMX Clicked" }) + router.get("/htmx/timeline", async ctx => { + return await ctx.render("htmx/timeline", { + timeLine: [ + { + icon: "第一份工作", + title: "???", + desc: `做游戏的。`, + }, + { + icon: "大学毕业", + title: "2014年09月", + desc: `我从江西师范大学毕业, + 获得了软件工程(虚拟现实与技术)专业的学士学位。`, + }, + { + icon: "高中", + title: "???", + desc: `宜春中学`, + }, + { + icon: "初中", + title: "???", + desc: `宜春实验中学`, + }, + { + icon: "小学(4-6年级)", + title: "???", + desc: `宜春二小`, + }, + { + icon: "小学(1-3年级)", + title: "???", + desc: `丰城市泉港镇小学`, + }, + { + icon: "出生", + title: "1996年06月", + desc: `我出生于江西省丰城市泉港镇`, + }, + ], + }) }) return router } diff --git a/src/controllers/Page/PageController.js b/src/controllers/Page/PageController.js index 5113454..28e41b5 100644 --- a/src/controllers/Page/PageController.js +++ b/src/controllers/Page/PageController.js @@ -14,6 +14,18 @@ class PageController { 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) { const { username, email, password } = ctx.request.body const result = await this.userService.login({ username, email, password }) @@ -21,18 +33,53 @@ class PageController { 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) { - const { username, email, password } = ctx.request.body - await this.userService.register({ username, email, password }) + const { username, password } = ctx.request.body + + // 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") } + logout(ctx) { + delete ctx.session.user + return ctx.redirect("/?msg=用户已退出") + } + pageGet(name, data) { return async ctx => { - return await ctx.render(name, { - ...(data || {}), - user: ctx.state.user, - }, { includeSite: true }) + return await ctx.render( + name, + { + ...(data || {}), + }, + { includeSite: true, includeUser: true } + ) } } @@ -40,18 +87,19 @@ class PageController { const controller = new PageController() 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("/article/:id", controller.pageGet("page/articles/index"), { auth: false }) router.get("/articles", controller.pageGet("page/articles/index"), { auth: false }) - + router.get("/about", controller.pageGet("page/about/index"), { auth: false }) - router.get("/login", controller.pageGet("page/login/index"), { auth: false }) + router.get("/login", controller.loginGet.bind(controller), { auth: "try" }) 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("/logout", controller.logout.bind(controller), { auth: true }) return router } } diff --git a/src/db/index.js b/src/db/index.js index 5bc05aa..db7743d 100644 --- a/src/db/index.js +++ b/src/db/index.js @@ -1,21 +1,21 @@ import buildKnex from "knex" import knexConfig from "../../knexfile.mjs" -const cache = {} - -buildKnex.QueryBuilder.extend("cache", async function () { - try { - const cacheKey = this.toString() - if (cache[cacheKey]) { - return cache[cacheKey] - } - const data = await this - cache[cacheKey] = data - return data - } catch (e) { - throw new Error(e) - } -}) +// const cache = {} + +// buildKnex.QueryBuilder.extend("cache", async function () { +// try { +// const cacheKey = this.toString() +// if (cache[cacheKey]) { +// return cache[cacheKey] +// } +// const data = await this +// cache[cacheKey] = data +// return data +// } catch (e) { +// throw new Error(e) +// } +// }) const environment = process.env.NODE_ENV || "development" const db = buildKnex(knexConfig[environment]) diff --git a/src/db/migrations/20250616065041_create_users_table.mjs b/src/db/migrations/20250616065041_create_users_table.mjs index 0e401b2..a431899 100644 --- a/src/db/migrations/20250616065041_create_users_table.mjs +++ b/src/db/migrations/20250616065041_create_users_table.mjs @@ -6,7 +6,7 @@ export const up = async knex => { return knex.schema.createTable("users", function (table) { table.increments("id").primary() // 自增主键 table.string("username", 100).notNullable() // 字符串字段(最大长度100) - table.string("email", 100).unique().notNullable() // 唯一邮箱 + table.string("email", 100).unique() // 唯一邮箱 table.string("password", 100).notNullable() // 密码 table.string("role", 100).notNullable() table.string("phone", 100) diff --git a/src/db/seeds/20250621013324_site_config_seed.mjs b/src/db/seeds/20250621013324_site_config_seed.mjs index e182c53..ec3c7c5 100644 --- a/src/db/seeds/20250621013324_site_config_seed.mjs +++ b/src/db/seeds/20250621013324_site_config_seed.mjs @@ -6,10 +6,10 @@ export const seed = async (knex) => { await knex('site_config').insert([ { key: 'site_title', 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_logo', value: '/static/logo.png' }, { key: 'site_bg', value: '/static/bg.jpg' }, - { key: 'keywords', value: 'blog' }, - { key: 'base', value: '/' } + { key: 'keywords', value: 'blog' } ]); }; diff --git a/src/middlewares/Views/index.js b/src/middlewares/Views/index.js index 4c69db1..2c685b3 100644 --- a/src/middlewares/Views/index.js +++ b/src/middlewares/Views/index.js @@ -25,6 +25,7 @@ function viewsMiddleware(path, { engineSource = consolidate, extension = "html", const otherData = { currentPath: ctx.path, $config: config, + isLogin: !!ctx.state && !!ctx.state.user, } if (renderOptions.includeSite) { otherData.$site = site diff --git a/src/middlewares/errorHandler/index.js b/src/middlewares/errorHandler/index.js index e0cda43..73de3d5 100644 --- a/src/middlewares/errorHandler/index.js +++ b/src/middlewares/errorHandler/index.js @@ -1,54 +1,52 @@ +import CommonError from "utils/error/CommonError" // src/plugins/errorHandler.js // 错误处理中间件插件 -function formatError(ctx, status, message, stack) { - const accept = ctx.accepts('json', 'html', 'text'); - const isDev = process.env.NODE_ENV === 'development'; - if (accept === 'json') { - ctx.type = 'application/json'; - ctx.body = isDev && stack - ? { success: false, error: message, stack } - : { success: false, error: message }; - } else if (accept === 'html') { - ctx.type = 'html'; - ctx.body = ` - - ${status} Error - -

${status} Error

-

${message}

- ${isDev && stack ? `
${stack}
` : ''} - - - `; - } else { - ctx.type = 'text'; - ctx.body = isDev && stack - ? `${status} - ${message}\n${stack}` - : `${status} - ${message}`; - } - ctx.status = status; +async function formatError(ctx, status, message, stack) { + const accept = ctx.accepts("json", "html", "text") + const isDev = process.env.NODE_ENV === "development" + if (accept === "json") { + ctx.type = "application/json" + ctx.body = isDev && stack ? { success: false, error: message, stack } : { success: false, error: message } + } else if (accept === "html") { + ctx.type = "html" + await ctx.render("error/index", { status, message, stack, isDev }) + } else { + ctx.type = "text" + ctx.body = isDev && stack ? `${status} - ${message}\n${stack}` : `${status} - ${message}` + } + ctx.status = status } export default function errorHandler() { - return async (ctx, next) => { - // 拦截 Chrome DevTools 探测请求,直接返回 204 - if (ctx.path === '/.well-known/appspecific/com.chrome.devtools.json') { - ctx.status = 204; - ctx.body = ''; - return; - } - try { - await next(); - if (ctx.status === 404) { - formatError(ctx, 404, 'Resource not found'); - } - } catch (err) { - const isDev = process.env.NODE_ENV === 'development'; - if (isDev && err.stack) { - console.error(err.stack); - } - formatError(ctx, err.statusCode || 500, err.message || err || 'Internal server error', isDev ? err.stack : undefined); + return async (ctx, next) => { + // 拦截 Chrome DevTools 探测请求,直接返回 204 + if (ctx.path === "/.well-known/appspecific/com.chrome.devtools.json") { + ctx.status = 204 + ctx.body = "" + return + } + try { + await next() + if (ctx.status === 404) { + await formatError(ctx, 404, "Resource not found") + } + } catch (err) { + console.error(err); + const isDev = process.env.NODE_ENV === "development" + if (isDev && err.stack) { + console.error(err.stack) + } + 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) + } } - }; } diff --git a/src/middlewares/install.js b/src/middlewares/install.js index bc6bf96..d54cede 100644 --- a/src/middlewares/install.js +++ b/src/middlewares/install.js @@ -25,6 +25,7 @@ export default app => { { pattern: "/**/*", auth: false }, ], blackList: [ + // 禁用api请求 "/api", "/api/", "/api/**/*", diff --git a/src/services/userService.js b/src/services/userService.js index 960ff1d..d6065ce 100644 --- a/src/services/userService.js +++ b/src/services/userService.js @@ -1,5 +1,6 @@ import UserModel from "db/models/UserModel.js" import { hashPassword, comparePassword } from "utils/bcrypt.js" +import CommonError from "utils/error/CommonError" import { JWT_SECRET } from "@/middlewares/Auth/auth.js" import jwt from "@/middlewares/Auth/jwt.js" @@ -37,14 +38,12 @@ class UserService { // 注册新用户 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) - if (existUser) throw new Error("用户名已存在") - const existEmail = await UserModel.findByEmail(data.email) - if (existEmail) throw new Error("邮箱已被注册") + if (existUser) throw new CommonError(`用户名${data.username}已存在`) // 密码加密 const hashed = await hashPassword(data.password) - + const user = await UserModel.create({ ...data, password: hashed }) // 返回脱敏信息 const { password, ...userInfo } = Array.isArray(user) ? user[0] : user diff --git a/src/utils/error/CommonError.js b/src/utils/error/CommonError.js new file mode 100644 index 0000000..a7c1995 --- /dev/null +++ b/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 + } +} diff --git a/src/views/error/index.pug b/src/views/error/index.pug new file mode 100644 index 0000000..5d39c06 --- /dev/null +++ b/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} \ No newline at end of file diff --git a/src/views/htmx/footer.pug b/src/views/htmx/footer.pug index 666a02e..5b47b89 100644 --- a/src/views/htmx/footer.pug +++ b/src/views/htmx/footer.pug @@ -1,6 +1,7 @@ .footer-panel .footer-content - p © 2023-#{new Date().getFullYear()} #{$site.site_title}. 保留所有权利。 + p © 2023-#{new Date().getFullYear()} #{$site.site_author}. 保留所有权利。 + ul.footer-links li a(href="/") 首页 diff --git a/src/views/htmx/fuck.pug b/src/views/htmx/fuck.pug deleted file mode 100644 index b27ccf8..0000000 --- a/src/views/htmx/fuck.pug +++ /dev/null @@ -1,4 +0,0 @@ -if title - h1 #{title} -else - h1 默认标题 \ No newline at end of file diff --git a/src/views/htmx/navbar.pug b/src/views/htmx/navbar.pug index 5e83f76..e69de29 100644 --- a/src/views/htmx/navbar.pug +++ b/src/views/htmx/navbar.pug @@ -1,2 +0,0 @@ -nav.navbar - .title 首页 \ No newline at end of file diff --git a/src/views/htmx/timeline.pug b/src/views/htmx/timeline.pug index 4766f11..6849e9b 100644 --- a/src/views/htmx/timeline.pug +++ b/src/views/htmx/timeline.pug @@ -3,48 +3,10 @@ ul.time-line each item in _dataList li.time-line-item .timeline-icon - div #{item.icon} + div !{item.icon} .time-line-item-content - .time-line-item-title #{item.title} - .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 我出生于江西省丰城市泉港镇。 + .time-line-item-title !{item.title} + .time-line-item-desc !{item.desc} style. .time-line { display: flex; diff --git a/src/views/layouts/base.pug b/src/views/layouts/base.pug index cc6c9ec..f59a22a 100644 --- a/src/views/layouts/base.pug +++ b/src/views/layouts/base.pug @@ -22,19 +22,22 @@ doctype html html(lang="zh-CN") head block head - title #{$site && $site.site_title || ''} - meta(name="description" content=$site && $site.site_description || '') - meta(name="keywords" content=$site && $site.keywords || '') + title #{site_title || $site && $site.site_title || ''} + meta(name="description" content=site_description || $site && $site.site_description || '') + meta(name="keywords" content=keywords || $site && $site.keywords || '') if $site && $site.site_favicon - +link($site.site_favicon, '') + link(rel="shortcut icon", href=$site.site_favicon) meta(charset="utf-8") meta(name="viewport" content="width=device-width, initial-scale=1") +css('reset.css') +js('lib/htmx.min.js') - //- +css('https://unpkg.com/simplebar@latest/dist/simplebar.css', true) - //- +css('simplebar-shim.css') - //- +js('https://unpkg.com/simplebar@latest/dist/simplebar.min.js', true) - body(style="--bg:url("+($site && $site.site_bg || '#fff')+")") + +js('https://cdn.tailwindcss.com') + +css('https://unpkg.com/simplebar@latest/dist/simplebar.css', true) + +css('simplebar-shim.css') + +js('https://unpkg.com/simplebar@latest/dist/simplebar.min.js', true) + //- body(style="--bg:url("+($site && $site.site_bg || '#fff')+")") + //- body(style="--bg:url(./static/bg2.webp)") + body noscript style. .simplebar-content-wrapper { @@ -48,8 +51,103 @@ html(lang="zh-CN") width: initial; height: initial; } - //- div(data-simplebar style="height: 100%") - //- block content - //- block scripts - block content - block scripts + div(data-simplebar style="height: 100%") + div(style="height: 100%; display: flex; flex-direction: column" id="aaaa") + block content + 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 { + 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); + } + } diff --git a/src/views/page/auth/no-auth.pug b/src/views/page/auth/no-auth.pug index 2a952ba..5db22eb 100644 --- a/src/views/page/auth/no-auth.pug +++ b/src/views/page/auth/no-auth.pug @@ -48,7 +48,7 @@ block pageHead color: #fff200; } -block pageScripts - script. - const curUrl = URL.parse(location.href).searchParams.get("from") - fetch(curUrl,{redirect: 'error'}).then(res=>location.href=curUrl).catch(e=>console.log(e)) \ No newline at end of file +//- block pageScripts +//- script. + //- const curUrl = URL.parse(location.href).searchParams.get("from") + //- fetch(curUrl,{redirect: 'error'}).then(res=>location.href=curUrl).catch(e=>console.log(e)) \ No newline at end of file diff --git a/src/views/page/index/index copy.pug b/src/views/page/index/index copy.pug new file mode 100644 index 0000000..6c53ce1 --- /dev/null +++ b/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 diff --git a/src/views/page/index/index.pug b/src/views/page/index/index.pug index 8b5ff59..12cdb2d 100644 --- a/src/views/page/index/index.pug +++ b/src/views/page/index/index.pug @@ -4,12 +4,6 @@ block pageHead +css("css/page/index.css") block pageContent - .home-hero - .avatar-container - .author #{$site.site_author} - 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 \ No newline at end of file + div(class="mt-[50px]") + +include() + include /htmx/navbar.pug \ No newline at end of file diff --git a/src/views/page/register/index.pug b/src/views/page/register/index.pug index b6dec11..79a5902 100644 --- a/src/views/page/register/index.pug +++ b/src/views/page/register/index.pug @@ -1,9 +1,6 @@ -doctype html -html - head - meta(charset="UTF-8") - meta(name="viewport", content="width=device-width, initial-scale=1.0") - title 注册 +extends /layouts/pure.pug + +block pageHead style. body { background: #f5f7fa; @@ -43,6 +40,7 @@ html font-size: 1rem; background: #f9fafb; transition: border 0.2s; + box-sizing: border-box; } input:focus { border-color: #409eff; @@ -74,7 +72,8 @@ html } .login-link:hover { text-decoration: underline; - body + +block pageContent .register-container .register-title 注册账号 form(action="/register" method="post") @@ -82,9 +81,6 @@ html label(for="username") 用户名 input(type="text" id="username" name="username" required placeholder="请输入用户名") .form-group - label(for="email") 邮箱 - input(type="email" id="email" name="email" required placeholder="请输入邮箱") - .form-group label(for="password") 密码 input(type="password" id="password" name="password" required placeholder="请输入密码") .form-group