723 lines
19 KiB
HTML
Executable File
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()">×</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 %} |