generated from dellevin/template
feat: complete local version to overwrite remote
This commit is contained in:
725
www_comments/templates/admin.html
Normal file
725
www_comments/templates/admin.html
Normal file
@@ -0,0 +1,725 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>评论后台管理</title>
|
||||
<style>
|
||||
:root {
|
||||
--el-color-primary: #409eff;
|
||||
--el-color-success: #67c23a;
|
||||
--el-color-danger: #f56c6c;
|
||||
--el-color-info: #909399;
|
||||
--el-text-primary: #303133;
|
||||
--el-text-regular: #606266;
|
||||
--el-text-secondary: #909399;
|
||||
--el-border-color: #dcdfe6;
|
||||
--el-border-light: #e4e7ed;
|
||||
--el-fill-color: #f5f7fa;
|
||||
--el-header-height: 60px;
|
||||
--el-aside-width: 200px;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif; background-color: #f0f2f5; color: var(--el-text-regular); }
|
||||
|
||||
/* ========== 顶部 Header ========== */
|
||||
.header {
|
||||
position: fixed; top: 0; left: 0; right: 0; height: var(--el-header-height);
|
||||
background: #fff; border-bottom: 1px solid var(--el-border-light);
|
||||
display: flex; align-items: center; justify-content: space-between; padding: 0 20px;
|
||||
z-index: 1000; box-shadow: 0 1px 4px rgba(0,0,0,.08);
|
||||
}
|
||||
.header-brand { font-size: 18px; font-weight: 600; color: var(--el-text-primary); letter-spacing: 1px; }
|
||||
.header-right { display: flex; align-items: center; gap: 20px; font-size: 14px; }
|
||||
.header-right a { color: var(--el-text-secondary); text-decoration: none; transition: color .3s; }
|
||||
.header-right a:hover { color: var(--el-color-primary); }
|
||||
|
||||
/* ========== 左侧 Aside ========== */
|
||||
.aside {
|
||||
position: fixed; top: var(--el-header-height); left: 0; bottom: 0;
|
||||
width: var(--el-aside-width); background: #304156;
|
||||
z-index: 1000; overflow-y: auto;
|
||||
}
|
||||
.menu-item {
|
||||
display: block; padding: 0 20px; height: 56px; line-height: 56px;
|
||||
color: #bfcbd9; font-size: 14px; cursor: pointer; transition: all .3s;
|
||||
text-decoration: none; border-left: 3px solid transparent;
|
||||
}
|
||||
.menu-item:hover, .menu-item.active {
|
||||
background: #263445; color: var(--el-color-primary); border-left-color: var(--el-color-primary);
|
||||
}
|
||||
.menu-icon { display: inline-block; width: 20px; margin-right: 10px; text-align: center; }
|
||||
|
||||
/* 左侧状态筛选 */
|
||||
.aside-status-group {
|
||||
padding: 10px 0;
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
margin-top: 10px;
|
||||
}
|
||||
.aside-menu-item {
|
||||
display: block; padding: 0 20px; height: 48px; line-height: 48px;
|
||||
color: #bfcbd9; font-size: 14px; cursor: pointer; transition: all .3s;
|
||||
text-decoration: none; border-left: 3px solid transparent;
|
||||
}
|
||||
.aside-menu-item:hover, .aside-menu-item.active {
|
||||
background: #263445; color: var(--el-color-primary); border-left-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
/* ========== 右侧 Main ========== */
|
||||
.main {
|
||||
margin-left: var(--el-aside-width); margin-top: var(--el-header-height);
|
||||
padding: 20px; min-height: calc(100vh - var(--el-header-height));
|
||||
}
|
||||
.main-card {
|
||||
background: #fff; border-radius: 4px; padding: 20px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,.08);
|
||||
}
|
||||
.page-title { font-size: 20px; font-weight: 500; color: var(--el-text-primary); margin-bottom: 20px; }
|
||||
|
||||
/* 筛选区 */
|
||||
.filters { display: flex; gap: 10px; margin-bottom: 20px; align-items: center; flex-wrap: wrap; padding: 15px; background: var(--el-fill-color); border-radius: 4px; }
|
||||
input[type="text"] {
|
||||
appearance: none; background-color: #fff; border-radius: 4px; border: 1px solid var(--el-border-color);
|
||||
color: var(--el-text-regular); height: 36px; line-height: 36px; outline: none; padding: 0 15px;
|
||||
transition: border-color .2s; width: 220px;
|
||||
}
|
||||
input[type="text"]:focus { border-color: var(--el-color-primary); }
|
||||
|
||||
/* 状态单选按钮组 */
|
||||
.status-group { display: inline-flex; border: 1px solid var(--el-border-color); border-radius: 4px; overflow: hidden; background: #fff; }
|
||||
.radio-label {
|
||||
cursor: pointer; padding: 0 16px; height: 34px; line-height: 34px; font-size: 14px;
|
||||
transition: all .3s; user-select: none; background: #fff; color: var(--el-text-regular); border-right: 1px solid var(--el-border-color);
|
||||
}
|
||||
.radio-label:last-child { border-right: none; }
|
||||
.radio-label:hover { color: var(--el-color-primary); }
|
||||
.radio-label.active { background-color: var(--el-color-primary); border-color: var(--el-color-primary); color: #fff; }
|
||||
input[type="radio"] { display: none; }
|
||||
|
||||
button.el-button {
|
||||
display: inline-block; line-height: 1; white-space: nowrap; cursor: pointer; background: #fff;
|
||||
border: 1px solid var(--el-border-color); color: var(--el-text-regular); text-align: center;
|
||||
outline: none; transition: .1s; font-weight: 500; padding: 9px 20px; font-size: 14px; border-radius: 4px;
|
||||
}
|
||||
button.el-button:hover { color: var(--el-color-primary); border-color: #c6e2ff; background-color: #ecf5ff; }
|
||||
button.el-button--primary { color: #fff; background-color: var(--el-color-primary); border-color: var(--el-color-primary); }
|
||||
button.el-button--primary:hover { background: #66b1ff; border-color: #66b1ff; }
|
||||
|
||||
/* 表格 */
|
||||
table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
||||
th { background-color: var(--el-fill-color); color: var(--el-text-secondary); font-weight: 500; text-align: left; padding: 12px 10px; border-bottom: 1px solid var(--el-border-light); }
|
||||
td { padding: 12px 10px; border-bottom: 1px solid var(--el-border-light); color: var(--el-text-regular); }
|
||||
tr:hover td { background-color: var(--el-fill-color); }
|
||||
|
||||
.status-badge { padding: 0 10px; height: 22px; line-height: 22px; border-radius: 4px; font-size: 12px; display: inline-block; }
|
||||
.status-approved { background-color: #f0f9eb; color: var(--el-color-success); border: 1px solid #e1f3d8; }
|
||||
.status-spam { background-color: #fef0f0; color: var(--el-color-danger); border: 1px solid #fde2e2; }
|
||||
.status-deleted { background-color: #f4f4f5; color: var(--el-color-info); border: 1px solid #e9e9eb; }
|
||||
|
||||
.actions button {
|
||||
margin-right: 8px; padding: 0; background: none; border: none; cursor: pointer;
|
||||
color: var(--el-color-primary); font-size: 14px; transition: .3s;
|
||||
}
|
||||
.actions button:hover { color: #66b1ff; }
|
||||
.actions button.text-danger { color: var(--el-color-danger); }
|
||||
.actions button.text-danger:hover { color: #f78989; }
|
||||
|
||||
/* 弹窗 */
|
||||
.modal-overlay {
|
||||
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: rgba(0,0,0,0.5); display: none; justify-content: center; align-items: center;
|
||||
z-index: 2000; backdrop-filter: blur(2px);
|
||||
}
|
||||
.modal-box {
|
||||
background: white; border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
width: 420px; animation: dialog-fade-in .3s;
|
||||
}
|
||||
@keyframes dialog-fade-in {
|
||||
from { transform: translateY(-20px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
.modal-header {
|
||||
padding: 20px; font-size: 16px; color: var(--el-text-primary);
|
||||
font-weight: 500; border-bottom: 1px solid var(--el-border-light);
|
||||
}
|
||||
.modal-body { padding: 20px; color: var(--el-text-regular); font-size: 14px; line-height: 1.6; }
|
||||
.modal-footer {
|
||||
padding: 15px 20px; text-align: right;
|
||||
border-top: 1px solid var(--el-border-light);
|
||||
}
|
||||
.modal-btn {
|
||||
border-radius: 4px; border: 1px solid var(--el-border-color); background: #fff;
|
||||
color: var(--el-text-regular); min-width: 80px; height: 36px; line-height: 34px;
|
||||
padding: 0 20px; cursor: pointer; font-size: 14px; transition: all .3s;
|
||||
}
|
||||
.modal-btn:hover { color: var(--el-color-primary); border-color: #c6e2ff; background-color: #ecf5ff; }
|
||||
.modal-btn.primary { color: #fff; background-color: var(--el-color-primary); border-color: var(--el-color-primary); }
|
||||
.modal-btn.primary:hover { background: #66b1ff; border-color: #66b1ff; }
|
||||
|
||||
/* 详情弹窗样式优化 */
|
||||
.detail-modal-content { width: 700px; max-width: 90%; }
|
||||
.detail-grid {
|
||||
display: grid; grid-template-columns: 100px 1fr;
|
||||
gap: 12px 20px; align-items: start;
|
||||
}
|
||||
.detail-label {
|
||||
color: var(--el-text-secondary); font-size: 14px;
|
||||
text-align: right; padding-top: 2px;
|
||||
}
|
||||
.detail-value {
|
||||
word-break: break-all; line-height: 1.8;
|
||||
color: var(--el-text-primary); font-size: 14px;
|
||||
}
|
||||
.comment-html {
|
||||
background: linear-gradient(to bottom, #fafafa, #f5f7fa);
|
||||
padding: 20px; border-radius: 6px;
|
||||
border: 1px solid var(--el-border-light);
|
||||
box-shadow: inset 0 1px 2px rgba(0,0,0,0.05);
|
||||
}
|
||||
.detail-divider {
|
||||
grid-column: 1 / -1; height: 1px;
|
||||
background: var(--el-border-light); margin: 8px 0;
|
||||
}
|
||||
|
||||
/* 分页 - Element Pagination 风格 */
|
||||
.pagination {
|
||||
display: flex; justify-content: flex-end; align-items: center; gap: 8px;
|
||||
margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--el-border-light);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.page-info { color: var(--el-text-secondary); font-size: 14px; margin-right: 10px; }
|
||||
.page-btn {
|
||||
min-width: 32px; height: 32px; line-height: 30px; padding: 0 6px;
|
||||
border: 1px solid var(--el-border-color); background: #fff; border-radius: 4px;
|
||||
cursor: pointer; font-size: 14px; color: var(--el-text-regular); transition: all .3s;
|
||||
}
|
||||
.page-btn:hover:not(.disabled):not(.active) { color: var(--el-color-primary); border-color: var(--el-color-primary); }
|
||||
.page-btn.active { background-color: var(--el-color-primary); border-color: var(--el-color-primary); color: #fff; }
|
||||
.page-btn.disabled { cursor: not-allowed; color: var(--el-text-secondary); background: #f5f7fa; }
|
||||
|
||||
/* 暂无数据样式 */
|
||||
.empty-state {
|
||||
text-align: center !important;
|
||||
padding: 40px 0 !important;
|
||||
color: var(--el-text-secondary) !important;
|
||||
font-size: 14px !important;
|
||||
background-color: #fff !important;
|
||||
}
|
||||
|
||||
/* ========== 移动端适配 ========== */
|
||||
@media screen and (max-width: 768px) {
|
||||
:root {
|
||||
--el-aside-width: 0px;
|
||||
}
|
||||
|
||||
/* 顶部 Header */
|
||||
.header { padding: 0 15px; }
|
||||
.header-brand { font-size: 16px; }
|
||||
.header-right span { display: none; }
|
||||
|
||||
/* 左侧侧边栏 - 隐藏 */
|
||||
.aside { display: none; }
|
||||
|
||||
/* 右侧 Main */
|
||||
.main { margin-left: 0; padding: 10px; }
|
||||
.main-card { padding: 15px; }
|
||||
.page-title { font-size: 18px; margin-bottom: 15px; }
|
||||
|
||||
/* 移动端底部状态筛选 */
|
||||
.mobile-status-bar {
|
||||
display: flex !important;
|
||||
gap: 8px; margin-bottom: 15px; overflow-x: auto;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.mobile-status-item {
|
||||
flex-shrink: 0; padding: 6px 14px; border-radius: 20px;
|
||||
border: 1px solid var(--el-border-color); background: #fff;
|
||||
color: var(--el-text-regular); font-size: 13px; cursor: pointer;
|
||||
transition: all .3s;
|
||||
}
|
||||
.mobile-status-item.active {
|
||||
background: var(--el-color-primary); color: #fff; border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
/* 筛选区 */
|
||||
.filters {
|
||||
flex-direction: column; align-items: stretch; gap: 10px;
|
||||
padding: 12px;
|
||||
}
|
||||
input[type="text"] { width: 100%; }
|
||||
button.el-button { width: 100%; }
|
||||
|
||||
/* 表格横向滚动 */
|
||||
.table-wrapper { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
||||
table { min-width: 600px; }
|
||||
th, td { padding: 10px 8px; font-size: 13px; }
|
||||
th:first-child, td:first-child { padding-left: 8px; }
|
||||
th:last-child, td:last-child { padding-right: 8px; }
|
||||
|
||||
/* 操作按钮调整 */
|
||||
.actions button { font-size: 13px; margin-right: 6px; }
|
||||
|
||||
/* 分页 */
|
||||
.pagination { justify-content: center; }
|
||||
.page-info { width: 100%; text-align: center; margin-bottom: 8px; }
|
||||
|
||||
/* 弹窗 */
|
||||
.modal-box { width: 90% !important; max-width: 400px; }
|
||||
.detail-modal-content { width: 92% !important; }
|
||||
.detail-grid { grid-template-columns: 70px 1fr; gap: 8px 12px; }
|
||||
.detail-label { font-size: 13px; }
|
||||
.detail-value { font-size: 13px; }
|
||||
}
|
||||
|
||||
/* 小于 480px 的屏幕 */
|
||||
@media screen and (max-width: 480px) {
|
||||
.main { padding: 8px; }
|
||||
.main-card { padding: 12px; border-radius: 8px; }
|
||||
.page-title { font-size: 16px; }
|
||||
|
||||
th, td { padding: 8px 6px; font-size: 12px; }
|
||||
.status-badge { padding: 0 6px; height: 20px; line-height: 20px; font-size: 11px; }
|
||||
.actions button { font-size: 12px; }
|
||||
|
||||
.modal-header { padding: 15px; font-size: 15px; }
|
||||
.modal-body { padding: 15px; }
|
||||
.modal-footer { padding: 12px 15px; }
|
||||
.modal-btn { min-width: 70px; height: 34px; line-height: 32px; padding: 0 15px; font-size: 13px; }
|
||||
|
||||
.detail-grid { grid-template-columns: 60px 1fr; gap: 6px 10px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 顶部 Header -->
|
||||
<header class="header">
|
||||
<div class="header-brand">评论后台管理系统</div>
|
||||
<div class="header-right">
|
||||
<span>欢迎,admin</span>
|
||||
<a href="/logout">退出登录</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 左侧 Aside -->
|
||||
<aside class="aside">
|
||||
|
||||
<div class="aside-status-group">
|
||||
<div class="aside-menu-item active" onclick="selectStatus(this, '')">全部</div>
|
||||
<div class="aside-menu-item" onclick="selectStatus(this, 'approved')">通过</div>
|
||||
<div class="aside-menu-item" onclick="selectStatus(this, 'spam')">垃圾</div>
|
||||
<div class="aside-menu-item" onclick="selectStatus(this, 'deleted')">删除</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 右侧 Main -->
|
||||
<main class="main">
|
||||
<div class="main-card">
|
||||
<div class="page-title">评论列表</div>
|
||||
|
||||
<!-- 确认弹窗 -->
|
||||
<div id="confirmModal" class="modal-overlay">
|
||||
<div class="modal-box">
|
||||
<div class="modal-header">提示</div>
|
||||
<div class="modal-body" id="modalTitle"></div>
|
||||
<div class="modal-footer">
|
||||
<button class="modal-btn" onclick="closeModal(false)">取消</button>
|
||||
<button class="modal-btn primary" onclick="closeModal(true)">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<div id="detailModal" class="modal-overlay" onclick="if(event.target === this) closeDetailModal()">
|
||||
<div class="modal-box detail-modal-content">
|
||||
<div class="modal-header">评论详情</div>
|
||||
<div class="modal-body" id="detailContent"></div>
|
||||
<div class="modal-footer">
|
||||
<button class="modal-btn primary" onclick="closeDetailModal()">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增留言弹窗 -->
|
||||
<div id="addCommentModal" class="modal-overlay" onclick="if(event.target === this) closeAddCommentModal()">
|
||||
<div class="modal-box" style="width: 500px;">
|
||||
<div class="modal-header">新增留言</div>
|
||||
<div class="modal-body">
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label style="display: block; margin-bottom: 5px; color: var(--el-text-secondary); font-size: 14px;">昵称:</label>
|
||||
<input type="text" id="addNick" placeholder="请输入昵称" style="width: 100%; height: 36px; padding: 0 12px; border: 1px solid var(--el-border-color); border-radius: 4px; outline: none; box-sizing: border-box;">
|
||||
</div>
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label style="display: block; margin-bottom: 5px; color: var(--el-text-secondary); font-size: 14px;">邮箱:</label>
|
||||
<input type="email" id="addMail" placeholder="请输入邮箱(选填)" style="width: 100%; height: 36px; padding: 0 12px; border: 1px solid var(--el-border-color); border-radius: 4px; outline: none; box-sizing: border-box;">
|
||||
</div>
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label style="display: block; margin-bottom: 5px; color: var(--el-text-secondary); font-size: 14px;">网站:</label>
|
||||
<input type="url" id="addLink" placeholder="请输入网站链接(选填)" style="width: 100%; height: 36px; padding: 0 12px; border: 1px solid var(--el-border-color); border-radius: 4px; outline: none; box-sizing: border-box;">
|
||||
</div>
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label style="display: block; margin-bottom: 5px; color: var(--el-text-secondary); font-size: 14px;">评论内容:</label>
|
||||
<textarea id="addComment" placeholder="请输入评论内容" rows="5" style="width: 100%; padding: 10px 12px; border: 1px solid var(--el-border-color); border-radius: 4px; outline: none; resize: vertical; font-family: inherit; font-size: 14px; box-sizing: border-box;"></textarea>
|
||||
</div>
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label style="display: block; margin-bottom: 5px; color: var(--el-text-secondary); font-size: 14px;">状态:</label>
|
||||
<select id="addStatus" style="width: 100%; height: 36px; padding: 0 12px; border: 1px solid var(--el-border-color); border-radius: 4px; outline: none; background: #fff; box-sizing: border-box;">
|
||||
<option value="approved">通过</option>
|
||||
<option value="spam">垃圾</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="modal-btn" onclick="closeAddCommentModal()">取消</button>
|
||||
<button class="modal-btn primary" onclick="submitNewComment()">提交</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filters">
|
||||
<!-- 移动端状态筛选(仅在手机显示) -->
|
||||
<div class="mobile-status-bar" style="display: none;">
|
||||
<div class="mobile-status-item active" onclick="selectStatusMobile(this, '')">全部</div>
|
||||
<div class="mobile-status-item" onclick="selectStatusMobile(this, 'approved')">通过</div>
|
||||
<div class="mobile-status-item" onclick="selectStatusMobile(this, 'spam')">垃圾</div>
|
||||
<div class="mobile-status-item" onclick="selectStatusMobile(this, 'deleted')">删除</div>
|
||||
</div>
|
||||
<input type="text" id="searchKeyword" placeholder="搜索昵称或评论内容...">
|
||||
<button class="el-button el-button--primary" onclick="loadComments()">搜索</button>
|
||||
<button class="el-button" id="addCommentBtn" onclick="showAddCommentModal()" style="margin-left: auto;">+ 新增留言</button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>昵称</th>
|
||||
<th>评论内容</th>
|
||||
<th>浏览器/系统</th>
|
||||
<th>时间</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="commentTableBody">
|
||||
<tr><td colspan="6" class="empty-state">加载中...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination" id="pagination">
|
||||
<span class="page-info">共 <strong id="totalCount">0</strong> 条</span>
|
||||
<button class="page-btn" id="prevPage" onclick="changePage(-1)"><</button>
|
||||
<div id="pageNumbers" style="display: inline-flex; gap: 4px;"></div>
|
||||
<button class="page-btn" id="nextPage" onclick="changePage(1)">></button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
let modalResolve = null;
|
||||
let currentStatusFilter = '';
|
||||
let currentPage = 1;
|
||||
let totalPages = 0;
|
||||
let totalCount = 0;
|
||||
|
||||
function selectStatus(element, status) {
|
||||
// 更新左侧菜单高亮
|
||||
document.querySelectorAll('.aside-menu-item').forEach(el => el.classList.remove('active'));
|
||||
if (element.classList.contains('aside-menu-item')) {
|
||||
element.classList.add('active');
|
||||
}
|
||||
currentStatusFilter = status;
|
||||
|
||||
// 在“删除”状态下隐藏新增按钮
|
||||
const addBtn = document.getElementById('addCommentBtn');
|
||||
if (addBtn) {
|
||||
addBtn.style.display = status === 'deleted' ? 'none' : 'inline-block';
|
||||
}
|
||||
|
||||
loadComments(); // 点击状态后自动搜索
|
||||
}
|
||||
|
||||
function selectStatusMobile(element, status) {
|
||||
document.querySelectorAll('.mobile-status-item').forEach(el => el.classList.remove('active'));
|
||||
element.classList.add('active');
|
||||
currentStatusFilter = status;
|
||||
loadComments();
|
||||
}
|
||||
|
||||
function showConfirm(message) {
|
||||
return new Promise((resolve) => {
|
||||
document.getElementById('modalTitle').textContent = message;
|
||||
document.getElementById('confirmModal').style.display = 'flex';
|
||||
modalResolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
function closeModal(result) {
|
||||
document.getElementById('confirmModal').style.display = 'none';
|
||||
if (modalResolve) modalResolve(result);
|
||||
}
|
||||
|
||||
function showDetail(comment) {
|
||||
const date = new Date(comment.time).toLocaleString('zh-CN', {
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit'
|
||||
});
|
||||
const content = `
|
||||
<div class="detail-grid">
|
||||
<div class="detail-label">ID:</div>
|
||||
<div class="detail-value">${comment.id}</div>
|
||||
|
||||
<div class="detail-label">昵称:</div>
|
||||
<div class="detail-value">${comment.nick || '匿名'}</div>
|
||||
|
||||
<div class="detail-label">邮箱:</div>
|
||||
<div class="detail-value">${comment.mail || '-'}</div>
|
||||
|
||||
<div class="detail-label">网站:</div>
|
||||
<div class="detail-value">${comment.link ? `<a href="${comment.link}" target="_blank" style="color: var(--el-color-primary); text-decoration: none;">${comment.link}</a>` : '-'}</div>
|
||||
|
||||
<div class="detail-label">浏览器:</div>
|
||||
<div class="detail-value">${comment.browser || '-'} / ${comment.os || '-'}</div>
|
||||
|
||||
<div class="detail-divider"></div>
|
||||
|
||||
<div class="detail-label">评论内容:</div>
|
||||
<div class="detail-value comment-html">${comment.comment}</div>
|
||||
|
||||
<div class="detail-divider"></div>
|
||||
|
||||
<div class="detail-label">发布时间:</div>
|
||||
<div class="detail-value">${date}</div>
|
||||
|
||||
<div class="detail-label">User Agent:</div>
|
||||
<div class="detail-value" style="font-size: 12px; color: var(--el-text-secondary); font-family: monospace;">${comment.ua || '-'}</div>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('detailContent').innerHTML = content;
|
||||
document.getElementById('detailModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeDetailModal() {
|
||||
document.getElementById('detailModal').style.display = 'none';
|
||||
}
|
||||
|
||||
function showAddCommentModal() {
|
||||
// 清空表单
|
||||
document.getElementById('addNick').value = '';
|
||||
document.getElementById('addMail').value = '';
|
||||
document.getElementById('addLink').value = '';
|
||||
document.getElementById('addComment').value = '';
|
||||
document.getElementById('addStatus').value = 'approved';
|
||||
document.getElementById('addCommentModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeAddCommentModal() {
|
||||
document.getElementById('addCommentModal').style.display = 'none';
|
||||
}
|
||||
|
||||
function submitNewComment() {
|
||||
const nick = document.getElementById('addNick').value.trim();
|
||||
const mail = document.getElementById('addMail').value.trim();
|
||||
const link = document.getElementById('addLink').value.trim();
|
||||
const comment = document.getElementById('addComment').value.trim();
|
||||
const status = document.getElementById('addStatus').value;
|
||||
|
||||
if (!comment) {
|
||||
showConfirm('请输入评论内容').then(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
nick: nick || '匿名',
|
||||
mail: mail,
|
||||
link: link,
|
||||
comment: comment,
|
||||
ua: navigator.userAgent,
|
||||
status: status
|
||||
};
|
||||
|
||||
fetch('/api/comment', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(result => {
|
||||
if (result.errno === 0) {
|
||||
closeAddCommentModal();
|
||||
loadComments();
|
||||
} else {
|
||||
showConfirm('添加失败: ' + result.errmsg).then(() => {});
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
showConfirm('网络错误,请重试').then(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
function selectStatusMobile(element, status) {
|
||||
document.querySelectorAll('.mobile-status-item').forEach(el => el.classList.remove('active'));
|
||||
element.classList.add('active');
|
||||
currentStatusFilter = status;
|
||||
loadComments();
|
||||
}
|
||||
|
||||
function changePage(delta) {
|
||||
const newPage = currentPage + delta;
|
||||
if (newPage >= 1 && newPage <= totalPages) {
|
||||
loadComments(newPage);
|
||||
}
|
||||
}
|
||||
|
||||
function goToPage(page) {
|
||||
if (page >= 1 && page <= totalPages && page !== currentPage) {
|
||||
loadComments(page);
|
||||
}
|
||||
}
|
||||
|
||||
function renderPagination(total, page, pages) {
|
||||
totalCount = total;
|
||||
totalPages = pages;
|
||||
document.getElementById('totalCount').textContent = total;
|
||||
|
||||
const paginationDiv = document.getElementById('pagination');
|
||||
const prevBtn = document.getElementById('prevPage');
|
||||
const nextBtn = document.getElementById('nextPage');
|
||||
const pageNumbers = document.getElementById('pageNumbers');
|
||||
|
||||
// 更新上一页/下一页按钮状态
|
||||
prevBtn.classList.toggle('disabled', page <= 1);
|
||||
nextBtn.classList.toggle('disabled', page >= pages);
|
||||
|
||||
// 生成页码按钮
|
||||
pageNumbers.innerHTML = '';
|
||||
|
||||
if (pages <= 1 && total === 0) {
|
||||
// 如果没有数据,完全隐藏分页器
|
||||
paginationDiv.style.display = 'none';
|
||||
} else if (pages <= 1) {
|
||||
// 如果只有一页但有数据,只显示总数,隐藏翻页按钮
|
||||
prevBtn.style.display = 'none';
|
||||
nextBtn.style.display = 'none';
|
||||
pageNumbers.style.display = 'none';
|
||||
paginationDiv.style.display = 'flex';
|
||||
} else {
|
||||
// 多页时显示完整分页器
|
||||
prevBtn.style.display = 'inline-block';
|
||||
nextBtn.style.display = 'inline-block';
|
||||
pageNumbers.style.display = 'inline-flex';
|
||||
paginationDiv.style.display = 'flex';
|
||||
|
||||
// 显示最多 5 个页码
|
||||
let start = Math.max(1, page - 2);
|
||||
let end = Math.min(pages, start + 4);
|
||||
if (end - start < 4) {
|
||||
start = Math.max(1, end - 4);
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = `page-btn ${i === page ? 'active' : ''}`;
|
||||
btn.textContent = i;
|
||||
btn.onclick = () => goToPage(i);
|
||||
pageNumbers.appendChild(btn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadComments(page = 1) {
|
||||
const keyword = document.getElementById('searchKeyword').value;
|
||||
currentPage = page;
|
||||
|
||||
fetch(`/api/admin/comments?keyword=${encodeURIComponent(keyword)}&status=${currentStatusFilter}&page=${page}&pageSize=10`)
|
||||
.then(res => res.json())
|
||||
.then(result => {
|
||||
if (result.errno === 0) {
|
||||
renderTable(result.data.list);
|
||||
renderPagination(result.data.total, result.data.page, result.data.totalPages);
|
||||
} else {
|
||||
showConfirm('加载失败: ' + result.errmsg).then(() => {});
|
||||
}
|
||||
})
|
||||
.catch(err => console.error('Error:', err));
|
||||
}
|
||||
|
||||
function renderTable(comments) {
|
||||
const tbody = document.getElementById('commentTableBody');
|
||||
|
||||
if (!comments || comments.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="empty-state">暂无数据</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
comments.forEach(c => {
|
||||
const date = new Date(c.time).toLocaleString();
|
||||
|
||||
let statusClass = 'status-approved';
|
||||
let statusText = '通过';
|
||||
if (c.status === 'spam') { statusClass = 'status-spam'; statusText = '垃圾'; }
|
||||
if (c.status === 'deleted') { statusClass = 'status-deleted'; statusText = '删除'; }
|
||||
|
||||
let actionButtons = '';
|
||||
if (currentStatusFilter === 'deleted') {
|
||||
actionButtons = `
|
||||
<button onclick="updateStatus(${c.id}, 'approved')">通过</button>
|
||||
<button onclick="showDetail(${JSON.stringify(c).replace(/"/g, '"')})">详情</button>
|
||||
`;
|
||||
} else if (currentStatusFilter === 'spam') {
|
||||
actionButtons = `
|
||||
<button onclick="updateStatus(${c.id}, 'approved')">通过</button>
|
||||
<button class="text-danger" onclick="updateStatus(${c.id}, 'deleted')">删除</button>
|
||||
<button onclick="showDetail(${JSON.stringify(c).replace(/"/g, '"')})">详情</button>
|
||||
`;
|
||||
} else {
|
||||
actionButtons = `
|
||||
<button onclick="updateStatus(${c.id}, 'approved')">通过</button>
|
||||
<button class="text-danger" onclick="updateStatus(${c.id}, 'spam')">垃圾</button>
|
||||
<button class="text-danger" onclick="updateStatus(${c.id}, 'deleted')">删除</button>
|
||||
<button onclick="showDetail(${JSON.stringify(c).replace(/"/g, '"')})">详情</button>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `<tr>
|
||||
<td>${c.nick || '匿名'}</td>
|
||||
<td style="max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${c.comment.replace(/<[^>]*>/g, '')}</td>
|
||||
<td>${c.browser} / ${c.os}</td>
|
||||
<td>${date}</td>
|
||||
<td><span class="status-badge ${statusClass}">${statusText}</span></td>
|
||||
<td class="actions">
|
||||
${actionButtons}
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
tbody.innerHTML = html;
|
||||
}
|
||||
|
||||
async function updateStatus(id, status) {
|
||||
const confirmed = await showConfirm(`确定要将此评论标记为 "${status}" 吗?`);
|
||||
if (!confirmed) return;
|
||||
|
||||
fetch(`/api/admin/comment/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: status })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(result => {
|
||||
if (result.errno === 0) {
|
||||
loadComments(); // 重新加载列表
|
||||
} else {
|
||||
showConfirm('更新失败: ' + result.errmsg).then(() => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 初始加载
|
||||
loadComments();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
58
www_comments/templates/login.html
Normal file
58
www_comments/templates/login.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>管理员登录</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; background-color: #f0f2f5; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }
|
||||
.login-box { background: white; padding: 40px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); width: 320px; text-align: center; }
|
||||
h2 { margin-top: 0; color: #333; }
|
||||
input { width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; }
|
||||
button { width: 100%; padding: 10px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; margin-top: 10px; }
|
||||
button:hover { background-color: #0056b3; }
|
||||
.error { color: red; font-size: 14px; margin-top: 10px; display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h2>后台管理登录</h2>
|
||||
<input type="text" id="username" placeholder="用户名">
|
||||
<input type="password" id="password" placeholder="密码">
|
||||
<button onclick="handleLogin()">登录</button>
|
||||
<div id="errorMsg" class="error"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function handleLogin() {
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
const errorDiv = document.getElementById('errorMsg');
|
||||
|
||||
fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(result => {
|
||||
if (result.errno === 0) {
|
||||
window.location.href = '/admin';
|
||||
} else {
|
||||
errorDiv.textContent = result.errmsg;
|
||||
errorDiv.style.display = 'block';
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
errorDiv.textContent = '网络错误,请重试';
|
||||
errorDiv.style.display = 'block';
|
||||
});
|
||||
}
|
||||
|
||||
// 允许按回车键登录
|
||||
document.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') handleLogin();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user