577 lines
16 KiB
HTML
Executable File
577 lines
16 KiB
HTML
Executable File
{% extends "base.html" %}
|
|
|
|
|
|
{% block content %}
|
|
{% include 'admin_nav.html' %}
|
|
<div class="admin-container wiki-comments-review">
|
|
<div class="admin-header">
|
|
<h1>Wiki 评论审核</h1>
|
|
<div class="header-actions">
|
|
<div class="comments-filter">
|
|
<select id="status-filter" class="form-select">
|
|
<option value="all">全部状态</option>
|
|
<option value="pending">待审核</option>
|
|
<option value="approved">已通过</option>
|
|
<option value="rejected">已拒绝</option>
|
|
</select>
|
|
<select id="type-filter" class="form-select">
|
|
<option value="all">全部类型</option>
|
|
<option value="feature_request">功能建议</option>
|
|
<option value="bug_report">问题报告</option>
|
|
<option value="experience_share">新增功能</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- <div class="debug-info">
|
|
<p>总评论数: {{ comments|length }}</p>
|
|
{% if comments %}
|
|
<p>第一条评论详情:</p>
|
|
<pre>{{ comments[0]|tojson(indent=2) }}</pre>
|
|
{% endif %}
|
|
</div> -->
|
|
|
|
<div class="comments-table-container">
|
|
<table class="admin-table comments-table">
|
|
<thead>
|
|
<tr>
|
|
<th class="text-center">ID</th>
|
|
<th>Wiki 标题</th>
|
|
<th>用户</th>
|
|
<th>评论类型</th>
|
|
<th class="content-column">内容</th>
|
|
<th>状态</th>
|
|
<th>创建时间</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="comments-list">
|
|
{% for comment in comments %}
|
|
<tr data-comment-id="{{ comment.id }}"
|
|
data-status="{{ comment.status }}"
|
|
data-type="{{ comment.comment_type }}"
|
|
class="comment-row {% if comment.status == 'pending' %}pending-review{% endif %}">
|
|
<td class="text-center">{{ comment.id }}</td>
|
|
<td>
|
|
<div class="wiki-title-cell">
|
|
<span>{{ comment.wiki_title }}</span>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="user-cell">
|
|
{% if comment.user_avatar %}
|
|
<div class="user-avatar">
|
|
<img src="{{ comment.user_avatar }}" alt="{{ comment.user_name }}">
|
|
</div>
|
|
{% endif %}
|
|
<span class="user-name">{{ comment.user_name }}</span>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="comment-type-badge
|
|
{% if comment.comment_type == 'feature_request' %}badge-feature
|
|
{% elif comment.comment_type == 'bug_report' %}badge-bug
|
|
{% else %}badge-experience{% endif %}">
|
|
{% if comment.comment_type == 'feature_request' %}功能建议
|
|
{% elif comment.comment_type == 'bug_report' %}问题报告
|
|
{% else %}新增功能{% endif %}
|
|
</span>
|
|
</td>
|
|
<td class="content-column">
|
|
<div class="content-wrapper">
|
|
<div class="comment-content-preview" title="{{ comment.content }}">
|
|
{{ comment.content }}
|
|
</div>
|
|
<span class="comment-expand-btn" style="display: none;">
|
|
<i class="fas fa-chevron-right"></i>
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td class="status-cell">
|
|
{% if comment.status == 'pending' %}
|
|
<span class="badge badge-warning">待审核</span>
|
|
{% elif comment.status == 'approved' %}
|
|
<span class="badge badge-success">已通过</span>
|
|
{% else %}
|
|
<span class="badge badge-danger">已拒绝</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ comment.created_at }}</td>
|
|
<td>
|
|
<div class="action-buttons">
|
|
{% if comment.status == 'pending' %}
|
|
<button class="btn btn-success approve-btn" title="通过">
|
|
<i class="fas fa-check"></i>
|
|
</button>
|
|
<button class="btn btn-danger reject-btn" title="拒绝">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
{% endif %}
|
|
<button class="btn btn-secondary delete-btn" title="删除">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.comment-content-preview {
|
|
max-width: 300px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
vertical-align: middle;
|
|
line-height: 1.4;
|
|
position: relative;
|
|
}
|
|
|
|
.comment-expand-btn {
|
|
cursor: pointer;
|
|
margin-left: 8px;
|
|
color: var(--text-secondary);
|
|
transition: transform 0.3s ease, color 0.3s ease;
|
|
}
|
|
|
|
.comment-expand-btn:hover {
|
|
color: var(--primary-color);
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
.comment-content-full {
|
|
display: none;
|
|
width: 100%;
|
|
white-space: normal;
|
|
word-break: break-all;
|
|
margin-top: 8px;
|
|
padding: 8px;
|
|
background-color: var(--background-secondary);
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.comment-content-full.show {
|
|
display: block;
|
|
}
|
|
|
|
/* 展开模式 */
|
|
.comment-content-preview.expanded {
|
|
position: fixed;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
z-index: 1000;
|
|
background-color: var(--background-primary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 12px;
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
|
max-width: 80vw;
|
|
max-height: 70vh;
|
|
width: 600px;
|
|
padding: 20px;
|
|
overflow-y: auto;
|
|
white-space: normal;
|
|
word-break: break-all;
|
|
}
|
|
|
|
/* 遮罩层 */
|
|
.comment-preview-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: rgba(0,0,0,0.5);
|
|
z-index: 999;
|
|
display: none;
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
.comment-preview-overlay.show {
|
|
display: block;
|
|
opacity: 1;
|
|
}
|
|
|
|
.expanded {
|
|
position: fixed;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
z-index: 1000;
|
|
background-color: var(--background-primary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 12px;
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
|
max-width: 80vw;
|
|
max-height: 70vh;
|
|
width: 600px;
|
|
padding: 20px;
|
|
overflow-y: auto;
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
.expanded.show {
|
|
opacity: 1;
|
|
}
|
|
|
|
.expanded-comment-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
border-bottom: 1px solid var(--border-color);
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
.expanded-comment-close {
|
|
background: none;
|
|
border: none;
|
|
font-size: 20px;
|
|
cursor: pointer;
|
|
color: var(--text-secondary);
|
|
transition: color 0.3s ease;
|
|
}
|
|
|
|
.expanded-comment-close:hover {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.expanded-comment-content {
|
|
line-height: 1.6;
|
|
font-size: 16px;
|
|
color: var(--text-primary);
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
/* 深色模式适配 */
|
|
[data-theme="dark"] .comment-content-preview:hover {
|
|
background-color: rgba(255,255,255,0.1);
|
|
}
|
|
|
|
[data-theme="dark"] .comment-content-preview.expanded {
|
|
background-color: var(--background-secondary);
|
|
}
|
|
|
|
.user-avatar {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
overflow: hidden;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.user-avatar img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.user-cell {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.user-name {
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.wiki-comments-review {
|
|
background-color: var(--background-primary);
|
|
border-radius: 12px;
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.admin-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1.5rem;
|
|
padding-bottom: 1rem;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
|
|
.header-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.form-select {
|
|
padding: 0.5rem 1rem;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 6px;
|
|
background-color: var(--background-secondary);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.comments-table {
|
|
width: 100%;
|
|
border-collapse: separate;
|
|
border-spacing: 0 0.5rem;
|
|
}
|
|
|
|
.comments-table thead th {
|
|
background-color: var(--background-secondary);
|
|
padding: 0.75rem;
|
|
text-align: left;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.comment-row {
|
|
background-color: var(--background-secondary);
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.comment-row:hover {
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.pending-review {
|
|
border-left: 4px solid #ffc107;
|
|
}
|
|
|
|
.content-column {
|
|
max-width: 200px;
|
|
position: relative;
|
|
}
|
|
|
|
.content-wrapper {
|
|
display: flex;
|
|
align-items: center;
|
|
width: 100%;
|
|
}
|
|
|
|
.comment-content-preview {
|
|
flex-grow: 1;
|
|
max-width: calc(100% - 30px); /* 为箭头预留空间 */
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.comment-expand-btn {
|
|
cursor: pointer;
|
|
margin-left: 8px;
|
|
color: var(--text-secondary);
|
|
transition: transform 0.3s ease, color 0.3s ease;
|
|
display: none; /* 默认隐藏 */
|
|
}
|
|
|
|
.comment-expand-btn:hover {
|
|
color: var(--primary-color);
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
.comment-content-full {
|
|
display: none;
|
|
width: 100%;
|
|
white-space: normal;
|
|
word-break: break-all;
|
|
margin-top: 8px;
|
|
padding: 8px;
|
|
background-color: var(--background-secondary);
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.comment-content-full.show {
|
|
display: block;
|
|
}
|
|
|
|
.comment-type-badge {
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 4px;
|
|
font-size: 0.75rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.badge-feature {
|
|
background-color: rgba(59, 130, 246, 0.1);
|
|
color: #3b82f6;
|
|
}
|
|
|
|
.badge-bug {
|
|
background-color: rgba(239, 68, 68, 0.1);
|
|
color: #ef4444;
|
|
}
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.action-buttons .btn {
|
|
width: 35px;
|
|
height: 35px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 0;
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.action-buttons .btn i {
|
|
font-size: 1rem;
|
|
}
|
|
|
|
[data-theme="dark"] .wiki-comments-review {
|
|
background-color: var(--background-secondary);
|
|
box-shadow: 0 4px 6px rgba(255, 255, 255, 0.05);
|
|
}
|
|
|
|
.badge-experience {
|
|
background-color: rgba(52, 211, 153, 0.1);
|
|
color: #10b981;
|
|
}
|
|
|
|
.comments-filter {
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
|
|
/* 删除按钮样式 */
|
|
.action-buttons .delete-btn {
|
|
background-color: rgba(220, 53, 69, 0.1);
|
|
color: #dc3545;
|
|
margin-left: 0.5rem;
|
|
}
|
|
|
|
.action-buttons .delete-btn:hover {
|
|
background-color: rgba(220, 53, 69, 0.2);
|
|
}
|
|
|
|
[data-theme="dark"] .action-buttons .delete-btn {
|
|
background-color: rgba(220, 53, 69, 0.2);
|
|
color: #ff6b6b;
|
|
}
|
|
|
|
[data-theme="dark"] .action-buttons .delete-btn:hover {
|
|
background-color: rgba(220, 53, 69, 0.3);
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const commentsList = document.getElementById('comments-list');
|
|
|
|
// 审核和删除操作
|
|
commentsList.addEventListener('click', function(event) {
|
|
const approveBtn = event.target.closest('.approve-btn');
|
|
const rejectBtn = event.target.closest('.reject-btn');
|
|
const deleteBtn = event.target.closest('.delete-btn');
|
|
|
|
if (approveBtn || rejectBtn || deleteBtn) {
|
|
// 找到最近的 tr 行
|
|
const row = event.target.closest('tr');
|
|
|
|
// 获取评论ID
|
|
const commentId = row.getAttribute('data-comment-id');
|
|
|
|
let url, method;
|
|
if (approveBtn) {
|
|
url = `/admin/wiki/comment/${commentId}/approve`;
|
|
method = '通过';
|
|
} else if (rejectBtn) {
|
|
url = `/admin/wiki/comment/${commentId}/reject`;
|
|
method = '拒绝';
|
|
} else if (deleteBtn) {
|
|
url = `/admin/wiki/comment/${commentId}/delete`;
|
|
method = '删除';
|
|
}
|
|
|
|
// 确认删除操作
|
|
if (deleteBtn && !confirm(`确定要${method}这条评论吗?`)) {
|
|
return;
|
|
}
|
|
|
|
// 发送请求
|
|
fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Network response was not ok');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(result => {
|
|
if (result.success) {
|
|
if (approveBtn) {
|
|
const statusCell = row.querySelector('.status-cell');
|
|
statusCell.innerHTML = '<span class="badge badge-success">已通过</span>';
|
|
} else if (rejectBtn) {
|
|
const statusCell = row.querySelector('.status-cell');
|
|
statusCell.innerHTML = '<span class="badge badge-danger">已拒绝</span>';
|
|
} else if (deleteBtn) {
|
|
// 删除整行
|
|
row.remove();
|
|
}
|
|
|
|
// 移除操作按钮
|
|
const actionButtons = row.querySelector('.action-buttons');
|
|
if (actionButtons && (approveBtn || rejectBtn)) {
|
|
actionButtons.remove();
|
|
}
|
|
} else {
|
|
alert(result.error || `${method}操作失败`);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert(`${method}操作失败,请重试`);
|
|
});
|
|
}
|
|
});
|
|
|
|
// 原有的内容预览和筛选代码保持不变
|
|
const contentPreviews = document.querySelectorAll('.content-wrapper');
|
|
|
|
contentPreviews.forEach(wrapper => {
|
|
const preview = wrapper.querySelector('.comment-content-preview');
|
|
const expandBtn = wrapper.querySelector('.comment-expand-btn');
|
|
|
|
const fullContent = preview.textContent.trim();
|
|
|
|
// 如果文本超过单元格宽度,显示展开箭头
|
|
if (preview.scrollWidth > preview.clientWidth) {
|
|
expandBtn.style.display = 'inline-block';
|
|
}
|
|
|
|
const previewText = fullContent.length > 50
|
|
? fullContent.substring(0, 50) + '...'
|
|
: fullContent;
|
|
|
|
// 创建完整内容容器
|
|
const fullContentDiv = document.createElement('div');
|
|
fullContentDiv.classList.add('comment-content-full');
|
|
fullContentDiv.textContent = fullContent;
|
|
wrapper.parentNode.appendChild(fullContentDiv);
|
|
|
|
// 添加展开/收起事件
|
|
expandBtn.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
fullContentDiv.classList.toggle('show');
|
|
|
|
// 旋转箭头
|
|
expandBtn.querySelector('i').style.transform =
|
|
fullContentDiv.classList.contains('show')
|
|
? 'rotate(90deg)'
|
|
: 'rotate(0deg)';
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %} |