CzRger месяцев назад: 3
Родитель
Сommit
cefd676819

Разница между файлами не показана из-за своего большого размера
+ 1 - 0
src/assets/svg/czr_md-bold-em.svg


Разница между файлами не показана из-за своего большого размера
+ 1 - 0
src/assets/svg/czr_md-bold.svg


Разница между файлами не показана из-за своего большого размера
+ 1 - 0
src/assets/svg/czr_md-code.svg


Разница между файлами не показана из-за своего большого размера
+ 1 - 0
src/assets/svg/czr_md-em.svg


Разница между файлами не показана из-за своего большого размера
+ 1 - 0
src/assets/svg/czr_md-img.svg


Разница между файлами не показана из-за своего большого размера
+ 1 - 0
src/assets/svg/czr_md-link.svg


Разница между файлами не показана из-за своего большого размера
+ 1 - 0
src/assets/svg/czr_md-s.svg


Разница между файлами не показана из-за своего большого размера
+ 1 - 0
src/assets/svg/czr_md-title.svg


Разница между файлами не показана из-за своего большого размера
+ 1 - 0
src/assets/svg/czr_md-u.svg


+ 277 - 80
src/components/czr-ui/CzrMarkdown/CzrMarkdown.vue

@@ -1,21 +1,55 @@
 <template>
-  <div class="markdown-container">
-    <div class="toolbar">
-      <button @click="insertText('**', '**')">B</button>
-      <button @click="insertText('*', '*')">I</button>
-      <button @click="insertText('# ', '')">H1</button>
-      <button @click="insertText('## ', '')">H2</button>
-      <button @click="insertText('[', '](url)')">Link</button>
-      <button @click="insertText('![', '](image.png)')">Image</button>
+  <div class="czr-markdown-main">
+    <div class="czr-markdown-main-menus">
+      <template v-for="item in Menus">
+        <template v-if="item.options?.length > 0">
+          <el-dropdown>
+            <div
+              class="__hover button flex items-center gap-1"
+              :title="item.title"
+            >
+              <SvgIcon :name="item.icon" color="#000000" />
+              <SvgIcon
+                name="czr_arrow"
+                :rotate="90"
+                size="12"
+                color="#000000"
+              />
+            </div>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <template v-for="op in item.options">
+                  <el-dropdown-item @click="insertText(op.start, op.end)">{{
+                    op.title
+                  }}</el-dropdown-item>
+                </template>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+        </template>
+        <template v-else>
+          <div
+            class="__hover button flex items-center gap-1"
+            @click="insertText(item.start, item.end)"
+            :title="item.title"
+          >
+            <SvgIcon :name="item.icon" color="#000000" />
+          </div>
+        </template>
+        <template v-if="item.split">
+          <div class="split" />
+        </template>
+      </template>
     </div>
-    <div class="editor-wrapper">
-      <textarea
-        ref="textarea"
-        v-model="markdownText"
-        class="editor"
-        @input="updateMarkdown"
-      ></textarea>
-      <div class="preview" v-html="compiledMarkdown"></div>
+    <div class="czr-markdown-main-content" :class="`layout-${layout}`">
+      <div class="editor">
+        <textarea
+          ref="ref_textarea"
+          v-model="markdownValue"
+          @input="updateMarkdown"
+        ></textarea>
+      </div>
+      <div class="preview" v-html="markdownHtmlCpt"></div>
     </div>
   </div>
 </template>
@@ -28,97 +62,260 @@ import { ref, computed, onMounted } from 'vue'
 import { marked } from 'marked'
 import DOMPurify from 'dompurify'
 
+const renderer: any = new marked.Renderer()
+renderer.link = ({ href, text }) => {
+  return `<a href="${href}">${text}</a>`
+}
+DOMPurify.addHook('afterSanitizeAttributes', (node) => {
+  if (node.tagName === 'A') {
+    node.setAttribute('target', '_blank')
+    node.setAttribute('rel', 'noopener noreferrer')
+  }
+})
 const emit = defineEmits(['update:modelValue'])
 const props = defineProps({
   modelValue: {
     type: String,
     default: '',
   },
+  layout: {
+    type: String,
+    default: 'x',
+  },
 })
