Browse Source

去除多余的东西

pure
dash 2 months ago
parent
commit
46abe373d8
  1. 1688
      public/css/admin.css
  2. 54
      public/css/layouts/bg-page.css
  3. 174
      public/css/layouts/empty.css
  4. 677
      public/css/layouts/markdown-reset.scss
  5. 53
      public/css/page/index copy.css
  6. 146
      public/css/page/index.css
  7. 973
      public/js/admin.js
  8. 58
      public/js/login.js
  9. 631
      public/js/profile.js
  10. 48
      public/js/register.js
  11. 429
      public/lib/bg-change.js
  12. 225
      src/views/admin/articles/create.pug
  13. 251
      src/views/admin/articles/edit.pug
  14. 198
      src/views/admin/articles/index.pug
  15. 158
      src/views/admin/articles/show.pug
  16. 243
      src/views/admin/contacts/index.pug
  17. 229
      src/views/admin/contacts/show.pug
  18. 125
      src/views/admin/dashboard.pug
  19. 0
      src/views/helper/utils.pug
  20. 128
      src/views/layouts/admin.pug
  21. 58
      src/views/layouts/base.pug
  22. 18
      src/views/layouts/bg-page.pug
  23. 15
      src/views/layouts/empty.pug
  24. 31
      src/views/layouts/page.pug
  25. 16
      src/views/layouts/pure.pug
  26. 2
      src/views/layouts/root.pug
  27. 62
      src/views/page/extra/contactSuccess.pug
  28. 51
      src/views/page/index/index copy.pug
  29. 10
      src/views/page/index/index.pug
  30. 9
      src/views/temp/person.pug

1688
public/css/admin.css

File diff suppressed because it is too large

54
public/css/layouts/bg-page.css

@ -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;
}

174
public/css/layouts/empty.css

@ -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;
}
}
}

677
public/css/layouts/markdown-reset.scss

@ -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;
}
}
}

53
public/css/page/index copy.css

@ -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;
}
}

146
public/css/page/index.css

@ -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;
}
}

973
public/js/admin.js

@ -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 = `![${altText}](URL)`;
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);
})();

58
public/js/login.js

@ -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 || "登录失败")
}
}

631
public/js/profile.js

@ -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';
}
}
}
})();

48
public/js/register.js

@ -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;
}
});
}
});

429
public/lib/bg-change.js

@ -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();

225
src/views/admin/articles/create.pug

@ -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;
}
});

251
src/views/admin/articles/edit.pug

@ -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;
}
});

198
src/views/admin/articles/index.pug

@ -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);
});
}

158
src/views/admin/articles/show.pug

@ -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);
});
}

243
src/views/admin/contacts/index.pug

@ -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 = '';
});

229
src/views/admin/contacts/show.pug

@ -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);
});
}

125
src/views/admin/dashboard.pug

@ -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 查看用户联系信息

0
src/views/layouts/utils.pug → src/views/helper/utils.pug

128
src/views/layouts/admin.pug

@ -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();

58
src/views/layouts/base.pug

@ -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')

18
src/views/layouts/bg-page.pug

@ -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

15
src/views/layouts/empty.pug

@ -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

31
src/views/layouts/page.pug

@ -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

16
src/views/layouts/pure.pug

@ -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

2
src/views/layouts/root.pug

@ -1,4 +1,4 @@
include utils.pug
include /helper/utils.pug
doctype html
html(lang="zh-CN")

62
src/views/page/extra/contactSuccess.pug

@ -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") 如有其他问题,欢迎随时与我们联系

51
src/views/page/index/index copy.pug

@ -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,
//- },
});

10
src/views/page/index/index.pug

@ -20,6 +20,14 @@ block $$content
.container
div(class="mt-[75px] p-[40px] h-[350px] flex justify-center items-center text-xl" style="line-height: 2.1;")
.typed(style="letter-spacing: 0.2em;")
.text-2xl.font-bold.mb-6.ml-3.text-center 作品
.grid.grid-cols-1.gap-6(class="md:grid-cols-2 lg:grid-cols-3")
.p-3.flex.gap-4
img(src="https://bpic.588ku.com/element_origin_min_pic/23/07/11/d32dabe266d10da8b21bd640a2e9b611.jpg!r650", alt="", class="w-20 h-20 shadow rounded-[50%] object-cover")
.flex.justify-center.flex-col
a.font-bold.text-lg.mb-2(href="/") 灵巧代码
p.text-gray-600.line-clamp-2(style="min-height: 2em") 管理文本
.text-center.text-sm.my-6.p-2(class) 查看更多
include /htmx/footer/index.pug
block $$scripts
@ -34,7 +42,7 @@ block $$scripts
var options = {
strings: [`烟锁长堤柳色新,<br>霞映归舟渡水滨。<br>过客停桡寻旧梦,<br>一川风月正宜人。`],
typeSpeed: 60,
typeSpeed: 80,
shuffle: true,
showCursor: false,
};

9
src/views/temp/person.pug

@ -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…
Cancel
Save