Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 
 
 
 

199 rader
7.6 KiB

  1. {% extends "../layout.html" %}
  2. {% block title %}{{ columnInfo.name if columnInfo else '单页管理' }}{% endblock %}
  3. {% block content %}
  4. <div id="pageApp">
  5. <el-card shadow="never">
  6. <template #header>
  7. <div class="flex items-center justify-between">
  8. <div class="flex items-center gap-2">
  9. <span class="font-medium">{{ columnInfo.name if columnInfo else '单页管理' }}</span>
  10. <el-tag type="info" size="small">单页</el-tag>
  11. <el-tag :type="pageStatus ? 'success' : 'warning'" size="small">${ pageStatus ? '已发布' : '草稿' }</el-tag>
  12. <span v-if="updateInfo" class="text-gray-400 text-xs ml-2">${ updateInfo }</span>
  13. </div>
  14. <div class="flex gap-2">
  15. <el-button :icon="View" @click="openPreview">预览</el-button>
  16. <el-button type="primary" :icon="DocumentChecked" @click="savePage" :loading="saving">保存</el-button>
  17. </div>
  18. </div>
  19. </template>
  20. <!-- WangEditor 富文本编辑器 -->
  21. <div class="editor-wrap">
  22. <div id="toolbar-container"></div>
  23. <div id="editor-container"></div>
  24. </div>
  25. <!-- 发布状态 -->
  26. <div class="flex items-center justify-between mt-4 pt-4 border-t">
  27. <div class="flex items-center gap-4">
  28. <span class="text-gray-500">发布状态:</span>
  29. <el-switch v-model="pageStatus" :active-value="1" :inactive-value="0" active-text="已发布" inactive-text="草稿"></el-switch>
  30. </div>
  31. <div class="flex gap-2">
  32. <el-button :icon="View" @click="openPreview">预览</el-button>
  33. <el-button type="primary" :icon="DocumentChecked" @click="savePage" :loading="saving">保存</el-button>
  34. </div>
  35. </div>
  36. </el-card>
  37. </div>
  38. <style>
  39. .editor-wrap { border: 1px solid #e4e7ed; border-radius: 4px; overflow: hidden; }
  40. #toolbar-container { border-bottom: 1px solid #e4e7ed; }
  41. #editor-container { min-height: 500px; }
  42. </style>
  43. {% endblock %}
  44. {% block js %}
  45. <!-- WangEditor -->
  46. <link href="https://unpkg.com/@wangeditor/editor@5.1.23/dist/css/style.css" rel="stylesheet">
  47. <script src="https://unpkg.com/@wangeditor/editor@5.1.23/dist/index.js"></script>
  48. <script>
  49. const col = '{{ col }}';
  50. const initContent = `{{ pageData.content | safe if pageData.content else "" }}`;
  51. const initStatus = parseInt('{{ pageData.status if pageData.status is defined else 1 }}') || 1;
  52. const initUpdateTime = '{{ pageData.update_time if pageData.update_time else "" }}';
  53. const initUpdateBy = '{{ pageData.update_by if pageData.update_by else "" }}';
  54. const { createApp, ref, onMounted, onBeforeUnmount } = Vue;
  55. const app = createApp({
  56. delimiters: ['${', '}'],
  57. setup() {
  58. const saving = ref(false);
  59. const pageStatus = ref(initStatus);
  60. const updateInfo = ref('');
  61. let editor = null;
  62. // 格式化更新信息
  63. if (initUpdateTime) {
  64. updateInfo.value = '最后更新:' + initUpdateTime.slice(0, 16).replace('T', ' ') + (initUpdateBy ? ' · ' + initUpdateBy : '');
  65. }
  66. onMounted(() => {
  67. // 创建编辑器
  68. const { createEditor, createToolbar } = window.wangEditor;
  69. editor = createEditor({
  70. selector: '#editor-container',
  71. html: initContent || '<p></p>',
  72. config: {
  73. placeholder: '请输入页面内容...',
  74. MENU_CONF: {
  75. uploadImage: {
  76. server: '/admin/upload',
  77. fieldName: 'file',
  78. maxFileSize: 10 * 1024 * 1024,
  79. customInsert(res, insertFn) {
  80. if (res.code === 0) {
  81. insertFn(res.data.url, res.data.name || '', res.data.url);
  82. } else {
  83. ElementPlus.ElMessage.error(res.msg || '上传失败');
  84. }
  85. }
  86. }
  87. }
  88. }
  89. });
  90. createToolbar({
  91. editor,
  92. selector: '#toolbar-container',
  93. config: {}
  94. });
  95. });
  96. onBeforeUnmount(() => {
  97. if (editor) {
  98. editor.destroy();
  99. editor = null;
  100. }
  101. });
  102. function openPreview() {
  103. if (!editor) return;
  104. const content = editor.getHtml();
  105. const columnName = '{{ columnInfo.name if columnInfo else "页面预览" }}';
  106. const html = `<!DOCTYPE html>
  107. <html lang="zh-CN">
  108. <head>
  109. <meta charset="UTF-8">
  110. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  111. <title>${columnName} - 预览</title>
  112. <style>
  113. * { margin: 0; padding: 0; box-sizing: border-box; }
  114. body { font-family: "Source Han Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif; color: #303133; background: #fff; }
  115. .preview-tip { background: #ff7800; color: #fff; padding: 10px 0; text-align: center; font-size: 13px; }
  116. .preview-tip span { background: rgba(0,0,0,.15); padding: 3px 10px; border-radius: 3px; font-size: 12px; margin-left: 8px; }
  117. .preview-header { background: #fff; border-bottom: 1px solid #e4e7ed; padding: 20px 40px; }
  118. .preview-header h1 { font-size: 24px; color: #1A3550; font-weight: 700; }
  119. .preview-body { max-width: 960px; margin: 0 auto; padding: 40px 20px 60px; font-size: 15px; line-height: 2; }
  120. .preview-body h1 { font-size: 28px; margin: 24px 0 16px; font-weight: 700; color: #1A3550; }
  121. .preview-body h2 { font-size: 22px; margin: 20px 0 12px; font-weight: 600; color: #1A3550; border-left: 4px solid #ff7800; padding-left: 14px; }
  122. .preview-body h3 { font-size: 17px; margin: 16px 0 10px; font-weight: 600; }
  123. .preview-body p { margin-bottom: 14px; }
  124. .preview-body blockquote { border-left: 4px solid #ff7800; padding: 14px 20px; margin: 16px 0; background: #fff8f0; color: #606266; }
  125. .preview-body ul, .preview-body ol { padding-left: 26px; margin-bottom: 14px; }
  126. .preview-body li { margin-bottom: 6px; }
  127. .preview-body table { width: 100%; border-collapse: collapse; margin: 16px 0; }
  128. .preview-body td, .preview-body th { border: 1px solid #e4e7ed; padding: 10px 14px; font-size: 14px; }
  129. .preview-body th { background: #fafafa; font-weight: 600; }
  130. .preview-body img { max-width: 100%; border-radius: 6px; margin: 12px 0; }
  131. .preview-body a { color: #ff7800; }
  132. </style>
  133. </head>
  134. <body>
  135. <div class="preview-tip">前台预览模式 <span>内容尚未发布,仅供预览</span></div>
  136. <div class="preview-header"><h1>${columnName}</h1></div>
  137. <div class="preview-body">${content}</div>
  138. </body>
  139. </html>`;
  140. const win = window.open('', '_blank');
  141. win.document.write(html);
  142. win.document.close();
  143. }
  144. async function savePage() {
  145. if (!editor) return;
  146. const content = editor.getHtml();
  147. saving.value = true;
  148. try {
  149. const res = await fetch('/admin/content/page/save', {
  150. method: 'POST',
  151. headers: { 'Content-Type': 'application/json' },
  152. body: JSON.stringify({ col, content, status: pageStatus.value })
  153. }).then(r => r.json());
  154. if (res.code === 0) {
  155. ElementPlus.ElMessage.success('保存成功');
  156. // 更新时间显示
  157. const now = new Date();
  158. const timeStr = now.toISOString().slice(0, 16).replace('T', ' ');
  159. updateInfo.value = '最后更新:' + timeStr + ' · 管理员';
  160. } else {
  161. ElementPlus.ElMessage.error(res.msg || '保存失败');
  162. }
  163. } finally {
  164. saving.value = false;
  165. }
  166. }
  167. return { saving, pageStatus, updateInfo, openPreview, savePage, View: ElementPlusIconsVue.View, DocumentChecked: ElementPlusIconsVue.DocumentChecked };
  168. }
  169. });
  170. for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  171. app.component(key, component);
  172. }
  173. app.use(ElementPlus, { locale: ElementPlusLocaleZhCn });
  174. app.mount('#pageApp');
  175. </script>
  176. {% endblock %}