Files
ns2.0/templates/admin_wiki_list.html

723 lines
19 KiB
HTML
Executable File

{% extends "base.html" %}
{% block title %}Wiki 条目管理{% endblock %}
{% block head %}
<link href="{{ url_for('static', filename='libs/quill/quill.snow.css') }}" rel="stylesheet">
<script src="{{ url_for('static', filename='libs/highlight/highlight.min.js') }}"></script>
<script src="{{ url_for('static', filename='libs/highlight/languages/javascript.min.js') }}"></script>
<script src="{{ url_for('static', filename='libs/highlight/languages/python.min.js') }}"></script>
<script src="{{ url_for('static', filename='libs/highlight/languages/bash.min.js') }}"></script>
<script src="{{ url_for('static', filename='libs/highlight/languages/xml.min.js') }}"></script>
<script src="{{ url_for('static', filename='libs/quill/quill.min.js') }}"></script>
{% endblock %}
{% block content %}
{% include 'admin_nav.html' %}
<div class="admin-wiki-container">
<h1 class="page-title">Wiki 条目列表 <span class="entry-count">({{ entries|length }})</span></h1>
<div class="search-section">
<div class="search-box">
<i class="fas fa-search"></i>
<input type="text" id="searchInput" placeholder="搜索条目...">
</div>
</div>
<div class="entries-grid">
{% for entry in entries %}
<div class="entry-card" data-id="{{ entry.id }}">
{% if entry.image_path %}
<div class="entry-image">
<img src="{{ url_for('static', filename=entry.image_path) }}" alt="{{ entry.title }}">
</div>
{% endif %}
<div class="entry-info">
<h3>{{ entry.title }}</h3>
<div class="entry-meta">
<span class="version">{{ entry.version }}</span>
<span class="date">{{ entry.created_at }}</span>
</div>
<div class="entry-content" style="display: none;">{{ entry.content }}</div>
</div>
<div class="entry-actions">
<button class="btn-edit" onclick="editEntry({{ entry.id }})">
<i class="fas fa-edit"></i>
</button>
<button class="btn-delete" onclick="deleteEntry({{ entry.id }})">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{% endfor %}
</div>
</div>
<div id="editModal" class="modal" style="display: none;">
<div class="modal-content">
<span class="close" onclick="closeEditModal()">&times;</span>
<h2>编辑条目</h2>
<form id="editEntryForm" class="entry-form" enctype="multipart/form-data">
<input type="hidden" id="editEntryId" name="entry_id">
<div class="form-group">
<label for="editTitle">标题</label>
<input type="text" id="editTitle" name="title" required>
</div>
<div class="form-group">
<label for="editVersion">版本</label>
<input type="text" id="editVersion" name="version" required>
</div>
<div class="form-group">
<label for="editWikiType">条目类型</label>
<select id="editWikiType" name="wiki_type" class="form-control">
<option value="version">版本号</option>
<option value="news">资讯</option>
</select>
</div>
<div class="form-group">
<label for="editContent">内容</label>
<div id="editContent-editor" class="editor"></div>
<input type="hidden" id="editContent" name="content">
</div>
<button type="submit" class="btn-primary">保存更改</button>
</form>
</div>
</div>
<style>
/* 容器布局 */
.admin-wiki-container {
margin-left: 250px;
padding: 20px;
min-height: 100vh;
background: #f8f9fa;
}
/* 模态框样式 */
.modal {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 2000;
padding: 20px;
box-sizing: border-box;
}
.modal-content {
background: #fff;
padding: 30px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
max-width: 800px;
width: 90%;
position: relative;
max-height: 90vh;
overflow-y: auto;
margin: auto;
animation: modalFadeIn 0.3s ease;
}
@keyframes modalFadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.close {
position: absolute;
top: 10px;
right: 10px;
font-size: 24px;
cursor: pointer;
color: #333;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.3s ease;
z-index: 1;
}
.close:hover {
background: rgba(0, 0, 0, 0.1);
}
.modal-content h2 {
margin: 0 0 20px 0;
font-size: 20px;
font-weight: 600;
color: #333;
padding-right: 30px;
}
.modal-content .form-group {
margin-bottom: 20px;
}
.modal-content .form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
}
.modal-content .form-group input {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
.modal-content .editor {
height: 300px;
margin-bottom: 20px;
border-radius: 4px;
overflow: hidden;
}
.modal-content .btn-primary {
width: 100%;
margin-top: 20px;
padding: 12px;
font-size: 16px;
border-radius: 4px;
}
[data-theme="dark"] .modal-content {
background: #1a1a1a;
color: #fff;
}
[data-theme="dark"] .close {
color: #fff;
}
[data-theme="dark"] .close:hover {
background: rgba(255, 255, 255, 0.1);
}
[data-theme="dark"] .modal-content h2 {
color: #fff;
}
[data-theme="dark"] .modal-content .form-group label {
color: #ccc;
}
[data-theme="dark"] .modal-content .form-group input {
background: #333;
border-color: #444;
color: #fff;
}
/* 搜索框样式 */
.search-section {
margin-bottom: 30px;
}
.search-box {
position: relative;
max-width: 500px;
margin: 0 auto;
}
.search-box i {
position: absolute;
left: 15px;
top: 50%;
transform: translateY(-50%);
color: #666;
}
.search-box input {
width: 100%;
padding: 12px 20px 12px 45px;
border: 1px solid #ddd;
border-radius: 25px;
font-size: 16px;
transition: all 0.3s ease;
}
/* 条目网格样式 */
.entries-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
padding: 20px;
}
.entry-card {
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow: hidden;
transition: transform 0.3s ease;
}
.entry-card:hover {
transform: translateY(-5px);
}
.entry-image {
height: 150px;
overflow: hidden;
}
.entry-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.entry-info {
padding: 15px;
}
.entry-info h3 {
margin: 0 0 10px 0;
font-size: 18px;
color: #333;
}
.entry-meta {
display: flex;
justify-content: space-between;
color: #666;
font-size: 14px;
}
.entry-actions {
display: flex;
justify-content: flex-end;
padding: 10px;
background: #f8f9fa;
gap: 10px;
}
.btn-edit,
.btn-delete {
border: none;
padding: 8px;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-edit {
background: #28a745;
color: #fff;
}
.btn-delete {
background: #dc3545;
color: #fff;
}
.btn-edit:hover {
background: #218838;
}
.btn-delete:hover {
background: #c82333;
}
/* 暗色模式适配 */
[data-theme="dark"] .admin-wiki-container {
background: #121212;
}
[data-theme="dark"] .entry-card {
background: #1a1a1a;
}
[data-theme="dark"] .entry-info h3 {
color: #fff;
}
[data-theme="dark"] .entry-meta {
color: #999;
}
[data-theme="dark"] .entry-actions {
background: #2d2d2d;
}
[data-theme="dark"] .search-box input {
background: #2d2d2d;
border-color: #444;
color: #fff;
}
[data-theme="dark"] .search-box i {
color: #999;
}
/* 移动端适配 */
@media (max-width: 768px) {
.admin-wiki-container {
margin-left: 0;
padding: 15px;
}
.search-box input {
font-size: 14px;
padding: 10px 15px 10px 40px;
}
.entries-grid {
grid-template-columns: 1fr;
padding: 10px;
}
.entry-card {
margin-bottom: 15px;
}
.entry-info h3 {
font-size: 16px;
}
.modal-content {
padding: 20px;
width: 95%;
}
.modal-content .editor {
height: 250px;
}
}
.wiki-entry-type-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
.wiki-entry-type-table th,
.wiki-entry-type-table td {
border: 1px solid #eee;
padding: 8px;
text-align: left;
}
.entry-type-select {
width: 100%;
padding: 5px;
border: 1px solid #ddd;
border-radius: 4px;
}
.badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 0.8rem;
}
.version-badge {
background-color: #e6f3ff;
color: #3b82f6;
}
.news-badge {
background-color: #e6fff0;
color: #10b981;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const toolbarOptions = [
['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
[{ 'header': 1 }, { 'header': 2 }],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
[{ 'script': 'sub'}, { 'script': 'super' }],
[{ 'indent': '-1'}, { 'indent': '+1' }],
[{ 'direction': 'rtl' }],
[{ 'size': ['small', false, 'large', 'huge'] }],
[{ 'header': [1, 2, 3, 4, 5, 6, false] }],
[{ 'color': [] }, { 'background': [] }],
[{ 'font': [] }],
[{ 'align': [] }],
['clean'],
['link', 'image', 'video']
];
const editEditor = new Quill('#editContent-editor', {
modules: {
toolbar: {
container: toolbarOptions,
handlers: {
link: function(value) {
if (value) {
const range = this.quill.getSelection();
if (range) {
let url = prompt('请输入链接URL:');
if (url) {
if (!/^https?:\/\//i.test(url)) {
url = 'http://' + url;
}
this.quill.format('link', url);
}
}
} else {
this.quill.format('link', false);
}
}
}
},
syntax: {
highlight: (text) => hljs.highlightAuto(text).value
}
},
theme: 'snow'
});
// 安全地解析内容
function safeParseContent(content) {
console.log('原始内容:', content);
console.log('内容类型:', typeof content);
if (!content) return { ops: [{ insert: '' }] };
try {
// 处理可能的多层转义
let parsed;
if (typeof content === 'string') {
// 尝试多次解析
let currentContent = content;
for (let i = 0; i < 3; i++) {
try {
currentContent = JSON.parse(currentContent);
} catch (e) {
break;
}
}
parsed = currentContent;
} else {
parsed = content;
}
console.log('解析后的内容:', parsed);
// 验证是否为有效的 Quill Delta
if (parsed && parsed.ops && Array.isArray(parsed.ops)) {
return parsed;
}
// 如果不是标准格式,尝试转换
return {
ops: [{
insert: typeof parsed === 'string' ? parsed : JSON.stringify(parsed)
}]
};
} catch (e) {
console.error('内容解析错误:', e);
return {
ops: [{
insert: '无法解析的内容:' + (typeof content === 'string' ? content : JSON.stringify(content))
}]
};
}
}
window.editEntry = function(entryId) {
const entryCard = document.querySelector(`.entry-card[data-id="${entryId}"]`);
if (!entryCard) return;
const title = entryCard.querySelector('h3')?.textContent || '';
const version = entryCard.querySelector('.version')?.textContent || '';
const contentElement = entryCard.querySelector('.entry-content');
const content = contentElement?.textContent || '';
console.log('编辑条目详情:', { entryId, title, version, content });
document.getElementById('editEntryId').value = entryId;
document.getElementById('editTitle').value = title;
document.getElementById('editVersion').value = version;
try {
// 安全地解析内容
const contentObj = safeParseContent(content);
console.log('解析后的内容:', contentObj);
// 设置编辑器内容
editEditor.setContents(contentObj);
document.getElementById('editModal').style.display = 'block';
} catch (e) {
console.error('设置内容时出错:', e);
editEditor.setText(content || '');
document.getElementById('editModal').style.display = 'block';
}
};
document.getElementById('editEntryForm').addEventListener('submit', function(event) {
event.preventDefault();
// 获取富文本编辑器内容
const content = JSON.stringify(editEditor.getContents());
document.getElementById('editContent').value = content;
const formData = new FormData(this);
const entryId = document.getElementById('editEntryId').value;
fetch(`/admin/wiki/edit/${entryId}`, {
method: 'POST',
body: formData,
headers: {
// 明确指定不使用默认的 Content-Type
// 让浏览器自动设置 multipart/form-data
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
console.log('响应状态:', response.status);
console.log('响应头:', response.headers);
// 检查响应状态
if (!response.ok) {
return response.text().then(text => {
console.error('错误响应内容:', text);
throw new Error(`HTTP error! status: ${response.status}, content: ${text}`);
});
}
return response.json();
})
.then(data => {
if (data.success) {
window.location.reload();
} else {
console.error('详细错误信息:', data);
alert('保存失败: ' + (data.error || '未知错误'));
}
})
.catch(error => {
console.error('保存错误:', error);
alert('保存失败: ' + error);
});
});
// 其他现有的函数保持不变
window.deleteEntry = function(entryId) {
if (!confirm('确定要删除这个条目吗?')) return;
fetch(`/admin/wiki/delete/${entryId}`, {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data.success) {
const entryCard = document.querySelector(`.entry-card[data-id="${entryId}"]`);
if (entryCard) {
entryCard.remove();
}
} else {
alert('删除失败: ' + data.error);
}
})
.catch(error => {
alert('删除失败: ' + error);
});
};
document.getElementById('searchInput').addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
document.querySelectorAll('.entry-card').forEach(card => {
const title = card.querySelector('h3').textContent.toLowerCase();
const version = card.querySelector('.version').textContent.toLowerCase();
card.style.display = title.includes(searchTerm) || version.includes(searchTerm) ? '' : 'none';
});
});
window.closeEditModal = function() {
document.getElementById('editModal').style.display = 'none';
};
function saveWikiDisplaySettings() {
const showVersion = document.getElementById('show-version').checked;
const showNews = document.getElementById('show-news').checked;
const sortOrder = document.getElementById('sort-order').value;
const itemsPerPage = document.getElementById('items-per-page').value;
const previewLength = document.getElementById('preview-length').value;
// 收集单个 Wiki 条目的类型变更
const entryTypeChanges = [];
document.querySelectorAll('.entry-type-select').forEach(select => {
entryTypeChanges.push({
id: select.dataset.entryId,
type: select.value
});
});
// 发送设置到后端保存
fetch('/wiki/admin/save-display-settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
show_version: showVersion,
show_news: showNews,
sort_order: sortOrder,
items_per_page: itemsPerPage,
preview_length: previewLength,
entry_type_changes: entryTypeChanges
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// 刷新页面或局部更新
location.reload();
} else {
alert('保存失败:' + data.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('保存设置时发生错误');
});
closeWikiDisplaySettingsModal();
}
function openEditModal(entryId, title, version, wikiType, content) {
document.getElementById('editEntryId').value = entryId;
document.getElementById('editTitle').value = title;
document.getElementById('editVersion').value = version;
document.getElementById('editWikiType').value = wikiType;
// 如果使用富文本编辑器,更新编辑器内容
if (window.editContentEditor) {
window.editContentEditor.root.innerHTML = content;
}
document.getElementById('editModal').style.display = 'block';
}
});
</script>
{% endblock %}