-const textarea = ref(null)
-const markdownText = ref(props.modelValue)
+const Menus: any = [
+  {
+    icon: 'czr_md-title',
+    title: '标题',
+    options: [
+      { title: '一级标题', start: '# ', end: '' },
+      { title: '二级标题', start: '## ', end: '' },
+      { title: '三级标题', start: '### ', end: '' },
+      { title: '四级标题', start: '#### ', end: '' },
+      { title: '五级标题', start: '##### ', end: '' },
+      {
+        label: 'H6',
+        title: '六级标题',
+        start: '###### ',
+        end: '',
+      },
+    ],
+  },
+  { icon: 'czr_md-bold', title: '加粗', start: '**', end: '**' },
+  { icon: 'czr_md-em', title: '斜体', start: '*', end: '*' },
+  { icon: 'czr_md-bold-em', title: '加粗斜体', start: '***', end: '***' },
+  {
+    icon: 'czr_md-s',
+    title: '删除线',
+    start: '~~',
+    end: '~~',
+  },
+  {
+    icon: 'czr_md-u',
+    title: '下划线',
+    start: '<u>',
+    end: '</u>',
+    split: true,
+  },
+  { icon: 'czr_md-code', title: '代码块', start: '\n```\n', end: '\n```' },
+  {
+    icon: 'czr_md-link',
+    title: '链接',
+    start: '[链接](',
+    end: ')',
+  },
+  { icon: 'czr_md-img', title: '图片', start: '![图片](', end: ')' },
+]
+const ref_textarea = ref(null)
+const markdownValue = ref(props.modelValue)
 
