Browse Source
- 修改 `package.json`,新增 `cross-env` 和其他依赖 - 在 `README.md` 中添加数据库查询缓存的使用说明 - 更新 `PageController.js`,优化登录和登出逻辑 - 在 `index.js` 中实现查询缓存功能,支持 TTL 和自定义 key - 修改多个视图文件,调整导航栏和页脚样式,增强用户体验 - 删除不再使用的 CSS 文件和静态资源re
30 changed files with 884 additions and 230 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,48 @@ |
|||
// 注册页面验证码点击刷新功能
|
|||
document.addEventListener('DOMContentLoaded', function() { |
|||
const captchaImg = document.querySelector('img[src="/captcha"]'); |
|||
|
|||
if (captchaImg) { |
|||
// 添加点击事件
|
|||
captchaImg.addEventListener('click', function() { |
|||
// 添加时间戳防止缓存
|
|||
const timestamp = new Date().getTime(); |
|||
this.src = `/captcha?t=${timestamp}`; |
|||
|
|||
// 添加点击反馈效果
|
|||
this.style.transform = 'scale(0.95)'; |
|||
setTimeout(() => { |
|||
this.style.transform = 'scale(1)'; |
|||
}, 150); |
|||
}); |
|||
|
|||
// 添加鼠标样式提示
|
|||
captchaImg.style.cursor = 'pointer'; |
|||
captchaImg.title = '点击刷新验证码'; |
|||
|
|||
// 添加悬停效果
|
|||
captchaImg.addEventListener('mouseenter', function() { |
|||
this.style.opacity = '0.8'; |
|||
this.style.transition = 'opacity 0.2s ease'; |
|||
}); |
|||
|
|||
captchaImg.addEventListener('mouseleave', function() { |
|||
this.style.opacity = '1'; |
|||
}); |
|||
} |
|||
|
|||
// 表单验证
|
|||
const registerForm = document.querySelector('form[action="/register"]'); |
|||
if (registerForm) { |
|||
registerForm.addEventListener('submit', function(e) { |
|||
const password = document.getElementById('password'); |
|||
const confirmPassword = document.getElementById('confirm_password'); |
|||
|
|||
if (password.value !== confirmPassword.value) { |
|||
e.preventDefault(); |
|||
alert('两次输入的密码不一致,请重新输入'); |
|||
return false; |
|||
} |
|||
}); |
|||
} |
|||
}); |
|||
@ -0,0 +1,230 @@ |
|||
[data-simplebar] { |
|||
position: relative; |
|||
flex-direction: column; |
|||
flex-wrap: wrap; |
|||
justify-content: flex-start; |
|||
align-content: flex-start; |
|||
align-items: flex-start; |
|||
} |
|||
|
|||
.simplebar-wrapper { |
|||
overflow: hidden; |
|||
width: inherit; |
|||
height: inherit; |
|||
max-width: inherit; |
|||
max-height: inherit; |
|||
} |
|||
|
|||
.simplebar-mask { |
|||
direction: inherit; |
|||
position: absolute; |
|||
overflow: hidden; |
|||
padding: 0; |
|||
margin: 0; |
|||
left: 0; |
|||
top: 0; |
|||
bottom: 0; |
|||
right: 0; |
|||
width: auto !important; |
|||
height: auto !important; |
|||
z-index: 0; |
|||
} |
|||
|
|||
.simplebar-offset { |
|||
direction: inherit !important; |
|||
box-sizing: inherit !important; |
|||
resize: none !important; |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
bottom: 0; |
|||
right: 0; |
|||
padding: 0; |
|||
margin: 0; |
|||
-webkit-overflow-scrolling: touch; |
|||
} |
|||
|
|||
.simplebar-content-wrapper { |
|||
direction: inherit; |
|||
box-sizing: border-box !important; |
|||
position: relative; |
|||
display: block; |
|||
height: 100%; /* Required for horizontal native scrollbar to not appear if parent is taller than natural height */ |
|||
width: auto; |
|||
max-width: 100%; /* Not required for horizontal scroll to trigger */ |
|||
max-height: 100%; /* Needed for vertical scroll to trigger */ |
|||
overflow: auto; |
|||
scrollbar-width: none; |
|||
-ms-overflow-style: none; |
|||
} |
|||
|
|||
.simplebar-content-wrapper::-webkit-scrollbar, |
|||
.simplebar-hide-scrollbar::-webkit-scrollbar { |
|||
display: none; |
|||
width: 0; |
|||
height: 0; |
|||
} |
|||
|
|||
.simplebar-content:before, |
|||
.simplebar-content:after { |
|||
content: ' '; |
|||
display: table; |
|||
} |
|||
|
|||
.simplebar-placeholder { |
|||
max-height: 100%; |
|||
max-width: 100%; |
|||
width: 100%; |
|||
pointer-events: none; |
|||
} |
|||
|
|||
.simplebar-height-auto-observer-wrapper { |
|||
box-sizing: inherit !important; |
|||
height: 100%; |
|||
width: 100%; |
|||
max-width: 1px; |
|||
position: relative; |
|||
float: left; |
|||
max-height: 1px; |
|||
overflow: hidden; |
|||
z-index: -1; |
|||
padding: 0; |
|||
margin: 0; |
|||
pointer-events: none; |
|||
flex-grow: inherit; |
|||
flex-shrink: 0; |
|||
flex-basis: 0; |
|||
} |
|||
|
|||
.simplebar-height-auto-observer { |
|||
box-sizing: inherit; |
|||
display: block; |
|||
opacity: 0; |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
height: 1000%; |
|||
width: 1000%; |
|||
min-height: 1px; |
|||
min-width: 1px; |
|||
overflow: hidden; |
|||
pointer-events: none; |
|||
z-index: -1; |
|||
} |
|||
|
|||
.simplebar-track { |
|||
z-index: 1; |
|||
position: absolute; |
|||
right: 0; |
|||
bottom: 0; |
|||
pointer-events: none; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
[data-simplebar].simplebar-dragging { |
|||
pointer-events: none; |
|||
-webkit-touch-callout: none; |
|||
-webkit-user-select: none; |
|||
-khtml-user-select: none; |
|||
-moz-user-select: none; |
|||
-ms-user-select: none; |
|||
user-select: none; |
|||
} |
|||
|
|||
[data-simplebar].simplebar-dragging .simplebar-content { |
|||
pointer-events: none; |
|||
-webkit-touch-callout: none; |
|||
-webkit-user-select: none; |
|||
-khtml-user-select: none; |
|||
-moz-user-select: none; |
|||
-ms-user-select: none; |
|||
user-select: none; |
|||
} |
|||
|
|||
[data-simplebar].simplebar-dragging .simplebar-track { |
|||
pointer-events: all; |
|||
} |
|||
|
|||
.simplebar-scrollbar { |
|||
position: absolute; |
|||
left: 0; |
|||
right: 0; |
|||
min-height: 10px; |
|||
} |
|||
|
|||
.simplebar-scrollbar:before { |
|||
position: absolute; |
|||
content: ''; |
|||
background: black; |
|||
border-radius: 7px; |
|||
left: 2px; |
|||
right: 2px; |
|||
opacity: 0; |
|||
transition: opacity 0.2s 0.5s linear; |
|||
} |
|||
|
|||
.simplebar-scrollbar.simplebar-visible:before { |
|||
opacity: 0.5; |
|||
transition-delay: 0s; |
|||
transition-duration: 0s; |
|||
} |
|||
|
|||
.simplebar-track.simplebar-vertical { |
|||
top: 0; |
|||
width: 11px; |
|||
} |
|||
|
|||
.simplebar-scrollbar:before { |
|||
top: 2px; |
|||
bottom: 2px; |
|||
left: 2px; |
|||
right: 2px; |
|||
} |
|||
|
|||
.simplebar-track.simplebar-horizontal { |
|||
left: 0; |
|||
height: 11px; |
|||
} |
|||
|
|||
.simplebar-track.simplebar-horizontal .simplebar-scrollbar { |
|||
right: auto; |
|||
left: 0; |
|||
top: 0; |
|||
bottom: 0; |
|||
min-height: 0; |
|||
min-width: 10px; |
|||
width: auto; |
|||
} |
|||
|
|||
/* Rtl support */ |
|||
[data-simplebar-direction='rtl'] .simplebar-track.simplebar-vertical { |
|||
right: auto; |
|||
left: 0; |
|||
} |
|||
|
|||
.simplebar-dummy-scrollbar-size { |
|||
direction: rtl; |
|||
position: fixed; |
|||
opacity: 0; |
|||
visibility: hidden; |
|||
height: 500px; |
|||
width: 500px; |
|||
overflow-y: hidden; |
|||
overflow-x: scroll; |
|||
-ms-overflow-style: scrollbar !important; |
|||
} |
|||
|
|||
.simplebar-dummy-scrollbar-size > div { |
|||
width: 200%; |
|||
height: 200%; |
|||
margin: 10px 0; |
|||
} |
|||
|
|||
.simplebar-hide-scrollbar { |
|||
position: fixed; |
|||
left: 0; |
|||
visibility: hidden; |
|||
overflow-y: scroll; |
|||
scrollbar-width: none; |
|||
-ms-overflow-style: none; |
|||
} |
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@ |
|||
asd |
|||
@ -0,0 +1,28 @@ |
|||
/** |
|||
* @param { import("knex").Knex } knex |
|||
* @returns { Promise<void> } |
|||
*/ |
|||
export const up = async knex => { |
|||
return knex.schema.createTable("bookmarks", function (table) { |
|||
table.increments("id").primary() |
|||
table.integer("user_id").unsigned() |
|||
.references("id").inTable("users").onDelete("CASCADE") |
|||
table.string("title", 200).notNullable() |
|||
table.string("url", 500) |
|||
table.text("description") |
|||
table.timestamp("created_at").defaultTo(knex.fn.now()) |
|||
table.timestamp("updated_at").defaultTo(knex.fn.now()) |
|||
|
|||
table.index(["user_id"]) // 常用查询索引
|
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* @param { import("knex").Knex } knex |
|||
* @returns { Promise<void> } |
|||
*/ |
|||
export const down = async knex => { |
|||
return knex.schema.dropTable("bookmarks") |
|||
} |
|||
|
|||
|
|||
@ -0,0 +1,68 @@ |
|||
import db from "../index.js" |
|||
|
|||
class BookmarkModel { |
|||
static async findAllByUser(userId) { |
|||
return db("bookmarks").where("user_id", userId).orderBy("id", "desc") |
|||
} |
|||
|
|||
static async findById(id) { |
|||
return db("bookmarks").where("id", id).first() |
|||
} |
|||
|
|||
static async create(data) { |
|||
const userId = data.user_id |
|||
const url = typeof data.url === "string" ? data.url.trim() : data.url |
|||
|
|||
if (userId != null && url) { |
|||
const exists = await db("bookmarks").where({ user_id: userId, url }).first() |
|||
if (exists) { |
|||
throw new Error("该用户下已存在相同 URL 的书签") |
|||
} |
|||
} |
|||
|
|||
return db("bookmarks").insert({ |
|||
...data, |
|||
url, |
|||
updated_at: db.fn.now(), |
|||
}).returning("*") |
|||
} |
|||
|
|||
static async update(id, data) { |
|||
// 若更新后 user_id 与 url 同时存在,则做排他性查重(排除自身)
|
|||
const current = await db("bookmarks").where("id", id).first() |
|||
if (!current) return [] |
|||
|
|||
const nextUserId = data.user_id != null ? data.user_id : current.user_id |
|||
const nextUrlRaw = data.url != null ? data.url : current.url |
|||
const nextUrl = typeof nextUrlRaw === "string" ? nextUrlRaw.trim() : nextUrlRaw |
|||
|
|||
if (nextUserId != null && nextUrl) { |
|||
const exists = await db("bookmarks") |
|||
.where({ user_id: nextUserId, url: nextUrl }) |
|||
.andWhereNot({ id }) |
|||
.first() |
|||
if (exists) { |
|||
throw new Error("该用户下已存在相同 URL 的书签") |
|||
} |
|||
} |
|||
|
|||
return db("bookmarks").where("id", id).update({ |
|||
...data, |
|||
url: data.url != null ? nextUrl : data.url, |
|||
updated_at: db.fn.now(), |
|||
}).returning("*") |
|||
} |
|||
|
|||
static async delete(id) { |
|||
return db("bookmarks").where("id", id).del() |
|||
} |
|||
|
|||
static async findByUserAndUrl(userId, url) { |
|||
return db("bookmarks").where({ user_id: userId, url }).first() |
|||
} |
|||
} |
|||
|
|||
export default BookmarkModel |
|||
export { BookmarkModel } |
|||
|
|||
|
|||
@ -1,3 +1,3 @@ |
|||
// 统一导出所有中间件
|
|||
import auth from "./auth.js" |
|||
export { auth } |
|||
import Auth from "./auth.js" |
|||
export { Auth } |
|||
|
|||
@ -1,18 +1,13 @@ |
|||
extends /layouts/base.pug |
|||
extends /layouts/root.pug |
|||
|
|||
block head |
|||
block $$head |
|||
+css('styles.css') |
|||
block pageHead |
|||
|
|||
block content |
|||
.page-layout |
|||
.page |
|||
.content |
|||
block pageContent |
|||
block $$content |
|||
block pageContent |
|||
footer |
|||
+include() |
|||
- var edit = false |
|||
include /htmx/footer.pug |
|||
include /htmx/footer.pug |
|||
|
|||
block scripts |
|||
block $$scripts |
|||
block pageScripts |
|||
|
|||
@ -0,0 +1,67 @@ |
|||
include utils.pug |
|||
|
|||
doctype html |
|||
html(lang="zh-CN") |
|||
head |
|||
block $$head |
|||
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(rel="shortcut icon", href=$site.site_favicon) |
|||
meta(charset="utf-8") |
|||
meta(name="viewport" content="width=device-width, initial-scale=1") |
|||
+css('reset.css') |
|||
+css('lib/simplebar.css', true) |
|||
+css('lib/simplebar-shim.css') |
|||
+js('lib/htmx.min.js') |
|||
+js('lib/tailwindcss.3.4.17.js') |
|||
+js('lib/simplebar.min.js', true) |
|||
body |
|||
noscript |
|||
style. |
|||
.simplebar-content-wrapper { |
|||
scrollbar-width: auto; |
|||
-ms-overflow-style: auto; |
|||
} |
|||
|
|||
.simplebar-content-wrapper::-webkit-scrollbar, |
|||
.simplebar-hide-scrollbar::-webkit-scrollbar { |
|||
display: initial; |
|||
width: initial; |
|||
height: initial; |
|||
} |
|||
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) |
|||
}) |
|||
//- 处理点击慢慢回到顶部 |
|||
const backToTopBtn = document.querySelector('.back-to-top'); |
|||
if (backToTopBtn) { |
|||
backToTopBtn.addEventListener('click', function(e) { |
|||
e.preventDefault(); |
|||
const el = document.querySelector('.simplebar-content-wrapper'); |
|||
if (!el) return; |
|||
const duration = 400; |
|||
const start = el.scrollTop; |
|||
const startTime = performance.now(); |
|||
function animateScroll(currentTime) { |
|||
const elapsed = currentTime - startTime; |
|||
const progress = Math.min(elapsed / duration, 1); |
|||
el.scrollTop = start * (1 - progress); |
|||
if (progress < 1) { |
|||
requestAnimationFrame(animateScroll); |
|||
} |
|||
} |
|||
requestAnimationFrame(animateScroll); |
|||
}); |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
mixin include() |
|||
if block |
|||
block |
|||
//- include的使用方法 |
|||
//- +include() |
|||
//- - var edit = false |
|||
//- include /htmx/footer.pug |
|||
|
|||
mixin css(url, extranl = false) |
|||
if extranl || url.startsWith('http') || url.startsWith('//') |
|||
link(rel="stylesheet" type="text/css" href=url) |
|||
else |
|||
link(rel="stylesheet", href=($config && $config.base || "") + url) |
|||
|
|||
mixin js(url, extranl = false) |
|||
if extranl || url.startsWith('http') || url.startsWith('//') |
|||
script(type="text/javascript" src=url) |
|||
else |
|||
script(src=($config && $config.base || "") + url) |
|||
|
|||
mixin link(href, name) |
|||
//- attributes == {class: "btn"} |
|||
a(href=href)&attributes(attributes)= name |
|||
Loading…
Reference in new issue