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