31 changed files with 33 additions and 6773 deletions
File diff suppressed because it is too large
@ -1,54 +0,0 @@ |
|||||
html, |
|
||||
body { |
|
||||
margin: 0; |
|
||||
padding: 0; |
|
||||
height: 100%; |
|
||||
font-family: Arial, sans-serif; |
|
||||
color: #333; |
|
||||
} |
|
||||
|
|
||||
body { |
|
||||
min-height: 100vh; |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
} |
|
||||
|
|
||||
.page-layout { |
|
||||
flex: 1; |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
} |
|
||||
|
|
||||
.page { |
|
||||
width: 100%; |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
flex: 1; |
|
||||
position: relative; |
|
||||
|
|
||||
max-width: 1226px; |
|
||||
margin-right: auto; |
|
||||
margin-left: auto; |
|
||||
color: white; |
|
||||
|
|
||||
} |
|
||||
|
|
||||
.content { |
|
||||
flex: 1; |
|
||||
width: 0; |
|
||||
} |
|
||||
|
|
||||
@media screen and (max-width: 768px) { |
|
||||
.content { |
|
||||
padding: 0 10px; |
|
||||
padding-top: 40px; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.card { |
|
||||
padding: 20px; |
|
||||
border-radius: 28px; |
|
||||
background: rgba(255, 255, 255, 0.1); |
|
||||
backdrop-filter: blur(12px); |
|
||||
color: #fff; |
|
||||
} |
|
||||
@ -1,174 +0,0 @@ |
|||||
|
|
||||
html, |
|
||||
body { |
|
||||
margin: 0; |
|
||||
padding: 0; |
|
||||
height: 100%; |
|
||||
font-family: Arial, sans-serif; |
|
||||
color: #333; |
|
||||
} |
|
||||
|
|
||||
body { |
|
||||
min-height: 100vh; |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
background-color: #fcfcfc; |
|
||||
} |
|
||||
|
|
||||
.page-layout { |
|
||||
flex: 1; |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
} |
|
||||
|
|
||||
.page { |
|
||||
width: 100%; |
|
||||
/* display: flex; */ |
|
||||
/* flex-direction: column; */ |
|
||||
flex: 1; |
|
||||
position: relative; |
|
||||
|
|
||||
} |
|
||||
|
|
||||
.container { |
|
||||
max-width: 1226px; |
|
||||
margin-right: auto; |
|
||||
margin-left: auto; |
|
||||
/* padding-left: 20px; |
|
||||
padding-right: 20px; */ |
|
||||
} |
|
||||
|
|
||||
@media (max-width: 640px) { |
|
||||
.container { |
|
||||
padding-left: 10px; |
|
||||
padding-right: 10px; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.clearfix::after { |
|
||||
content: ''; |
|
||||
display: table; |
|
||||
clear: both; |
|
||||
} |
|
||||
|
|
||||
.navbar-brand { |
|
||||
float: left; |
|
||||
height: 100%; |
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
} |
|
||||
|
|
||||
/* ===== 顶部导航 响应式 ===== */ |
|
||||
.navbar { |
|
||||
position: relative; |
|
||||
} |
|
||||
|
|
||||
.navbar .menu-toggle { |
|
||||
display: none; |
|
||||
position: absolute; |
|
||||
right: 12px; |
|
||||
top: 50%; |
|
||||
transform: translateY(-50%); |
|
||||
width: 40px; |
|
||||
height: 40px; |
|
||||
border: none; |
|
||||
background: transparent; |
|
||||
padding: 0; |
|
||||
cursor: pointer; |
|
||||
} |
|
||||
|
|
||||
.navbar .menu-toggle .bar { |
|
||||
display: block; |
|
||||
width: 22px; |
|
||||
height: 2px; |
|
||||
background: #333; |
|
||||
margin: 5px auto; |
|
||||
transition: transform .2s ease, opacity .2s ease; |
|
||||
} |
|
||||
|
|
||||
.navbar .mobile-menu { |
|
||||
display: none; |
|
||||
position: relative; |
|
||||
overflow: hidden; |
|
||||
max-height: 0; |
|
||||
transition: max-height .25s ease; |
|
||||
background: rgba(255, 255, 255, 0.95); |
|
||||
border-radius: 0 0 12px 12px; |
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, .06); |
|
||||
} |
|
||||
|
|
||||
.navbar.open .mobile-menu { |
|
||||
display: none; |
|
||||
max-height: 400px; |
|
||||
} |
|
||||
|
|
||||
.navbar .mobile-menu .menu-item { |
|
||||
display: block; |
|
||||
padding: 12px 0; |
|
||||
} |
|
||||
|
|
||||
/* 桌面端可见区域 */ |
|
||||
.desktop-only { |
|
||||
display: block; |
|
||||
} |
|
||||
|
|
||||
/* <= 1024 宽度:显示切换按钮,隐藏桌面菜单 */ |
|
||||
@media (max-width: 1024px) { |
|
||||
.desktop-only { |
|
||||
display: none; |
|
||||
} |
|
||||
|
|
||||
.navbar .menu-toggle { |
|
||||
display: inline-block; |
|
||||
} |
|
||||
|
|
||||
.navbar .mobile-menu { |
|
||||
padding: 8px 20px 12px; |
|
||||
} |
|
||||
|
|
||||
.navbar.open .mobile-menu { |
|
||||
display: block; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.menu { |
|
||||
height: 100%; |
|
||||
margin-left: 20px; |
|
||||
|
|
||||
.menu-item { |
|
||||
height: 100%; |
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
padding: 0 10px; |
|
||||
cursor: pointer; |
|
||||
|
|
||||
&:hover { |
|
||||
background: rgba(175, 175, 175, 0.1); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.menu.left { |
|
||||
float: left; |
|
||||
|
|
||||
.menu-item { |
|
||||
float: left; |
|
||||
|
|
||||
&+.menu-item { |
|
||||
margin-left: 5px; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.right.menu { |
|
||||
float: right; |
|
||||
|
|
||||
.menu-item { |
|
||||
float: right; |
|
||||
|
|
||||
&+.menu-item { |
|
||||
margin-right: 5px; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@ -1,677 +0,0 @@ |
|||||
|
|
||||
/* 深色主题媒体查询 - 当用户系统偏好深色模式时应用 */ |
|
||||
@media (prefers-color-scheme: dark) { |
|
||||
.markdown-body { |
|
||||
/* 告诉浏览器使用深色配色方案,影响滚动条等系统UI元素 */ |
|
||||
color-scheme: dark; |
|
||||
} |
|
||||
} |
|
||||
/* 浅色主题媒体查询 - 当用户系统偏好浅色模式时应用 */ |
|
||||
@media (prefers-color-scheme: light) { |
|
||||
// https://verytoolz.com/blog/03bfb3598f/ |
|
||||
.markdown-body { |
|
||||
/* 告诉浏览器使用浅色配色方案,影响滚动条等系统UI元素 */ |
|
||||
color-scheme: light; |
|
||||
/* 定义CSS自定义属性,用于主题色彩管理 */ |
|
||||
--color-fg-default: #24292f; // 文本色-默认 - 主要文本颜色 |
|
||||
--color-fg-muted: #57606a; // 文本色-柔和 - 次要文本颜色 |
|
||||
--color-fg-subtle: #6e7781; // 文本色-微妙 - 最淡的文本颜色 |
|
||||
--color-canvas-default: #ffffff; // 底色-默认 - 主要背景颜色 |
|
||||
--color-canvas-subtle: #f6f8fa; // 底色-微妙 - 次要背景颜色 |
|
||||
--color-border-default: #d0d7de; // 边框色-默认 - 主要边框颜色 |
|
||||
--color-border-muted: hsla(210, 18%, 87%, 1); // 边框色-柔和 - 次要边框颜色 |
|
||||
--color-neutral-muted: rgba(175, 184, 193, 0.2); // 边框色-中性 - 中性边框颜色 |
|
||||
--color-accent-fg: #0969da; // 文本强调色 - 强调文本颜色 |
|
||||
--color-accent-emphasis: #0969da; // 背景强调色 - 强调背景颜色 |
|
||||
--color-attention-subtle: #fff8c5; // 背景注意色 - 注意提示背景色 |
|
||||
--color-danger-fg: #cf222e; // 文本危险色 - 危险/错误文本颜色 |
|
||||
--color-mark-default: rgb(255, 255, 0); // mark 默认色 - 标记默认背景色 |
|
||||
--color-mark-fg: rgb(255, 187, 0); // mark 强调色 - 标记强调背景色 |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/* Markdown内容主体样式 - 用于渲染Markdown文档的容器 */ |
|
||||
.markdown-body { |
|
||||
/* 防止iOS Safari自动调整文本大小 */ |
|
||||
-webkit-text-size-adjust: 100%; |
|
||||
/* 防止IE自动调整文本大小 */ |
|
||||
-ms-text-size-adjust: 100%; |
|
||||
/* 优化文本渲染质量,提升可读性 */ |
|
||||
text-rendering: optimizelegibility; |
|
||||
/* 重置外边距为0 */ |
|
||||
margin: 0; |
|
||||
/* 允许长单词在必要时换行,防止溢出 */ |
|
||||
word-wrap: break-word; |
|
||||
/* 使用CSS变量设置文本颜色 */ |
|
||||
color: var(--color-fg-muted); |
|
||||
/* 伪元素before - 用于清除浮动 */ |
|
||||
&::before { |
|
||||
/* 设置为表格显示模式,用于清除浮动 */ |
|
||||
display: table; |
|
||||
/* 空内容,仅用于布局 */ |
|
||||
content: ""; |
|
||||
} |
|
||||
/* 伪元素after - 用于清除浮动 */ |
|
||||
&::after { |
|
||||
/* 设置为表格显示模式,用于清除浮动 */ |
|
||||
display: table; |
|
||||
/* 清除左右浮动 */ |
|
||||
clear: both; |
|
||||
/* 空内容,仅用于布局 */ |
|
||||
content: ""; |
|
||||
} |
|
||||
/* 第一个子元素 - 移除顶部外边距 */ |
|
||||
> *:first-child { |
|
||||
/* 强制移除顶部外边距,避免不必要的空白 */ |
|
||||
margin-top: 0 !important; |
|
||||
} |
|
||||
/* 最后一个子元素 - 移除底部外边距 */ |
|
||||
> *:last-child { |
|
||||
/* 强制移除底部外边距,避免不必要的空白 */ |
|
||||
margin-bottom: 0 !important; |
|
||||
} |
|
||||
/* 块级元素统一间距设置 - 段落、引用、列表、表格等 */ |
|
||||
p, |
|
||||
blockquote, |
|
||||
ul, |
|
||||
ol, |
|
||||
dl, |
|
||||
table, |
|
||||
hr, |
|
||||
form, |
|
||||
pre, |
|
||||
details { |
|
||||
/* 移除顶部外边距,避免重复间距 */ |
|
||||
margin-top: 0; |
|
||||
/* 设置底部外边距为1em,保持适当间距 */ |
|
||||
margin-bottom: 1em; |
|
||||
} |
|
||||
/* 引用块内部元素间距处理 */ |
|
||||
blockquote { |
|
||||
/* 引用块内第一个子元素 - 移除顶部外边距 */ |
|
||||
& > :first-child { |
|
||||
margin-top: 0; |
|
||||
} |
|
||||
/* 引用块内最后一个子元素 - 移除底部外边距 */ |
|
||||
& > :last-child { |
|
||||
margin-bottom: 0; |
|
||||
} |
|
||||
} |
|
||||
/* 统一显示成块状元素 - 确保这些元素独占一行 */ |
|
||||
details, |
|
||||
figcaption, |
|
||||
figure { |
|
||||
/* 设置为块级元素,独占一行显示 */ |
|
||||
display: block; |
|
||||
} |
|
||||
/* HTML5 媒体文件跟 img 保持一致 - 内联块级元素 */ |
|
||||
audio, |
|
||||
canvas, |
|
||||
video { |
|
||||
/* 设置为内联块级元素,可以设置宽高但不会独占一行 */ |
|
||||
display: inline-block; |
|
||||
} |
|
||||
/* 按钮内部间距统一 - 移除Firefox默认内边距 */ |
|
||||
button::-moz-focus-inner, |
|
||||
input::-moz-focus-inner { |
|
||||
/* 移除Firefox浏览器按钮和输入框的内部内边距 */ |
|
||||
padding: 0; |
|
||||
/* 移除Firefox浏览器按钮和输入框的内部边框 */ |
|
||||
border: 0; |
|
||||
} |
|
||||
/* 定义元素显示为斜体 - 术语定义样式 */ |
|
||||
dfn { |
|
||||
/* 设置字体为斜体,用于术语定义 */ |
|
||||
font-style: italic; |
|
||||
} |
|
||||
/* 去掉各Table cell 的边距并让其边重合 - 表格样式统一 */ |
|
||||
table { |
|
||||
/* 合并表格边框,相邻单元格边框合并为一条 */ |
|
||||
border-collapse: collapse; |
|
||||
/* 设置表格单元格间距为0 */ |
|
||||
border-spacing: 0; |
|
||||
/* 设置为块级元素,可以设置宽高 */ |
|
||||
display: block; |
|
||||
/* 宽度根据内容自适应 */ |
|
||||
width: max-content; |
|
||||
/* 最大宽度不超过父容器 */ |
|
||||
max-width: 100%; |
|
||||
/* 内容溢出时显示滚动条 */ |
|
||||
overflow: auto; |
|
||||
} |
|
||||
/* 可拖动文件添加拖动手势 - 拖拽元素样式 */ |
|
||||
[draggable] { |
|
||||
/* 设置鼠标悬停时显示移动光标 */ |
|
||||
cursor: move; |
|
||||
} |
|
||||
/* 加粗元素 - 粗体文本样式 */ |
|
||||
b, |
|
||||
strong { |
|
||||
/* 设置字体粗细,使用CSS变量或默认600 */ |
|
||||
font-weight: var(--base-text-weight-semibold, 600); |
|
||||
} |
|
||||
/* 缩写元素样式统一 - 缩写和首字母缩写样式 */ |
|
||||
abbr, |
|
||||
acronym { |
|
||||
/* 移除底部边框 */ |
|
||||
border-bottom: none; |
|
||||
/* 设置字体变体为正常 */ |
|
||||
font-variant: normal; |
|
||||
/* 设置虚线下划线装饰 */ |
|
||||
text-decoration: underline dotted; |
|
||||
} |
|
||||
/* 添加鼠标问号,进一步确保应用的语义是正确的(要知道,交互他们也有洁癖,如果你不去掉,那得多花点口舌) */ |
|
||||
abbr { |
|
||||
/* 设置鼠标悬停时显示帮助光标 */ |
|
||||
cursor: help; |
|
||||
} |
|
||||
/* 一致的 del 样式 - 删除线文本样式 */ |
|
||||
del { |
|
||||
/* 设置文本装饰为删除线 */ |
|
||||
text-decoration: line-through; |
|
||||
} |
|
||||
/* a标签去除下划线 - 链接样式处理 */ |
|
||||
a { |
|
||||
/* 默认移除下划线,保持页面简洁 */ |
|
||||
text-decoration: none; |
|
||||
/* 没有href属性的链接样式 */ |
|
||||
&:not([href]) { |
|
||||
/* 继承父元素颜色 */ |
|
||||
color: inherit; |
|
||||
/* 移除下划线装饰 */ |
|
||||
text-decoration: none; |
|
||||
} |
|
||||
/* 鼠标悬停时显示下划线 */ |
|
||||
&:hover { |
|
||||
text-decoration: underline; |
|
||||
} |
|
||||
} |
|
||||
/* 默认不显示下划线,保持页面简洁 - 插入文本样式 */ |
|
||||
ins { |
|
||||
/* 移除下划线装饰,保持页面简洁 */ |
|
||||
text-decoration: none; |
|
||||
} |
|
||||
/* 专名号:虽然 u 已经重回 html5 Draft,但在所有浏览器中都是可以使用的, |
|
||||
* 要做到更好,向后兼容的话,添加 class="typo-u" 来显示专名号 |
|
||||
* 关于 <u> 标签:http://www.whatwg.org/specs/web-apps/current-work/multipage/text-level-semantics.html#the-u-element |
|
||||
* 被放弃的是 4,之前一直搞错 http://www.w3.org/TR/html401/appendix/changes.html#idx-deprecated |
|
||||
* 一篇关于 <u> 标签的很好文章:http://html5doctor.com/u-element/ |
|
||||
*/ |
|
||||
u, |
|
||||
.typo-u { |
|
||||
/* 设置文本装饰为下划线,用于专名号显示 */ |
|
||||
text-decoration: underline; |
|
||||
} |
|
||||
/* 隐藏指定元素 - 隐藏带有hidden属性的元素 */ |
|
||||
[hidden] { |
|
||||
/* 强制隐藏元素,优先级最高 */ |
|
||||
display: none !important; |
|
||||
} |
|
||||
/* 伸缩框显示为列表元素 - 详情框摘要样式 */ |
|
||||
summary { |
|
||||
/* 设置为列表项显示,显示为可点击的列表项 */ |
|
||||
display: list-item; |
|
||||
} |
|
||||
/* 引用元素前后内容 - 移除默认引号 */ |
|
||||
q:before, |
|
||||
q:after { |
|
||||
/* 移除引用元素前后的默认引号内容 */ |
|
||||
content: ""; |
|
||||
} |
|
||||
/* 表格标题和表头文本对齐 - 默认左对齐 */ |
|
||||
caption, |
|
||||
th { |
|
||||
/* 设置文本左对齐 */ |
|
||||
text-align: left; |
|
||||
} |
|
||||
/* 居中对齐的表格标题和表头 */ |
|
||||
caption[align="center"], |
|
||||
th[align="center"] { |
|
||||
/* 设置文本居中对齐 */ |
|
||||
text-align: center; |
|
||||
} |
|
||||
/* 特定元素字体粗细统一 - 地址、标题、引用等 */ |
|
||||
address, |
|
||||
caption, |
|
||||
cite, |
|
||||
em, |
|
||||
th, |
|
||||
var { |
|
||||
/* 设置字体粗细为正常(400) */ |
|
||||
font-weight: 400; |
|
||||
} |
|
||||
/* 标记,类似于手写的荧光笔的作用 - 高亮标记样式 */ |
|
||||
mark { |
|
||||
/* 设置标记背景色,使用CSS变量 */ |
|
||||
background: var(--color-mark-default); |
|
||||
// background: #fffdd1; // 备用背景色 |
|
||||
// border-bottom: 1px solid #ffedce; // 备用底部边框 |
|
||||
/* 设置内边距,增加标记的可读性 */ |
|
||||
padding: 2px; |
|
||||
|
|
||||
/* 激活状态的标记样式 */ |
|
||||
&.active { |
|
||||
/* 激活时使用强调色背景 */ |
|
||||
background: var(--color-mark-fg); |
|
||||
} |
|
||||
// margin: 0 5px; // 备用外边距 |
|
||||
} |
|
||||
/* 统一h1元素的间隔和字体大小 - 一级标题样式 */ |
|
||||
h1 { |
|
||||
/* 设置上下外边距为0.67em */ |
|
||||
margin: 0.67em 0; |
|
||||
/* 设置字体粗细,使用CSS变量或默认600 */ |
|
||||
font-weight: var(--base-text-weight-semibold, 600); |
|
||||
/* 设置字体大小为2倍基础大小 */ |
|
||||
font-size: 2em; |
|
||||
} |
|
||||
/* small字体缩小 - 小字体文本样式 */ |
|
||||
small { |
|
||||
/* 设置字体大小为父元素的90% */ |
|
||||
font-size: 90%; |
|
||||
} |
|
||||
/* 上下标显示 - 下标和上标文本样式 */ |
|
||||
sub, |
|
||||
sup { |
|
||||
/* 设置字体大小为75% */ |
|
||||
font-size: 75%; |
|
||||
/* 设置行高为0,避免影响行间距 */ |
|
||||
line-height: 0; |
|
||||
/* 设置相对定位,用于精确控制位置 */ |
|
||||
position: relative; |
|
||||
/* 设置垂直对齐为基线 */ |
|
||||
vertical-align: baseline; |
|
||||
} |
|
||||
/* 上下标内链接样式 */ |
|
||||
sub a, |
|
||||
sup a { |
|
||||
/* 设置左右内边距为0.1em */ |
|
||||
padding: 0 0.1em; |
|
||||
} |
|
||||
/* 下标位置调整 */ |
|
||||
sub { |
|
||||
/* 向下偏移0.25em */ |
|
||||
bottom: -0.25em; |
|
||||
} |
|
||||
/* 上标位置调整 */ |
|
||||
sup { |
|
||||
/* 向上偏移0.5em */ |
|
||||
top: -0.5em; |
|
||||
} |
|
||||
/* 代码相关的字体大小统一 - 代码元素字体样式 */ |
|
||||
code, |
|
||||
kbd, |
|
||||
pre, |
|
||||
samp, |
|
||||
pre tt { |
|
||||
/* 设置字体为等宽字体,便于代码阅读 */ |
|
||||
font-family: monospace; |
|
||||
/* 设置字体大小为1em,保持一致性 */ |
|
||||
font-size: 1em; |
|
||||
} |
|
||||
/* 去除默认边框 - 移除字段集和图片的默认边框 */ |
|
||||
fieldset, |
|
||||
img { |
|
||||
/* 移除边框 */ |
|
||||
border: 0; |
|
||||
} |
|
||||
/* 图片初始化样式 - 图片元素基础样式 */ |
|
||||
img { |
|
||||
/* 设置边框样式为无 */ |
|
||||
border-style: none; |
|
||||
/* 设置最大宽度为100%,防止溢出 */ |
|
||||
max-width: 100%; |
|
||||
/* 设置盒模型为内容盒模型 */ |
|
||||
box-sizing: content-box; |
|
||||
/* 设置左右外边距为自动,实现居中 */ |
|
||||
margin: 0 auto; |
|
||||
/* 设置背景色,使用CSS变量 */ |
|
||||
background-color: var(--color-canvas-default); |
|
||||
} |
|
||||
/* 可附标题内容元素的间距 - 图片容器样式 */ |
|
||||
figure { |
|
||||
/* 设置上下外边距为1em,左右外边距为40px */ |
|
||||
margin: 1em 40px; |
|
||||
} |
|
||||
/* 间隔线 - 水平分隔线样式 */ |
|
||||
/* 一致化 horizontal rule - 统一水平分隔线样式 */ |
|
||||
hr { |
|
||||
/* 设置盒模型为内容盒模型 */ |
|
||||
box-sizing: content-box; |
|
||||
/* 隐藏溢出内容 */ |
|
||||
overflow: hidden; |
|
||||
/* 设置背景为透明 */ |
|
||||
background: transparent; |
|
||||
/* 设置底部边框,使用CSS变量 */ |
|
||||
border-bottom: 1px solid var(--color-border-muted); |
|
||||
/* 设置高度为0.25em */ |
|
||||
height: 0.25em; |
|
||||
/* 移除内边距 */ |
|
||||
padding: 0; |
|
||||
/* 设置上下外边距为24px */ |
|
||||
margin: 24px 0; |
|
||||
/* 设置背景色,使用CSS变量 */ |
|
||||
background-color: var(--color-border-default); |
|
||||
/* 移除边框 */ |
|
||||
border: 0; |
|
||||
/* 伪元素before - 用于清除浮动 */ |
|
||||
&::before { |
|
||||
/* 设置为表格显示模式,用于清除浮动 */ |
|
||||
display: table; |
|
||||
/* 空内容,仅用于布局 */ |
|
||||
content: ""; |
|
||||
} |
|
||||
/* 伪元素after - 用于清除浮动 */ |
|
||||
&::after { |
|
||||
/* 设置为表格显示模式,用于清除浮动 */ |
|
||||
display: table; |
|
||||
/* 清除左右浮动 */ |
|
||||
clear: both; |
|
||||
/* 空内容,仅用于布局 */ |
|
||||
content: ""; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/* 表单元素并不继承父级 font 的问题 - 表单元素字体继承 */ |
|
||||
button, |
|
||||
input, |
|
||||
select, |
|
||||
textarea { |
|
||||
/* 继承父元素的字体样式 */ |
|
||||
font: inherit; |
|
||||
/* 移除外边距 */ |
|
||||
margin: 0; |
|
||||
/* 设置溢出为可见 */ |
|
||||
overflow: visible; |
|
||||
/* 继承父元素的字体族 */ |
|
||||
font-family: inherit; |
|
||||
/* 继承父元素的字体大小 */ |
|
||||
font-size: inherit; |
|
||||
/* 继承父元素的行高 */ |
|
||||
line-height: inherit; |
|
||||
} |
|
||||
/* 外观显示为按钮 - 按钮类型输入框样式 */ |
|
||||
[type="button"], |
|
||||
[type="reset"], |
|
||||
[type="submit"] { |
|
||||
/* 设置WebKit浏览器按钮外观 */ |
|
||||
-webkit-appearance: button; |
|
||||
/* 设置标准按钮外观,提高兼容性 */ |
|
||||
appearance: button; |
|
||||
} |
|
||||
/* 这两个表单样式规则覆盖 - 复选框和单选框样式 */ |
|
||||
[type="checkbox"], |
|
||||
[type="radio"] { |
|
||||
/* 设置盒模型为边框盒模型 */ |
|
||||
box-sizing: border-box; |
|
||||
/* 移除内边距 */ |
|
||||
padding: 0; |
|
||||
} |
|
||||
/* 数字按钮内部高度自动 - 数字输入框按钮样式 */ |
|
||||
[type="number"]::-webkit-inner-spin-button, |
|
||||
[type="number"]::-webkit-outer-spin-button { |
|
||||
/* 设置高度为自动,适应内容 */ |
|
||||
height: auto; |
|
||||
} |
|
||||
/* 搜索按钮内图标外观去除 - 搜索输入框样式 */ |
|
||||
[type="search"]::-webkit-search-cancel-button, |
|
||||
[type="search"]::-webkit-search-decoration { |
|
||||
/* 移除WebKit浏览器搜索框默认样式 */ |
|
||||
-webkit-appearance: none; |
|
||||
} |
|
||||
/* 输入框的占位符样式 - WebKit浏览器占位符样式 */ |
|
||||
::-webkit-input-placeholder { |
|
||||
/* 继承父元素颜色 */ |
|
||||
color: inherit; |
|
||||
/* 设置透明度为0.54,创建半透明效果 */ |
|
||||
opacity: 0.54; |
|
||||
} |
|
||||
/* 文件选择按钮样式统一 - 文件上传按钮样式 */ |
|
||||
::-webkit-file-upload-button { |
|
||||
/* 设置WebKit浏览器按钮外观 */ |
|
||||
-webkit-appearance: button; |
|
||||
/* 继承父元素字体样式 */ |
|
||||
font: inherit; |
|
||||
} |
|
||||
/* 占位符显示统一 - 通用占位符样式 */ |
|
||||
::placeholder { |
|
||||
/* 设置占位符颜色,使用CSS变量 */ |
|
||||
color: var(--color-fg-subtle); |
|
||||
/* 设置完全不透明 */ |
|
||||
opacity: 1; |
|
||||
} |
|
||||
/* table内的td,th去除留白 - 表格单元格样式 */ |
|
||||
td, |
|
||||
th { |
|
||||
/* 移除表格单元格内边距 */ |
|
||||
padding: 0; |
|
||||
} |
|
||||
/* 伸缩框鼠标显示 - 详情框摘要样式 */ |
|
||||
details summary { |
|
||||
/* 设置鼠标悬停时显示手型光标 */ |
|
||||
cursor: pointer; |
|
||||
} |
|
||||
|
|
||||
/* 未展开的详情框隐藏内容 - 详情框内容显示控制 */ |
|
||||
details:not([open]) > *:not(summary) { |
|
||||
/* 强制隐藏未展开详情框内的非摘要内容 */ |
|
||||
display: none !important; |
|
||||
} |
|
||||
/* 按键显示 - 键盘按键样式 */ |
|
||||
kbd { |
|
||||
/* 设置为内联块级元素,可以设置宽高但不会独占一行 */ |
|
||||
display: inline-block; |
|
||||
/* 设置内边距为3px上下,5px左右 */ |
|
||||
padding: 3px 5px; |
|
||||
/* 设置字体为11px等宽字体,包含多种等宽字体备选 */ |
|
||||
font: 11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; |
|
||||
/* 设置行高为10px */ |
|
||||
line-height: 10px; |
|
||||
/* 设置文本颜色,使用CSS变量 */ |
|
||||
color: var(--color-fg-default); |
|
||||
/* 设置垂直对齐为中间 */ |
|
||||
vertical-align: middle; |
|
||||
/* 设置背景色,使用CSS变量 */ |
|
||||
background-color: var(--color-canvas-subtle); |
|
||||
/* 设置边框为1px实线,使用CSS变量 */ |
|
||||
border: solid 1px var(--color-neutral-muted); |
|
||||
/* 设置底部边框颜色,使用CSS变量 */ |
|
||||
border-bottom-color: var(--color-neutral-muted); |
|
||||
/* 设置圆角为6px */ |
|
||||
border-radius: 6px; |
|
||||
/* 设置内阴影,创建按键凹陷效果 */ |
|
||||
box-shadow: inset 0 -1px 0 var(--color-neutral-muted); |
|
||||
} |
|
||||
/* 清除浮动工具类 - 清除浮动伪元素 */ |
|
||||
.clearfix:before, |
|
||||
.clearfix:after { |
|
||||
/* 空内容,仅用于布局 */ |
|
||||
content: ""; |
|
||||
/* 设置为表格显示模式,用于清除浮动 */ |
|
||||
display: table; |
|
||||
} |
|
||||
/* 清除浮动工具类 - after伪元素 */ |
|
||||
.clearfix:after { |
|
||||
/* 清除左右浮动 */ |
|
||||
clear: both; |
|
||||
} |
|
||||
/* 清除浮动工具类 - 主容器 */ |
|
||||
.clearfix { |
|
||||
/* 触发IE的hasLayout属性,用于清除浮动 */ |
|
||||
zoom: 1; |
|
||||
} |
|
||||
/* 强制文本换行 - 文本换行工具类 */ |
|
||||
.textwrap, |
|
||||
.textwrap td, |
|
||||
.textwrap th { |
|
||||
/* 允许长单词在必要时换行,防止溢出 */ |
|
||||
word-wrap: break-word; |
|
||||
/* 强制在任意字符间换行,防止溢出 */ |
|
||||
word-break: break-all; |
|
||||
} |
|
||||
/* 文本换行表格 - 固定表格布局 */ |
|
||||
.textwrap-table { |
|
||||
/* 设置表格布局为固定,提高渲染性能 */ |
|
||||
table-layout: fixed; |
|
||||
} |
|
||||
/* 无序列表样式 - 项目符号列表 */ |
|
||||
ul { |
|
||||
/* 重置左边距为0 */ |
|
||||
margin-left: 0; |
|
||||
/* 重置左内边距为0 */ |
|
||||
padding-left: 0; |
|
||||
/* 设置左边距为2em,创建缩进效果 */ |
|
||||
margin-left: 2em; |
|
||||
/* 设置列表样式为实心圆点 */ |
|
||||
list-style: disc; |
|
||||
} |
|
||||
/* 有序列表样式 - 数字列表 */ |
|
||||
ol { |
|
||||
/* 重置左边距为0 */ |
|
||||
margin-left: 0; |
|
||||
/* 重置左内边距为0 */ |
|
||||
padding-left: 0; |
|
||||
/* 设置左边距为2em,创建缩进效果 */ |
|
||||
margin-left: 2em; |
|
||||
/* 设置列表样式为数字 */ |
|
||||
list-style: decimal; |
|
||||
/* 列表项样式 */ |
|
||||
li { |
|
||||
/* 设置左内边距为0.4em,增加数字与文本间距 */ |
|
||||
padding-left: 0.4em; |
|
||||
} |
|
||||
} |
|
||||
/* 相邻列表项间距 - 列表项之间的间距 */ |
|
||||
li + li { |
|
||||
/* 设置顶部外边距为0.25em,增加列表项间距 */ |
|
||||
margin-top: 0.25em; |
|
||||
} |
|
||||
/* 嵌套列表样式 - 列表项内的子列表 */ |
|
||||
li { |
|
||||
/* 无序子列表样式 */ |
|
||||
ul { |
|
||||
/* 设置底部外边距为0.8em */ |
|
||||
margin-bottom: 0.8em; |
|
||||
/* 设置左边距为2em,创建嵌套缩进 */ |
|
||||
margin-left: 2em; |
|
||||
/* 设置列表样式为空心圆点 */ |
|
||||
list-style: circle; |
|
||||
/* 三级无序列表样式 */ |
|
||||
li { |
|
||||
ul { |
|
||||
/* 设置列表样式为实心方块 */ |
|
||||
list-style: square; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
/* 有序子列表样式 */ |
|
||||
ol { |
|
||||
/* 设置底部外边距为0.8em */ |
|
||||
margin-bottom: 0.8em; |
|
||||
/* 设置左边距为2em,创建嵌套缩进 */ |
|
||||
margin-left: 2em; |
|
||||
} |
|
||||
} |
|
||||
/* 任务列表项样式 - 待办事项列表项 */ |
|
||||
.task-list-item { |
|
||||
/* 移除列表样式,不显示项目符号 */ |
|
||||
list-style-type: none; |
|
||||
/* 设置相对定位,用于绝对定位子元素 */ |
|
||||
position: relative; |
|
||||
/* 第一个子输入框样式 */ |
|
||||
> input { |
|
||||
/* 第一个子元素右边距 */ |
|
||||
&:nth-child(1) { |
|
||||
/* 设置右边距为6px */ |
|
||||
margin-right: 6px; |
|
||||
} |
|
||||
} |
|
||||
/* 标签样式 */ |
|
||||
label { |
|
||||
/* 设置字体粗细为正常(400) */ |
|
||||
font-weight: 400; |
|
||||
} |
|
||||
/* 拖拽手柄样式 */ |
|
||||
.handle { |
|
||||
/* 隐藏拖拽手柄 */ |
|
||||
display: none; |
|
||||
} |
|
||||
/* 复选框样式 */ |
|
||||
input[type="checkbox"] { |
|
||||
/* 设置宽度为0.9em */ |
|
||||
width: 0.9em; |
|
||||
/* 设置高度为0.9em */ |
|
||||
height: 0.9em; |
|
||||
/* 设置绝对定位 */ |
|
||||
position: absolute; |
|
||||
/* 向左偏移1.3em */ |
|
||||
left: -1.3em; |
|
||||
/* 向下偏移0.35em */ |
|
||||
top: 0.35em; |
|
||||
} |
|
||||
} |
|
||||
/* 启用的任务列表项样式 */ |
|
||||
.task-list-item.enabled { |
|
||||
/* 标签样式 */ |
|
||||
label { |
|
||||
/* 设置鼠标悬停时显示手型光标 */ |
|
||||
cursor: pointer; |
|
||||
} |
|
||||
} |
|
||||
/* 相邻任务列表项间距 */ |
|
||||
.task-list-item + .task-list-item { |
|
||||
/* 设置顶部外边距为3px */ |
|
||||
margin-top: 3px; |
|
||||
} |
|
||||
/* 包含任务列表的容器样式 */ |
|
||||
.contains-task-list { |
|
||||
// margin-left: 0.6em; // 备用左边距 |
|
||||
/* 从右到左文本方向样式 */ |
|
||||
&:dir(rtl) { |
|
||||
.task-list-item { |
|
||||
input[type="checkbox"] { |
|
||||
/* 设置复选框外边距,适配RTL布局 */ |
|
||||
margin: 0 -1.6em 0.25em 0.2em; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/* 目录样式 - 表格目录容器 */ |
|
||||
.toc { |
|
||||
/* 重置左边距为0 */ |
|
||||
margin-left: 0; |
|
||||
} |
|
||||
|
|
||||
/* 定义列表样式 - 描述列表容器 */ |
|
||||
dl { |
|
||||
/* 设置为块级元素 */ |
|
||||
display: block; |
|
||||
/* 设置块级起始外边距为1em */ |
|
||||
margin-block-start: 1em; |
|
||||
/* 设置块级结束外边距为1em */ |
|
||||
margin-block-end: 1em; |
|
||||
/* 设置内联起始外边距为0px */ |
|
||||
margin-inline-start: 0px; |
|
||||
/* 设置内联结束外边距为0px */ |
|
||||
margin-inline-end: 0px; |
|
||||
/* 设置Unicode双向算法为隔离 */ |
|
||||
unicode-bidi: isolate; |
|
||||
/* 定义术语样式 */ |
|
||||
dt { |
|
||||
/* 设置为块级元素 */ |
|
||||
display: block; |
|
||||
/* 设置Unicode双向算法为隔离 */ |
|
||||
unicode-bidi: isolate; |
|
||||
} |
|
||||
/* 定义描述样式 */ |
|
||||
dd { |
|
||||
/* 设置为块级元素 */ |
|
||||
display: block; |
|
||||
/* 设置内联起始外边距为40px,创建缩进效果 */ |
|
||||
margin-inline-start: 40px; |
|
||||
/* 设置Unicode双向算法为隔离 */ |
|
||||
unicode-bidi: isolate; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,53 +0,0 @@ |
|||||
.home-hero { |
|
||||
margin: 40px 20px 40px; |
|
||||
/* background: rgba(255, 255, 255, 0.1); |
|
||||
backdrop-filter: blur(12px); */ |
|
||||
text-align: center; |
|
||||
} |
|
||||
.avatar-container { |
|
||||
width: 120px; |
|
||||
height: 120px; |
|
||||
margin: 0 auto; |
|
||||
position: relative; |
|
||||
} |
|
||||
.avatar-container .author { |
|
||||
position: absolute; |
|
||||
top: 50%; |
|
||||
left: 50%; |
|
||||
transform: translate(-50%, -50%); |
|
||||
color: white; |
|
||||
font-size: 20px; |
|
||||
font-weight: bold; |
|
||||
} |
|
||||
.avatar-container:hover .avatar { |
|
||||
transform: rotate(360deg); |
|
||||
left: 100%; |
|
||||
} |
|
||||
.avatar { |
|
||||
position: relative; |
|
||||
width: 100%; |
|
||||
height: 100%; |
|
||||
border-radius: 50%; |
|
||||
cursor: pointer; |
|
||||
left: 0; |
|
||||
transform-origin: center center; |
|
||||
transition: 0.5s transform ease-in-out, 0.5s left ease-in-out; |
|
||||
} |
|
||||
/* |
|
||||
.card { |
|
||||
background: rgba(255, 255, 255, 0.1); |
|
||||
backdrop-filter: blur(12px); |
|
||||
border-radius: 8px; |
|
||||
padding: 20px; |
|
||||
width: 300px; |
|
||||
margin: 0 auto; |
|
||||
margin-bottom: 40px; |
|
||||
text-align: center; |
|
||||
} */ |
|
||||
|
|
||||
@media screen and (max-width: 768px) { |
|
||||
.home-hero { |
|
||||
margin: 0; |
|
||||
margin-top: 20px; |
|
||||
} |
|
||||
} |
|
||||
@ -1,146 +0,0 @@ |
|||||
/* 首页样式 */ |
|
||||
|
|
||||
.hero-section { |
|
||||
position: relative; |
|
||||
overflow: hidden; |
|
||||
} |
|
||||
|
|
||||
.hero-section::before { |
|
||||
content: ""; |
|
||||
position: absolute; |
|
||||
top: 0; |
|
||||
left: 0; |
|
||||
right: 0; |
|
||||
bottom: 0; |
|
||||
background: url('/images/hero-bg.svg') no-repeat center center; |
|
||||
background-size: cover; |
|
||||
opacity: 0.1; |
|
||||
z-index: 0; |
|
||||
} |
|
||||
|
|
||||
.hero-content { |
|
||||
position: relative; |
|
||||
z-index: 1; |
|
||||
} |
|
||||
|
|
||||
.feature-card { |
|
||||
transition: all 0.3s ease; |
|
||||
} |
|
||||
|
|
||||
.feature-card:hover { |
|
||||
transform: translateY(-5px); |
|
||||
} |
|
||||
|
|
||||
.feature-card .material-symbols-light--article, |
|
||||
.feature-card .material-symbols-light--bookmark, |
|
||||
.feature-card .material-symbols-light--person { |
|
||||
transition: all 0.3s ease; |
|
||||
} |
|
||||
|
|
||||
.feature-card:hover .material-symbols-light--article, |
|
||||
.feature-card:hover .material-symbols-light--bookmark, |
|
||||
.feature-card:hover .material-symbols-light--person { |
|
||||
transform: scale(1.1); |
|
||||
} |
|
||||
|
|
||||
.stats-section { |
|
||||
position: relative; |
|
||||
} |
|
||||
|
|
||||
.stats-section::before { |
|
||||
content: ""; |
|
||||
position: absolute; |
|
||||
top: 0; |
|
||||
left: 0; |
|
||||
right: 0; |
|
||||
bottom: 0; |
|
||||
background: url('/images/stats-bg.svg') no-repeat center center; |
|
||||
background-size: cover; |
|
||||
opacity: 0.05; |
|
||||
z-index: 0; |
|
||||
} |
|
||||
|
|
||||
.stat-item { |
|
||||
transition: all 0.3s ease; |
|
||||
} |
|
||||
|
|
||||
.stat-item:hover { |
|
||||
transform: scale(1.05); |
|
||||
} |
|
||||
|
|
||||
.user-dashboard { |
|
||||
position: relative; |
|
||||
} |
|
||||
|
|
||||
.user-dashboard::before { |
|
||||
content: ""; |
|
||||
position: absolute; |
|
||||
top: 0; |
|
||||
left: 0; |
|
||||
right: 0; |
|
||||
bottom: 0; |
|
||||
background: url('/images/dashboard-bg.svg') no-repeat center center; |
|
||||
background-size: cover; |
|
||||
opacity: 0.03; |
|
||||
z-index: 0; |
|
||||
} |
|
||||
|
|
||||
.avatar { |
|
||||
transition: all 0.3s ease; |
|
||||
} |
|
||||
|
|
||||
.avatar:hover { |
|
||||
transform: scale(1.05); |
|
||||
} |
|
||||
|
|
||||
/* 响应式设计 */ |
|
||||
@media (max-width: 768px) { |
|
||||
.hero-section { |
|
||||
padding: 4rem 0; |
|
||||
} |
|
||||
|
|
||||
.hero-content h1 { |
|
||||
font-size: 2.5rem; |
|
||||
} |
|
||||
|
|
||||
.features-grid { |
|
||||
grid-template-columns: 1fr; |
|
||||
} |
|
||||
|
|
||||
.stats-grid { |
|
||||
grid-template-columns: 1fr 1fr; |
|
||||
} |
|
||||
|
|
||||
.user-info { |
|
||||
text-align: center; |
|
||||
margin-bottom: 1.5rem; |
|
||||
} |
|
||||
|
|
||||
.user-actions { |
|
||||
justify-content: center; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@media (max-width: 480px) { |
|
||||
.hero-content h1 { |
|
||||
font-size: 2rem; |
|
||||
} |
|
||||
|
|
||||
.hero-content p { |
|
||||
font-size: 1rem; |
|
||||
} |
|
||||
|
|
||||
.stats-grid { |
|
||||
grid-template-columns: 1fr; |
|
||||
} |
|
||||
|
|
||||
.hero-actions { |
|
||||
flex-direction: column; |
|
||||
gap: 1rem; |
|
||||
} |
|
||||
|
|
||||
.hero-actions a { |
|
||||
width: 100%; |
|
||||
text-align: center; |
|
||||
} |
|
||||
} |
|
||||
@ -1,973 +0,0 @@ |
|||||
/** |
|
||||
* Admin 后台管理系统 JavaScript |
|
||||
* 提供通用的交互功能和工具函数 |
|
||||
*/ |
|
||||
|
|
||||
(function() { |
|
||||
'use strict'; |
|
||||
|
|
||||
// 通用工具函数
|
|
||||
const AdminUtils = { |
|
||||
/** |
|
||||
* 显示Toast消息 |
|
||||
* @param {string} type - 消息类型 (success, error, warning, info) |
|
||||
* @param {string} message - 消息内容 |
|
||||
* @param {number} duration - 显示时长(毫秒) |
|
||||
*/ |
|
||||
showToast: function(type, message, duration = 3000) { |
|
||||
// 移除现有的toast
|
|
||||
const existingToast = document.querySelector('.admin-toast'); |
|
||||
if (existingToast) { |
|
||||
existingToast.remove(); |
|
||||
} |
|
||||
|
|
||||
const toast = document.createElement('div'); |
|
||||
toast.className = `admin-toast toast-${type}`; |
|
||||
toast.innerHTML = ` |
|
||||
<span>${message}</span> |
|
||||
<button class="toast-close" aria-label="关闭">×</button> |
|
||||
`;
|
|
||||
|
|
||||
document.body.appendChild(toast); |
|
||||
|
|
||||
// 自动消失
|
|
||||
setTimeout(() => { |
|
||||
this.hideToast(toast); |
|
||||
}, duration); |
|
||||
|
|
||||
// 点击关闭
|
|
||||
const closeBtn = toast.querySelector('.toast-close'); |
|
||||
if (closeBtn) { |
|
||||
closeBtn.addEventListener('click', () => { |
|
||||
this.hideToast(toast); |
|
||||
}); |
|
||||
} |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 隐藏Toast消息 |
|
||||
* @param {HTMLElement} toast - Toast元素 |
|
||||
*/ |
|
||||
hideToast: function(toast) { |
|
||||
if (toast && toast.parentNode) { |
|
||||
toast.style.opacity = '0'; |
|
||||
setTimeout(() => { |
|
||||
if (toast.parentNode) { |
|
||||
toast.parentNode.removeChild(toast); |
|
||||
} |
|
||||
}, 300); |
|
||||
} |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 确认对话框 |
|
||||
* @param {string} message - 确认消息 |
|
||||
* @param {string} title - 对话框标题 |
|
||||
* @returns {boolean} 用户确认结果 |
|
||||
*/ |
|
||||
confirm: function(message, title = '确认') { |
|
||||
return confirm(`${title}\n\n${message}`); |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 发送AJAX请求 |
|
||||
* @param {string} url - 请求URL |
|
||||
* @param {object} options - 请求选项 |
|
||||
* @returns {Promise} 请求Promise |
|
||||
*/ |
|
||||
ajax: function(url, options = {}) { |
|
||||
const defaultOptions = { |
|
||||
method: 'GET', |
|
||||
headers: { |
|
||||
'Content-Type': 'application/json', |
|
||||
}, |
|
||||
credentials: 'same-origin' |
|
||||
}; |
|
||||
|
|
||||
const mergedOptions = Object.assign(defaultOptions, options); |
|
||||
|
|
||||
if (mergedOptions.body && typeof mergedOptions.body === 'object') { |
|
||||
mergedOptions.body = JSON.stringify(mergedOptions.body); |
|
||||
} |
|
||||
|
|
||||
return fetch(url, mergedOptions) |
|
||||
.then(response => { |
|
||||
if (!response.ok) { |
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`); |
|
||||
} |
|
||||
return response.json(); |
|
||||
}) |
|
||||
.catch(error => { |
|
||||
console.error('AJAX请求失败:', error); |
|
||||
throw error; |
|
||||
}); |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 防抖函数 |
|
||||
* @param {Function} func - 要防抖的函数 |
|
||||
* @param {number} delay - 延迟时间(毫秒) |
|
||||
* @returns {Function} 防抖后的函数 |
|
||||
*/ |
|
||||
debounce: function(func, delay) { |
|
||||
let timeoutId; |
|
||||
return function(...args) { |
|
||||
clearTimeout(timeoutId); |
|
||||
timeoutId = setTimeout(() => func.apply(this, args), delay); |
|
||||
}; |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 格式化日期 |
|
||||
* @param {Date|string} date - 日期对象或字符串 |
|
||||
* @param {string} format - 格式类型 (date, time, datetime) |
|
||||
* @returns {string} 格式化后的日期字符串 |
|
||||
*/ |
|
||||
formatDate: function(date, format = 'datetime') { |
|
||||
const d = new Date(date); |
|
||||
if (isNaN(d.getTime())) { |
|
||||
return '无效日期'; |
|
||||
} |
|
||||
|
|
||||
const options = { |
|
||||
date: { year: 'numeric', month: '2-digit', day: '2-digit' }, |
|
||||
time: { hour: '2-digit', minute: '2-digit' }, |
|
||||
datetime: { |
|
||||
year: 'numeric', |
|
||||
month: '2-digit', |
|
||||
day: '2-digit', |
|
||||
hour: '2-digit', |
|
||||
minute: '2-digit' |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
return d.toLocaleString('zh-CN', options[format] || options.datetime); |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 复制文本到剪贴板 |
|
||||
* @param {string} text - 要复制的文本 |
|
||||
* @returns {Promise<boolean>} 复制是否成功 |
|
||||
*/ |
|
||||
copyToClipboard: function(text) { |
|
||||
if (navigator.clipboard) { |
|
||||
return navigator.clipboard.writeText(text) |
|
||||
.then(() => { |
|
||||
this.showToast('success', '已复制到剪贴板'); |
|
||||
return true; |
|
||||
}) |
|
||||
.catch(err => { |
|
||||
console.error('复制失败:', err); |
|
||||
return this.fallbackCopyTextToClipboard(text); |
|
||||
}); |
|
||||
} else { |
|
||||
return Promise.resolve(this.fallbackCopyTextToClipboard(text)); |
|
||||
} |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 降级复制文本到剪贴板 |
|
||||
* @param {string} text - 要复制的文本 |
|
||||
* @returns {boolean} 复制是否成功 |
|
||||
*/ |
|
||||
fallbackCopyTextToClipboard: function(text) { |
|
||||
const textArea = document.createElement('textarea'); |
|
||||
textArea.value = text; |
|
||||
textArea.style.position = 'fixed'; |
|
||||
textArea.style.left = '-999999px'; |
|
||||
textArea.style.top = '-999999px'; |
|
||||
document.body.appendChild(textArea); |
|
||||
textArea.focus(); |
|
||||
textArea.select(); |
|
||||
|
|
||||
try { |
|
||||
const successful = document.execCommand('copy'); |
|
||||
if (successful) { |
|
||||
this.showToast('success', '已复制到剪贴板'); |
|
||||
} else { |
|
||||
this.showToast('error', '复制失败,请手动复制'); |
|
||||
} |
|
||||
return successful; |
|
||||
} catch (err) { |
|
||||
console.error('降级复制失败:', err); |
|
||||
this.showToast('error', '复制失败,请手动复制'); |
|
||||
return false; |
|
||||
} finally { |
|
||||
document.body.removeChild(textArea); |
|
||||
} |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
// 初始化函数
|
|
||||
const AdminApp = { |
|
||||
/** |
|
||||
* 初始化应用 |
|
||||
*/ |
|
||||
init: function() { |
|
||||
this.initDropdowns(); |
|
||||
this.initMobileNav(); |
|
||||
this.initToasts(); |
|
||||
this.initFormValidation(); |
|
||||
this.initTableActions(); |
|
||||
this.initSearch(); |
|
||||
this.initArticleEditor(); |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 初始化下拉菜单 |
|
||||
*/ |
|
||||
initDropdowns: function() { |
|
||||
document.addEventListener('click', (e) => { |
|
||||
// 关闭所有下拉菜单
|
|
||||
const dropdowns = document.querySelectorAll('.dropdown'); |
|
||||
dropdowns.forEach(dropdown => { |
|
||||
if (!dropdown.contains(e.target)) { |
|
||||
dropdown.classList.remove('active'); |
|
||||
} |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
// 下拉菜单触发器
|
|
||||
const dropdownTriggers = document.querySelectorAll('.dropdown-trigger'); |
|
||||
dropdownTriggers.forEach(trigger => { |
|
||||
trigger.addEventListener('click', (e) => { |
|
||||
e.stopPropagation(); |
|
||||
const dropdown = trigger.closest('.dropdown'); |
|
||||
dropdown.classList.toggle('active'); |
|
||||
}); |
|
||||
}); |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 初始化移动端导航 |
|
||||
*/ |
|
||||
initMobileNav: function() { |
|
||||
// 创建移动端菜单按钮
|
|
||||
if (window.innerWidth <= 768) { |
|
||||
this.createMobileMenuButton(); |
|
||||
} |
|
||||
|
|
||||
window.addEventListener('resize', () => { |
|
||||
if (window.innerWidth <= 768) { |
|
||||
this.createMobileMenuButton(); |
|
||||
} else { |
|
||||
this.removeMobileMenuButton(); |
|
||||
document.body.classList.remove('sidebar-open'); |
|
||||
} |
|
||||
}); |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 创建移动端菜单按钮 |
|
||||
*/ |
|
||||
createMobileMenuButton: function() { |
|
||||
if (document.querySelector('.mobile-menu-btn')) return; |
|
||||
|
|
||||
const button = document.createElement('button'); |
|
||||
button.className = 'mobile-menu-btn'; |
|
||||
button.innerHTML = '☰'; |
|
||||
button.style.cssText = ` |
|
||||
background: none; |
|
||||
border: none; |
|
||||
font-size: 1.25rem; |
|
||||
cursor: pointer; |
|
||||
padding: 0.5rem; |
|
||||
color: #4a5568; |
|
||||
`;
|
|
||||
|
|
||||
button.addEventListener('click', () => { |
|
||||
document.body.classList.toggle('sidebar-open'); |
|
||||
}); |
|
||||
|
|
||||
const headerLeft = document.querySelector('.admin-header-left'); |
|
||||
if (headerLeft) { |
|
||||
headerLeft.insertBefore(button, headerLeft.firstChild); |
|
||||
} |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 移除移动端菜单按钮 |
|
||||
*/ |
|
||||
removeMobileMenuButton: function() { |
|
||||
const button = document.querySelector('.mobile-menu-btn'); |
|
||||
if (button) { |
|
||||
button.remove(); |
|
||||
} |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 初始化现有Toast消息 |
|
||||
*/ |
|
||||
initToasts: function() { |
|
||||
const existingToasts = document.querySelectorAll('.admin-toast'); |
|
||||
existingToasts.forEach(toast => { |
|
||||
setTimeout(() => { |
|
||||
AdminUtils.hideToast(toast); |
|
||||
}, 3000); |
|
||||
|
|
||||
const closeBtn = toast.querySelector('.toast-close'); |
|
||||
if (closeBtn) { |
|
||||
closeBtn.addEventListener('click', () => { |
|
||||
AdminUtils.hideToast(toast); |
|
||||
}); |
|
||||
} |
|
||||
}); |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 初始化表单验证 |
|
||||
*/ |
|
||||
initFormValidation: function() { |
|
||||
const forms = document.querySelectorAll('form'); |
|
||||
forms.forEach(form => { |
|
||||
form.addEventListener('submit', (e) => { |
|
||||
const requiredFields = form.querySelectorAll('[required]'); |
|
||||
let isValid = true; |
|
||||
|
|
||||
requiredFields.forEach(field => { |
|
||||
if (!field.value.trim()) { |
|
||||
isValid = false; |
|
||||
field.style.borderColor = '#f56565'; |
|
||||
|
|
||||
// 移除已有的错误提示
|
|
||||
const existingError = field.parentNode.querySelector('.field-error'); |
|
||||
if (existingError) { |
|
||||
existingError.remove(); |
|
||||
} |
|
||||
|
|
||||
// 添加错误提示
|
|
||||
const error = document.createElement('div'); |
|
||||
error.className = 'field-error'; |
|
||||
error.style.cssText = 'color: #f56565; font-size: 0.75rem; margin-top: 0.25rem;'; |
|
||||
error.textContent = '此字段为必填项'; |
|
||||
field.parentNode.appendChild(error); |
|
||||
} else { |
|
||||
field.style.borderColor = ''; |
|
||||
const existingError = field.parentNode.querySelector('.field-error'); |
|
||||
if (existingError) { |
|
||||
existingError.remove(); |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
if (!isValid) { |
|
||||
e.preventDefault(); |
|
||||
AdminUtils.showToast('error', '请填写所有必填字段'); |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
// 实时验证
|
|
||||
const requiredFields = form.querySelectorAll('[required]'); |
|
||||
requiredFields.forEach(field => { |
|
||||
field.addEventListener('blur', () => { |
|
||||
if (!field.value.trim()) { |
|
||||
field.style.borderColor = '#f56565'; |
|
||||
} else { |
|
||||
field.style.borderColor = ''; |
|
||||
const existingError = field.parentNode.querySelector('.field-error'); |
|
||||
if (existingError) { |
|
||||
existingError.remove(); |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
}); |
|
||||
}); |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 初始化表格操作 |
|
||||
*/ |
|
||||
initTableActions: function() { |
|
||||
// 表格行点击
|
|
||||
const tableRows = document.querySelectorAll('tbody tr'); |
|
||||
tableRows.forEach(row => { |
|
||||
row.addEventListener('click', (e) => { |
|
||||
// 如果点击的是按钮或链接,不执行行点击事件
|
|
||||
if (e.target.closest('button, a, select, input')) { |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
// 高亮当前行
|
|
||||
tableRows.forEach(r => r.classList.remove('selected')); |
|
||||
row.classList.add('selected'); |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
// 状态选择器
|
|
||||
const statusSelects = document.querySelectorAll('.status-select'); |
|
||||
statusSelects.forEach(select => { |
|
||||
select.addEventListener('change', (e) => { |
|
||||
e.stopPropagation(); |
|
||||
}); |
|
||||
}); |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 初始化搜索功能 |
|
||||
*/ |
|
||||
initSearch: function() { |
|
||||
const searchInputs = document.querySelectorAll('.search-input'); |
|
||||
searchInputs.forEach(input => { |
|
||||
// 搜索防抖
|
|
||||
const debouncedSearch = AdminUtils.debounce((value) => { |
|
||||
if (value.length >= 2 || value.length === 0) { |
|
||||
// 可以在这里添加实时搜索功能
|
|
||||
console.log('搜索:', value); |
|
||||
} |
|
||||
}, 300); |
|
||||
|
|
||||
input.addEventListener('input', (e) => { |
|
||||
debouncedSearch(e.target.value); |
|
||||
}); |
|
||||
|
|
||||
// 回车搜索
|
|
||||
input.addEventListener('keypress', (e) => { |
|
||||
if (e.key === 'Enter') { |
|
||||
e.preventDefault(); |
|
||||
e.target.closest('form').submit(); |
|
||||
} |
|
||||
}); |
|
||||
}); |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 初始化文章编辑器增强功能 |
|
||||
*/ |
|
||||
initArticleEditor: function() { |
|
||||
const titleInput = document.getElementById('title'); |
|
||||
const slugInput = document.getElementById('slug'); |
|
||||
const contentTextarea = document.getElementById('content'); |
|
||||
|
|
||||
if (titleInput && slugInput) { |
|
||||
// 自动生成slug
|
|
||||
titleInput.addEventListener('input', AdminUtils.debounce((e) => { |
|
||||
if (!slugInput.value.trim() || slugInput.dataset.autoGenerated === 'true') { |
|
||||
const slug = this.generateSlug(e.target.value); |
|
||||
slugInput.value = slug; |
|
||||
slugInput.dataset.autoGenerated = 'true'; |
|
||||
} |
|
||||
}, 300)); |
|
||||
|
|
||||
// 手动编辑slug时停止自动生成
|
|
||||
slugInput.addEventListener('input', () => { |
|
||||
slugInput.dataset.autoGenerated = 'false'; |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
if (contentTextarea) { |
|
||||
// 添加工具栏
|
|
||||
this.addEditorToolbar(contentTextarea); |
|
||||
|
|
||||
// Tab键支持
|
|
||||
contentTextarea.addEventListener('keydown', (e) => { |
|
||||
if (e.key === 'Tab') { |
|
||||
e.preventDefault(); |
|
||||
const start = contentTextarea.selectionStart; |
|
||||
const end = contentTextarea.selectionEnd; |
|
||||
const value = contentTextarea.value; |
|
||||
|
|
||||
contentTextarea.value = value.substring(0, start) + ' ' + value.substring(end); |
|
||||
contentTextarea.selectionStart = contentTextarea.selectionEnd = start + 4; |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
// 初始化字符计数
|
|
||||
this.initCharCounters(); |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 生成URL别名 |
|
||||
*/ |
|
||||
generateSlug: function(title) { |
|
||||
return title |
|
||||
.toLowerCase() |
|
||||
.replace(/[^\w\u4e00-\u9fa5]+/g, '-') |
|
||||
.replace(/^-+|-+$/g, '') |
|
||||
.substring(0, 100); |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 添加编辑器工具栏 |
|
||||
*/ |
|
||||
addEditorToolbar: function(textarea) { |
|
||||
const toolbar = document.createElement('div'); |
|
||||
toolbar.className = 'editor-toolbar'; |
|
||||
toolbar.innerHTML = ` |
|
||||
<div class="editor-toolbar-group"> |
|
||||
<button type="button" class="editor-btn" data-action="bold" title="粗体 (Ctrl+B)">💪</button> |
|
||||
<button type="button" class="editor-btn" data-action="italic" title="斜体 (Ctrl+I)">🌯</button> |
|
||||
<button type="button" class="editor-btn" data-action="code" title="代码">💻</button> |
|
||||
</div> |
|
||||
<div class="editor-toolbar-group"> |
|
||||
<button type="button" class="editor-btn" data-action="h1" title="标题 1">🏆</button> |
|
||||
<button type="button" class="editor-btn" data-action="h2" title="标题 2">🥈</button> |
|
||||
<button type="button" class="editor-btn" data-action="h3" title="标题 3">🥉</button> |
|
||||
</div> |
|
||||
<div class="editor-toolbar-group"> |
|
||||
<button type="button" class="editor-btn" data-action="ul" title="无序列表">📝</button> |
|
||||
<button type="button" class="editor-btn" data-action="ol" title="有序列表">🔢</button> |
|
||||
<button type="button" class="editor-btn" data-action="quote" title="引用">💬</button> |
|
||||
</div> |
|
||||
<div class="editor-toolbar-group"> |
|
||||
<button type="button" class="editor-btn" data-action="link" title="链接">🔗</button> |
|
||||
<button type="button" class="editor-btn" data-action="image" title="图片">🖼️</button> |
|
||||
</div> |
|
||||
`;
|
|
||||
|
|
||||
// 添加样式
|
|
||||
const style = document.createElement('style'); |
|
||||
style.textContent = ` |
|
||||
.editor-toolbar { |
|
||||
display: flex; |
|
||||
gap: 0.5rem; |
|
||||
padding: 0.75rem; |
|
||||
background: #f8fafc; |
|
||||
border: 1px solid #e2e8f0; |
|
||||
border-bottom: none; |
|
||||
border-radius: 6px 6px 0 0; |
|
||||
flex-wrap: wrap; |
|
||||
} |
|
||||
.editor-toolbar-group { |
|
||||
display: flex; |
|
||||
gap: 0.25rem; |
|
||||
padding: 0 0.5rem; |
|
||||
border-right: 1px solid #e2e8f0; |
|
||||
} |
|
||||
.editor-toolbar-group:last-child { |
|
||||
border-right: none; |
|
||||
} |
|
||||
.editor-btn { |
|
||||
background: none; |
|
||||
border: 1px solid transparent; |
|
||||
border-radius: 4px; |
|
||||
padding: 0.25rem 0.5rem; |
|
||||
cursor: pointer; |
|
||||
font-size: 0.875rem; |
|
||||
transition: all 0.2s; |
|
||||
} |
|
||||
.editor-btn:hover { |
|
||||
background: #e2e8f0; |
|
||||
border-color: #cbd5e0; |
|
||||
} |
|
||||
.editor-toolbar + textarea { |
|
||||
border-radius: 0 0 6px 6px; |
|
||||
} |
|
||||
`;
|
|
||||
|
|
||||
if (!document.querySelector('#editor-toolbar-style')) { |
|
||||
style.id = 'editor-toolbar-style'; |
|
||||
document.head.appendChild(style); |
|
||||
} |
|
||||
|
|
||||
// 插入工具栏
|
|
||||
textarea.parentNode.insertBefore(toolbar, textarea); |
|
||||
|
|
||||
// 绑定事件
|
|
||||
toolbar.addEventListener('click', (e) => { |
|
||||
if (e.target.classList.contains('editor-btn')) { |
|
||||
e.preventDefault(); |
|
||||
const action = e.target.dataset.action; |
|
||||
this.executeEditorAction(textarea, action); |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
// 键盘快捷键
|
|
||||
textarea.addEventListener('keydown', (e) => { |
|
||||
if (e.ctrlKey || e.metaKey) { |
|
||||
switch (e.key.toLowerCase()) { |
|
||||
case 'b': |
|
||||
e.preventDefault(); |
|
||||
this.executeEditorAction(textarea, 'bold'); |
|
||||
break; |
|
||||
case 'i': |
|
||||
e.preventDefault(); |
|
||||
this.executeEditorAction(textarea, 'italic'); |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 执行编辑器动作 |
|
||||
*/ |
|
||||
executeEditorAction: function(textarea, action) { |
|
||||
const start = textarea.selectionStart; |
|
||||
const end = textarea.selectionEnd; |
|
||||
const selectedText = textarea.value.substring(start, end); |
|
||||
const beforeText = textarea.value.substring(0, start); |
|
||||
const afterText = textarea.value.substring(end); |
|
||||
|
|
||||
let insertText = ''; |
|
||||
let cursorOffset = 0; |
|
||||
|
|
||||
switch (action) { |
|
||||
case 'bold': |
|
||||
insertText = `**${selectedText || '粗体文字'}**`; |
|
||||
cursorOffset = selectedText ? 0 : -2; |
|
||||
break; |
|
||||
case 'italic': |
|
||||
insertText = `*${selectedText || '斜体文字'}*`; |
|
||||
cursorOffset = selectedText ? 0 : -1; |
|
||||
break; |
|
||||
case 'code': |
|
||||
insertText = `\`${selectedText || '代码'}\``; |
|
||||
cursorOffset = selectedText ? 0 : -1; |
|
||||
break; |
|
||||
case 'h1': |
|
||||
insertText = `# ${selectedText || '标题 1'}`; |
|
||||
break; |
|
||||
case 'h2': |
|
||||
insertText = `## ${selectedText || '标题 2'}`; |
|
||||
break; |
|
||||
case 'h3': |
|
||||
insertText = `### ${selectedText || '标题 3'}`; |
|
||||
break; |
|
||||
case 'ul': |
|
||||
insertText = `- ${selectedText || '列表项'}`; |
|
||||
break; |
|
||||
case 'ol': |
|
||||
insertText = `1. ${selectedText || '列表项'}`; |
|
||||
break; |
|
||||
case 'quote': |
|
||||
insertText = `> ${selectedText || '引用文字'}`; |
|
||||
break; |
|
||||
case 'link': |
|
||||
const linkText = selectedText || '链接文字'; |
|
||||
insertText = `[${linkText}](URL)`; |
|
||||
cursorOffset = -4; |
|
||||
break; |
|
||||
case 'image': |
|
||||
const altText = selectedText || '图片描述'; |
|
||||
insertText = ``; |
|
||||
cursorOffset = -4; |
|
||||
break; |
|
||||
} |
|
||||
|
|
||||
textarea.value = beforeText + insertText + afterText; |
|
||||
const newCursorPos = start + insertText.length + cursorOffset; |
|
||||
textarea.setSelectionRange(newCursorPos, newCursorPos); |
|
||||
textarea.focus(); |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 初始化字符计数器 |
|
||||
*/ |
|
||||
initCharCounters: function() { |
|
||||
const fields = [ |
|
||||
{ id: 'title', max: 200 }, |
|
||||
{ id: 'excerpt', max: 500 }, |
|
||||
{ id: 'tags', max: 200 }, |
|
||||
{ id: 'slug', max: 100 } |
|
||||
]; |
|
||||
|
|
||||
fields.forEach(field => { |
|
||||
const element = document.getElementById(field.id); |
|
||||
if (element) { |
|
||||
this.setupCharCounter(element, field.max); |
|
||||
} |
|
||||
}); |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* 设置字符计数器 |
|
||||
*/ |
|
||||
setupCharCounter: function(element, maxLength) { |
|
||||
const helpElement = element.nextElementSibling; |
|
||||
if (!helpElement || !helpElement.classList.contains('form-help')) { |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
const originalText = helpElement.textContent; |
|
||||
|
|
||||
const updateCounter = () => { |
|
||||
const currentLength = element.value.length; |
|
||||
const remaining = maxLength - currentLength; |
|
||||
|
|
||||
if (remaining < 50) { |
|
||||
helpElement.textContent = `${originalText} (还可输入${remaining}字符)`; |
|
||||
helpElement.style.color = remaining < 10 ? '#f56565' : '#ed8936'; |
|
||||
} else { |
|
||||
helpElement.textContent = originalText; |
|
||||
helpElement.style.color = ''; |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
element.addEventListener('input', updateCounter); |
|
||||
updateCounter(); |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
// 全局删除函数
|
|
||||
window.deleteArticle = function(id, title) { |
|
||||
if (AdminUtils.confirm(`确定要删除文章《${title}》吗?此操作不可撤销。`, '删除确认')) { |
|
||||
AdminUtils.ajax(`/admin/articles/${id}`, { |
|
||||
method: 'DELETE' |
|
||||
}) |
|
||||
.then(data => { |
|
||||
if (data.success) { |
|
||||
AdminUtils.showToast('success', data.message || '文章删除成功'); |
|
||||
setTimeout(() => { |
|
||||
window.location.reload(); |
|
||||
}, 1000); |
|
||||
} else { |
|
||||
AdminUtils.showToast('error', data.message || '删除失败'); |
|
||||
} |
|
||||
}) |
|
||||
.catch(error => { |
|
||||
console.error('删除失败:', error); |
|
||||
AdminUtils.showToast('error', '删除失败,请稍后重试'); |
|
||||
}); |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
// 全局联系信息删除函数
|
|
||||
window.deleteContact = function(id, title) { |
|
||||
if (AdminUtils.confirm(`确定要删除联系信息《${title}》吗?此操作不可撤销。`, '删除确认')) { |
|
||||
AdminUtils.ajax(`/admin/contacts/${id}`, { |
|
||||
method: 'DELETE' |
|
||||
}) |
|
||||
.then(data => { |
|
||||
if (data.success) { |
|
||||
AdminUtils.showToast('success', data.message || '联系信息删除成功'); |
|
||||
setTimeout(() => { |
|
||||
window.location.reload(); |
|
||||
}, 1000); |
|
||||
} else { |
|
||||
AdminUtils.showToast('error', data.message || '删除失败'); |
|
||||
} |
|
||||
}) |
|
||||
.catch(error => { |
|
||||
console.error('删除失败:', error); |
|
||||
AdminUtils.showToast('error', '删除失败,请稍后重试'); |
|
||||
}); |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
// 全局状态更新函数
|
|
||||
window.updateContactStatus = function(id, status) { |
|
||||
if (!status) return; |
|
||||
|
|
||||
AdminUtils.ajax(`/admin/contacts/${id}/status`, { |
|
||||
method: 'PUT', |
|
||||
body: { status } |
|
||||
}) |
|
||||
.then(data => { |
|
||||
if (data.success) { |
|
||||
AdminUtils.showToast('success', data.message || '状态更新成功'); |
|
||||
setTimeout(() => { |
|
||||
window.location.reload(); |
|
||||
}, 1000); |
|
||||
} else { |
|
||||
AdminUtils.showToast('error', data.message || '状态更新失败'); |
|
||||
} |
|
||||
}) |
|
||||
.catch(error => { |
|
||||
console.error('状态更新失败:', error); |
|
||||
AdminUtils.showToast('error', '状态更新失败,请稍后重试'); |
|
||||
}); |
|
||||
}; |
|
||||
|
|
||||
// 全局复制邮箱函数
|
|
||||
window.copyEmail = function(email) { |
|
||||
AdminUtils.copyToClipboard(email || document.querySelector('[data-email]')?.dataset.email || ''); |
|
||||
}; |
|
||||
|
|
||||
// 全局预览文章函数
|
|
||||
window.previewArticle = function() { |
|
||||
const titleElement = document.getElementById('title'); |
|
||||
const contentElement = document.getElementById('content'); |
|
||||
|
|
||||
if (!titleElement || !contentElement) { |
|
||||
AdminUtils.showToast('error', '无法找到文章内容'); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
const title = titleElement.value.trim(); |
|
||||
const content = contentElement.value.trim(); |
|
||||
|
|
||||
if (!content) { |
|
||||
AdminUtils.showToast('warning', '请先输入文章内容'); |
|
||||
contentElement.focus(); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
// 简单的Markdown预览
|
|
||||
const previewWindow = window.open('', '_blank', 'width=800,height=600,scrollbars=yes'); |
|
||||
if (!previewWindow) { |
|
||||
AdminUtils.showToast('error', '无法打开预览窗口,请检查浏览器弹窗设置'); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
// 基础的Markdown转换
|
|
||||
let htmlContent = content |
|
||||
.replace(/\n/g, '<br>') |
|
||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') |
|
||||
.replace(/\*(.*?)\*/g, '<em>$1</em>') |
|
||||
.replace(/`(.*?)`/g, '<code>$1</code>') |
|
||||
.replace(/^# (.*$)/gim, '<h1>$1</h1>') |
|
||||
.replace(/^## (.*$)/gim, '<h2>$1</h2>') |
|
||||
.replace(/^### (.*$)/gim, '<h3>$1</h3>'); |
|
||||
|
|
||||
previewWindow.document.write(` |
|
||||
<!DOCTYPE html> |
|
||||
<html lang="zh-CN"> |
|
||||
<head> |
|
||||
<meta charset="UTF-8"> |
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
||||
<title>文章预览 - ${title || '未设置标题'}</title> |
|
||||
<style> |
|
||||
body { |
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif; |
|
||||
max-width: 800px; |
|
||||
margin: 0 auto; |
|
||||
padding: 2rem; |
|
||||
line-height: 1.7; |
|
||||
color: #2d3748; |
|
||||
background: #ffffff; |
|
||||
} |
|
||||
h1, h2, h3, h4, h5, h6 { |
|
||||
color: #1a202c; |
|
||||
margin-top: 2rem; |
|
||||
margin-bottom: 1rem; |
|
||||
font-weight: 600; |
|
||||
} |
|
||||
h1 { font-size: 2rem; border-bottom: 2px solid #e2e8f0; padding-bottom: 0.5rem; } |
|
||||
h2 { font-size: 1.5rem; } |
|
||||
h3 { font-size: 1.25rem; } |
|
||||
p { margin-bottom: 1rem; } |
|
||||
code { |
|
||||
background: #f7fafc; |
|
||||
padding: 0.25rem 0.5rem; |
|
||||
border-radius: 0.25rem; |
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; |
|
||||
font-size: 0.875rem; |
|
||||
color: #e53e3e; |
|
||||
} |
|
||||
pre { |
|
||||
background: #f7fafc; |
|
||||
padding: 1rem; |
|
||||
border-radius: 0.5rem; |
|
||||
overflow-x: auto; |
|
||||
border: 1px solid #e2e8f0; |
|
||||
} |
|
||||
pre code { |
|
||||
background: none; |
|
||||
padding: 0; |
|
||||
color: #2d3748; |
|
||||
} |
|
||||
blockquote { |
|
||||
border-left: 4px solid #4299e1; |
|
||||
padding-left: 1rem; |
|
||||
margin: 1rem 0; |
|
||||
color: #4a5568; |
|
||||
background: #f7fafc; |
|
||||
padding: 1rem; |
|
||||
border-radius: 0.5rem; |
|
||||
} |
|
||||
strong { font-weight: 600; color: #1a202c; } |
|
||||
em { font-style: italic; color: #4a5568; } |
|
||||
.preview-header { |
|
||||
background: #4299e1; |
|
||||
color: white; |
|
||||
padding: 1rem; |
|
||||
margin: -2rem -2rem 2rem -2rem; |
|
||||
border-radius: 0; |
|
||||
display: flex; |
|
||||
justify-content: space-between; |
|
||||
align-items: center; |
|
||||
} |
|
||||
.preview-title { |
|
||||
font-size: 1.125rem; |
|
||||
font-weight: 600; |
|
||||
margin: 0; |
|
||||
} |
|
||||
.preview-close { |
|
||||
background: rgba(255, 255, 255, 0.2); |
|
||||
border: none; |
|
||||
color: white; |
|
||||
padding: 0.5rem 1rem; |
|
||||
border-radius: 0.25rem; |
|
||||
cursor: pointer; |
|
||||
font-size: 0.875rem; |
|
||||
} |
|
||||
.preview-close:hover { |
|
||||
background: rgba(255, 255, 255, 0.3); |
|
||||
} |
|
||||
</style> |
|
||||
</head> |
|
||||
<body> |
|
||||
<div class="preview-header"> |
|
||||
<h1 class="preview-title">📄 文章预览</h1> |
|
||||
<button class="preview-close" onclick="window.close()">关闭预览</button> |
|
||||
</div> |
|
||||
<h1>${title || '未设置标题'}</h1> |
|
||||
<div class="article-content">${htmlContent}</div> |
|
||||
<script> |
|
||||
// 键盘快捷键
|
|
||||
document.addEventListener('keydown', (e) => { |
|
||||
if (e.key === 'Escape') { |
|
||||
window.close(); |
|
||||
} |
|
||||
}); |
|
||||
</script> |
|
||||
</body> |
|
||||
</html> |
|
||||
`);
|
|
||||
previewWindow.document.close(); |
|
||||
|
|
||||
// 聚焦到预览窗口
|
|
||||
previewWindow.focus(); |
|
||||
}; |
|
||||
|
|
||||
// 全局工具函数
|
|
||||
window.AdminUtils = AdminUtils; |
|
||||
|
|
||||
// 页面加载完成后初始化
|
|
||||
if (document.readyState === 'loading') { |
|
||||
document.addEventListener('DOMContentLoaded', () => { |
|
||||
AdminApp.init(); |
|
||||
}); |
|
||||
} else { |
|
||||
AdminApp.init(); |
|
||||
} |
|
||||
|
|
||||
// CSS样式注入(移动端菜单按钮样式)
|
|
||||
const style = document.createElement('style'); |
|
||||
style.textContent = ` |
|
||||
.selected { |
|
||||
background-color: #ebf8ff !important; |
|
||||
} |
|
||||
|
|
||||
.field-error { |
|
||||
color: #f56565; |
|
||||
font-size: 0.75rem; |
|
||||
margin-top: 0.25rem; |
|
||||
} |
|
||||
|
|
||||
@media (max-width: 768px) { |
|
||||
.admin-sidebar { |
|
||||
transform: translateX(-100%); |
|
||||
transition: transform 0.3s ease; |
|
||||
} |
|
||||
|
|
||||
.sidebar-open .admin-sidebar { |
|
||||
transform: translateX(0); |
|
||||
} |
|
||||
|
|
||||
.sidebar-open::before { |
|
||||
content: ''; |
|
||||
position: fixed; |
|
||||
top: 60px; |
|
||||
left: 0; |
|
||||
right: 0; |
|
||||
bottom: 0; |
|
||||
background: rgba(0, 0, 0, 0.5); |
|
||||
z-index: 40; |
|
||||
} |
|
||||
} |
|
||||
`;
|
|
||||
document.head.appendChild(style); |
|
||||
|
|
||||
})(); |
|
||||
@ -1,58 +0,0 @@ |
|||||
let loginToastTimer = null; |
|
||||
function showLoginToast(msg, isSuccess = false) { |
|
||||
let toast = document.getElementById("login-toast"); |
|
||||
if (!toast) { |
|
||||
toast = document.createElement("div"); |
|
||||
toast.id = "login-toast"; |
|
||||
toast.style.position = "fixed"; |
|
||||
toast.style.top = "20px"; |
|
||||
toast.style.right = "20px"; |
|
||||
toast.style.left = "auto"; |
|
||||
toast.style.transform = "none"; |
|
||||
toast.style.minWidth = "220px"; |
|
||||
toast.style.maxWidth = "80vw"; |
|
||||
toast.style.background = isSuccess ? "linear-gradient(90deg,#7ec6f7,#b2f7ef)" : "#fff"; |
|
||||
toast.style.color = isSuccess ? "#1976d2" : "#ff4d4f"; |
|
||||
toast.style.fontSize = "1.08rem"; |
|
||||
toast.style.fontWeight = "600"; |
|
||||
toast.style.padding = "1.1em 2.2em"; |
|
||||
toast.style.borderRadius = "18px"; |
|
||||
toast.style.boxShadow = "0 4px 24px rgba(30,136,229,0.13),0 1.5px 8px rgba(79,209,255,0.08)"; |
|
||||
toast.style.zIndex = 9999; |
|
||||
toast.style.textAlign = "center"; |
|
||||
toast.style.opacity = "0"; |
|
||||
toast.style.transition = "opacity 0.2s"; |
|
||||
document.body.appendChild(toast); |
|
||||
} |
|
||||
toast.textContent = msg; |
|
||||
toast.style.opacity = "1"; |
|
||||
if (loginToastTimer) clearTimeout(loginToastTimer); |
|
||||
loginToastTimer = setTimeout(() => { |
|
||||
toast.style.opacity = "0"; |
|
||||
setTimeout(() => { |
|
||||
toast.remove(); |
|
||||
}, 300); |
|
||||
loginToastTimer = null; |
|
||||
}, 2000); |
|
||||
} |
|
||||
|
|
||||
const loginForm = document.getElementById("login-form") |
|
||||
loginForm.onsubmit = async function (e) { |
|
||||
e.preventDefault() |
|
||||
const form = e.target |
|
||||
const data = Object.fromEntries(new FormData(form)) |
|
||||
const res = await fetch(form.action, { |
|
||||
method: "POST", |
|
||||
headers: { "Content-Type": "application/json" }, |
|
||||
body: JSON.stringify(data), |
|
||||
}) |
|
||||
const result = await res.json() |
|
||||
if (result.success) { |
|
||||
showLoginToast("登录成功,2秒后跳转到首页", true) |
|
||||
setTimeout(() => { |
|
||||
window.location.href = "/" |
|
||||
}, 2000) |
|
||||
} else { |
|
||||
showLoginToast(result.message || "登录失败") |
|
||||
} |
|
||||
} |
|
||||
@ -1,631 +0,0 @@ |
|||||
// 用户资料页面JavaScript
|
|
||||
(function() { |
|
||||
'use strict'; |
|
||||
|
|
||||
// 页面初始化
|
|
||||
document.addEventListener('DOMContentLoaded', function() { |
|
||||
initProfilePage(); |
|
||||
}); |
|
||||
|
|
||||
function initProfilePage() { |
|
||||
bindFormEvents(); |
|
||||
bindInputValidation(); |
|
||||
showInitialMessage(); |
|
||||
initTabs(); |
|
||||
initAvatarUpload(); |
|
||||
} |
|
||||
|
|
||||
// 绑定表单事件
|
|
||||
function bindFormEvents() { |
|
||||
const profileForm = document.getElementById('profileForm'); |
|
||||
const passwordForm = document.getElementById('passwordForm'); |
|
||||
|
|
||||
console.log('Profile form found:', !!profileForm); |
|
||||
console.log('Password form found:', !!passwordForm); |
|
||||
|
|
||||
if (profileForm) { |
|
||||
profileForm.addEventListener('submit', handleProfileUpdate); |
|
||||
console.log('Profile form event listener added'); |
|
||||
} |
|
||||
|
|
||||
if (passwordForm) { |
|
||||
passwordForm.addEventListener('submit', handlePasswordChange); |
|
||||
console.log('Password form event listener added'); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 处理用户资料更新
|
|
||||
async function handleProfileUpdate(event) { |
|
||||
event.preventDefault(); |
|
||||
|
|
||||
const form = event.target; |
|
||||
const submitBtn = form.querySelector('button[type="submit"]'); |
|
||||
const originalText = submitBtn.textContent; |
|
||||
|
|
||||
try { |
|
||||
setButtonLoading(submitBtn, true); |
|
||||
|
|
||||
const formData = new FormData(form); |
|
||||
const data = Object.fromEntries(formData.entries()); |
|
||||
|
|
||||
// 验证必填字段
|
|
||||
if (!validateProfileData(data)) { |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
const response = await fetch('/profile/update', { |
|
||||
method: 'POST', |
|
||||
headers: { 'Content-Type': 'application/json' }, |
|
||||
body: JSON.stringify(data) |
|
||||
}); |
|
||||
|
|
||||
const result = await response.json(); |
|
||||
|
|
||||
if (result.success) { |
|
||||
showMessage('资料更新成功!', 'success', 'profileForm'); |
|
||||
updateUserInfoDisplay(result.user); |
|
||||
} else { |
|
||||
throw new Error(result.message || '更新失败'); |
|
||||
} |
|
||||
|
|
||||
} catch (error) { |
|
||||
showMessage(error.message || '更新失败,请重试', 'error', 'profileForm'); |
|
||||
console.error('Profile update error:', error); |
|
||||
} finally { |
|
||||
setButtonLoading(submitBtn, false, originalText); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 处理密码修改
|
|
||||
async function handlePasswordChange(event) { |
|
||||
event.preventDefault(); |
|
||||
|
|
||||
const form = event.target; |
|
||||
const submitBtn = form.querySelector('button[type="submit"]'); |
|
||||
const originalText = submitBtn.textContent; |
|
||||
|
|
||||
try { |
|
||||
setButtonLoading(submitBtn, true); |
|
||||
|
|
||||
const formData = new FormData(form); |
|
||||
const data = Object.fromEntries(formData.entries()); |
|
||||
|
|
||||
if (!validatePasswordData(data)) { |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
const response = await fetch('/profile/change-password', { |
|
||||
method: 'POST', |
|
||||
headers: { 'Content-Type': 'application/json' }, |
|
||||
body: JSON.stringify(data) |
|
||||
}); |
|
||||
|
|
||||
const result = await response.json(); |
|
||||
|
|
||||
if (result.success) { |
|
||||
showMessage('密码修改成功!', 'success', 'passwordForm'); |
|
||||
form.reset(); |
|
||||
} else { |
|
||||
throw new Error(result.message || '密码修改失败'); |
|
||||
} |
|
||||
|
|
||||
} catch (error) { |
|
||||
showMessage(error.message || '密码修改失败,请重试', 'error', 'passwordForm'); |
|
||||
console.error('Password change error:', error); |
|
||||
} finally { |
|
||||
setButtonLoading(submitBtn, false, originalText); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 验证用户资料数据
|
|
||||
function validateProfileData(data) { |
|
||||
if (!data.username.trim()) { |
|
||||
showMessage('用户名不能为空', 'error', 'profileForm'); |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
if (data.username.length < 3) { |
|
||||
showMessage('用户名长度不能少于3位', 'error', 'profileForm'); |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
if (data.email && !isValidEmail(data.email)) { |
|
||||
showMessage('请输入有效的邮箱地址', 'error', 'profileForm'); |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
if (data.avatar && !isValidImageUrl(data.avatar)) { |
|
||||
showMessage('请输入有效的图片链接或路径', 'error', 'profileForm'); |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
// 验证密码数据
|
|
||||
function validatePasswordData(data) { |
|
||||
const { oldPassword, newPassword, confirmPassword } = data; |
|
||||
|
|
||||
if (!oldPassword || !newPassword || !confirmPassword) { |
|
||||
showMessage('请填写所有密码字段', 'error', 'passwordForm'); |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
if (newPassword.length < 6) { |
|
||||
showMessage('新密码长度不能少于6位', 'error', 'passwordForm'); |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
if (newPassword !== confirmPassword) { |
|
||||
showMessage('新密码与确认密码不匹配', 'error', 'passwordForm'); |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
if (oldPassword === newPassword) { |
|
||||
showMessage('新密码不能与当前密码相同', 'error', 'passwordForm'); |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
// 绑定输入验证
|
|
||||
function bindInputValidation() { |
|
||||
const inputs = document.querySelectorAll('.form-input, .form-textarea'); |
|
||||
|
|
||||
inputs.forEach(input => { |
|
||||
input.addEventListener('blur', function() { |
|
||||
validateField(this); |
|
||||
}); |
|
||||
|
|
||||
input.addEventListener('input', function() { |
|
||||
clearFieldError(this); |
|
||||
}); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
// 验证单个字段
|
|
||||
function validateField(field) { |
|
||||
const value = field.value.trim(); |
|
||||
const fieldName = field.name; |
|
||||
|
|
||||
clearFieldError(field); |
|
||||
|
|
||||
let isValid = true; |
|
||||
let errorMessage = ''; |
|
||||
|
|
||||
switch (fieldName) { |
|
||||
case 'username': |
|
||||
if (!value) { |
|
||||
isValid = false; |
|
||||
errorMessage = '用户名不能为空'; |
|
||||
} else if (value.length < 3) { |
|
||||
isValid = false; |
|
||||
errorMessage = '用户名长度不能少于3位'; |
|
||||
} |
|
||||
break; |
|
||||
|
|
||||
case 'email': |
|
||||
if (value && !isValidEmail(value)) { |
|
||||
isValid = false; |
|
||||
errorMessage = '请输入有效的邮箱地址'; |
|
||||
} |
|
||||
break; |
|
||||
|
|
||||
case 'avatar': |
|
||||
if (value && !isValidImageUrl(value)) { |
|
||||
isValid = false; |
|
||||
errorMessage = '请输入有效的图片链接或路径'; |
|
||||
} |
|
||||
break; |
|
||||
|
|
||||
case 'newPassword': |
|
||||
if (value && value.length < 6) { |
|
||||
isValid = false; |
|
||||
errorMessage = '密码长度不能少于6位'; |
|
||||
} |
|
||||
break; |
|
||||
} |
|
||||
|
|
||||
if (!isValid) { |
|
||||
showFieldError(field, errorMessage); |
|
||||
} |
|
||||
|
|
||||
return isValid; |
|
||||
} |
|
||||
|
|
||||
// 显示字段错误
|
|
||||
function showFieldError(field, message) { |
|
||||
field.classList.add('error'); |
|
||||
|
|
||||
let errorElement = field.parentNode.querySelector('.error-message'); |
|
||||
if (!errorElement) { |
|
||||
errorElement = document.createElement('div'); |
|
||||
errorElement.className = 'error-message'; |
|
||||
field.parentNode.appendChild(errorElement); |
|
||||
} |
|
||||
|
|
||||
errorElement.textContent = message; |
|
||||
errorElement.classList.add('show'); |
|
||||
} |
|
||||
|
|
||||
// 清除字段错误
|
|
||||
function clearFieldError(field) { |
|
||||
field.classList.remove('error'); |
|
||||
|
|
||||
const errorElement = field.parentNode.querySelector('.error-message'); |
|
||||
if (errorElement) { |
|
||||
errorElement.classList.remove('show'); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 验证邮箱格式
|
|
||||
function isValidEmail(email) { |
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; |
|
||||
return emailRegex.test(email); |
|
||||
} |
|
||||
|
|
||||
// 验证图片URL格式(支持相对路径和绝对路径)
|
|
||||
function isValidImageUrl(url) { |
|
||||
if (!url) return true; // 空值时认为有效
|
|
||||
|
|
||||
// 支持的图片格式
|
|
||||
const imageExtensions = /\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?.*)?$/i; |
|
||||
|
|
||||
// 检查是否以支持的图片格式结尾
|
|
||||
if (imageExtensions.test(url)) { |
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
// 检查是否为完整的URL(http或https开头)
|
|
||||
try { |
|
||||
const urlObj = new URL(url); |
|
||||
return urlObj.protocol === 'http:' || urlObj.protocol === 'https:'; |
|
||||
} catch { |
|
||||
// 如果不是完整URL,检查是否为相对路径(以/开头)
|
|
||||
return url.startsWith('/'); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 设置按钮加载状态
|
|
||||
function setButtonLoading(button, loading, originalText = null) { |
|
||||
if (loading) { |
|
||||
button.disabled = true; |
|
||||
button.textContent = '处理中...'; |
|
||||
button.classList.add('loading'); |
|
||||
} else { |
|
||||
button.disabled = false; |
|
||||
button.textContent = originalText || button.textContent; |
|
||||
button.classList.remove('loading'); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 显示消息
|
|
||||
function showMessage(message, type = 'info', formId = null) { |
|
||||
console.log('showMessage called with:', { message, type, formId }); |
|
||||
removeExistingMessages(); |
|
||||
|
|
||||
let targetContainer; |
|
||||
if (formId === 'profileForm') { |
|
||||
targetContainer = document.querySelector('#profileForm .message-container'); |
|
||||
console.log('Looking for profileForm message container:', !!targetContainer); |
|
||||
} else if (formId === 'passwordForm') { |
|
||||
targetContainer = document.querySelector('#passwordForm .message-container'); |
|
||||
console.log('Looking for passwordForm message container:', !!targetContainer); |
|
||||
} else { |
|
||||
// 如果没有指定表单,使用默认容器
|
|
||||
targetContainer = document.querySelector('.profile-content'); |
|
||||
console.log('Using default container:', !!targetContainer); |
|
||||
} |
|
||||
|
|
||||
if (!targetContainer) { |
|
||||
console.warn('Message container not found for formId:', formId); |
|
||||
console.warn('Available containers:', document.querySelectorAll('.message-container')); |
|
||||
// 尝试使用备用容器
|
|
||||
targetContainer = document.querySelector('.message-container'); |
|
||||
if (!targetContainer) { |
|
||||
console.error('No message container found anywhere'); |
|
||||
return; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
console.log('Target container found:', targetContainer); |
|
||||
|
|
||||
const messageElement = document.createElement('div'); |
|
||||
messageElement.className = `message ${type} show`; |
|
||||
|
|
||||
const messageText = document.createElement('span'); |
|
||||
messageText.textContent = message; |
|
||||
messageElement.appendChild(messageText); |
|
||||
|
|
||||
const closeButton = document.createElement('button'); |
|
||||
closeButton.className = 'message-close'; |
|
||||
closeButton.type = 'button'; |
|
||||
closeButton.innerHTML = '×'; |
|
||||
closeButton.onclick = function() { |
|
||||
messageElement.remove(); |
|
||||
}; |
|
||||
messageElement.appendChild(closeButton); |
|
||||
|
|
||||
targetContainer.appendChild(messageElement); |
|
||||
console.log('Message added to container'); |
|
||||
|
|
||||
// 5秒后自动隐藏
|
|
||||
setTimeout(() => { |
|
||||
if (messageElement.parentNode) { |
|
||||
messageElement.remove(); |
|
||||
} |
|
||||
}, 5000); |
|
||||
} |
|
||||
|
|
||||
// 关闭指定消息
|
|
||||
function closeMessage(messageId) { |
|
||||
const messageElement = document.getElementById(messageId); |
|
||||
if (messageElement) { |
|
||||
messageElement.remove(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 移除现有消息
|
|
||||
function removeExistingMessages() { |
|
||||
const existingMessages = document.querySelectorAll('.message'); |
|
||||
existingMessages.forEach(msg => msg.remove()); |
|
||||
} |
|
||||
|
|
||||
// 显示初始消息
|
|
||||
function showInitialMessage() { |
|
||||
const urlParams = new URLSearchParams(window.location.search); |
|
||||
const msg = urlParams.get('msg'); |
|
||||
const msgType = urlParams.get('type') || 'info'; |
|
||||
|
|
||||
if (msg) { |
|
||||
showMessage(decodeURIComponent(msg), msgType); |
|
||||
const newUrl = window.location.pathname; |
|
||||
window.history.replaceState({}, document.title, newUrl); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 更新用户信息显示
|
|
||||
function updateUserInfoDisplay(user) { |
|
||||
const infoItems = document.querySelectorAll('.info-value'); |
|
||||
infoItems.forEach(item => { |
|
||||
const label = item.previousElementSibling.textContent; |
|
||||
if (label.includes('最后更新')) { |
|
||||
item.textContent = new Date().toLocaleDateString('zh-CN'); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
// 重置表单
|
|
||||
function resetForm() { |
|
||||
const form = document.getElementById('profileForm'); |
|
||||
if (form) { |
|
||||
form.reset(); |
|
||||
const inputs = form.querySelectorAll('.form-input, .form-textarea'); |
|
||||
inputs.forEach(input => clearFieldError(input)); |
|
||||
showMessage('表单已重置', 'info'); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 重置密码表单
|
|
||||
function resetPasswordForm() { |
|
||||
console.log('resetPasswordForm called'); |
|
||||
const form = document.getElementById('passwordForm'); |
|
||||
console.log('Password form found:', !!form); |
|
||||
|
|
||||
if (form) { |
|
||||
form.reset(); |
|
||||
const inputs = form.querySelectorAll('.form-input'); |
|
||||
inputs.forEach(input => clearFieldError(input)); |
|
||||
|
|
||||
// 查找消息容器
|
|
||||
const messageContainer = form.querySelector('.message-container'); |
|
||||
console.log('Message container found:', !!messageContainer); |
|
||||
|
|
||||
showMessage('密码表单已清空', 'info', 'passwordForm'); |
|
||||
} else { |
|
||||
console.error('Password form not found'); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 初始化标签页
|
|
||||
function initTabs() { |
|
||||
const tabBtns = document.querySelectorAll('.tab-btn'); |
|
||||
const tabPanes = document.querySelectorAll('.tab-pane'); |
|
||||
|
|
||||
console.log('Initializing tabs...'); |
|
||||
console.log('Tab buttons found:', tabBtns.length); |
|
||||
console.log('Tab panes found:', tabPanes.length); |
|
||||
|
|
||||
if (tabBtns.length === 0 || tabPanes.length === 0) { |
|
||||
console.warn('Tab elements not found'); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
tabBtns.forEach(btn => { |
|
||||
btn.addEventListener('click', function() { |
|
||||
const targetTab = this.getAttribute('data-tab'); |
|
||||
console.log('Tab clicked:', targetTab); // 调试日志
|
|
||||
|
|
||||
// 移除所有活动状态
|
|
||||
tabBtns.forEach(b => b.classList.remove('active')); |
|
||||
tabPanes.forEach(p => p.classList.remove('active')); |
|
||||
|
|
||||
// 添加活动状态到当前标签
|
|
||||
this.classList.add('active'); |
|
||||
const targetPane = document.getElementById(targetTab + '-tab'); |
|
||||
if (targetPane) { |
|
||||
targetPane.classList.add('active'); |
|
||||
console.log('Tab pane activated:', targetTab + '-tab'); // 调试日志
|
|
||||
|
|
||||
// 重新绑定表单事件,确保新显示的Tab中的表单能正常工作
|
|
||||
setTimeout(() => { |
|
||||
bindFormEvents(); |
|
||||
}, 100); |
|
||||
} else { |
|
||||
console.error('Target tab pane not found:', targetTab + '-tab'); |
|
||||
} |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
// 确保默认标签页是激活状态
|
|
||||
const defaultTab = document.querySelector('.tab-btn.active'); |
|
||||
const defaultPane = document.querySelector('.tab-pane.active'); |
|
||||
if (defaultTab && defaultPane) { |
|
||||
console.log('Default tab initialized:', defaultTab.getAttribute('data-tab')); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 导出函数供HTML调用
|
|
||||
window.resetForm = resetForm; |
|
||||
window.resetPasswordForm = resetPasswordForm; |
|
||||
window.closeMessage = closeMessage; |
|
||||
window.handleAvatarSelect = handleAvatarSelect; |
|
||||
|
|
||||
// 初始化头像上传功能
|
|
||||
function initAvatarUpload() { |
|
||||
const avatarInput = document.getElementById('avatarFile'); |
|
||||
const avatarPreview = document.querySelector('.avatar-preview'); |
|
||||
const avatarUrlInput = document.getElementById('avatar'); |
|
||||
|
|
||||
if (avatarUrlInput) { |
|
||||
avatarUrlInput.addEventListener('input', function() { |
|
||||
if (this.value.trim()) { |
|
||||
updateAvatarPreview(this.value.trim()); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 处理头像文件选择
|
|
||||
function handleAvatarSelect(input) { |
|
||||
const file = input.files[0]; |
|
||||
if (!file) return; |
|
||||
|
|
||||
// 验证文件类型
|
|
||||
if (!file.type.startsWith('image/')) { |
|
||||
showMessage('请选择图片文件', 'error', 'profileForm'); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
// 验证文件大小 (5MB)
|
|
||||
if (file.size > 5 * 1024 * 1024) { |
|
||||
showMessage('图片文件大小不能超过 5MB', 'error', 'profileForm'); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
// 预览图片
|
|
||||
const reader = new FileReader(); |
|
||||
reader.onload = function(e) { |
|
||||
updateAvatarPreview(e.target.result); |
|
||||
}; |
|
||||
reader.readAsDataURL(file); |
|
||||
|
|
||||
// 上传文件
|
|
||||
uploadAvatarFile(file); |
|
||||
} |
|
||||
|
|
||||
// 更新头像预览
|
|
||||
function updateAvatarPreview(imageSrc) { |
|
||||
const avatarPreview = document.querySelector('.avatar-preview'); |
|
||||
const sidebarAvatar = document.querySelector('.profile-avatar'); |
|
||||
|
|
||||
if (avatarPreview) { |
|
||||
// 移除现有内容
|
|
||||
avatarPreview.innerHTML = ''; |
|
||||
|
|
||||
// 创建新的图片元素
|
|
||||
const img = document.createElement('img'); |
|
||||
img.src = imageSrc; |
|
||||
img.alt = '头像预览'; |
|
||||
img.id = 'avatarPreviewImg'; |
|
||||
avatarPreview.appendChild(img); |
|
||||
|
|
||||
// 添加覆盖层
|
|
||||
const overlay = document.createElement('div'); |
|
||||
overlay.className = 'upload-overlay'; |
|
||||
overlay.innerHTML = '<div><div>📷</div><div>点击更换头像</div></div>'; |
|
||||
avatarPreview.appendChild(overlay); |
|
||||
} |
|
||||
|
|
||||
// 同时更新侧边栏头像
|
|
||||
if (sidebarAvatar) { |
|
||||
sidebarAvatar.innerHTML = `<img src="${imageSrc}" alt="用户头像">`; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 上传头像文件
|
|
||||
async function uploadAvatarFile(file) { |
|
||||
const progressContainer = document.getElementById('uploadProgress'); |
|
||||
const progressBar = document.getElementById('uploadProgressBar'); |
|
||||
|
|
||||
try { |
|
||||
// 显示进度条
|
|
||||
if (progressContainer) { |
|
||||
progressContainer.style.display = 'block'; |
|
||||
progressBar.style.width = '0%'; |
|
||||
} |
|
||||
|
|
||||
const formData = new FormData(); |
|
||||
formData.append('avatar', file); |
|
||||
|
|
||||
// 模拟进度更新
|
|
||||
let progress = 0; |
|
||||
const progressInterval = setInterval(() => { |
|
||||
progress += Math.random() * 30; |
|
||||
if (progress > 90) { |
|
||||
clearInterval(progressInterval); |
|
||||
progress = 90; |
|
||||
} |
|
||||
if (progressBar) { |
|
||||
progressBar.style.width = progress + '%'; |
|
||||
} |
|
||||
}, 200); |
|
||||
|
|
||||
const response = await fetch('/profile/upload-avatar', { |
|
||||
method: 'POST', |
|
||||
body: formData |
|
||||
}); |
|
||||
|
|
||||
const result = await response.json(); |
|
||||
|
|
||||
// 清除进度条动画
|
|
||||
clearInterval(progressInterval); |
|
||||
|
|
||||
if (result.success) { |
|
||||
// 完成进度条
|
|
||||
if (progressBar) { |
|
||||
progressBar.style.width = '100%'; |
|
||||
} |
|
||||
|
|
||||
// 更新头像URL输入框
|
|
||||
const avatarUrlInput = document.getElementById('avatar'); |
|
||||
if (avatarUrlInput && result.url) { |
|
||||
avatarUrlInput.value = result.url; |
|
||||
} |
|
||||
|
|
||||
showMessage('头像上传成功!', 'success', 'profileForm'); |
|
||||
|
|
||||
// 隐藏进度条
|
|
||||
setTimeout(() => { |
|
||||
if (progressContainer) { |
|
||||
progressContainer.style.display = 'none'; |
|
||||
} |
|
||||
}, 1000); |
|
||||
|
|
||||
} else { |
|
||||
throw new Error(result.message || '头像上传失败'); |
|
||||
} |
|
||||
|
|
||||
} catch (error) { |
|
||||
showMessage(error.message || '头像上传失败,请重试', 'error', 'profileForm'); |
|
||||
console.error('Avatar upload error:', error); |
|
||||
|
|
||||
// 隐藏进度条
|
|
||||
if (progressContainer) { |
|
||||
progressContainer.style.display = 'none'; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
})(); |
|
||||
@ -1,48 +0,0 @@ |
|||||
// 注册页面验证码点击刷新功能
|
|
||||
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; |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
}); |
|
||||
@ -1,429 +0,0 @@ |
|||||
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模式
|
|
||||
let 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, opts = {}) { |
|
||||
if (url === "/api/random-wallpaper") { |
|
||||
return window.fetchRandomWallpaper() |
|
||||
} |
|
||||
return window.originalFetch(...arguments) |
|
||||
} |
|
||||
|
|
||||
console.log("模拟壁纸API已启动") |
|
||||
} |
|
||||
|
|
||||
// 初始化模拟API
|
|
||||
createMockWallpaperApi() |
|
||||
|
|
||||
// 启动API模式的随机壁纸切换(每10秒请求一次)
|
|
||||
apiBgSwitcher.startRandomApiSwitch("/api/random-wallpaper", 10000) |
|
||||
|
|
||||
window.addEventListener("pageshow", function (event) { |
|
||||
if (event.persisted) { |
|
||||
apiBgSwitcher = new BgSwitcher([], { interval: 5000 }) |
|
||||
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();
|
|
||||
@ -1,225 +0,0 @@ |
|||||
extends ../../layouts/admin |
|
||||
|
|
||||
block $$content |
|
||||
.page-container |
|
||||
//- 页面头部 |
|
||||
.page-header |
|
||||
.page-header-left |
|
||||
.breadcrumb |
|
||||
a(href="/admin/articles") 文章管理 |
|
||||
span.breadcrumb-separator / |
|
||||
span.breadcrumb-current 新建文章 |
|
||||
h1.page-title 新建文章 |
|
||||
p.page-subtitle 创建一篇新的文章内容 |
|
||||
|
|
||||
//- 文章表单 |
|
||||
.content-section |
|
||||
form.article-form(method="POST" action="/admin/articles") |
|
||||
.form-grid |
|
||||
//- 左侧主要内容 |
|
||||
.form-main |
|
||||
.form-group |
|
||||
label.form-label(for="title") 文章标题 * |
|
||||
input.form-input( |
|
||||
type="text" |
|
||||
id="title" |
|
||||
name="title" |
|
||||
placeholder="请输入文章标题" |
|
||||
required |
|
||||
maxlength="200" |
|
||||
) |
|
||||
.form-help 建议标题长度在10-50字之间 |
|
||||
|
|
||||
.form-group |
|
||||
label.form-label(for="slug") URL别名 |
|
||||
input.form-input( |
|
||||
type="text" |
|
||||
id="slug" |
|
||||
name="slug" |
|
||||
placeholder="自动生成或手动输入" |
|
||||
pattern="[a-z0-9-]+" |
|
||||
maxlength="100" |
|
||||
) |
|
||||
.form-help 用于生成友好的URL,只能包含小写字母、数字和连字符 |
|
||||
|
|
||||
.form-group |
|
||||
label.form-label(for="excerpt") 文章摘要 |
|
||||
textarea.form-textarea( |
|
||||
id="excerpt" |
|
||||
name="excerpt" |
|
||||
placeholder="请输入文章摘要(可选)" |
|
||||
rows="3" |
|
||||
maxlength="500" |
|
||||
) |
|
||||
.form-help 简短描述文章内容,有助于SEO |
|
||||
|
|
||||
.form-group |
|
||||
label.form-label(for="content") 文章内容 * |
|
||||
textarea.form-textarea.content-editor( |
|
||||
id="content" |
|
||||
name="content" |
|
||||
placeholder="请输入文章内容,支持Markdown格式" |
|
||||
rows="20" |
|
||||
required |
|
||||
) |
|
||||
.form-help 支持Markdown语法,可直接粘贴Markdown内容 |
|
||||
|
|
||||
//- 右侧设置面板 |
|
||||
.form-sidebar |
|
||||
.sidebar-section |
|
||||
h3.sidebar-title 发布设置 |
|
||||
|
|
||||
.form-group |
|
||||
label.form-label(for="status") 发布状态 |
|
||||
select.form-select(id="status" name="status") |
|
||||
option(value="draft") 📝 保存为草稿 |
|
||||
option(value="published") ✅ 立即发布 |
|
||||
.form-help 草稿状态下文章不会在前台显示 |
|
||||
|
|
||||
.form-group |
|
||||
label.form-label(for="category") 文章分类 |
|
||||
input.form-input( |
|
||||
type="text" |
|
||||
id="category" |
|
||||
name="category" |
|
||||
placeholder="如:技术、生活、随笔" |
|
||||
maxlength="50" |
|
||||
) |
|
||||
.form-help 用于组织和分类文章 |
|
||||
|
|
||||
.form-group |
|
||||
label.form-label(for="tags") 文章标签 |
|
||||
input.form-input( |
|
||||
type="text" |
|
||||
id="tags" |
|
||||
name="tags" |
|
||||
placeholder="标签1,标签2,标签3" |
|
||||
maxlength="200" |
|
||||
) |
|
||||
.form-help 多个标签用英文逗号分隔 |
|
||||
|
|
||||
.sidebar-section |
|
||||
h3.sidebar-title 高级设置 |
|
||||
|
|
||||
.form-group |
|
||||
label.form-label(for="featured_image") 特色图片URL |
|
||||
input.form-input( |
|
||||
type="url" |
|
||||
id="featured_image" |
|
||||
name="featured_image" |
|
||||
placeholder="https://example.com/image.jpg" |
|
||||
) |
|
||||
.form-help 文章封面图片链接 |
|
||||
|
|
||||
//- 表单按钮 |
|
||||
.form-actions |
|
||||
.action-buttons |
|
||||
button.btn.btn-primary(type="submit") 💾 保存文章 |
|
||||
a.btn.btn-outline(href="/admin/articles") 取消 |
|
||||
button.btn.btn-secondary(type="button" onclick="previewArticle()") 👁️ 预览 |
|
||||
|
|
||||
block $$scripts |
|
||||
script. |
|
||||
// 自动生成slug |
|
||||
document.getElementById('title').addEventListener('input', function() { |
|
||||
const title = this.value; |
|
||||
const slugInput = document.getElementById('slug'); |
|
||||
|
|
||||
if (!slugInput.value) { |
|
||||
// 简单的slug生成逻辑 |
|
||||
const slug = title |
|
||||
.toLowerCase() |
|
||||
.replace(/[^a-z0-9\u4e00-\u9fa5]/g, '-') |
|
||||
.replace(/-+/g, '-') |
|
||||
.replace(/^-|-$/g, ''); |
|
||||
slugInput.value = slug; |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
// 字符计数 |
|
||||
function setupCharCounter(inputId, maxLength) { |
|
||||
const input = document.getElementById(inputId); |
|
||||
const help = input.nextElementSibling; |
|
||||
|
|
||||
function updateCounter() { |
|
||||
const current = input.value.length; |
|
||||
const remaining = maxLength - current; |
|
||||
const originalText = help.textContent; |
|
||||
|
|
||||
if (remaining < 50) { |
|
||||
help.textContent = `${originalText} (还可输入${remaining}字符)`; |
|
||||
help.style.color = remaining < 10 ? '#ff4757' : '#ffa726'; |
|
||||
} else { |
|
||||
help.style.color = ''; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
input.addEventListener('input', updateCounter); |
|
||||
updateCounter(); |
|
||||
} |
|
||||
|
|
||||
// 设置字符计数 |
|
||||
setupCharCounter('title', 200); |
|
||||
setupCharCounter('excerpt', 500); |
|
||||
setupCharCounter('tags', 200); |
|
||||
|
|
||||
// 预览功能 |
|
||||
function previewArticle() { |
|
||||
const content = document.getElementById('content').value; |
|
||||
if (!content.trim()) { |
|
||||
alert('请先输入文章内容'); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
// 简单的Markdown预览(实际项目中可以使用更完善的Markdown解析器) |
|
||||
const previewWindow = window.open('', '_blank', 'width=800,height=600'); |
|
||||
previewWindow.document.write(` |
|
||||
<!DOCTYPE html> |
|
||||
<html> |
|
||||
<head> |
|
||||
<title>文章预览</title> |
|
||||
<style> |
|
||||
body { font-family: Arial, sans-serif; padding: 20px; line-height: 1.6; } |
|
||||
h1, h2, h3 { color: #333; } |
|
||||
pre { background: #f4f4f4; padding: 10px; border-radius: 4px; } |
|
||||
code { background: #f4f4f4; padding: 2px 4px; border-radius: 2px; } |
|
||||
</style> |
|
||||
</head> |
|
||||
<body> |
|
||||
<h1>${document.getElementById('title').value || '未设置标题'}</h1> |
|
||||
<div>${content.replace(/\n/g, '<br>')}</div> |
|
||||
</body> |
|
||||
</html> |
|
||||
`); |
|
||||
previewWindow.document.close(); |
|
||||
} |
|
||||
|
|
||||
// 表单验证 |
|
||||
document.querySelector('.article-form').addEventListener('submit', function(e) { |
|
||||
const title = document.getElementById('title').value.trim(); |
|
||||
const content = document.getElementById('content').value.trim(); |
|
||||
|
|
||||
if (!title) { |
|
||||
alert('请输入文章标题'); |
|
||||
e.preventDefault(); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
if (!content) { |
|
||||
alert('请输入文章内容'); |
|
||||
e.preventDefault(); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
if (title.length < 5) { |
|
||||
alert('文章标题至少需要5个字符'); |
|
||||
e.preventDefault(); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
if (content.length < 10) { |
|
||||
alert('文章内容至少需要10个字符'); |
|
||||
e.preventDefault(); |
|
||||
return; |
|
||||
} |
|
||||
}); |
|
||||
@ -1,251 +0,0 @@ |
|||||
extends ../../layouts/admin |
|
||||
|
|
||||
block $$content |
|
||||
.page-container |
|
||||
//- 页面头部 |
|
||||
.page-header |
|
||||
.page-header-left |
|
||||
.breadcrumb |
|
||||
a(href="/admin/articles") 文章管理 |
|
||||
span.breadcrumb-separator / |
|
||||
a(href=`/admin/articles/${article.id}`)= article.title |
|
||||
span.breadcrumb-separator / |
|
||||
span.breadcrumb-current 编辑 |
|
||||
h1.page-title 编辑文章 |
|
||||
p.page-subtitle= `编辑:${article.title}` |
|
||||
|
|
||||
//- 文章表单 |
|
||||
.content-section |
|
||||
form.article-form(method="POST" action=`/admin/articles/${article.id}`) |
|
||||
input(type="hidden" name="_method" value="PUT") |
|
||||
|
|
||||
.form-grid |
|
||||
//- 左侧主要内容 |
|
||||
.form-main |
|
||||
.form-group |
|
||||
label.form-label(for="title") 文章标题 * |
|
||||
input.form-input( |
|
||||
type="text" |
|
||||
id="title" |
|
||||
name="title" |
|
||||
value=article.title |
|
||||
placeholder="请输入文章标题" |
|
||||
required |
|
||||
maxlength="200" |
|
||||
) |
|
||||
.form-help 建议标题长度在10-50字之间 |
|
||||
|
|
||||
.form-group |
|
||||
label.form-label(for="slug") URL别名 |
|
||||
input.form-input( |
|
||||
type="text" |
|
||||
id="slug" |
|
||||
name="slug" |
|
||||
value=article.slug || '' |
|
||||
placeholder="自动生成或手动输入" |
|
||||
pattern="[a-z0-9-]+" |
|
||||
maxlength="100" |
|
||||
) |
|
||||
.form-help 用于生成友好的URL,只能包含小写字母、数字和连字符 |
|
||||
|
|
||||
.form-group |
|
||||
label.form-label(for="excerpt") 文章摘要 |
|
||||
textarea.form-textarea( |
|
||||
id="excerpt" |
|
||||
name="excerpt" |
|
||||
placeholder="请输入文章摘要(可选)" |
|
||||
rows="3" |
|
||||
maxlength="500" |
|
||||
)= article.excerpt || '' |
|
||||
.form-help 简短描述文章内容,有助于SEO |
|
||||
|
|
||||
.form-group |
|
||||
label.form-label(for="content") 文章内容 * |
|
||||
textarea.form-textarea.content-editor( |
|
||||
id="content" |
|
||||
name="content" |
|
||||
placeholder="请输入文章内容,支持Markdown格式" |
|
||||
rows="20" |
|
||||
required |
|
||||
)= article.content || '' |
|
||||
.form-help 支持Markdown语法,可直接粘贴Markdown内容 |
|
||||
|
|
||||
//- 右侧设置面板 |
|
||||
.form-sidebar |
|
||||
.sidebar-section |
|
||||
h3.sidebar-title 发布设置 |
|
||||
|
|
||||
.form-group |
|
||||
label.form-label(for="status") 发布状态 |
|
||||
select.form-select(id="status" name="status") |
|
||||
option(value="draft" selected=article.status === 'draft') 📝 保存为草稿 |
|
||||
option(value="published" selected=article.status === 'published') ✅ 立即发布 |
|
||||
.form-help 草稿状态下文章不会在前台显示 |
|
||||
|
|
||||
.form-group |
|
||||
label.form-label(for="category") 文章分类 |
|
||||
input.form-input( |
|
||||
type="text" |
|
||||
id="category" |
|
||||
name="category" |
|
||||
value=article.category || '' |
|
||||
placeholder="如:技术、生活、随笔" |
|
||||
maxlength="50" |
|
||||
) |
|
||||
.form-help 用于组织和分类文章 |
|
||||
|
|
||||
.form-group |
|
||||
label.form-label(for="tags") 文章标签 |
|
||||
input.form-input( |
|
||||
type="text" |
|
||||
id="tags" |
|
||||
name="tags" |
|
||||
value=article.tags || '' |
|
||||
placeholder="标签1,标签2,标签3" |
|
||||
maxlength="200" |
|
||||
) |
|
||||
.form-help 多个标签用英文逗号分隔 |
|
||||
|
|
||||
.sidebar-section |
|
||||
h3.sidebar-title 高级设置 |
|
||||
|
|
||||
.form-group |
|
||||
label.form-label(for="featured_image") 特色图片URL |
|
||||
input.form-input( |
|
||||
type="url" |
|
||||
id="featured_image" |
|
||||
name="featured_image" |
|
||||
value=article.featured_image || '' |
|
||||
placeholder="https://example.com/image.jpg" |
|
||||
) |
|
||||
.form-help 文章封面图片链接 |
|
||||
|
|
||||
.sidebar-section |
|
||||
h3.sidebar-title 文章信息 |
|
||||
.info-list |
|
||||
.info-item |
|
||||
.info-label 创建时间 |
|
||||
.info-value= new Date(article.created_at).toLocaleString('zh-CN') |
|
||||
.info-item |
|
||||
.info-label 更新时间 |
|
||||
.info-value= new Date(article.updated_at).toLocaleString('zh-CN') |
|
||||
if article.view_count |
|
||||
.info-item |
|
||||
.info-label 阅读量 |
|
||||
.info-value= article.view_count |
|
||||
|
|
||||
//- 表单按钮 |
|
||||
.form-actions |
|
||||
.action-buttons |
|
||||
button.btn.btn-primary(type="submit") 💾 更新文章 #{article.id} |
|
||||
a.btn.btn-outline(href=`/admin/articles/${article.id}`) 取消 |
|
||||
button.btn.btn-secondary(type="button" onclick="previewArticle()") 👁️ 预览 |
|
||||
if article.status === 'published' && article.slug |
|
||||
a.btn.btn-outline(href=`/articles/${article.slug}` target="_blank") 🔗 查看前台 |
|
||||
|
|
||||
block $$scripts |
|
||||
script. |
|
||||
// 自动生成slug(仅在为空时) |
|
||||
document.getElementById('title').addEventListener('input', function() { |
|
||||
const title = this.value; |
|
||||
const slugInput = document.getElementById('slug'); |
|
||||
|
|
||||
if (!slugInput.value.trim()) { |
|
||||
// 简单的slug生成逻辑 |
|
||||
const slug = title |
|
||||
.toLowerCase() |
|
||||
.replace(/[^a-z0-9\u4e00-\u9fa5]/g, '-') |
|
||||
.replace(/-+/g, '-') |
|
||||
.replace(/^-|-$/g, ''); |
|
||||
slugInput.value = slug; |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
// 字符计数 |
|
||||
function setupCharCounter(inputId, maxLength) { |
|
||||
const input = document.getElementById(inputId); |
|
||||
const help = input.nextElementSibling; |
|
||||
|
|
||||
function updateCounter() { |
|
||||
const current = input.value.length; |
|
||||
const remaining = maxLength - current; |
|
||||
const originalText = help.textContent.split('(')[0].trim(); |
|
||||
|
|
||||
if (remaining < 50) { |
|
||||
help.textContent = `${originalText} (还可输入${remaining}字符)`; |
|
||||
help.style.color = remaining < 10 ? '#ff4757' : '#ffa726'; |
|
||||
} else { |
|
||||
help.textContent = originalText; |
|
||||
help.style.color = ''; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
input.addEventListener('input', updateCounter); |
|
||||
updateCounter(); |
|
||||
} |
|
||||
|
|
||||
// 设置字符计数 |
|
||||
setupCharCounter('title', 200); |
|
||||
setupCharCounter('excerpt', 500); |
|
||||
setupCharCounter('tags', 200); |
|
||||
|
|
||||
// 预览功能 |
|
||||
function previewArticle() { |
|
||||
const content = document.getElementById('content').value; |
|
||||
if (!content.trim()) { |
|
||||
alert('请先输入文章内容'); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
// 简单的Markdown预览 |
|
||||
const previewWindow = window.open('', '_blank', 'width=800,height=600'); |
|
||||
previewWindow.document.write(` |
|
||||
<!DOCTYPE html> |
|
||||
<html> |
|
||||
<head> |
|
||||
<title>文章预览</title> |
|
||||
<style> |
|
||||
body { font-family: Arial, sans-serif; padding: 20px; line-height: 1.6; } |
|
||||
h1, h2, h3 { color: #333; } |
|
||||
pre { background: #f4f4f4; padding: 10px; border-radius: 4px; } |
|
||||
code { background: #f4f4f4; padding: 2px 4px; border-radius: 2px; } |
|
||||
</style> |
|
||||
</head> |
|
||||
<body> |
|
||||
<h1>${document.getElementById('title').value || '未设置标题'}</h1> |
|
||||
<div>${content.replace(/\n/g, '<br>')}</div> |
|
||||
</body> |
|
||||
</html> |
|
||||
`); |
|
||||
previewWindow.document.close(); |
|
||||
} |
|
||||
|
|
||||
// 表单验证 |
|
||||
document.querySelector('.article-form').addEventListener('submit', function(e) { |
|
||||
const title = document.getElementById('title').value.trim(); |
|
||||
const content = document.getElementById('content').value.trim(); |
|
||||
|
|
||||
if (!title) { |
|
||||
alert('请输入文章标题'); |
|
||||
e.preventDefault(); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
if (!content) { |
|
||||
alert('请输入文章内容'); |
|
||||
e.preventDefault(); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
if (title.length < 5) { |
|
||||
alert('文章标题至少需要5个字符'); |
|
||||
e.preventDefault(); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
if (content.length < 10) { |
|
||||
alert('文章内容至少需要10个字符'); |
|
||||
e.preventDefault(); |
|
||||
return; |
|
||||
} |
|
||||
}); |
|
||||
@ -1,198 +0,0 @@ |
|||||
extends ../../layouts/admin |
|
||||
|
|
||||
block $$content |
|
||||
.page-container |
|
||||
//- 页面头部 |
|
||||
.page-header |
|
||||
.page-header-left |
|
||||
h1.page-title 文章管理 |
|
||||
p.page-subtitle 管理您的所有文章内容 |
|
||||
.page-header-right |
|
||||
a.btn.btn-primary(href="/admin/articles/create") ✨ 新建文章 |
|
||||
|
|
||||
//- 筛选和搜索 |
|
||||
.page-filters |
|
||||
form.filter-form(method="GET") |
|
||||
.filter-group |
|
||||
label.filter-label 状态筛选: |
|
||||
select.filter-select(name="status" onchange="this.form.submit()") |
|
||||
option(value="" selected=!filters.status) 全部状态 |
|
||||
option(value="published" selected=filters.status === 'published') 已发布 |
|
||||
option(value="draft" selected=filters.status === 'draft') 草稿 |
|
||||
|
|
||||
.filter-group |
|
||||
label.filter-label 搜索: |
|
||||
.search-box |
|
||||
input.search-input( |
|
||||
type="text" |
|
||||
name="q" |
|
||||
placeholder="搜索文章标题、内容或标签..." |
|
||||
value=filters.q || '' |
|
||||
) |
|
||||
button.search-btn(type="submit") 🔍 |
|
||||
|
|
||||
if filters.status || filters.q |
|
||||
a.filter-clear(href="/admin/articles") 清除筛选 |
|
||||
|
|
||||
//- 文章列表 |
|
||||
.content-section |
|
||||
if articles && articles.length > 0 |
|
||||
.article-table-container |
|
||||
table.article-table |
|
||||
thead |
|
||||
tr |
|
||||
th.col-title 标题 |
|
||||
th.col-status 状态 |
|
||||
th.col-category 分类 |
|
||||
th.col-date 更新时间 |
|
||||
th.col-actions 操作 |
|
||||
tbody |
|
||||
each article in articles |
|
||||
tr.article-row |
|
||||
td.col-title |
|
||||
.article-title-cell |
|
||||
h3.article-title |
|
||||
a(href=`/admin/articles/${article.id}`)= article.title |
|
||||
if article.excerpt |
|
||||
p.article-summary= article.excerpt.substring(0, 80) + (article.excerpt.length > 80 ? '...' : '') |
|
||||
if article.tags |
|
||||
.article-tags |
|
||||
each tag in article.tags.split(',') |
|
||||
span.tag= tag.trim() |
|
||||
|
|
||||
td.col-status |
|
||||
span.status-badge(class=`status-${article.status}`) |
|
||||
if article.status === 'published' |
|
||||
| ✅ 已发布 |
|
||||
else if article.status === 'draft' |
|
||||
| 📝 草稿 |
|
||||
else |
|
||||
| ❓ #{article.status} |
|
||||
|
|
||||
td.col-category |
|
||||
if article.category |
|
||||
span.category-badge= article.category |
|
||||
else |
|
||||
span.text-muted 未分类 |
|
||||
|
|
||||
td.col-date |
|
||||
.date-info |
|
||||
.primary-date= new Date(article.updated_at).toLocaleDateString('zh-CN') |
|
||||
.secondary-date= new Date(article.updated_at).toLocaleTimeString('zh-CN', {hour: '2-digit', minute: '2-digit'}) |
|
||||
|
|
||||
td.col-actions |
|
||||
.action-buttons |
|
||||
a.btn.btn-sm.btn-outline(href=`/admin/articles/${article.id}` title="查看详情") 👁️ |
|
||||
a.btn.btn-sm.btn-outline(href=`/admin/articles/${article.id}/edit` title="编辑文章") ✏️ |
|
||||
if article.status === 'published' |
|
||||
a.btn.btn-sm.btn-outline(href=`/articles/${article.slug}` target="_blank" title="预览文章") 🔗 |
|
||||
button.btn.btn-sm.btn-danger( |
|
||||
onclick=`deleteArticle(${article.id}, '${article.title.replace(/'/g, "\\'")}')` |
|
||||
title="删除文章" |
|
||||
) 🗑️ |
|
||||
|
|
||||
//- 分页导航 |
|
||||
if pagination.totalPages > 1 |
|
||||
.pagination-container |
|
||||
nav.pagination |
|
||||
if pagination.hasPrev |
|
||||
a.pagination-link(href=`?page=${pagination.page - 1}${filters.status ? '&status=' + filters.status : ''}${filters.q ? '&q=' + encodeURIComponent(filters.q) : ''}`) ‹ 上一页 |
|
||||
|
|
||||
- var start = Math.max(1, pagination.page - 2) |
|
||||
- var end = Math.min(pagination.totalPages, pagination.page + 2) |
|
||||
|
|
||||
if start > 1 |
|
||||
a.pagination-link(href=`?page=1${filters.status ? '&status=' + filters.status : ''}${filters.q ? '&q=' + encodeURIComponent(filters.q) : ''}`) 1 |
|
||||
if start > 2 |
|
||||
span.pagination-ellipsis ... |
|
||||
|
|
||||
- for (var i = start; i <= end; i++) |
|
||||
if i === pagination.page |
|
||||
span.pagination-link.active= i |
|
||||
else |
|
||||
a.pagination-link(href=`?page=${i}${filters.status ? '&status=' + filters.status : ''}${filters.q ? '&q=' + encodeURIComponent(filters.q) : ''}`)= i |
|
||||
|
|
||||
if end < pagination.totalPages |
|
||||
if end < pagination.totalPages - 1 |
|
||||
span.pagination-ellipsis ... |
|
||||
a.pagination-link(href=`?page=${pagination.totalPages}${filters.status ? '&status=' + filters.status : ''}${filters.q ? '&q=' + encodeURIComponent(filters.q) : ''}`)= pagination.totalPages |
|
||||
|
|
||||
if pagination.hasNext |
|
||||
a.pagination-link(href=`?page=${pagination.page + 1}${filters.status ? '&status=' + filters.status : ''}${filters.q ? '&q=' + encodeURIComponent(filters.q) : ''}`) 下一页 › |
|
||||
|
|
||||
//- 统计信息 |
|
||||
.list-footer |
|
||||
.list-stats |
|
||||
| 共 #{pagination.total} 篇文章 |
|
||||
if filters.status || filters.q |
|
||||
|,当前显示筛选结果 |
|
||||
else |
|
||||
//- 空状态 |
|
||||
.empty-state |
|
||||
.empty-icon 📝 |
|
||||
.empty-title 暂无文章 |
|
||||
if filters.status || filters.q |
|
||||
.empty-text 没有找到符合筛选条件的文章 |
|
||||
a.btn.btn-outline(href="/admin/articles") 查看全部文章 |
|
||||
else |
|
||||
.empty-text 您还没有创建任何文章 |
|
||||
a.btn.btn-primary(href="/admin/articles/create") 创建第一篇文章 |
|
||||
|
|
||||
block $$scripts |
|
||||
script. |
|
||||
// 删除文章确认 |
|
||||
function deleteArticle(id, title) { |
|
||||
if (confirm(`确定要删除文章《${title}》吗?此操作不可撤销。`)) { |
|
||||
fetch(`/admin/articles/${id}`, { |
|
||||
method: 'DELETE', |
|
||||
headers: { |
|
||||
'Content-Type': 'application/json', |
|
||||
} |
|
||||
}) |
|
||||
.then(response => response.json()) |
|
||||
.then(data => { |
|
||||
if (data.success) { |
|
||||
// 显示成功消息 |
|
||||
showToast('success', data.message || '文章删除成功'); |
|
||||
// 刷新页面或移除行 |
|
||||
setTimeout(() => { |
|
||||
window.location.reload(); |
|
||||
}, 1000); |
|
||||
} else { |
|
||||
showToast('error', data.message || '删除失败'); |
|
||||
} |
|
||||
}) |
|
||||
.catch(error => { |
|
||||
console.error('删除失败:', error); |
|
||||
showToast('error', '删除失败,请稍后重试'); |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 显示提示消息 |
|
||||
function showToast(type, message) { |
|
||||
const toast = document.createElement('div'); |
|
||||
toast.className = `admin-toast toast-${type}`; |
|
||||
toast.innerHTML = ` |
|
||||
<span>${message}</span> |
|
||||
<button class="toast-close">×</button> |
|
||||
`; |
|
||||
|
|
||||
document.body.appendChild(toast); |
|
||||
|
|
||||
// 自动消失 |
|
||||
setTimeout(() => { |
|
||||
toast.style.opacity = '0'; |
|
||||
setTimeout(() => { |
|
||||
toast.remove(); |
|
||||
}, 300); |
|
||||
}, 3000); |
|
||||
|
|
||||
// 点击关闭 |
|
||||
toast.querySelector('.toast-close').addEventListener('click', () => { |
|
||||
toast.style.opacity = '0'; |
|
||||
setTimeout(() => { |
|
||||
toast.remove(); |
|
||||
}, 300); |
|
||||
}); |
|
||||
} |
|
||||
@ -1,158 +0,0 @@ |
|||||
extends ../../layouts/admin |
|
||||
|
|
||||
block $$content |
|
||||
.page-container |
|
||||
//- 页面头部 |
|
||||
.page-header |
|
||||
.page-header-left |
|
||||
.breadcrumb |
|
||||
a(href="/admin/articles") 文章管理 |
|
||||
span.breadcrumb-separator / |
|
||||
span.breadcrumb-current= article.title |
|
||||
h1.page-title= article.title |
|
||||
.article-meta |
|
||||
span.meta-item |
|
||||
strong 状态: |
|
||||
span.status-badge(class=`status-${article.status}`) |
|
||||
if article.status === 'published' |
|
||||
| ✅ 已发布 |
|
||||
else if article.status === 'draft' |
|
||||
| 📝 草稿 |
|
||||
if article.category |
|
||||
span.meta-item |
|
||||
strong 分类: |
|
||||
span.category-badge= article.category |
|
||||
span.meta-item |
|
||||
strong 创建时间: |
|
||||
span= new Date(article.created_at).toLocaleString('zh-CN') |
|
||||
span.meta-item |
|
||||
strong 更新时间: |
|
||||
span= new Date(article.updated_at).toLocaleString('zh-CN') |
|
||||
if article.view_count |
|
||||
span.meta-item |
|
||||
strong 阅读量: |
|
||||
span= article.view_count |
|
||||
|
|
||||
.page-header-right |
|
||||
.action-buttons |
|
||||
a.btn.btn-outline(href=`/admin/articles/${article.id}/edit`) ✏️ 编辑 |
|
||||
if article.status === 'published' && article.slug |
|
||||
a.btn.btn-outline(href=`/articles/${article.slug}` target="_blank") 🔗 预览 |
|
||||
button.btn.btn-danger( |
|
||||
onclick=`deleteArticle(${article.id}, '${article.title.replace(/'/g, "\\'")}')` |
|
||||
) 🗑️ 删除 |
|
||||
|
|
||||
//- 文章内容 |
|
||||
.content-section |
|
||||
.article-view |
|
||||
//- 文章摘要 |
|
||||
if article.excerpt |
|
||||
.article-summary-section |
|
||||
h3.section-title 文章摘要 |
|
||||
.article-summary= article.excerpt |
|
||||
|
|
||||
//- 文章内容 |
|
||||
.article-content-section |
|
||||
h3.section-title 文章内容 |
|
||||
.article-content |
|
||||
if article.content |
|
||||
!= article.content.replace(/\n/g, '<br>') |
|
||||
else |
|
||||
.empty-content 暂无内容 |
|
||||
|
|
||||
//- 文章标签 |
|
||||
if article.tags |
|
||||
.article-tags-section |
|
||||
h3.section-title 标签 |
|
||||
.article-tags |
|
||||
each tag in article.tags.split(',') |
|
||||
span.tag= tag.trim() |
|
||||
|
|
||||
//- 文章链接信息 |
|
||||
if article.slug |
|
||||
.article-link-section |
|
||||
h3.section-title 文章链接 |
|
||||
.link-info |
|
||||
strong 访问链接: |
|
||||
if article.status === 'published' |
|
||||
a.article-link(href=`/articles/${article.slug}` target="_blank")= `/articles/${article.slug}` |
|
||||
else |
|
||||
span.text-muted= `/articles/${article.slug}` |
|
||||
small (草稿状态,暂不可访问) |
|
||||
|
|
||||
//- 技术信息 |
|
||||
.article-technical-section |
|
||||
h3.section-title 技术信息 |
|
||||
.technical-info |
|
||||
.info-grid |
|
||||
.info-item |
|
||||
.info-label ID |
|
||||
.info-value= article.id |
|
||||
.info-item |
|
||||
.info-label Slug |
|
||||
.info-value= article.slug || '未设置' |
|
||||
.info-item |
|
||||
.info-label 作者ID |
|
||||
.info-value= article.author |
|
||||
if article.featured_image |
|
||||
.info-item |
|
||||
.info-label 特色图片 |
|
||||
.info-value |
|
||||
a(href=article.featured_image target="_blank")= article.featured_image |
|
||||
|
|
||||
block $$scripts |
|
||||
script. |
|
||||
// 删除文章确认 |
|
||||
function deleteArticle(id, title) { |
|
||||
if (confirm(`确定要删除文章《${title}》吗?此操作不可撤销。`)) { |
|
||||
fetch(`/admin/articles/${id}`, { |
|
||||
method: 'DELETE', |
|
||||
headers: { |
|
||||
'Content-Type': 'application/json', |
|
||||
} |
|
||||
}) |
|
||||
.then(response => response.json()) |
|
||||
.then(data => { |
|
||||
if (data.success) { |
|
||||
showToast('success', data.message || '文章删除成功'); |
|
||||
setTimeout(() => { |
|
||||
window.location.href = '/admin/articles'; |
|
||||
}, 1000); |
|
||||
} else { |
|
||||
showToast('error', data.message || '删除失败'); |
|
||||
} |
|
||||
}) |
|
||||
.catch(error => { |
|
||||
console.error('删除失败:', error); |
|
||||
showToast('error', '删除失败,请稍后重试'); |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 显示提示消息 |
|
||||
function showToast(type, message) { |
|
||||
const toast = document.createElement('div'); |
|
||||
toast.className = `admin-toast toast-${type}`; |
|
||||
toast.innerHTML = ` |
|
||||
<span>${message}</span> |
|
||||
<button class="toast-close">×</button> |
|
||||
`; |
|
||||
|
|
||||
document.body.appendChild(toast); |
|
||||
|
|
||||
// 自动消失 |
|
||||
setTimeout(() => { |
|
||||
toast.style.opacity = '0'; |
|
||||
setTimeout(() => { |
|
||||
toast.remove(); |
|
||||
}, 300); |
|
||||
}, 3000); |
|
||||
|
|
||||
// 点击关闭 |
|
||||
toast.querySelector('.toast-close').addEventListener('click', () => { |
|
||||
toast.style.opacity = '0'; |
|
||||
setTimeout(() => { |
|
||||
toast.remove(); |
|
||||
}, 300); |
|
||||
}); |
|
||||
} |
|
||||
@ -1,243 +0,0 @@ |
|||||
extends ../../layouts/admin |
|
||||
|
|
||||
block $$content |
|
||||
.page-container |
|
||||
//- 页面头部 |
|
||||
.page-header |
|
||||
.page-header-left |
|
||||
h1.page-title 联系信息管理 |
|
||||
p.page-subtitle 管理用户提交的联系表单信息 |
|
||||
.page-header-right |
|
||||
.stats-summary |
|
||||
span.stat-item |
|
||||
.stat-number= pagination.total |
|
||||
.stat-label 总数 |
|
||||
span.stat-item.unread |
|
||||
.stat-number |
|
||||
| #{contacts.filter(c => c.status === 'unread').length} |
|
||||
.stat-label 未读 |
|
||||
|
|
||||
//- 筛选和搜索 |
|
||||
.page-filters |
|
||||
form.filter-form(method="GET") |
|
||||
.filter-group |
|
||||
label.filter-label 状态筛选: |
|
||||
select.filter-select(name="status" onchange="this.form.submit()") |
|
||||
option(value="" selected=!filters.status) 全部状态 |
|
||||
option(value="unread" selected=filters.status === 'unread') 📧 未读 |
|
||||
option(value="read" selected=filters.status === 'read') 👁️ 已读 |
|
||||
option(value="replied" selected=filters.status === 'replied') ✅ 已回复 |
|
||||
|
|
||||
.filter-group |
|
||||
label.filter-label 搜索: |
|
||||
.search-box |
|
||||
input.search-input( |
|
||||
type="text" |
|
||||
name="q" |
|
||||
placeholder="搜索姓名、邮箱、主题或内容..." |
|
||||
value=filters.q || '' |
|
||||
) |
|
||||
button.search-btn(type="submit") 🔍 |
|
||||
|
|
||||
if filters.status || filters.q |
|
||||
a.filter-clear(href="/admin/contacts") 清除筛选 |
|
||||
|
|
||||
//- 联系信息列表 |
|
||||
.content-section |
|
||||
if contacts && contacts.length > 0 |
|
||||
.contact-table-container |
|
||||
table.contact-table |
|
||||
thead |
|
||||
tr |
|
||||
th.col-contact 联系人信息 |
|
||||
th.col-subject 主题 |
|
||||
th.col-status 状态 |
|
||||
th.col-date 提交时间 |
|
||||
th.col-actions 操作 |
|
||||
tbody |
|
||||
each contact in contacts |
|
||||
tr.contact-row(class=`status-${contact.status}`) |
|
||||
td.col-contact |
|
||||
.contact-info |
|
||||
.contact-name= contact.name |
|
||||
.contact-email= contact.email |
|
||||
if contact.ip_address |
|
||||
.contact-ip IP: #{contact.ip_address} |
|
||||
|
|
||||
td.col-subject |
|
||||
.subject-content |
|
||||
h4.subject-title= contact.subject |
|
||||
.subject-preview= contact.message.substring(0, 80) + (contact.message.length > 80 ? '...' : '') |
|
||||
|
|
||||
td.col-status |
|
||||
span.status-badge(class=`status-${contact.status}`) |
|
||||
if contact.status === 'unread' |
|
||||
| 📧 未读 |
|
||||
else if contact.status === 'read' |
|
||||
| 👁️ 已读 |
|
||||
else if contact.status === 'replied' |
|
||||
| ✅ 已回复 |
|
||||
else |
|
||||
| ❓ #{contact.status} |
|
||||
|
|
||||
td.col-date |
|
||||
.date-info |
|
||||
.primary-date= new Date(contact.created_at).toLocaleDateString('zh-CN') |
|
||||
.secondary-date= new Date(contact.created_at).toLocaleTimeString('zh-CN', {hour: '2-digit', minute: '2-digit'}) |
|
||||
|
|
||||
td.col-actions |
|
||||
.action-buttons |
|
||||
a.btn.btn-sm.btn-outline(href=`/admin/contacts/${contact.id}` title="查看详情") 👁️ |
|
||||
|
|
||||
.status-dropdown |
|
||||
select.status-select(onchange=`updateContactStatus(${contact.id}, this.value)`) |
|
||||
option(value="" disabled selected) 状态... |
|
||||
option(value="unread" class=contact.status === 'unread' ? 'disabled' : '') 标记未读 |
|
||||
option(value="read" class=contact.status === 'read' ? 'disabled' : '') 标记已读 |
|
||||
option(value="replied" class=contact.status === 'replied' ? 'disabled' : '') 标记已回复 |
|
||||
|
|
||||
button.btn.btn-sm.btn-danger( |
|
||||
onclick=`deleteContact(${contact.id}, '${contact.name} - ${contact.subject}')` |
|
||||
title="删除联系信息" |
|
||||
) 🗑️ |
|
||||
|
|
||||
//- 分页导航 |
|
||||
if pagination.totalPages > 1 |
|
||||
.pagination-container |
|
||||
nav.pagination |
|
||||
if pagination.hasPrev |
|
||||
a.pagination-link(href=`?page=${pagination.page - 1}${filters.status ? '&status=' + filters.status : ''}${filters.q ? '&q=' + encodeURIComponent(filters.q) : ''}`) ‹ 上一页 |
|
||||
|
|
||||
- var start = Math.max(1, pagination.page - 2) |
|
||||
- var end = Math.min(pagination.totalPages, pagination.page + 2) |
|
||||
|
|
||||
if start > 1 |
|
||||
a.pagination-link(href=`?page=1${filters.status ? '&status=' + filters.status : ''}${filters.q ? '&q=' + encodeURIComponent(filters.q) : ''}`) 1 |
|
||||
if start > 2 |
|
||||
span.pagination-ellipsis ... |
|
||||
|
|
||||
- for (var i = start; i <= end; i++) |
|
||||
if i === pagination.page |
|
||||
span.pagination-link.active= i |
|
||||
else |
|
||||
a.pagination-link(href=`?page=${i}${filters.status ? '&status=' + filters.status : ''}${filters.q ? '&q=' + encodeURIComponent(filters.q) : ''}`)= i |
|
||||
|
|
||||
if end < pagination.totalPages |
|
||||
if end < pagination.totalPages - 1 |
|
||||
span.pagination-ellipsis ... |
|
||||
a.pagination-link(href=`?page=${pagination.totalPages}${filters.status ? '&status=' + filters.status : ''}${filters.q ? '&q=' + encodeURIComponent(filters.q) : ''}`)= pagination.totalPages |
|
||||
|
|
||||
if pagination.hasNext |
|
||||
a.pagination-link(href=`?page=${pagination.page + 1}${filters.status ? '&status=' + filters.status : ''}${filters.q ? '&q=' + encodeURIComponent(filters.q) : ''}`) 下一页 › |
|
||||
|
|
||||
//- 统计信息 |
|
||||
.list-footer |
|
||||
.list-stats |
|
||||
| 共 #{pagination.total} 条联系信息 |
|
||||
if filters.status || filters.q |
|
||||
|,当前显示筛选结果 |
|
||||
|
|
||||
//- 批量操作(如果需要的话) |
|
||||
.bulk-actions |
|
||||
.bulk-info 提示:点击联系人姓名可以查看完整信息 |
|
||||
else |
|
||||
//- 空状态 |
|
||||
.empty-state |
|
||||
.empty-icon 📧 |
|
||||
.empty-title 暂无联系信息 |
|
||||
if filters.status || filters.q |
|
||||
.empty-text 没有找到符合筛选条件的联系信息 |
|
||||
a.btn.btn-outline(href="/admin/contacts") 查看全部联系信息 |
|
||||
else |
|
||||
.empty-text 还没有用户提交联系表单 |
|
||||
.empty-help 用户可以通过前台的联系页面提交联系信息 |
|
||||
|
|
||||
block $$scripts |
|
||||
script. |
|
||||
// 更新联系信息状态 |
|
||||
function updateContactStatus(id, status) { |
|
||||
if (!status) return; |
|
||||
|
|
||||
fetch(`/admin/contacts/${id}/status`, { |
|
||||
method: 'PUT', |
|
||||
headers: { |
|
||||
'Content-Type': 'application/json', |
|
||||
}, |
|
||||
body: JSON.stringify({ status }) |
|
||||
}) |
|
||||
.then(response => response.json()) |
|
||||
.then(data => { |
|
||||
if (data.success) { |
|
||||
showToast('success', data.message || '状态更新成功'); |
|
||||
setTimeout(() => { |
|
||||
window.location.reload(); |
|
||||
}, 1000); |
|
||||
} else { |
|
||||
showToast('error', data.message || '状态更新失败'); |
|
||||
} |
|
||||
}) |
|
||||
.catch(error => { |
|
||||
console.error('状态更新失败:', error); |
|
||||
showToast('error', '状态更新失败,请稍后重试'); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
// 删除联系信息确认 |
|
||||
function deleteContact(id, title) { |
|
||||
if (confirm(`确定要删除联系信息《${title}》吗?此操作不可撤销。`)) { |
|
||||
fetch(`/admin/contacts/${id}`, { |
|
||||
method: 'DELETE', |
|
||||
headers: { |
|
||||
'Content-Type': 'application/json', |
|
||||
} |
|
||||
}) |
|
||||
.then(response => response.json()) |
|
||||
.then(data => { |
|
||||
if (data.success) { |
|
||||
showToast('success', data.message || '联系信息删除成功'); |
|
||||
setTimeout(() => { |
|
||||
window.location.reload(); |
|
||||
}, 1000); |
|
||||
} else { |
|
||||
showToast('error', data.message || '删除失败'); |
|
||||
} |
|
||||
}) |
|
||||
.catch(error => { |
|
||||
console.error('删除失败:', error); |
|
||||
showToast('error', '删除失败,请稍后重试'); |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 显示提示消息 |
|
||||
function showToast(type, message) { |
|
||||
const toast = document.createElement('div'); |
|
||||
toast.className = `admin-toast toast-${type}`; |
|
||||
toast.innerHTML = ` |
|
||||
<span>${message}</span> |
|
||||
<button class="toast-close">×</button> |
|
||||
`; |
|
||||
|
|
||||
document.body.appendChild(toast); |
|
||||
|
|
||||
// 自动消失 |
|
||||
setTimeout(() => { |
|
||||
toast.style.opacity = '0'; |
|
||||
setTimeout(() => { |
|
||||
toast.remove(); |
|
||||
}, 300); |
|
||||
}, 3000); |
|
||||
|
|
||||
// 点击关闭 |
|
||||
toast.querySelector('.toast-close').addEventListener('click', () => { |
|
||||
toast.style.opacity = '0'; |
|
||||
setTimeout(() => { |
|
||||
toast.remove(); |
|
||||
}, 300); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
// 重置状态下拉框 |
|
||||
document.querySelectorAll('.status-select').forEach(select => { |
|
||||
select.value = ''; |
|
||||
}); |
|
||||
@ -1,229 +0,0 @@ |
|||||
extends ../../layouts/admin |
|
||||
|
|
||||
block $$content |
|
||||
.page-container |
|
||||
//- 页面头部 |
|
||||
.page-header |
|
||||
.page-header-left |
|
||||
.breadcrumb |
|
||||
a(href="/admin/contacts") 联系信息管理 |
|
||||
span.breadcrumb-separator / |
|
||||
span.breadcrumb-current= contact.subject |
|
||||
h1.page-title= contact.subject |
|
||||
.contact-meta |
|
||||
span.meta-item |
|
||||
strong 状态: |
|
||||
span.status-badge(class=`status-${contact.status}`) |
|
||||
if contact.status === 'unread' |
|
||||
| 📧 未读 |
|
||||
else if contact.status === 'read' |
|
||||
| 👁️ 已读 |
|
||||
else if contact.status === 'replied' |
|
||||
| ✅ 已回复 |
|
||||
span.meta-item |
|
||||
strong 提交时间: |
|
||||
span= new Date(contact.created_at).toLocaleString('zh-CN') |
|
||||
|
|
||||
.page-header-right |
|
||||
.action-buttons |
|
||||
.status-actions |
|
||||
if contact.status !== 'read' |
|
||||
button.btn.btn-outline(onclick=`updateContactStatus(${contact.id}, 'read')`) 👁️ 标记已读 |
|
||||
if contact.status !== 'replied' |
|
||||
button.btn.btn-success(onclick=`updateContactStatus(${contact.id}, 'replied')`) ✅ 标记已回复 |
|
||||
if contact.status !== 'unread' |
|
||||
button.btn.btn-outline(onclick=`updateContactStatus(${contact.id}, 'unread')`) 📧 标记未读 |
|
||||
|
|
||||
button.btn.btn-danger( |
|
||||
onclick=`deleteContact(${contact.id}, '${contact.name} - ${contact.subject}')` |
|
||||
) 🗑️ 删除 |
|
||||
|
|
||||
//- 联系信息详情 |
|
||||
.content-section |
|
||||
.contact-details |
|
||||
//- 联系人信息 |
|
||||
.detail-section |
|
||||
h3.section-title 联系人信息 |
|
||||
.info-grid |
|
||||
.info-item |
|
||||
.info-label 姓名 |
|
||||
.info-value= contact.name |
|
||||
.info-item |
|
||||
.info-label 邮箱 |
|
||||
.info-value |
|
||||
a(href=`mailto:${contact.email}`)= contact.email |
|
||||
if contact.ip_address |
|
||||
.info-item |
|
||||
.info-label IP地址 |
|
||||
.info-value= contact.ip_address |
|
||||
if contact.user_agent |
|
||||
.info-item |
|
||||
.info-label 浏览器信息 |
|
||||
.info-value.user-agent= contact.user_agent |
|
||||
|
|
||||
//- 联系主题 |
|
||||
.detail-section |
|
||||
h3.section-title 联系主题 |
|
||||
.subject-content= contact.subject |
|
||||
|
|
||||
//- 联系内容 |
|
||||
.detail-section |
|
||||
h3.section-title 联系内容 |
|
||||
.message-content |
|
||||
.message-text= contact.message |
|
||||
|
|
||||
//- 系统信息 |
|
||||
.detail-section |
|
||||
h3.section-title 系统信息 |
|
||||
.info-grid |
|
||||
.info-item |
|
||||
.info-label ID |
|
||||
.info-value= contact.id |
|
||||
.info-item |
|
||||
.info-label 创建时间 |
|
||||
.info-value= new Date(contact.created_at).toLocaleString('zh-CN') |
|
||||
.info-item |
|
||||
.info-label 更新时间 |
|
||||
.info-value= new Date(contact.updated_at).toLocaleString('zh-CN') |
|
||||
.info-item |
|
||||
.info-label 当前状态 |
|
||||
.info-value |
|
||||
span.status-badge(class=`status-${contact.status}`) |
|
||||
if contact.status === 'unread' |
|
||||
| 📧 未读 |
|
||||
else if contact.status === 'read' |
|
||||
| 👁️ 已读 |
|
||||
else if contact.status === 'replied' |
|
||||
| ✅ 已回复 |
|
||||
|
|
||||
//- 快速回复(可选功能) |
|
||||
.detail-section |
|
||||
h3.section-title 快速操作 |
|
||||
.quick-actions |
|
||||
.action-group |
|
||||
h4.action-title 邮件操作 |
|
||||
.action-buttons |
|
||||
a.btn.btn-primary(href=`mailto:${contact.email}?subject=Re: ${encodeURIComponent(contact.subject)}&body=${encodeURIComponent('您好,感谢您的联系...')}`) 📧 回复邮件 |
|
||||
button.btn.btn-outline(onclick="copyEmail()") 📋 复制邮箱 |
|
||||
|
|
||||
.action-group |
|
||||
h4.action-title 状态操作 |
|
||||
.action-buttons |
|
||||
if contact.status === 'unread' |
|
||||
button.btn.btn-success(onclick=`updateContactStatus(${contact.id}, 'read')`) 标记已读 |
|
||||
button.btn.btn-primary(onclick=`updateContactStatus(${contact.id}, 'replied')`) 标记已回复 |
|
||||
else if contact.status === 'read' |
|
||||
button.btn.btn-primary(onclick=`updateContactStatus(${contact.id}, 'replied')`) 标记已回复 |
|
||||
button.btn.btn-outline(onclick=`updateContactStatus(${contact.id}, 'unread')`) 标记未读 |
|
||||
else if contact.status === 'replied' |
|
||||
button.btn.btn-outline(onclick=`updateContactStatus(${contact.id}, 'read')`) 标记已读 |
|
||||
button.btn.btn-outline(onclick=`updateContactStatus(${contact.id}, 'unread')`) 标记未读 |
|
||||
|
|
||||
//- 导航 |
|
||||
.detail-navigation |
|
||||
a.btn.btn-outline(href="/admin/contacts") ← 返回列表 |
|
||||
|
|
||||
block $$scripts |
|
||||
script. |
|
||||
// 更新联系信息状态 |
|
||||
function updateContactStatus(id, status) { |
|
||||
fetch(`/admin/contacts/${id}/status`, { |
|
||||
method: 'PUT', |
|
||||
headers: { |
|
||||
'Content-Type': 'application/json', |
|
||||
}, |
|
||||
body: JSON.stringify({ status }) |
|
||||
}) |
|
||||
.then(response => response.json()) |
|
||||
.then(data => { |
|
||||
if (data.success) { |
|
||||
showToast('success', data.message || '状态更新成功'); |
|
||||
setTimeout(() => { |
|
||||
window.location.reload(); |
|
||||
}, 1000); |
|
||||
} else { |
|
||||
showToast('error', data.message || '状态更新失败'); |
|
||||
} |
|
||||
}) |
|
||||
.catch(error => { |
|
||||
console.error('状态更新失败:', error); |
|
||||
showToast('error', '状态更新失败,请稍后重试'); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
// 删除联系信息确认 |
|
||||
function deleteContact(id, title) { |
|
||||
if (confirm(`确定要删除联系信息《${title}》吗?此操作不可撤销。`)) { |
|
||||
fetch(`/admin/contacts/${id}`, { |
|
||||
method: 'DELETE', |
|
||||
headers: { |
|
||||
'Content-Type': 'application/json', |
|
||||
} |
|
||||
}) |
|
||||
.then(response => response.json()) |
|
||||
.then(data => { |
|
||||
if (data.success) { |
|
||||
showToast('success', data.message || '联系信息删除成功'); |
|
||||
setTimeout(() => { |
|
||||
window.location.href = '/admin/contacts'; |
|
||||
}, 1000); |
|
||||
} else { |
|
||||
showToast('error', data.message || '删除失败'); |
|
||||
} |
|
||||
}) |
|
||||
.catch(error => { |
|
||||
console.error('删除失败:', error); |
|
||||
showToast('error', '删除失败,请稍后重试'); |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 复制邮箱地址 |
|
||||
function copyEmail() { |
|
||||
const email = '#{contact.email}'; |
|
||||
navigator.clipboard.writeText(email).then(() => { |
|
||||
showToast('success', '邮箱地址已复制到剪贴板'); |
|
||||
}).catch(err => { |
|
||||
console.error('复制失败:', err); |
|
||||
// 降级处理 |
|
||||
const textArea = document.createElement('textarea'); |
|
||||
textArea.value = email; |
|
||||
document.body.appendChild(textArea); |
|
||||
textArea.select(); |
|
||||
try { |
|
||||
document.execCommand('copy'); |
|
||||
showToast('success', '邮箱地址已复制到剪贴板'); |
|
||||
} catch (fallbackErr) { |
|
||||
showToast('error', '复制失败,请手动复制'); |
|
||||
} |
|
||||
document.body.removeChild(textArea); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
// 显示提示消息 |
|
||||
function showToast(type, message) { |
|
||||
const toast = document.createElement('div'); |
|
||||
toast.className = `admin-toast toast-${type}`; |
|
||||
toast.innerHTML = ` |
|
||||
<span>${message}</span> |
|
||||
<button class="toast-close">×</button> |
|
||||
`; |
|
||||
|
|
||||
document.body.appendChild(toast); |
|
||||
|
|
||||
// 自动消失 |
|
||||
setTimeout(() => { |
|
||||
toast.style.opacity = '0'; |
|
||||
setTimeout(() => { |
|
||||
toast.remove(); |
|
||||
}, 300); |
|
||||
}, 3000); |
|
||||
|
|
||||
// 点击关闭 |
|
||||
toast.querySelector('.toast-close').addEventListener('click', () => { |
|
||||
toast.style.opacity = '0'; |
|
||||
setTimeout(() => { |
|
||||
toast.remove(); |
|
||||
}, 300); |
|
||||
}); |
|
||||
} |
|
||||
@ -1,125 +0,0 @@ |
|||||
extends ../layouts/admin |
|
||||
|
|
||||
block $$content |
|
||||
.dashboard |
|
||||
//- 页面标题 |
|
||||
.page-header |
|
||||
h1.page-title 仪表盘 |
|
||||
p.page-subtitle 欢迎回到后台管理系统 |
|
||||
|
|
||||
//- 统计卡片 |
|
||||
.stats-grid |
|
||||
.stats-card |
|
||||
.stats-icon.stats-icon-primary 📧 |
|
||||
.stats-info |
|
||||
.stats-number= contactStats.total |
|
||||
.stats-label 总联系信息 |
|
||||
.stats-detail |
|
||||
.stats-breakdown |
|
||||
span.breakdown-item |
|
||||
span.breakdown-label 未读: |
|
||||
span.breakdown-value= contactStats.unread |
|
||||
span.breakdown-item |
|
||||
span.breakdown-label 已读: |
|
||||
span.breakdown-value= contactStats.read |
|
||||
span.breakdown-item |
|
||||
span.breakdown-label 已回复: |
|
||||
span.breakdown-value= contactStats.replied |
|
||||
|
|
||||
.stats-card |
|
||||
.stats-icon.stats-icon-success 📝 |
|
||||
.stats-info |
|
||||
.stats-number= articleStats.total |
|
||||
.stats-label 我的文章 |
|
||||
.stats-detail |
|
||||
.stats-breakdown |
|
||||
span.breakdown-item |
|
||||
span.breakdown-label 已发布: |
|
||||
span.breakdown-value= articleStats.published |
|
||||
span.breakdown-item |
|
||||
span.breakdown-label 草稿: |
|
||||
span.breakdown-value= articleStats.draft |
|
||||
|
|
||||
//- 主要内容区域 |
|
||||
.dashboard-content |
|
||||
.dashboard-grid |
|
||||
//- 最近联系信息 |
|
||||
.dashboard-section |
|
||||
.section-header |
|
||||
h2.section-title 最近联系信息 |
|
||||
a.section-action(href="/admin/contacts") 查看全部 → |
|
||||
|
|
||||
if recentContacts && recentContacts.length > 0 |
|
||||
.contact-list |
|
||||
each contact in recentContacts |
|
||||
.contact-item(class=`status-${contact.status}`) |
|
||||
.contact-header |
|
||||
.contact-info |
|
||||
strong.contact-name= contact.name |
|
||||
span.contact-email= contact.email |
|
||||
.contact-meta |
|
||||
span.contact-status(class=`status-${contact.status}`) |
|
||||
if contact.status === 'unread' |
|
||||
| 未读 |
|
||||
else if contact.status === 'read' |
|
||||
| 已读 |
|
||||
else if contact.status === 'replied' |
|
||||
| 已回复 |
|
||||
span.contact-date= new Date(contact.created_at).toLocaleDateString('zh-CN') |
|
||||
.contact-subject= contact.subject |
|
||||
.contact-actions |
|
||||
a.btn.btn-sm(href=`/admin/contacts/${contact.id}`) 查看详情 |
|
||||
else |
|
||||
.empty-state |
|
||||
.empty-icon 📧 |
|
||||
.empty-text 暂无联系信息 |
|
||||
|
|
||||
//- 最近文章 |
|
||||
.dashboard-section |
|
||||
.section-header |
|
||||
h2.section-title 最近文章 |
|
||||
a.section-action(href="/admin/articles") 查看全部 → |
|
||||
|
|
||||
if recentArticles && recentArticles.length > 0 |
|
||||
.article-list |
|
||||
each article in recentArticles |
|
||||
.article-item |
|
||||
.article-header |
|
||||
.article-info |
|
||||
strong.article-title= article.title |
|
||||
span.article-status(class=`status-${article.status}`) |
|
||||
if article.status === 'published' |
|
||||
| 已发布 |
|
||||
else if article.status === 'draft' |
|
||||
| 草稿 |
|
||||
.article-meta |
|
||||
span.article-date= new Date(article.updated_at).toLocaleDateString('zh-CN') |
|
||||
if article.excerpt |
|
||||
.article-summary= article.excerpt.substring(0, 100) + (article.excerpt.length > 100 ? '...' : '') |
|
||||
.article-actions |
|
||||
a.btn.btn-sm(href=`/admin/articles/${article.id}`) 查看 |
|
||||
a.btn.btn-sm.btn-outline(href=`/admin/articles/${article.id}/edit`) 编辑 |
|
||||
else |
|
||||
.empty-state |
|
||||
.empty-icon 📝 |
|
||||
.empty-text 暂无文章 |
|
||||
a.btn.btn-primary(href="/admin/articles/create") 创建第一篇文章 |
|
||||
|
|
||||
//- 快速操作 |
|
||||
.quick-actions |
|
||||
h3.quick-actions-title 快速操作 |
|
||||
.quick-actions-grid |
|
||||
a.quick-action-card(href="/admin/articles/create") |
|
||||
.quick-action-icon 📝 |
|
||||
.quick-action-title 新建文章 |
|
||||
.quick-action-desc 创建一篇新的文章 |
|
||||
|
|
||||
a.quick-action-card(href="/admin/articles") |
|
||||
.quick-action-icon 📚 |
|
||||
.quick-action-title 管理文章 |
|
||||
.quick-action-desc 查看和编辑我的文章 |
|
||||
|
|
||||
a.quick-action-card(href="/admin/contacts") |
|
||||
.quick-action-icon 📧 |
|
||||
.quick-action-title 联系信息 |
|
||||
.quick-action-desc 查看用户联系信息 |
|
||||
@ -1,128 +0,0 @@ |
|||||
include utils.pug |
|
||||
|
|
||||
doctype html |
|
||||
html(lang="zh-CN") |
|
||||
head |
|
||||
block $$head |
|
||||
title #{title ? title + ' - ' : ''}后台管理 - #{$site && $site.site_title || ''} |
|
||||
meta(name="description" content="后台管理系统") |
|
||||
meta(charset="utf-8") |
|
||||
meta(name="viewport" content="width=device-width, initial-scale=1") |
|
||||
+css('lib/reset.css') |
|
||||
+css('css/admin.css') |
|
||||
+js('lib/htmx.min.js') |
|
||||
+js('lib/tailwindcss.3.4.17.js') |
|
||||
body.admin-body |
|
||||
#admin-app |
|
||||
//- 顶部导航栏 |
|
||||
header.admin-header |
|
||||
.admin-header-left |
|
||||
.admin-logo |
|
||||
a(href="/admin") 后台管理 |
|
||||
.admin-header-center |
|
||||
.admin-breadcrumb |
|
||||
span.breadcrumb-item |
|
||||
a(href="/admin") 首页 |
|
||||
if title |
|
||||
span.breadcrumb-separator / |
|
||||
span.breadcrumb-item= title |
|
||||
.admin-header-right |
|
||||
.admin-user-menu |
|
||||
.dropdown |
|
||||
button.dropdown-trigger |
|
||||
span= $user ? $user.name || $user.username : '用户' |
|
||||
i.dropdown-arrow ▼ |
|
||||
.dropdown-menu |
|
||||
a.dropdown-item(href="/profile") 个人资料 |
|
||||
a.dropdown-item(href="/") 前台首页 |
|
||||
.dropdown-divider |
|
||||
a.dropdown-item(href="/logout") 退出登录 |
|
||||
|
|
||||
//- 主要内容区域 |
|
||||
.admin-main |
|
||||
//- 左侧导航 |
|
||||
aside.admin-sidebar |
|
||||
nav.admin-nav |
|
||||
.nav-section |
|
||||
.nav-title 主要功能 |
|
||||
ul.nav-list |
|
||||
li.nav-item |
|
||||
a.nav-link(href="/admin" class=title === '后台管理' ? 'active' : '') |
|
||||
i.nav-icon 📊 |
|
||||
span 仪表盘 |
|
||||
li.nav-item |
|
||||
a.nav-link(href="/admin/articles" class=title === '文章管理' ? 'active' : '') |
|
||||
i.nav-icon 📝 |
|
||||
span 文章管理 |
|
||||
li.nav-item |
|
||||
a.nav-link(href="/admin/contacts" class=title === '联系信息管理' ? 'active' : '') |
|
||||
i.nav-icon 📧 |
|
||||
span 联系信息 |
|
||||
|
|
||||
//- 右侧内容区域 |
|
||||
main.admin-content |
|
||||
if toast |
|
||||
.admin-toast(class=`toast-${toast.type}`) |
|
||||
span= toast.message |
|
||||
button.toast-close × |
|
||||
|
|
||||
.admin-content-inner |
|
||||
block $$content |
|
||||
|
|
||||
//- JavaScript |
|
||||
script(src="/js/admin.js") |
|
||||
block $$scripts |
|
||||
|
|
||||
//- 处理Toast消息 |
|
||||
script. |
|
||||
// Toast消息处理 |
|
||||
const toast = document.querySelector('.admin-toast'); |
|
||||
if (toast) { |
|
||||
setTimeout(() => { |
|
||||
toast.style.opacity = '0'; |
|
||||
setTimeout(() => { |
|
||||
toast.remove(); |
|
||||
}, 300); |
|
||||
}, 3000); |
|
||||
|
|
||||
const closeBtn = toast.querySelector('.toast-close'); |
|
||||
if (closeBtn) { |
|
||||
closeBtn.addEventListener('click', () => { |
|
||||
toast.style.opacity = '0'; |
|
||||
setTimeout(() => { |
|
||||
toast.remove(); |
|
||||
}, 300); |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 下拉菜单处理 |
|
||||
const dropdown = document.querySelector('.dropdown'); |
|
||||
if (dropdown) { |
|
||||
const trigger = dropdown.querySelector('.dropdown-trigger'); |
|
||||
const menu = dropdown.querySelector('.dropdown-menu'); |
|
||||
|
|
||||
trigger.addEventListener('click', (e) => { |
|
||||
e.stopPropagation(); |
|
||||
dropdown.classList.toggle('active'); |
|
||||
}); |
|
||||
|
|
||||
document.addEventListener('click', () => { |
|
||||
dropdown.classList.remove('active'); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
// 移动端侧边栏切换 |
|
||||
function toggleSidebar() { |
|
||||
document.body.classList.toggle('sidebar-open'); |
|
||||
} |
|
||||
|
|
||||
// 响应式处理 |
|
||||
function handleResize() { |
|
||||
if (window.innerWidth > 768) { |
|
||||
document.body.classList.remove('sidebar-open'); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
window.addEventListener('resize', handleResize); |
|
||||
handleResize(); |
|
||||
@ -1,58 +0,0 @@ |
|||||
mixin include() |
|
||||
if block |
|
||||
block |
|
||||
|
|
||||
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 |
|
||||
|
|
||||
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') |
|
||||
+js('lib/htmx.min.js') |
|
||||
+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 { |
|
||||
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") |
|
||||
block content |
|
||||
block scripts |
|
||||
+js('lib/bg-change.js') |
|
||||
@ -1,18 +0,0 @@ |
|||||
extends /layouts/root.pug |
|
||||
//- 采用纯背景页面的布局,背景图片随机切换,卡片采用高斯滤镜类玻璃化效果 |
|
||||
//- .card |
|
||||
|
|
||||
block $$head |
|
||||
+css('css/layouts/bg-page.css') |
|
||||
block pageHead |
|
||||
|
|
||||
block $$content |
|
||||
.page-layout |
|
||||
.page |
|
||||
block pageContent |
|
||||
footer |
|
||||
include /htmx/footer.pug |
|
||||
|
|
||||
block $$scripts |
|
||||
+js('lib/bg-change.js') |
|
||||
block pageScripts |
|
||||
@ -1,15 +0,0 @@ |
|||||
extends /layouts/root.pug |
|
||||
|
|
||||
block $$head |
|
||||
+css('css/layouts/empty.css') |
|
||||
block pageHead |
|
||||
|
|
||||
block $$content |
|
||||
include /htmx/navbar/index.pug |
|
||||
.page-layout |
|
||||
.page.my-5 |
|
||||
block pageContent |
|
||||
include /htmx/footer/index.pug |
|
||||
|
|
||||
block $$scripts |
|
||||
block pageScripts |
|
||||
@ -1,31 +0,0 @@ |
|||||
extends /layouts/base.pug |
|
||||
|
|
||||
block head |
|
||||
+css('styles.css') |
|
||||
block pageHead |
|
||||
|
|
||||
block content |
|
||||
.page-layout |
|
||||
.page |
|
||||
- const navs = []; |
|
||||
- navs.push({ href: '/', label: '首页' }); |
|
||||
- navs.push({ href: '/articles', label: '文章' }); |
|
||||
- navs.push({ href: '/article', label: '收藏' }); |
|
||||
- navs.push({ href: '/about', label: '关于' }); |
|
||||
nav.nav |
|
||||
ul.flota-nav |
|
||||
each nav in navs |
|
||||
li |
|
||||
a.item( |
|
||||
href=nav.href, |
|
||||
class=currentPath === nav.href ? 'active' : '' |
|
||||
) #{nav.label} |
|
||||
.content |
|
||||
block pageContent |
|
||||
footer |
|
||||
+include() |
|
||||
- var edit = false |
|
||||
include /htmx/footer.pug |
|
||||
|
|
||||
block scripts |
|
||||
block pageScripts |
|
||||
@ -1,16 +0,0 @@ |
|||||
extends /layouts/root.pug |
|
||||
|
|
||||
block $$head |
|
||||
+css('styles.css') |
|
||||
block pageHead |
|
||||
|
|
||||
block $$content |
|
||||
.page-layout |
|
||||
.page |
|
||||
.content |
|
||||
block pageContent |
|
||||
footer |
|
||||
include /htmx/footer.pug |
|
||||
|
|
||||
block $$scripts |
|
||||
block pageScripts |
|
||||
@ -1,62 +0,0 @@ |
|||||
extends /layouts/empty.pug |
|
||||
|
|
||||
block pageHead |
|
||||
|
|
||||
block pageContent |
|
||||
.contact-success.container(class="bg-white rounded-[12px] shadow p-6 border border-gray-100") |
|
||||
.success-content(class="text-center py-8") |
|
||||
// 成功图标 |
|
||||
.success-icon(class="mb-6") |
|
||||
.icon-container(class="inline-flex items-center justify-center w-20 h-20 bg-green-100 rounded-full") |
|
||||
span(class="text-4xl text-green-600") ✓ |
|
||||
|
|
||||
// 成功标题 |
|
||||
h1(class="text-3xl font-bold mb-4 text-gray-800") 留言提交成功! |
|
||||
|
|
||||
// 成功消息 |
|
||||
.success-message(class="mb-8") |
|
||||
p(class="text-lg text-gray-600 mb-3") 感谢您的留言,我们已经收到了您的反馈。 |
|
||||
p(class="text-gray-500") 我们会在 <strong class="text-green-600">24小时内</strong> 通过邮箱回复您,请注意查收。 |
|
||||
|
|
||||
// 联系信息卡片 |
|
||||
.contact-cards(class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8") |
|
||||
.contact-card(class="text-center p-4 bg-blue-50 rounded-lg border border-blue-200") |
|
||||
.icon(class="text-2xl mb-2") 📧 |
|
||||
h3(class="font-semibold text-blue-800 mb-1") 邮箱回复 |
|
||||
p(class="text-sm text-gray-600") 24小时内回复 |
|
||||
.contact-card(class="text-center p-4 bg-green-50 rounded-lg border border-green-200") |
|
||||
.icon(class="text-2xl mb-2") 💬 |
|
||||
h3(class="font-semibold text-green-800 mb-1") 在线客服 |
|
||||
p(class="text-sm text-gray-600") 工作日 9:00-18:00 |
|
||||
.contact-card(class="text-center p-4 bg-purple-50 rounded-lg border border-purple-200") |
|
||||
.icon(class="text-2xl mb-2") 📱 |
|
||||
h3(class="font-semibold text-purple-800 mb-1") 社交媒体 |
|
||||
p(class="text-sm text-gray-600") 实时互动 |
|
||||
|
|
||||
// 操作按钮 |
|
||||
.actions(class="flex flex-col sm:flex-row justify-center gap-4") |
|
||||
a(href="/" class="inline-flex items-center justify-center px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors") |
|
||||
span(class="mr-2") 🏠 |
|
||||
| 返回首页 |
|
||||
a(href="/contact" class="inline-flex items-center justify-center px-6 py-3 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors") |
|
||||
span(class="mr-2") ✍️ |
|
||||
| 再次留言 |
|
||||
a(href="/about" class="inline-flex items-center justify-center px-6 py-3 bg-green-100 text-green-700 rounded-lg hover:bg-green-200 transition-colors") |
|
||||
span(class="mr-2") ℹ️ |
|
||||
| 了解我们 |
|
||||
|
|
||||
// 温馨提示 |
|
||||
.tips(class="mt-8 p-4 bg-yellow-50 rounded-lg border border-yellow-200") |
|
||||
h3(class="font-semibold text-yellow-800 mb-2 flex items-center") |
|
||||
span(class="mr-2") 💡 |
|
||||
| 温馨提示 |
|
||||
ul(class="text-sm text-yellow-700 space-y-1 text-left") |
|
||||
li • 请确保您提供的邮箱地址正确,以便我们及时回复 |
|
||||
li • 如有紧急问题,建议通过在线客服或电话联系我们 |
|
||||
li • 您也可以关注我们的社交媒体获取最新动态和回复 |
|
||||
li • 我们承诺保护您的隐私,不会泄露您的联系信息 |
|
||||
|
|
||||
// 页脚信息 |
|
||||
.contact-footer(class="text-center mt-8 pt-6 border-t border-gray-200") |
|
||||
p(class="text-gray-500 text-sm") 再次感谢您对我们的关注和支持! |
|
||||
p(class="text-gray-400 text-xs mt-2") 如有其他问题,欢迎随时与我们联系 |
|
||||
@ -1,51 +0,0 @@ |
|||||
extends /layouts/empty.pug |
|
||||
|
|
||||
block pageHead |
|
||||
+js("https://unpkg.com/tiny-swiper@latest/lib/index.min.js") |
|
||||
|
|
||||
mixin PeopleCared(name, role, desc, avatar) |
|
||||
.bg-white.shadow-md.rounded-md.p-6.flex.items-center.gap-4 |
|
||||
img.w-16.h-16.rounded-full.object-cover(src=avatar alt=name) |
|
||||
.flex-1 |
|
||||
.text-lg.font-semibold.text-gray-900 #{name} |
|
||||
if role |
|
||||
.text-sm.text-gray-500.mt-1 #{role} |
|
||||
if desc |
|
||||
p.text-sm.text-gray-600.mt-2 #{desc} |
|
||||
|
|
||||
|
|
||||
block pageContent |
|
||||
.-mt-5 |
|
||||
//- form(action="/upload" method="post" enctype="multipart/form-data" class="mb-4 flex items-center") |
|
||||
//- input(type="file" name="file" required) |
|
||||
//- button(type="submit" class="ml-2 px-4 py-2 bg-blue-500 text-white rounded") 上传文件 |
|
||||
//- box-shadow 是在所有内容底部 |
|
||||
.swiper-container(style="height:400px;box-shadow: inset 0 -100px 120px #fff;overflow:hidden;mask-image: linear-gradient(to top, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 40%);") |
|
||||
.swiper-wrapper.h-full |
|
||||
.swiper-slide(style="flex-shrink: 0;") |
|
||||
img(src="https://user-images.githubusercontent.com/10026019/102327264-712d5880-3fc0-11eb-8f07-7d58264938c1.png") |
|
||||
.swiper-slide(style="flex-shrink: 0;") |
|
||||
img(src="https://user-images.githubusercontent.com/10026019/102327264-712d5880-3fc0-11eb-8f07-7d58264938c1.png") |
|
||||
.swiper-slide(style="flex-shrink: 0;") |
|
||||
img(src="https://user-images.githubusercontent.com/10026019/102327264-712d5880-3fc0-11eb-8f07-7d58264938c1.png") |
|
||||
.container |
|
||||
.grid.grid-cols-1.gap-4(class="md:grid-cols-4") |
|
||||
+PeopleCared('张三', '产品经理', '专注首页体验优化与可访问性改进', 'https://bpic.588ku.com/element_origin_min_pic/23/07/11/d32dabe266d10da8b21bd640a2e9b611.jpg!r650') |
|
||||
+PeopleCared('张三', '产品经理', '专注首页体验优化与可访问性改进', 'https://bpic.588ku.com/element_origin_min_pic/23/07/11/d32dabe266d10da8b21bd640a2e9b611.jpg!r650') |
|
||||
+PeopleCared('张三', '产品经理', '专注首页体验优化与可访问性改进', 'https://bpic.588ku.com/element_origin_min_pic/23/07/11/d32dabe266d10da8b21bd640a2e9b611.jpg!r650') |
|
||||
+PeopleCared('张三', '产品经理', '专注首页体验优化与可访问性改进', 'https://bpic.588ku.com/element_origin_min_pic/23/07/11/d32dabe266d10da8b21bd640a2e9b611.jpg!r650') |
|
||||
+PeopleCared('张三', '产品经理', '专注首页体验优化与可访问性改进', 'https://bpic.588ku.com/element_origin_min_pic/23/07/11/d32dabe266d10da8b21bd640a2e9b611.jpg!r650') |
|
||||
|
|
||||
block pageScripts |
|
||||
script. |
|
||||
//- Swiper.use([ SwiperPluginLazyload, SwiperPluginPagination ]) |
|
||||
const swiper = new Swiper(".swiper-container", { |
|
||||
//- loop: true, |
|
||||
//- pagination: { |
|
||||
//- el: ".swiper-pagination", |
|
||||
//- clickable: true, |
|
||||
//- }, |
|
||||
//- lazy: { |
|
||||
//- loadPrevNext: true, |
|
||||
//- }, |
|
||||
}); |
|
||||
@ -1,9 +0,0 @@ |
|||||
extends /layouts/pure.pug |
|
||||
|
|
||||
block pageHead |
|
||||
+css("css/page/index.css") |
|
||||
|
|
||||
block pageContent |
|
||||
+include() |
|
||||
- let timeLine = [{icon: '11',title: "aaaa",desc:"asd"}] |
|
||||
include /htmx/timeline.pug |
|
||||
Loading…
Reference in new issue