document-editor.html 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>文档编辑器 - HubCmdUI</title>
  7. <link rel="icon" href="https://cdn.jsdelivr.net/gh/dqzboy/Blog-Image/BlogCourse/docker-proxy.png" type="image/png">
  8. <!-- 引入 Bootstrap CSS -->
  9. <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
  10. <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
  11. <!-- 引入 jQuery -->
  12. <script src="https://cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
  13. <!-- 引入 Editor.md -->
  14. <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/editormd.min.css">
  15. <script src="https://cdn.jsdelivr.net/npm/[email protected]/editormd.min.js"></script>
  16. <!-- 引入 SweetAlert2 -->
  17. <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
  18. <!-- 自定义样式 -->
  19. <link rel="stylesheet" href="style.css">
  20. <link rel="stylesheet" href="css/admin.css">
  21. <style>
  22. body {
  23. background-color: var(--background-color, #f8f9fa);
  24. font-family: var(--font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif);
  25. margin: 0;
  26. padding: 0;
  27. height: 100vh;
  28. overflow: hidden;
  29. }
  30. .editor-container {
  31. background-color: var(--container-bg, #ffffff);
  32. height: 100vh;
  33. display: flex;
  34. flex-direction: column;
  35. padding: 0;
  36. margin: 0;
  37. }
  38. .editor-header {
  39. display: flex;
  40. justify-content: space-between;
  41. align-items: center;
  42. padding: 1rem 1.5rem;
  43. border-bottom: 2px solid var(--border-light, #e9ecef);
  44. background-color: var(--container-bg, #ffffff);
  45. flex-shrink: 0;
  46. z-index: 10;
  47. }
  48. .editor-title {
  49. color: var(--text-primary, #2c3e50);
  50. font-size: 1.5rem;
  51. font-weight: 600;
  52. margin: 0;
  53. display: flex;
  54. align-items: center;
  55. gap: 0.75rem;
  56. }
  57. .editor-actions {
  58. display: flex;
  59. gap: 1rem;
  60. align-items: center;
  61. }
  62. .document-title-input {
  63. width: 100%;
  64. padding: 0.75rem 1.5rem;
  65. font-size: 1.1rem;
  66. font-weight: 500;
  67. border: none;
  68. border-bottom: 2px solid var(--border-light, #e9ecef);
  69. background-color: var(--container-bg, #ffffff);
  70. color: var(--text-primary, #2c3e50);
  71. transition: border-color 0.3s ease;
  72. flex-shrink: 0;
  73. }
  74. .document-title-input:focus {
  75. outline: none;
  76. border-bottom-color: var(--primary-color, #3d7cfa);
  77. }
  78. .document-title-input::placeholder {
  79. color: var(--text-secondary, #6c757d);
  80. }
  81. /* Editor.md 样式定制 - 铺满全屏 */
  82. .editormd {
  83. border: none !important;
  84. border-radius: 0 !important;
  85. flex: 1;
  86. height: auto !important;
  87. }
  88. /* 编辑器容器铺满剩余空间 */
  89. #editor-md {
  90. flex: 1;
  91. display: flex;
  92. flex-direction: column;
  93. overflow: hidden;
  94. }
  95. /* 确保 CodeMirror 和预览区域铺满高度 */
  96. .editormd .editormd-editor,
  97. .editormd .editormd-preview {
  98. height: 100% !important;
  99. }
  100. .CodeMirror {
  101. height: 100% !important;
  102. }
  103. .btn-custom {
  104. padding: 0.5rem 1rem;
  105. font-weight: 500;
  106. border-radius: var(--radius-md, 8px);
  107. transition: all 0.3s ease;
  108. text-decoration: none;
  109. display: inline-flex;
  110. align-items: center;
  111. gap: 0.5rem;
  112. font-size: 0.9rem;
  113. }
  114. .btn-primary-custom {
  115. background-color: var(--primary-color, #3d7cfa);
  116. color: white;
  117. border: 2px solid var(--primary-color, #3d7cfa);
  118. }
  119. .btn-primary-custom:hover {
  120. background-color: var(--primary-dark, #2c5aa0);
  121. border-color: var(--primary-dark, #2c5aa0);
  122. transform: translateY(-1px);
  123. box-shadow: 0 2px 4px rgba(61, 124, 244, 0.3);
  124. }
  125. .btn-secondary-custom {
  126. background-color: transparent;
  127. color: var(--text-secondary, #6c757d);
  128. border: 2px solid var(--border-light, #e9ecef);
  129. }
  130. .btn-secondary-custom:hover {
  131. background-color: var(--text-secondary, #6c757d);
  132. color: white;
  133. border-color: var(--text-secondary, #6c757d);
  134. }
  135. .btn-success-custom {
  136. background-color: var(--success-color, #28a745);
  137. color: white;
  138. border: 2px solid var(--success-color, #28a745);
  139. }
  140. .btn-success-custom:hover {
  141. background-color: #218838;
  142. border-color: #218838;
  143. transform: translateY(-1px);
  144. box-shadow: 0 2px 4px rgba(40, 167, 69, 0.3);
  145. }
  146. /* 响应式设计 */
  147. @media (max-width: 768px) {
  148. .editor-header {
  149. flex-direction: column;
  150. gap: 0.5rem;
  151. align-items: stretch;
  152. padding: 1rem;
  153. }
  154. .editor-title {
  155. font-size: 1.3rem;
  156. text-align: center;
  157. }
  158. .editor-actions {
  159. flex-direction: row;
  160. gap: 0.5rem;
  161. justify-content: center;
  162. }
  163. .btn-custom {
  164. flex: 1;
  165. justify-content: center;
  166. font-size: 0.85rem;
  167. padding: 0.4rem 0.8rem;
  168. }
  169. .document-title-input {
  170. padding: 0.6rem 1rem;
  171. font-size: 1rem;
  172. }
  173. }
  174. .loading-overlay {
  175. position: fixed;
  176. top: 0;
  177. left: 0;
  178. width: 100%;
  179. height: 100%;
  180. background-color: rgba(0, 0, 0, 0.5);
  181. display: flex;
  182. justify-content: center;
  183. align-items: center;
  184. z-index: 9999;
  185. }
  186. .loading-spinner {
  187. background-color: white;
  188. padding: 2rem;
  189. border-radius: var(--radius-lg, 12px);
  190. text-align: center;
  191. box-shadow: var(--shadow-lg, 0 8px 16px rgba(0, 0, 0, 0.15));
  192. }
  193. .spinner {
  194. border: 3px solid #f3f3f3;
  195. border-top: 3px solid var(--primary-color, #3d7cfa);
  196. border-radius: 50%;
  197. width: 40px;
  198. height: 40px;
  199. animation: spin 1s linear infinite;
  200. margin: 0 auto 1rem;
  201. }
  202. @keyframes spin {
  203. 0% { transform: rotate(0deg); }
  204. 100% { transform: rotate(360deg); }
  205. }
  206. </style>
  207. </head>
  208. <body>
  209. <div class="editor-container">
  210. <div class="editor-header">
  211. <h1 class="editor-title">
  212. <i class="fas fa-edit"></i>
  213. <span id="pageTitle">新建文档</span>
  214. </h1>
  215. <div class="editor-actions">
  216. <a href="/admin" class="btn-custom btn-secondary-custom">
  217. <i class="fas fa-arrow-left"></i> 返回管理面板
  218. </a>
  219. <button type="button" class="btn-custom btn-success-custom" id="saveBtn">
  220. <i class="fas fa-save"></i> 保存文档
  221. </button>
  222. </div>
  223. </div>
  224. <input
  225. type="text"
  226. id="documentTitle"
  227. class="document-title-input"
  228. placeholder="请输入文档标题..."
  229. autocomplete="off"
  230. >
  231. <div id="editor-md">
  232. <textarea style="display:none;" id="editorContent"></textarea>
  233. </div>
  234. </div>
  235. <!-- 加载覆盖层 -->
  236. <div class="loading-overlay" id="loadingOverlay" style="display: none;">
  237. <div class="loading-spinner">
  238. <div class="spinner"></div>
  239. <p>正在保存文档...</p>
  240. </div>
  241. </div>
  242. <script>
  243. let editor;
  244. let currentDocId = null;
  245. // 从 URL 参数获取文档 ID(如果是编辑模式)
  246. const urlParams = new URLSearchParams(window.location.search);
  247. const docId = urlParams.get('id');
  248. $(document).ready(function() {
  249. // 初始化 Editor.md
  250. initEditor();
  251. // 如果有文档 ID,则加载文档
  252. if (docId) {
  253. loadDocument(docId);
  254. document.getElementById('pageTitle').textContent = '编辑文档';
  255. }
  256. // 绑定保存按钮事件
  257. document.getElementById('saveBtn').addEventListener('click', saveDocument);
  258. // 绑定键盘快捷键 Ctrl+S
  259. document.addEventListener('keydown', function(e) {
  260. if (e.ctrlKey && e.key === 's') {
  261. e.preventDefault();
  262. saveDocument();
  263. }
  264. });
  265. });
  266. function initEditor() {
  267. editor = editormd("editor-md", {
  268. width: "100%",
  269. height: "100%", // 使用 CSS 的 flex 布局控制高度
  270. syncScrolling: "single",
  271. placeholder: "在这里编写您的 Markdown 内容...",
  272. path: "https://cdn.jsdelivr.net/npm/[email protected]/lib/",
  273. // 使用官方默认主题
  274. theme: "default",
  275. previewTheme: "default",
  276. editorTheme: "default",
  277. markdown: "",
  278. codeFold: true,
  279. saveHTMLToTextarea: true,
  280. searchReplace: true,
  281. htmlDecode: "style,script,iframe",
  282. emoji: true,
  283. taskList: true,
  284. tocm: true,
  285. tex: false,
  286. flowChart: false,
  287. sequenceDiagram: false,
  288. dialogLockScreen: false,
  289. dialogShowMask: false,
  290. previewCodeHighlight: true,
  291. toolbar: true,
  292. watch: true,
  293. lineNumbers: true,
  294. lineWrapping: false,
  295. autoCloseTags: true,
  296. autoFocus: true,
  297. indentUnit: 4,
  298. // 使用官方默认工具栏配置
  299. onload: function() {
  300. console.log('Editor.md 初始化完成');
  301. },
  302. onchange: function() {
  303. // 标记内容已修改
  304. markAsModified();
  305. }
  306. });
  307. }
  308. function markAsModified() {
  309. const saveBtn = document.getElementById('saveBtn');
  310. if (!saveBtn.classList.contains('modified')) {
  311. saveBtn.classList.add('modified');
  312. saveBtn.innerHTML = '<i class="fas fa-save"></i> 保存文档 *';
  313. }
  314. }
  315. function markAsSaved() {
  316. const saveBtn = document.getElementById('saveBtn');
  317. saveBtn.classList.remove('modified');
  318. saveBtn.innerHTML = '<i class="fas fa-save"></i> 保存文档';
  319. }
  320. async function loadDocument(id) {
  321. try {
  322. const response = await fetch(`/api/documents/${id}`, {
  323. credentials: 'same-origin'
  324. });
  325. if (!response.ok) {
  326. throw new Error(`加载文档失败: ${response.status}`);
  327. }
  328. const doc = await response.json();
  329. currentDocId = id;
  330. // 设置文档标题
  331. document.getElementById('documentTitle').value = doc.title || '';
  332. // 设置文档内容
  333. if (editor && editor.setMarkdown) {
  334. editor.setMarkdown(doc.content || '');
  335. }
  336. // 更新页面标题
  337. document.title = `编辑文档: ${doc.title} - HubCmdUI`;
  338. } catch (error) {
  339. console.error('加载文档失败:', error);
  340. Swal.fire({
  341. icon: 'error',
  342. title: '加载失败',
  343. text: '无法加载文档内容,请检查网络连接或文档是否存在。',
  344. confirmButtonColor: '#3d7cfa'
  345. });
  346. }
  347. }
  348. async function saveDocument() {
  349. const title = document.getElementById('documentTitle').value.trim();
  350. const content = editor ? editor.getMarkdown() : '';
  351. if (!title) {
  352. Swal.fire({
  353. icon: 'warning',
  354. title: '请输入标题',
  355. text: '文档标题不能为空',
  356. confirmButtonColor: '#3d7cfa'
  357. });
  358. return;
  359. }
  360. if (!content.trim()) {
  361. Swal.fire({
  362. icon: 'warning',
  363. title: '请输入内容',
  364. text: '文档内容不能为空',
  365. confirmButtonColor: '#3d7cfa'
  366. });
  367. return;
  368. }
  369. // 显示加载动画
  370. document.getElementById('loadingOverlay').style.display = 'flex';
  371. try {
  372. const url = currentDocId ? `/api/documents/${currentDocId}` : '/api/documents';
  373. const method = currentDocId ? 'PUT' : 'POST';
  374. const response = await fetch(url, {
  375. method: method,
  376. headers: {
  377. 'Content-Type': 'application/json',
  378. },
  379. credentials: 'same-origin',
  380. body: JSON.stringify({
  381. title: title,
  382. content: content
  383. })
  384. });
  385. if (!response.ok) {
  386. throw new Error(`保存失败: ${response.status}`);
  387. }
  388. const result = await response.json();
  389. // 如果是新建文档,更新当前文档 ID
  390. if (!currentDocId && result.id) {
  391. currentDocId = result.id;
  392. // 更新 URL
  393. window.history.replaceState({}, '', `?id=${result.id}`);
  394. document.getElementById('pageTitle').textContent = '编辑文档';
  395. }
  396. // 标记为已保存
  397. markAsSaved();
  398. // 更新页面标题
  399. document.title = `编辑文档: ${title} - HubCmdUI`;
  400. // 显示成功消息
  401. Swal.fire({
  402. icon: 'success',
  403. title: '保存成功',
  404. text: currentDocId ? '文档已更新' : '文档已创建',
  405. timer: 2000,
  406. showConfirmButton: false,
  407. toast: true,
  408. position: 'top-end'
  409. });
  410. } catch (error) {
  411. console.error('保存文档失败:', error);
  412. Swal.fire({
  413. icon: 'error',
  414. title: '保存失败',
  415. text: '无法保存文档,请检查网络连接或稍后重试。',
  416. confirmButtonColor: '#3d7cfa'
  417. });
  418. } finally {
  419. // 隐藏加载动画
  420. document.getElementById('loadingOverlay').style.display = 'none';
  421. }
  422. }
  423. // 页面卸载前提醒保存
  424. window.addEventListener('beforeunload', function(e) {
  425. const saveBtn = document.getElementById('saveBtn');
  426. if (saveBtn && saveBtn.classList.contains('modified')) {
  427. e.preventDefault();
  428. e.returnValue = '您有未保存的更改,确定要离开吗?';
  429. return e.returnValue;
  430. }
  431. });
  432. </script>
  433. </body>
  434. </html>