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" |
import Auth from "./auth.js" |
||||
export { auth } |
export { Auth } |
||||
|
|||||
@ -1,18 +1,13 @@ |
|||||
extends /layouts/base.pug |
extends /layouts/root.pug |
||||
|
|
||||
block head |
block $$head |
||||
+css('styles.css') |
+css('styles.css') |
||||
block pageHead |
block pageHead |
||||
|
|
||||
block content |
block $$content |
||||
.page-layout |
|
||||
.page |
|
||||
.content |
|
||||
block pageContent |
block pageContent |
||||
footer |
footer |
||||
+include() |
|
||||
- var edit = false |
|
||||
include /htmx/footer.pug |
include /htmx/footer.pug |
||||
|
|
||||
block scripts |
block $$scripts |
||||
block pageScripts |
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