CzrMarkdown.vue 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. <template>
  2. <div class="czr-markdown-main">
  3. <div class="czr-markdown-main-menus">
  4. <template v-for="item in Menus">
  5. <template v-if="item.options?.length > 0">
  6. <el-dropdown>
  7. <div
  8. class="__hover button flex items-center gap-1"
  9. :title="item.title"
  10. >
  11. <SvgIcon :name="item.icon" color="#000000" />
  12. <SvgIcon
  13. name="czr_arrow"
  14. :rotate="90"
  15. size="12"
  16. color="#000000"
  17. />
  18. </div>
  19. <template #dropdown>
  20. <el-dropdown-menu>
  21. <template v-for="op in item.options">
  22. <el-dropdown-item @click="insertText(op.start, op.end)">{{
  23. op.title
  24. }}</el-dropdown-item>
  25. </template>
  26. </el-dropdown-menu>
  27. </template>
  28. </el-dropdown>
  29. </template>
  30. <template v-else>
  31. <div
  32. class="__hover button flex items-center gap-1"
  33. @click="insertText(item.start, item.end)"
  34. :title="item.title"
  35. >
  36. <SvgIcon :name="item.icon" color="#000000" />
  37. </div>
  38. </template>
  39. <template v-if="item.split">
  40. <div class="split" />
  41. </template>
  42. </template>
  43. </div>
  44. <div class="czr-markdown-main-content" :class="`layout-${layout}`">
  45. <div class="editor">
  46. <textarea
  47. ref="ref_textarea"
  48. v-model="markdownValue"
  49. @input="updateMarkdown"
  50. ></textarea>
  51. </div>
  52. <div class="preview" v-html="markdownHtmlCpt"></div>
  53. </div>
  54. </div>
  55. </template>
  56. <script setup lang="ts">
  57. defineOptions({
  58. name: 'CzrMarkdown',
  59. })
  60. import { ref, computed, onMounted } from 'vue'
  61. import { marked } from 'marked'
  62. import DOMPurify from 'dompurify'
  63. const renderer: any = new marked.Renderer()
  64. renderer.link = ({ href, text }) => {
  65. return `<a href="${href}">${text}</a>`
  66. }
  67. DOMPurify.addHook('afterSanitizeAttributes', (node) => {
  68. if (node.tagName === 'A') {
  69. node.setAttribute('target', '_blank')
  70. node.setAttribute('rel', 'noopener noreferrer')
  71. }
  72. })
  73. const emit = defineEmits(['update:modelValue'])
  74. const props = defineProps({
  75. modelValue: {
  76. type: String,
  77. default: '',
  78. },
  79. layout: {
  80. type: String,
  81. default: 'x',
  82. },
  83. })
  84. const Menus: any = [
  85. {
  86. icon: 'czr_md-title',
  87. title: '标题',
  88. options: [
  89. { title: '一级标题', start: '# ', end: '' },
  90. { title: '二级标题', start: '## ', end: '' },
  91. { title: '三级标题', start: '### ', end: '' },
  92. { title: '四级标题', start: '#### ', end: '' },
  93. { title: '五级标题', start: '##### ', end: '' },
  94. {
  95. label: 'H6',
  96. title: '六级标题',
  97. start: '###### ',
  98. end: '',
  99. },
  100. ],
  101. },
  102. { icon: 'czr_md-bold', title: '加粗', start: '**', end: '**' },
  103. { icon: 'czr_md-em', title: '斜体', start: '*', end: '*' },
  104. { icon: 'czr_md-bold-em', title: '加粗斜体', start: '***', end: '***' },
  105. {
  106. icon: 'czr_md-s',
  107. title: '删除线',
  108. start: '~~',
  109. end: '~~',
  110. },
  111. {
  112. icon: 'czr_md-u',
  113. title: '下划线',
  114. start: '<u>',
  115. end: '</u>',
  116. split: true,
  117. },
  118. { icon: 'czr_md-code', title: '代码块', start: '\n```\n', end: '\n```' },
  119. {
  120. icon: 'czr_md-link',
  121. title: '链接',
  122. start: '[链接](',
  123. end: ')',
  124. },
  125. { icon: 'czr_md-img', title: '图片', start: '![图片](', end: ')' },
  126. ]
  127. const ref_textarea = ref(null)
  128. const markdownValue = ref(props.modelValue)
  129. const markdownHtmlCpt = computed(() => {
  130. return DOMPurify.sanitize(marked(markdownValue.value) as any)
  131. })
  132. const updateMarkdown = () => {
  133. emit('update:modelValue', markdownValue.value)
  134. }
  135. const insertText = (before, after) => {
  136. const dom: any = ref_textarea.value
  137. if (dom) {
  138. const start = dom.selectionStart
  139. const end = dom.selectionEnd
  140. const selectedText = markdownValue.value.substring(start, end)
  141. markdownValue.value =
  142. markdownValue.value.substring(0, start) +
  143. before +
  144. selectedText +
  145. after +
  146. markdownValue.value.substring(end)
  147. // 移动光标位置
  148. setTimeout(() => {
  149. dom.selectionStart = start + before.length
  150. dom.selectionEnd = end + before.length
  151. dom.focus()
  152. }, 0)
  153. emit('update:modelValue', markdownValue.value)
  154. }
  155. }
  156. onMounted(() => {
  157. marked.setOptions({
  158. renderer: renderer,
  159. breaks: true,
  160. gfm: true,
  161. })
  162. })
  163. </script>
  164. <style scoped lang="scss">
  165. .czr-markdown-main {
  166. width: 100%;
  167. height: 100%;
  168. display: flex;
  169. flex-direction: column;
  170. box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
  171. .czr-markdown-main-menus {
  172. display: flex;
  173. align-items: center;
  174. gap: 10px;
  175. box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
  176. padding: 10px;
  177. .split {
  178. width: 1px;
  179. height: 16px;
  180. background-color: rgba(0, 0, 0, 0.3);
  181. }
  182. }
  183. .czr-markdown-main-content {
  184. flex: 1;
  185. display: flex;
  186. overflow: hidden;
  187. &.layout-y {
  188. flex-direction: column;
  189. .editor {
  190. border-bottom: 1px solid rgba(0, 0, 0, 0.15);
  191. }
  192. }
  193. &.layout-x {
  194. .editor {
  195. border-right: 1px solid rgba(0, 0, 0, 0.15);
  196. }
  197. }
  198. .editor,
  199. .preview {
  200. flex: 1;
  201. padding: 14px;
  202. }
  203. .editor {
  204. textarea {
  205. width: 100%;
  206. height: 100%;
  207. resize: none;
  208. }
  209. }
  210. :deep(.preview) {
  211. overflow: auto;
  212. /* 通用标题样式 */
  213. h1,
  214. h2,
  215. h3,
  216. h4,
  217. h5,
  218. h6 {
  219. font-family:
  220. -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica,
  221. Arial, sans-serif;
  222. font-weight: 600;
  223. line-height: 1.25;
  224. margin-top: 1.5em;
  225. margin-bottom: 0.5em;
  226. color: #222;
  227. }
  228. /* H1 样式 */
  229. h1 {
  230. font-size: 2em;
  231. border-bottom: 1px solid #eaecef;
  232. padding-bottom: 0.3em;
  233. margin-top: 0;
  234. }
  235. /* H2 样式 */
  236. h2 {
  237. font-size: 1.5em;
  238. border-bottom: 1px solid #eaecef;
  239. padding-bottom: 0.3em;
  240. }
  241. /* H3 样式 */
  242. h3 {
  243. font-size: 1.25em;
  244. }
  245. /* H4 样式 */
  246. h4 {
  247. font-size: 1em;
  248. }
  249. /* H5 样式 */
  250. h5 {
  251. font-size: 0.875em;
  252. }
  253. /* H6 样式 */
  254. h6 {
  255. font-size: 0.85em;
  256. color: #6a737d;
  257. }
  258. /* 默认链接样式 */
  259. a {
  260. color: #0366d6; /* 链接颜色 */
  261. text-decoration: none; /* 去掉下划线 */
  262. transition: color 0.2s; /* 颜色过渡效果 */
  263. /* 鼠标悬停样式 */
  264. &:hover {
  265. color: #0550a8; /* 悬停颜色 */
  266. text-decoration: underline; /* 显示下划线 */
  267. }
  268. /* 已访问链接 */
  269. &:visited {
  270. color: #5a32a3; /* 紫色表示已访问 */
  271. }
  272. /* 活动链接 (点击时) */
  273. &:active {
  274. color: #d63384;
  275. }
  276. }
  277. /*
  278. * 暗色系代码块样式
  279. * 适用于夜间/暗色模式的代码展示
  280. */
  281. // 基础代码块样式
  282. pre {
  283. background-color: #1e1e1e; // 深灰背景
  284. border-radius: 6px; // 圆角
  285. padding: 1.25rem; // 内边距
  286. margin: 1.5rem 0; // 外边距
  287. overflow: auto; // 溢出滚动
  288. border: 1px solid #333; // 边框
  289. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); // 阴影效果
  290. // 代码内容样式
  291. code {
  292. display: block;
  293. color: #d4d4d4; // 主文本颜色
  294. font-family:
  295. 'Fira Code', 'Consolas', 'Monaco', monospace; // 等宽字体栈
  296. font-size: 0.95em;
  297. line-height: 1.5;
  298. text-shadow: none;
  299. white-space: pre;
  300. }
  301. }
  302. }
  303. }
  304. }
  305. </style>