初始化2
This commit is contained in:
609
app/components/admin/MarkdownEditor.vue
Normal file
609
app/components/admin/MarkdownEditor.vue
Normal file
@@ -0,0 +1,609 @@
|
||||
<template>
|
||||
<div class="markdown-editor-wrapper" :class="{ 'fullscreen': isFullscreen }">
|
||||
<!-- 编辑器工具栏 -->
|
||||
<div class="editor-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<a-space>
|
||||
<a-tooltip title="标题">
|
||||
<a-button size="small" @click="insertAtCursor('# ')">
|
||||
<template #icon><span class="toolbar-icon">H</span></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-divider type="vertical" />
|
||||
<a-tooltip title="加粗">
|
||||
<a-button size="small" @click="insertWrap('**', '**')">
|
||||
<template #icon><span class="toolbar-icon bold">B</span></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="斜体">
|
||||
<a-button size="small" @click="insertWrap('*', '*')">
|
||||
<template #icon><span class="toolbar-icon italic">I</span></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="删除线">
|
||||
<a-button size="small" @click="insertWrap('~~', '~~')">
|
||||
<template #icon><span class="toolbar-icon">S</span></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-divider type="vertical" />
|
||||
<a-tooltip title="引用">
|
||||
<a-button size="small" @click="insertAtCursor('> ')">
|
||||
<template #icon><span class="toolbar-icon">></span></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="代码块">
|
||||
<a-button size="small" @click="insertCodeBlock">
|
||||
<template #icon><span class="toolbar-icon"></></span></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="行内代码">
|
||||
<a-button size="small" @click="insertWrap('`', '`')">
|
||||
<template #icon><span class="toolbar-icon">`</span></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-divider type="vertical" />
|
||||
<a-tooltip title="链接">
|
||||
<a-button size="small" @click="insertLink">
|
||||
<template #icon><LinkOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="图片">
|
||||
<a-button size="small" @click="handleImageUpload">
|
||||
<template #icon><PictureOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-divider type="vertical" />
|
||||
<a-tooltip title="有序列表">
|
||||
<a-button size="small" @click="insertAtCursor('1. ')">
|
||||
<template #icon><UnorderedListOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="无序列表">
|
||||
<a-button size="small" @click="insertAtCursor('- ')">
|
||||
<template #icon><OrderedListOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-divider type="vertical" />
|
||||
<a-tooltip title="水平线">
|
||||
<a-button size="small" @click="insertAtCursor('\n---\n')">
|
||||
<template #icon><MinusOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<a-button size="small" @click="toggleFullscreen">
|
||||
<template #icon><FullscreenOutlined v-if="!isFullscreen" /><FullscreenExitOutlined v-else /></template>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑区域 -->
|
||||
<div class="editor-body">
|
||||
<div class="editor-pane">
|
||||
<textarea
|
||||
ref="textareaRef"
|
||||
v-model="localValue"
|
||||
class="markdown-textarea"
|
||||
:placeholder="placeholder"
|
||||
:style="{ height: isFullscreen ? '100%' : 'auto' }"
|
||||
@input="handleInput"
|
||||
@keydown="handleKeydown"
|
||||
@scroll="syncScroll"
|
||||
/>
|
||||
</div>
|
||||
<div class="preview-pane" v-if="showPreview">
|
||||
<div class="preview-label">预览</div>
|
||||
<div class="markdown-preview" v-html="renderedHtml"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 隐藏的文件上传 -->
|
||||
<a-upload
|
||||
ref="uploadRef"
|
||||
accept="image/*"
|
||||
:show-upload-list="false"
|
||||
:before-upload="beforeImageUpload"
|
||||
:custom-request="handleCoverUpload"
|
||||
style="display: none"
|
||||
/>
|
||||
|
||||
<!-- 插入链接弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="showLinkModal"
|
||||
title="插入链接"
|
||||
:width="400"
|
||||
@ok="insertLinkConfirm"
|
||||
>
|
||||
<a-form :model="linkForm" layout="vertical">
|
||||
<a-form-item label="链接文本" required>
|
||||
<a-input v-model:value="linkForm.text" placeholder="显示的文本" />
|
||||
</a-form-item>
|
||||
<a-form-item label="链接地址" required>
|
||||
<a-input v-model:value="linkForm.url" placeholder="https://..." />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 字数统计 -->
|
||||
<div class="editor-footer">
|
||||
<span class="word-count">{{ wordCount }} 字</span>
|
||||
<span class="char-count">{{ charCount }} 字符</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
LinkOutlined,
|
||||
PictureOutlined,
|
||||
UnorderedListOutlined,
|
||||
OrderedListOutlined,
|
||||
MinusOutlined,
|
||||
FullscreenOutlined,
|
||||
FullscreenExitOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { marked } from 'marked'
|
||||
import hljs from 'highlight.js'
|
||||
import 'highlight.js/styles/github.css'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { uploadFile } from '@/api/system/file'
|
||||
|
||||
interface Props {
|
||||
modelValue: string
|
||||
placeholder?: string
|
||||
showPreview?: boolean
|
||||
minHeight?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
placeholder: '请输入 Markdown 内容...',
|
||||
showPreview: true,
|
||||
minHeight: '300px',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
// 配置 marked
|
||||
marked.setOptions({
|
||||
highlight: function(code: string, lang: string) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
return hljs.highlight(code, { language: lang }).value
|
||||
}
|
||||
return hljs.highlightAuto(code).value
|
||||
},
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
})
|
||||
|
||||
const textareaRef = ref<HTMLTextAreaElement | null>(null)
|
||||
const uploadRef = ref<any>(null)
|
||||
const localValue = ref(props.modelValue)
|
||||
const isFullscreen = ref(false)
|
||||
const showLinkModal = ref(false)
|
||||
const linkForm = reactive({
|
||||
text: '',
|
||||
url: '',
|
||||
})
|
||||
|
||||
// 监听外部值变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
if (newVal !== localValue.value) {
|
||||
localValue.value = newVal
|
||||
}
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const renderedHtml = computed(() => {
|
||||
if (!localValue.value) return '<p class="placeholder-hint">预览区域</p>'
|
||||
try {
|
||||
return marked.parse(localValue.value) as string
|
||||
} catch (e) {
|
||||
return '<p class="error-hint">渲染出错</p>'
|
||||
}
|
||||
})
|
||||
|
||||
const wordCount = computed(() => {
|
||||
const text = localValue.value.trim()
|
||||
if (!text) return 0
|
||||
return text.split(/\s+/).filter(Boolean).length
|
||||
})
|
||||
|
||||
const charCount = computed(() => localValue.value.length)
|
||||
|
||||
// 方法
|
||||
function handleInput() {
|
||||
emit('update:modelValue', localValue.value)
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
// Tab 键插入空格
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault()
|
||||
insertAtCursor(' ')
|
||||
}
|
||||
// Ctrl/Cmd + B 加粗
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'b') {
|
||||
e.preventDefault()
|
||||
insertWrap('**', '**')
|
||||
}
|
||||
// Ctrl/Cmd + I 斜体
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'i') {
|
||||
e.preventDefault()
|
||||
insertWrap('*', '*')
|
||||
}
|
||||
// Ctrl/Cmd + K 链接
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||||
e.preventDefault()
|
||||
insertLink()
|
||||
}
|
||||
}
|
||||
|
||||
function insertAtCursor(text: string) {
|
||||
const textarea = textareaRef.value
|
||||
if (!textarea) return
|
||||
|
||||
const start = textarea.selectionStart
|
||||
const end = textarea.selectionEnd
|
||||
const before = localValue.value.substring(0, start)
|
||||
const after = localValue.value.substring(end)
|
||||
|
||||
localValue.value = before + text + after
|
||||
emit('update:modelValue', localValue.value)
|
||||
|
||||
nextTick(() => {
|
||||
textarea.focus()
|
||||
textarea.selectionStart = textarea.selectionEnd = start + text.length
|
||||
})
|
||||
}
|
||||
|
||||
function insertWrap(before: string, after: string) {
|
||||
const textarea = textareaRef.value
|
||||
if (!textarea) return
|
||||
|
||||
const start = textarea.selectionStart
|
||||
const end = textarea.selectionEnd
|
||||
const selectedText = localValue.value.substring(start, end) || '文本'
|
||||
const text = before + selectedText + after
|
||||
|
||||
localValue.value = localValue.value.substring(0, start) + text + localValue.value.substring(end)
|
||||
emit('update:modelValue', localValue.value)
|
||||
|
||||
nextTick(() => {
|
||||
textarea.focus()
|
||||
textarea.selectionStart = start + before.length
|
||||
textarea.selectionEnd = start + before.length + selectedText.length
|
||||
})
|
||||
}
|
||||
|
||||
function insertCodeBlock() {
|
||||
const textarea = textareaRef.value
|
||||
if (!textarea) return
|
||||
|
||||
const start = textarea.selectionStart
|
||||
const end = textarea.selectionEnd
|
||||
const selectedText = localValue.value.substring(start, end) || '代码'
|
||||
const text = '\n```javascript\n' + selectedText + '\n```\n'
|
||||
|
||||
localValue.value = localValue.value.substring(0, start) + text + localValue.value.substring(end)
|
||||
emit('update:modelValue', localValue.value)
|
||||
|
||||
nextTick(() => {
|
||||
textarea.focus()
|
||||
textarea.selectionStart = start + 14 // 跳过 ```javascript\n
|
||||
textarea.selectionEnd = start + 14 + selectedText.length
|
||||
})
|
||||
}
|
||||
|
||||
function insertLink() {
|
||||
const textarea = textareaRef.value
|
||||
if (textarea) {
|
||||
const selectedText = localValue.value.substring(textarea.selectionStart, textarea.selectionEnd)
|
||||
linkForm.text = selectedText || ''
|
||||
}
|
||||
showLinkModal.value = true
|
||||
}
|
||||
|
||||
function insertLinkConfirm() {
|
||||
if (!linkForm.text.trim()) {
|
||||
message.warning('请输入链接文本')
|
||||
return
|
||||
}
|
||||
if (!linkForm.url.trim()) {
|
||||
message.warning('请输入链接地址')
|
||||
return
|
||||
}
|
||||
insertAtCursor(`[${linkForm.text}](${linkForm.url})`)
|
||||
showLinkModal.value = false
|
||||
linkForm.text = ''
|
||||
linkForm.url = ''
|
||||
}
|
||||
|
||||
function handleImageUpload() {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = 'image/*'
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0]
|
||||
if (file) {
|
||||
await uploadAndInsertImage(file)
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
|
||||
function beforeImageUpload(file: File) {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
message.error('只能上传图片文件')
|
||||
return false
|
||||
}
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
message.error('图片大小不能超过 5MB')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
async function uploadAndInsertImage(file: File) {
|
||||
try {
|
||||
message.loading({ content: '上传图片中...', key: 'upload' })
|
||||
const record = await uploadFile(file)
|
||||
const url = (record?.url || record?.downloadUrl || '').trim()
|
||||
if (!url) throw new Error('上传成功但未返回图片地址')
|
||||
insertAtCursor(``)
|
||||
message.success({ content: '图片上传成功', key: 'upload' })
|
||||
} catch (e: any) {
|
||||
message.error({ content: e?.message || '图片上传失败', key: 'upload' })
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCoverUpload(option: any) {
|
||||
await uploadAndInsertImage(option.file)
|
||||
option.onSuccess?.()
|
||||
}
|
||||
|
||||
function syncScroll(e: Event) {
|
||||
const textarea = e.target as HTMLTextAreaElement
|
||||
const preview = textarea.parentElement?.nextElementSibling?.querySelector('.markdown-preview') as HTMLElement
|
||||
if (preview) {
|
||||
const ratio = textarea.scrollTop / (textarea.scrollHeight - textarea.clientHeight)
|
||||
preview.scrollTop = ratio * (preview.scrollHeight - preview.clientHeight)
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFullscreen() {
|
||||
isFullscreen.value = !isFullscreen.value
|
||||
}
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
focus: () => textareaRef.value?.focus(),
|
||||
insertText: insertAtCursor,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.markdown-editor-wrapper {
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.markdown-editor-wrapper.fullscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.editor-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.toolbar-left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.toolbar-icon {
|
||||
font-family: Georgia, serif;
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.toolbar-icon.bold {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.toolbar-icon.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.editor-body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: v-bind(minHeight);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editor-pane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.markdown-textarea {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
border: none;
|
||||
outline: none;
|
||||
resize: none;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: #24292e;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.markdown-textarea::placeholder {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.preview-pane {
|
||||
flex: 1;
|
||||
border-left: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-label {
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.markdown-preview {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
color: #24292e;
|
||||
}
|
||||
|
||||
.markdown-preview :deep(.placeholder-hint),
|
||||
.markdown-preview :deep(.error-hint) {
|
||||
color: #aaa;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.markdown-preview :deep(h1) {
|
||||
font-size: 1.8em;
|
||||
font-weight: 700;
|
||||
margin: 0.5em 0;
|
||||
padding-bottom: 0.3em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
}
|
||||
|
||||
.markdown-preview :deep(h2) {
|
||||
font-size: 1.5em;
|
||||
font-weight: 600;
|
||||
margin: 0.8em 0 0.4em;
|
||||
padding-bottom: 0.25em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
}
|
||||
|
||||
.markdown-preview :deep(h3) {
|
||||
font-size: 1.25em;
|
||||
font-weight: 600;
|
||||
margin: 0.6em 0 0.3em;
|
||||
}
|
||||
|
||||
.markdown-preview :deep(p) {
|
||||
margin: 0.8em 0;
|
||||
}
|
||||
|
||||
.markdown-preview :deep(code) {
|
||||
padding: 0.2em 0.4em;
|
||||
background: #f6f8fa;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.markdown-preview :deep(pre) {
|
||||
padding: 16px;
|
||||
background: #f6f8fa;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.markdown-preview :deep(pre code) {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.markdown-preview :deep(blockquote) {
|
||||
margin: 1em 0;
|
||||
padding: 0.5em 1em;
|
||||
border-left: 4px solid #dfe2e5;
|
||||
color: #6a737d;
|
||||
background: #f6f8fa;
|
||||
}
|
||||
|
||||
.markdown-preview :deep(ul),
|
||||
.markdown-preview :deep(ol) {
|
||||
padding-left: 2em;
|
||||
margin: 0.8em 0;
|
||||
}
|
||||
|
||||
.markdown-preview :deep(li) {
|
||||
margin: 0.3em 0;
|
||||
}
|
||||
|
||||
.markdown-preview :deep(a) {
|
||||
color: #0366d6;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-preview :deep(a:hover) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.markdown-preview :deep(img) {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.markdown-preview :deep(hr) {
|
||||
margin: 1.5em 0;
|
||||
border: none;
|
||||
border-top: 1px solid #dfe2e5;
|
||||
}
|
||||
|
||||
.markdown-preview :deep(table) {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.markdown-preview :deep(th),
|
||||
.markdown-preview :deep(td) {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #dfe2e5;
|
||||
}
|
||||
|
||||
.markdown-preview :deep(th) {
|
||||
background: #f6f8fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.editor-footer {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
background: #fafafa;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
</style>
|
||||
171
app/components/admin/MarkdownRenderer.vue
Normal file
171
app/components/admin/MarkdownRenderer.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div class="markdown-renderer" v-html="renderedHtml"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { marked } from 'marked'
|
||||
import hljs from 'highlight.js'
|
||||
import 'highlight.js/styles/github.css'
|
||||
|
||||
interface Props {
|
||||
content: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// 配置 marked
|
||||
marked.setOptions({
|
||||
highlight: function(code: string, lang: string) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
return hljs.highlight(code, { language: lang }).value
|
||||
}
|
||||
return hljs.highlightAuto(code).value
|
||||
},
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
})
|
||||
|
||||
const renderedHtml = computed(() => {
|
||||
if (!props.content) return ''
|
||||
try {
|
||||
return marked.parse(props.content) as string
|
||||
} catch (e) {
|
||||
return '<p>渲染出错</p>'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.markdown-renderer {
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.markdown-renderer :deep(h1) {
|
||||
font-size: 1.8em;
|
||||
font-weight: 700;
|
||||
margin: 0.5em 0;
|
||||
padding-bottom: 0.3em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
}
|
||||
|
||||
.markdown-renderer :deep(h2) {
|
||||
font-size: 1.5em;
|
||||
font-weight: 600;
|
||||
margin: 0.8em 0 0.4em;
|
||||
padding-bottom: 0.25em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
}
|
||||
|
||||
.markdown-renderer :deep(h3) {
|
||||
font-size: 1.25em;
|
||||
font-weight: 600;
|
||||
margin: 0.6em 0 0.3em;
|
||||
}
|
||||
|
||||
.markdown-renderer :deep(h4),
|
||||
.markdown-renderer :deep(h5),
|
||||
.markdown-renderer :deep(h6) {
|
||||
font-weight: 600;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.markdown-renderer :deep(p) {
|
||||
margin: 0.8em 0;
|
||||
}
|
||||
|
||||
.markdown-renderer :deep(code) {
|
||||
padding: 0.2em 0.4em;
|
||||
background: #f6f8fa;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.markdown-renderer :deep(pre) {
|
||||
padding: 16px;
|
||||
background: #f6f8fa;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.markdown-renderer :deep(pre code) {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.markdown-renderer :deep(blockquote) {
|
||||
margin: 1em 0;
|
||||
padding: 0.5em 1em;
|
||||
border-left: 4px solid #dfe2e5;
|
||||
color: #6a737d;
|
||||
background: #f6f8fa;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
.markdown-renderer :deep(blockquote p) {
|
||||
margin: 0.3em 0;
|
||||
}
|
||||
|
||||
.markdown-renderer :deep(ul),
|
||||
.markdown-renderer :deep(ol) {
|
||||
padding-left: 2em;
|
||||
margin: 0.8em 0;
|
||||
}
|
||||
|
||||
.markdown-renderer :deep(li) {
|
||||
margin: 0.3em 0;
|
||||
}
|
||||
|
||||
.markdown-renderer :deep(a) {
|
||||
color: #1890ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-renderer :deep(a:hover) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.markdown-renderer :deep(img) {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
margin: 1em 0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.markdown-renderer :deep(hr) {
|
||||
margin: 1.5em 0;
|
||||
border: none;
|
||||
border-top: 1px solid #dfe2e5;
|
||||
}
|
||||
|
||||
.markdown-renderer :deep(table) {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1em 0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.markdown-renderer :deep(th),
|
||||
.markdown-renderer :deep(td) {
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #dfe2e5;
|
||||
}
|
||||
|
||||
.markdown-renderer :deep(th) {
|
||||
background: #f6f8fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-renderer :deep(tr:hover) {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.markdown-renderer :deep(input[type="checkbox"]) {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user