-const compiledMarkdown = computed(() => {
-  return DOMPurify.sanitize(marked(markdownText.value))
+const markdownHtmlCpt = computed(() => {
+  return DOMPurify.sanitize(marked(markdownValue.value) as any)
 })
 
-function updateMarkdown() {
-  emit('update:modelValue', markdownText.value)
+const updateMarkdown = () => {
+  emit('update:modelValue', markdownValue.value)
 }
 
-function insertText(before, after) {
-  const textareaEl = textarea.value
-  const start = textareaEl.selectionStart
-  const end = textareaEl.selectionEnd
-  const selectedText = markdownText.value.substring(start, end)
-
-  markdownText.value =
-    markdownText.value.substring(0, start) +
-    before +
-    selectedText +
-    after +
-    markdownText.value.substring(end)
-
-  // 移动光标位置
-  setTimeout(() => {
-    textareaEl.selectionStart = start + before.length
-    textareaEl.selectionEnd = end + before.length
-    textareaEl.focus()
-  }, 0)
-
-  emit('update:modelValue', markdownText.value)
+const insertText = (before, after) => {
+  const dom: any = ref_textarea.value
+  if (dom) {
+    const start = dom.selectionStart
+    const end = dom.selectionEnd
+    const selectedText = markdownValue.value.substring(start, end)
+    markdownValue.value =
+      markdownValue.value.substring(0, start) +
+      before +
+      selectedText +
+      after +
+      markdownValue.value.substring(end)
+    // 移动光标位置
+    setTimeout(() => {
+      dom.selectionStart = start + before.length
+      dom.selectionEnd = end + before.length
+      dom.focus()
+    }, 0)
+    emit('update:modelValue', markdownValue.value)
+  }
 }
-
 onMounted(() => {
   marked.setOptions({
+    renderer: renderer,
     breaks: true,
     gfm: true,
   })
 })
 </script>
 
-<style>
-.markdown-container {
+<style scoped lang="scss">
+.czr-markdown-main {
+  width: 100%;
+  height: 100%;
   display: flex;
   flex-direction: column;
-  height: 100%;
-}
+  box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
+  .czr-markdown-main-menus {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
+    padding: 10px;
+    .split {
+      width: 1px;
+      height: 16px;
+      background-color: rgba(0, 0, 0, 0.3);
+    }
+  }
+  .czr-markdown-main-content {
+    flex: 1;
+    display: flex;
+    overflow: hidden;
+    &.layout-y {
+      flex-direction: column;
+      .editor {
+        border-bottom: 1px solid rgba(0, 0, 0, 0.15);
+      }
+    }
+    &.layout-x {
+      .editor {
+        border-right: 1px solid rgba(0, 0, 0, 0.15);
+      }
+    }
+    .editor,
+    .preview {
+      flex: 1;
+      padding: 14px;
+    }
+    .editor {
+      textarea {
+        width: 100%;
+        height: 100%;
+        resize: none;
+      }
+    }
+    :deep(.preview) {
+      overflow: auto;
+      /* 通用标题样式 */
+      h1,
+      h2,
+      h3,
+      h4,
+      h5,
+      h6 {
+        font-family:
+          -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica,
+          Arial, sans-serif;
+        font-weight: 600;
+        line-height: 1.25;
+        margin-top: 1.5em;
+        margin-bottom: 0.5em;
+        color: #222;
+      }
 
-.toolbar {
-  padding: 8px;
-  background: #f5f5f5;
-  border-bottom: 1px solid #ddd;
-}
+      /* H1 样式 */
+      h1 {
+        font-size: 2em;
+        border-bottom: 1px solid #eaecef;
+        padding-bottom: 0.3em;
+        margin-top: 0;
+      }
 
-.toolbar button {
-  margin-right: 8px;
-  padding: 4px 8px;
-  background: white;
-  border: 1px solid #ddd;
-  border-radius: 4px;
-  cursor: pointer;
-}
+      /* H2 样式 */
+      h2 {
+        font-size: 1.5em;
+        border-bottom: 1px solid #eaecef;
+        padding-bottom: 0.3em;
+      }
 
-.editor-wrapper {
-  display: flex;
-  flex: 1;
-  overflow: hidden;
-}
+      /* H3 样式 */
+      h3 {
+        font-size: 1.25em;
+      }
 
-.editor {
-  flex: 1;
-  padding: 10px;
-  border: none;
-  border-right: 1px solid #ddd;
-  resize: none;
-  font-family: monospace;
-  outline: none;
-}
+      /* H4 样式 */
+      h4 {
+        font-size: 1em;
+      }
+
+      /* H5 样式 */
+      h5 {
+        font-size: 0.875em;
+      }
+
+      /* H6 样式 */
+      h6 {
+        font-size: 0.85em;
+        color: #6a737d;
+      }
+      /* 默认链接样式 */
+      a {
+        color: #0366d6; /* 链接颜色 */
+        text-decoration: none; /* 去掉下划线 */
+        transition: color 0.2s; /* 颜色过渡效果 */
+        /* 鼠标悬停样式 */
+        &:hover {
+          color: #0550a8; /* 悬停颜色 */
+          text-decoration: underline; /* 显示下划线 */
+        }
+        /* 已访问链接 */
+        &:visited {
+          color: #5a32a3; /* 紫色表示已访问 */
+        }
+
+        /* 活动链接 (点击时) */
+        &:active {
+          color: #d63384;
+        }
+      }
+
+      /*
+ * 暗色系代码块样式
+ * 适用于夜间/暗色模式的代码展示
+ */
+
+      // 基础代码块样式
+      pre {
+        background-color: #1e1e1e; // 深灰背景
+        border-radius: 6px; // 圆角
+        padding: 1.25rem; // 内边距
+        margin: 1.5rem 0; // 外边距
+        overflow: auto; // 溢出滚动
+        border: 1px solid #333; // 边框
+        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); // 阴影效果
 
-.preview {
-  flex: 1;
-  padding: 10px;
-  overflow-y: auto;
-  background: white;
+        // 代码内容样式
+        code {
+          display: block;
+          color: #d4d4d4; // 主文本颜色
+          font-family:
+            'Fira Code', 'Consolas', 'Monaco', monospace; // 等宽字体栈
+          font-size: 0.95em;
+          line-height: 1.5;
+          text-shadow: none;
+          white-space: pre;
+        }
+      }
+    }
+  }
 }
 </style>

+ 3 - 1
src/views/manage/app/index.vue

@@ -4,7 +4,9 @@
       <!--      <CzrRich v-model="" />-->
     </el-card>
     <el-card>
-      <CzrMarkdown />
+      <div class="w-full h-[600px]">
+        <CzrMarkdown />
+      </div>
     </el-card>
     <div class="editor-container">
       <div