Files
ns2.0/templates/admin_wiki.html

965 lines
26 KiB
HTML
Executable File

{% extends "base.html" %}
{% block title %}Wiki 管理{% endblock %}
{% block head %}
<!-- 引入相关CSS和JS -->
<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 条目</h1>
<div class="add-entry-section">
<h2>添加新条目</h2>
<form id="addEntryForm" class="entry-form" enctype="multipart/form-data">
<div class="wiki-editor-container">
<div class="wiki-editor-sidebar">
<div class="form-group">
<label for="title">标题</label>
<input type="text" id="title" name="title" required>
</div>
<div class="form-group">
<label for="version">版本</label>
<input type="text" id="version" name="version" required>
</div>
<div class="form-group">
<label>条目类型</label>
<select id="wiki-type" class="form-control" required>
<option value="">选择条目类型</option>
<!-- 动态加载的选项将在这里生成 -->
</select>
<div id="selected-category-display" class="mt-2 text-muted"></div>
</div>
</div>
<div class="wiki-editor-main">
<div class="form-group">
<label for="content">内容</label>
<div id="content-editor" class="editor"></div>
<input type="hidden" id="content" name="content">
</div>
</div>
</div>
<button type="submit" class="btn-primary">添加条目</button>
<!-- 隐藏的分类字段 -->
<input type="hidden" id="first-level" name="first_level">
<input type="hidden" id="second-level" name="second_level">
<input type="hidden" id="third-level" name="third_level">
</form>
</div>
</div>
<style>
/* 新增样式 */
.wiki-editor-container {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.wiki-editor-sidebar {
width: 250px;
background: #f9fafb;
padding: 20px;
border-radius: 8px;
border: 1px solid #e5e7eb;
}
.wiki-editor-main {
flex-grow: 1;
}
.wiki-editor-sidebar .form-group {
margin-bottom: 15px;
}
.wiki-editor-sidebar .form-group label {
margin-bottom: 6px;
}
.wiki-editor-sidebar .form-group input,
.wiki-editor-sidebar .form-group select {
width: 100%;
padding: 8px;
border: 1px solid #d1d5db;
border-radius: 4px;
}
/* 暗色模式适配 */
[data-theme="dark"] .wiki-editor-sidebar {
background: #1f2937;
border-color: #374151;
}
[data-theme="dark"] .wiki-editor-sidebar .form-group input,
[data-theme="dark"] .wiki-editor-sidebar .form-group select {
background: #111827;
border-color: #374151;
color: #f9fafb;
}
/* 移动端适配 */
@media (max-width: 768px) {
.wiki-editor-container {
flex-direction: column;
}
.wiki-editor-sidebar {
width: 100%;
}
}
/* 容器布局 */
.admin-wiki-container {
margin-left: 250px;
padding: 20px;
min-height: 100vh;
background: #f8f9fa;
}
@media (max-width: 768px) {
.admin-wiki-container {
margin-left: 0;
padding: 15px;
}
}
/* 二级导航栏样式 */
.wiki-header {
position: sticky;
top: 0;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
margin: -20px -20px 20px -20px;
padding: 15px 20px;
z-index: 100;
}
.header-content {
max-width: 1200px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 15px;
}
.admin-title {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #333;
background: linear-gradient(120deg, #3b82f6, #2563eb);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.wiki-nav {
display: flex;
gap: 20px;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.nav-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
color: #666;
text-decoration: none;
font-weight: 500;
border-radius: 20px;
transition: all 0.3s ease;
}
.nav-item:hover {
color: #3b82f6;
background: rgba(59, 130, 246, 0.1);
}
.nav-item.active {
color: #3b82f6;
background: rgba(59, 130, 246, 0.1);
}
.nav-item i {
font-size: 16px;
}
/* 编辑器样式 */
.editor {
height: 400px;
margin-bottom: 20px;
background: #fff;
border-radius: 4px;
}
.ql-toolbar.ql-snow {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.ql-container.ql-snow {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
/* 表单样式 */
.add-entry-section {
background: #fff;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-top: 20px;
}
.add-entry-section h2 {
margin: 0 0 20px 0;
font-size: 20px;
color: #333;
}
.entry-form {
margin: 0 auto;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
.form-group input[type="text"],
.form-group input[type="file"] {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
}
.admin-title {
margin: 0;
font-size: 24px;
color: #333;
}
.wiki-actions {
display: flex;
gap: 15px;
}
.btn-primary,
.btn-secondary {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
text-decoration: none;
transition: all 0.3s ease;
}
.btn-primary {
background: #007AFF;
color: #fff;
}
.btn-secondary {
background: #f8f9fa;
color: #333;
border: 1px solid #ddd;
}
.btn-primary:hover {
background: #0056b3;
}
.btn-secondary:hover {
background: #e9ecef;
}
/* 暗色模式适配 */
[data-theme="dark"] .wiki-header {
background: rgba(26, 26, 26, 0.9);
border-bottom-color: rgba(255, 255, 255, 0.1);
}
[data-theme="dark"] .wiki-nav {
border-bottom-color: #333;
}
[data-theme="dark"] .nav-item {
color: #999;
}
[data-theme="dark"] .nav-item:hover {
color: #3b82f6;
background: rgba(59, 130, 246, 0.15);
}
[data-theme="dark"] .nav-item.active {
color: #3b82f6;
background: rgba(59, 130, 246, 0.15);
}
/* 移动端适配 */
@media (max-width: 768px) {
.wiki-header {
position: static;
margin: -15px -15px 15px -15px;
padding: 15px;
}
.header-content {
gap: 10px;
}
.admin-title {
font-size: 20px;
}
.wiki-nav {
gap: 10px;
}
.nav-item {
padding: 6px 12px;
font-size: 14px;
}
}
.page-title {
margin: 0 0 30px 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
.entry-count {
font-size: 14px;
color: #666;
font-weight: normal;
}
[data-theme="dark"] .page-title {
color: #fff;
}
[data-theme="dark"] .entry-count {
color: #999;
}
.image-upload-container {
margin-top: 10px;
}
.image-upload-area {
border: 2px dashed #e5e7eb;
border-radius: 8px;
padding: 20px;
text-align: center;
cursor: pointer;
position: relative;
transition: all 0.3s ease;
}
.image-upload-area:hover {
border-color: #3b82f6;
background: #f8fafc;
}
.image-input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
.upload-placeholder {
color: #6b7280;
}
.upload-placeholder i {
font-size: 32px;
margin-bottom: 10px;
color: #9ca3af;
}
.upload-placeholder p {
margin: 5px 0;
font-size: 16px;
}
.upload-hint {
font-size: 14px;
color: #9ca3af;
}
.image-preview-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 16px;
margin-top: 20px;
}
.preview-item {
position: relative;
border-radius: 8px;
overflow: hidden;
aspect-ratio: 1;
background: #f8fafc;
border: 1px solid #e5e7eb;
}
.preview-item img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.preview-remove {
position: absolute;
top: 8px;
right: 8px;
width: 24px;
height: 24px;
border-radius: 12px;
background: rgba(0, 0, 0, 0.5);
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
z-index: 10;
}
.preview-remove:hover {
background: rgba(0, 0, 0, 0.7);
transform: scale(1.1);
}
/* 暗色模式适配 */
[data-theme="dark"] .image-upload-area {
border-color: #374151;
}
[data-theme="dark"] .image-upload-area:hover {
border-color: #3b82f6;
background: #1f2937;
}
[data-theme="dark"] .preview-item {
background: #1f2937;
border-color: #374151;
}
[data-theme="dark"] .upload-placeholder {
color: #9ca3af;
}
[data-theme="dark"] .upload-placeholder i {
color: #6b7280;
}
.custom-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
z-index: 1000;
max-width: 400px;
width: 90%;
text-align: center;
}
.custom-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 999;
display: none;
}
.custom-modal-content {
margin-bottom: 20px;
}
.custom-modal-actions {
display: flex;
justify-content: center;
gap: 10px;
}
[data-theme="dark"] .custom-modal {
background: #2c2c2c;
color: white;
}
</style>
<div id="customModalOverlay" class="custom-modal-overlay">
<div class="custom-modal">
<div id="customModalContent" class="custom-modal-content"></div>
<div class="custom-modal-actions">
<button id="customModalConfirm" class="btn-primary">确定</button>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
console.log('=== 页面加载完成,开始初始化分类选择 ===');
// 获取关键元素
const wikiTypeSelect = document.getElementById('wiki-type');
const selectedCategoryDisplay = document.getElementById('selected-category-display');
const firstLevelInput = document.getElementById('first-level');
const secondLevelInput = document.getElementById('second-level');
const thirdLevelInput = document.getElementById('third-level');
// 详细的元素存在性检查
console.log('元素检查:', {
wikiTypeSelect: !!wikiTypeSelect,
selectedCategoryDisplay: !!selectedCategoryDisplay,
firstLevelInput: !!firstLevelInput,
secondLevelInput: !!secondLevelInput,
thirdLevelInput: !!thirdLevelInput
});
// 如果任何关键元素缺失,立即退出
if (!wikiTypeSelect || !selectedCategoryDisplay ||
!firstLevelInput || !secondLevelInput || !thirdLevelInput) {
console.error('未找到必要的页面元素,分类加载终止');
return;
}
// 分类加载函数
async function loadWikiCategories() {
try {
const response = await fetch('/admin/wiki/categories', {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
if (!response.ok) {
console.error('获取分类失败:', response.statusText);
return;
}
const categories = await response.json();
console.log('获取到的分类数据:', categories);
// 按一级分类分组
const groupedCategories = {};
categories.forEach(category => {
const { first_level, second_level, third_level } = category;
if (!groupedCategories[first_level]) {
groupedCategories[first_level] = {};
}
if (!groupedCategories[first_level][second_level]) {
groupedCategories[first_level][second_level] = [];
}
groupedCategories[first_level][second_level].push(third_level);
});
const wikiTypeSelect = document.getElementById('wiki-type');
wikiTypeSelect.innerHTML = ''; // 清空现有选项
// 动态生成分类选项
Object.entries(groupedCategories).forEach(([firstLevel, secondLevels]) => {
const firstLevelOptgroup = document.createElement('optgroup');
firstLevelOptgroup.label = firstLevel;
Object.entries(secondLevels).forEach(([secondLevel, thirdLevels]) => {
thirdLevels.forEach(thirdLevel => {
const option = document.createElement('option');
option.value = `${firstLevel} - ${secondLevel} - ${thirdLevel}`;
option.textContent = `${firstLevel} - ${secondLevel} - ${thirdLevel}`;
firstLevelOptgroup.appendChild(option);
});
});
wikiTypeSelect.appendChild(firstLevelOptgroup);
});
console.log('分类数据处理完成');
} catch (error) {
console.error('加载分类时发生错误:', error);
}
}
// 处理分类选择的函数
function handleCategorySelection() {
console.log('handleCategorySelection 被调用');
// 确保选择了有效选项
if (wikiTypeSelect.selectedIndex < 0) {
console.warn('未选择有效选项');
selectedCategoryDisplay.textContent = '请选择分类';
// 清空隐藏字段
firstLevelInput.value = '';
secondLevelInput.value = '';
thirdLevelInput.value = '';
selectedCategoryDisplay.style.color = '#6b7280';
return;
}
const selectedOption = wikiTypeSelect.options[wikiTypeSelect.selectedIndex];
console.log('选中的选项:', selectedOption);
// 安全地解析选择的分类
try {
// 直接使用 value 属性,不再假设使用下划线分隔
const categoryParts = selectedOption.value.split(' - ');
// 检查分类是否完整
if (categoryParts.length !== 3) {
throw new Error('分类格式不正确');
}
const [firstLevel, secondLevel, thirdLevel] = categoryParts;
console.log('解析后的分类:', { firstLevel, secondLevel, thirdLevel });
// 验证分类不为空
if (!firstLevel || !secondLevel || !thirdLevel) {
throw new Error('分类不能为空');
}
// 更新隐藏字段
firstLevelInput.value = firstLevel;
secondLevelInput.value = secondLevel;
thirdLevelInput.value = thirdLevel;
// 更新显示的分类信息
const displayText = `${firstLevel} - ${secondLevel} - ${thirdLevel}`;
selectedCategoryDisplay.textContent = displayText;
// 样式增强
selectedCategoryDisplay.style.color = '#3b82f6';
selectedCategoryDisplay.style.fontWeight = '600';
selectedCategoryDisplay.style.fontSize = '0.875rem';
} catch (error) {
console.error('分类选择错误:', error);
// 重置显示
selectedCategoryDisplay.textContent = '分类选择错误';
selectedCategoryDisplay.style.color = '#dc3545';
// 清空隐藏字段
firstLevelInput.value = '';
secondLevelInput.value = '';
thirdLevelInput.value = '';
}
}
// 添加 change 事件监听器
wikiTypeSelect.addEventListener('change', handleCategorySelection);
// 调用分类加载函数
loadWikiCategories();
// 额外的调试函数
function debugCategoryLoading() {
console.log('=== 分类加载调试 ===');
fetch('/admin/wiki/categories', {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
}
})
.then(response => {
console.log('完整响应对象:', response);
console.log('响应状态:', response.status);
console.log('响应状态文本:', response.statusText);
// 记录所有响应头
console.log('响应头:');
for (let [key, value] of response.headers.entries()) {
console.log(`${key}: ${value}`);
}
return response.json();
})
.then(data => {
console.log('=== 分类数据详细信息 ===');
console.log('是否成功:', data.success);
console.log('分类总数:', data.total_count);
console.log('分类详情:', data.categories);
})
.catch(error => {
console.error('分类加载调试失败:', error);
});
}
// 立即执行调试
debugCategoryLoading();
});
// 在页面加载时,默认选择第一个选项
document.addEventListener('DOMContentLoaded', function() {
const wikiTypeSelect = document.getElementById('wiki-type');
if (wikiTypeSelect && wikiTypeSelect.options.length > 0) {
// 选择第一个 option
wikiTypeSelect.selectedIndex = 0;
// 触发 change 事件以更新分类显示
wikiTypeSelect.dispatchEvent(new Event('change'));
}
});
// 自定义弹窗函数
function showCustomModal(options) {
const overlay = document.getElementById('customModalOverlay');
const content = document.getElementById('customModalContent');
const confirmBtn = document.getElementById('customModalConfirm');
// 设置内容
content.innerHTML = `
<h3>${options.title || '提示'}</h3>
<p>${options.text || ''}</p>
`;
// 设置样式
content.parentElement.style.backgroundColor = options.type === 'success' ? '#e6f3e6' : '#f8d7da';
confirmBtn.style.backgroundColor = options.type === 'success' ? '#28a745' : '#dc3545';
// 显示弹窗
overlay.style.display = 'block';
// 确认按钮事件
const handleConfirm = () => {
overlay.style.display = 'none';
confirmBtn.removeEventListener('click', handleConfirm);
// 执行关闭回调
if (options.didClose) {
options.didClose();
}
};
confirmBtn.addEventListener('click', handleConfirm);
}
document.addEventListener('DOMContentLoaded', function() {
// Quill 工具栏配置
const toolbarOptions = [
['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
[{ 'header': [1, 2, 3, 4, 5, 6, false] }],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
[{ 'script': 'sub'}, { 'script': 'super' }],
[{ 'indent': '-1'}, { 'indent': '+1' }],
[{ 'direction': 'rtl' }],
[{ 'color': [] }, { 'background': [] }],
[{ 'font': [] }],
[{ 'align': [] }],
['clean'],
['link', 'image', 'video']
];
// 确保编辑器容器存在
const editorContainer = document.getElementById('content-editor');
const hiddenContentInput = document.getElementById('content');
if (!editorContainer || !hiddenContentInput) {
console.error('富文本编辑器的必要元素未找到');
return;
}
// 初始化 Quill 编辑器
const editor = new Quill('#content-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'
});
// 同步编辑器内容到隐藏输入框
editor.on('text-change', function() {
// 将编辑器内容转换为 JSON 格式
const content = editor.getContents();
hiddenContentInput.value = JSON.stringify(content);
});
// 调试:检查编辑器是否正确初始化
console.log('Quill 编辑器初始化:', {
editor: !!editor,
container: editorContainer,
hiddenInput: hiddenContentInput
});
// 可选:如果需要预填充内容
try {
// 尝试从隐藏输入框读取初始内容
const initialContent = hiddenContentInput.value;
if (initialContent) {
const parsedContent = JSON.parse(initialContent);
editor.setContents(parsedContent);
}
} catch (error) {
console.warn('初始内容解析失败:', error);
}
});
document.addEventListener('DOMContentLoaded', function() {
// 表单提交处理
const addEntryForm = document.getElementById('addEntryForm');
if (!addEntryForm) {
console.error('未找到表单元素 #addEntryForm');
return;
}
addEntryForm.addEventListener('submit', async function(e) {
e.preventDefault();
// 获取表单元素
const titleInput = document.getElementById('title');
const versionInput = document.getElementById('version');
const contentInput = document.getElementById('content');
const firstLevelInput = document.getElementById('first-level');
const secondLevelInput = document.getElementById('second-level');
const thirdLevelInput = document.getElementById('third-level');
// 验证表单数据
if (!titleInput || !versionInput || !contentInput ||
!firstLevelInput || !secondLevelInput || !thirdLevelInput) {
console.error('表单元素缺失');
showCustomModal({
title: '错误',
text: '表单元素未正确加载,请刷新页面',
type: 'error'
});
return;
}
// 获取表单数据
const formData = {
title: titleInput.value.trim(),
version: versionInput.value.trim(),
content: contentInput.value,
first_level: firstLevelInput.value,
second_level: secondLevelInput.value,
third_level: thirdLevelInput.value
};
// 数据验证
const validationErrors = [];
if (!formData.title) validationErrors.push('标题不能为空');
if (!formData.version) validationErrors.push('版本不能为空');
if (!formData.content) validationErrors.push('内容不能为空');
if (!formData.first_level || !formData.second_level || !formData.third_level) {
validationErrors.push('请选择完整的分类');
}
if (validationErrors.length > 0) {
showCustomModal({
title: '表单验证错误',
text: validationErrors.join('<br>'),
type: 'error'
});
return;
}
try {
const response = await fetch('/admin/wiki/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
const result = await response.json();
if (result.success) {
showCustomModal({
title: '成功',
text: 'Wiki 条目添加成功',
type: 'success',
didClose: () => {
// 可选:重置表单或跳转
addEntryForm.reset();
window.location.href = '/admin/wiki';
}
});
} else {
showCustomModal({
title: '错误',
text: result.error || '添加 Wiki 条目失败',
type: 'error'
});
}
} catch (error) {
console.error('提交表单时发生错误:', error);
showCustomModal({
title: '网络错误',
text: '无法提交表单,请检查网络连接',
type: 'error'
});
}
});
});
</script>
{% endblock %}