123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>文档编辑器 - HubCmdUI</title>
- <link rel="icon" href="https://cdn.jsdelivr.net/gh/dqzboy/Blog-Image/BlogCourse/docker-proxy.png" type="image/png">
-
- <!-- 引入 Bootstrap CSS -->
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
-
- <!-- 引入 jQuery -->
- <script src="https://cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
-
- <!-- 引入 Editor.md -->
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/editormd.min.css">
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/editormd.min.js"></script>
-
- <!-- 引入 SweetAlert2 -->
- <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
-
- <!-- 自定义样式 -->
- <link rel="stylesheet" href="style.css">
- <link rel="stylesheet" href="css/admin.css">
-
- <style>
- body {
- background-color: var(--background-color, #f8f9fa);
- font-family: var(--font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif);
- margin: 0;
- padding: 0;
- height: 100vh;
- overflow: hidden;
- }
-
- .editor-container {
- background-color: var(--container-bg, #ffffff);
- height: 100vh;
- display: flex;
- flex-direction: column;
- padding: 0;
- margin: 0;
- }
-
- .editor-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 1rem 1.5rem;
- border-bottom: 2px solid var(--border-light, #e9ecef);
- background-color: var(--container-bg, #ffffff);
- flex-shrink: 0;
- z-index: 10;
- }
-
- .editor-title {
- color: var(--text-primary, #2c3e50);
- font-size: 1.5rem;
- font-weight: 600;
- margin: 0;
- display: flex;
- align-items: center;
- gap: 0.75rem;
- }
-
- .editor-actions {
- display: flex;
- gap: 1rem;
- align-items: center;
- }
-
- .document-title-input {
- width: 100%;
- padding: 0.75rem 1.5rem;
- font-size: 1.1rem;
- font-weight: 500;
- border: none;
- border-bottom: 2px solid var(--border-light, #e9ecef);
- background-color: var(--container-bg, #ffffff);
- color: var(--text-primary, #2c3e50);
- transition: border-color 0.3s ease;
- flex-shrink: 0;
- }
-
- .document-title-input:focus {
- outline: none;
- border-bottom-color: var(--primary-color, #3d7cfa);
- }
-
- .document-title-input::placeholder {
- color: var(--text-secondary, #6c757d);
- }
-
- /* Editor.md 样式定制 - 铺满全屏 */
- .editormd {
- border: none !important;
- border-radius: 0 !important;
- flex: 1;
- height: auto !important;
- }
-
- /* 编辑器容器铺满剩余空间 */
- #editor-md {
- flex: 1;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- }
-
- /* 确保 CodeMirror 和预览区域铺满高度 */
- .editormd .editormd-editor,
- .editormd .editormd-preview {
- height: 100% !important;
- }
-
- .CodeMirror {
- height: 100% !important;
- }
-
- .btn-custom {
- padding: 0.5rem 1rem;
- font-weight: 500;
- border-radius: var(--radius-md, 8px);
- transition: all 0.3s ease;
- text-decoration: none;
- display: inline-flex;
- align-items: center;
- gap: 0.5rem;
- font-size: 0.9rem;
- }
-
- .btn-primary-custom {
- background-color: var(--primary-color, #3d7cfa);
- color: white;
- border: 2px solid var(--primary-color, #3d7cfa);
- }
-
- .btn-primary-custom:hover {
- background-color: var(--primary-dark, #2c5aa0);
- border-color: var(--primary-dark, #2c5aa0);
- transform: translateY(-1px);
- box-shadow: 0 2px 4px rgba(61, 124, 244, 0.3);
- }
-
- .btn-secondary-custom {
- background-color: transparent;
- color: var(--text-secondary, #6c757d);
- border: 2px solid var(--border-light, #e9ecef);
- }
-
- .btn-secondary-custom:hover {
- background-color: var(--text-secondary, #6c757d);
- color: white;
- border-color: var(--text-secondary, #6c757d);
- }
-
- .btn-success-custom {
- background-color: var(--success-color, #28a745);
- color: white;
- border: 2px solid var(--success-color, #28a745);
- }
-
- .btn-success-custom:hover {
- background-color: #218838;
- border-color: #218838;
- transform: translateY(-1px);
- box-shadow: 0 2px 4px rgba(40, 167, 69, 0.3);
- }
-
- /* 响应式设计 */
- @media (max-width: 768px) {
- .editor-header {
- flex-direction: column;
- gap: 0.5rem;
- align-items: stretch;
- padding: 1rem;
- }
-
- .editor-title {
- font-size: 1.3rem;
- text-align: center;
- }
-
- .editor-actions {
- flex-direction: row;
- gap: 0.5rem;
- justify-content: center;
- }
-
- .btn-custom {
- flex: 1;
- justify-content: center;
- font-size: 0.85rem;
- padding: 0.4rem 0.8rem;
- }
-
- .document-title-input {
- padding: 0.6rem 1rem;
- font-size: 1rem;
- }
- }
-
- .loading-overlay {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background-color: rgba(0, 0, 0, 0.5);
- display: flex;
- justify-content: center;
- align-items: center;
- z-index: 9999;
- }
-
- .loading-spinner {
- background-color: white;
- padding: 2rem;
- border-radius: var(--radius-lg, 12px);
- text-align: center;
- box-shadow: var(--shadow-lg, 0 8px 16px rgba(0, 0, 0, 0.15));
- }
-
- .spinner {
- border: 3px solid #f3f3f3;
- border-top: 3px solid var(--primary-color, #3d7cfa);
- border-radius: 50%;
- width: 40px;
- height: 40px;
- animation: spin 1s linear infinite;
- margin: 0 auto 1rem;
- }
-
- @keyframes spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
- }
- </style>
- </head>
- <body>
- <div class="editor-container">
- <div class="editor-header">
- <h1 class="editor-title">
- <i class="fas fa-edit"></i>
- <span id="pageTitle">新建文档</span>
- </h1>
- <div class="editor-actions">
- <a href="/admin" class="btn-custom btn-secondary-custom">
- <i class="fas fa-arrow-left"></i> 返回管理面板
- </a>
- <button type="button" class="btn-custom btn-success-custom" id="saveBtn">
- <i class="fas fa-save"></i> 保存文档
- </button>
- </div>
- </div>
-
- <input
- type="text"
- id="documentTitle"
- class="document-title-input"
- placeholder="请输入文档标题..."
- autocomplete="off"
- >
-
- <div id="editor-md">
- <textarea style="display:none;" id="editorContent"></textarea>
- </div>
- </div>
-
- <!-- 加载覆盖层 -->
- <div class="loading-overlay" id="loadingOverlay" style="display: none;">
- <div class="loading-spinner">
- <div class="spinner"></div>
- <p>正在保存文档...</p>
- </div>
- </div>
- <script>
- let editor;
- let currentDocId = null;
-
- // 从 URL 参数获取文档 ID(如果是编辑模式)
- const urlParams = new URLSearchParams(window.location.search);
- const docId = urlParams.get('id');
-
- $(document).ready(function() {
- // 初始化 Editor.md
- initEditor();
-
- // 如果有文档 ID,则加载文档
- if (docId) {
- loadDocument(docId);
- document.getElementById('pageTitle').textContent = '编辑文档';
- }
-
- // 绑定保存按钮事件
- document.getElementById('saveBtn').addEventListener('click', saveDocument);
-
- // 绑定键盘快捷键 Ctrl+S
- document.addEventListener('keydown', function(e) {
- if (e.ctrlKey && e.key === 's') {
- e.preventDefault();
- saveDocument();
- }
- });
- });
-
- function initEditor() {
- editor = editormd("editor-md", {
- width: "100%",
- height: "100%", // 使用 CSS 的 flex 布局控制高度
- syncScrolling: "single",
- placeholder: "在这里编写您的 Markdown 内容...",
- path: "https://cdn.jsdelivr.net/npm/[email protected]/lib/",
- // 使用官方默认主题
- theme: "default",
- previewTheme: "default",
- editorTheme: "default",
- markdown: "",
- codeFold: true,
- saveHTMLToTextarea: true,
- searchReplace: true,
- htmlDecode: "style,script,iframe",
- emoji: true,
- taskList: true,
- tocm: true,
- tex: false,
- flowChart: false,
- sequenceDiagram: false,
- dialogLockScreen: false,
- dialogShowMask: false,
- previewCodeHighlight: true,
- toolbar: true,
- watch: true,
- lineNumbers: true,
- lineWrapping: false,
- autoCloseTags: true,
- autoFocus: true,
- indentUnit: 4,
- // 使用官方默认工具栏配置
- onload: function() {
- console.log('Editor.md 初始化完成');
- },
- onchange: function() {
- // 标记内容已修改
- markAsModified();
- }
- });
- }
-
- function markAsModified() {
- const saveBtn = document.getElementById('saveBtn');
- if (!saveBtn.classList.contains('modified')) {
- saveBtn.classList.add('modified');
- saveBtn.innerHTML = '<i class="fas fa-save"></i> 保存文档 *';
- }
- }
-
- function markAsSaved() {
- const saveBtn = document.getElementById('saveBtn');
- saveBtn.classList.remove('modified');
- saveBtn.innerHTML = '<i class="fas fa-save"></i> 保存文档';
- }
-
- async function loadDocument(id) {
- try {
- const response = await fetch(`/api/documents/${id}`, {
- credentials: 'same-origin'
- });
-
- if (!response.ok) {
- throw new Error(`加载文档失败: ${response.status}`);
- }
-
- const doc = await response.json();
- currentDocId = id;
-
- // 设置文档标题
- document.getElementById('documentTitle').value = doc.title || '';
-
- // 设置文档内容
- if (editor && editor.setMarkdown) {
- editor.setMarkdown(doc.content || '');
- }
-
- // 更新页面标题
- document.title = `编辑文档: ${doc.title} - HubCmdUI`;
-
- } catch (error) {
- console.error('加载文档失败:', error);
- Swal.fire({
- icon: 'error',
- title: '加载失败',
- text: '无法加载文档内容,请检查网络连接或文档是否存在。',
- confirmButtonColor: '#3d7cfa'
- });
- }
- }
-
- async function saveDocument() {
- const title = document.getElementById('documentTitle').value.trim();
- const content = editor ? editor.getMarkdown() : '';
-
- if (!title) {
- Swal.fire({
- icon: 'warning',
- title: '请输入标题',
- text: '文档标题不能为空',
- confirmButtonColor: '#3d7cfa'
- });
- return;
- }
-
- if (!content.trim()) {
- Swal.fire({
- icon: 'warning',
- title: '请输入内容',
- text: '文档内容不能为空',
- confirmButtonColor: '#3d7cfa'
- });
- return;
- }
-
- // 显示加载动画
- document.getElementById('loadingOverlay').style.display = 'flex';
-
- try {
- const url = currentDocId ? `/api/documents/${currentDocId}` : '/api/documents';
- const method = currentDocId ? 'PUT' : 'POST';
-
- const response = await fetch(url, {
- method: method,
- headers: {
- 'Content-Type': 'application/json',
- },
- credentials: 'same-origin',
- body: JSON.stringify({
- title: title,
- content: content
- })
- });
-
- if (!response.ok) {
- throw new Error(`保存失败: ${response.status}`);
- }
-
- const result = await response.json();
-
- // 如果是新建文档,更新当前文档 ID
- if (!currentDocId && result.id) {
- currentDocId = result.id;
- // 更新 URL
- window.history.replaceState({}, '', `?id=${result.id}`);
- document.getElementById('pageTitle').textContent = '编辑文档';
- }
-
- // 标记为已保存
- markAsSaved();
-
- // 更新页面标题
- document.title = `编辑文档: ${title} - HubCmdUI`;
-
- // 显示成功消息
- Swal.fire({
- icon: 'success',
- title: '保存成功',
- text: currentDocId ? '文档已更新' : '文档已创建',
- timer: 2000,
- showConfirmButton: false,
- toast: true,
- position: 'top-end'
- });
-
- } catch (error) {
- console.error('保存文档失败:', error);
- Swal.fire({
- icon: 'error',
- title: '保存失败',
- text: '无法保存文档,请检查网络连接或稍后重试。',
- confirmButtonColor: '#3d7cfa'
- });
- } finally {
- // 隐藏加载动画
- document.getElementById('loadingOverlay').style.display = 'none';
- }
- }
-
- // 页面卸载前提醒保存
- window.addEventListener('beforeunload', function(e) {
- const saveBtn = document.getElementById('saveBtn');
- if (saveBtn && saveBtn.classList.contains('modified')) {
- e.preventDefault();
- e.returnValue = '您有未保存的更改,确定要离开吗?';
- return e.returnValue;
- }
- });
- </script>
- </body>
- </html>
|