feat(editor): 调整字体大小并增强链接语法高亮
- 将编辑器字体大小从15px调整为16-17px,提升可读性 - 在编辑状态为链接语法标记添加.cm-md-syntax样式,使其可见且颜色正确 - 修复编辑器内链接和光标颜色,统一使用主题主色
This commit is contained in:
parent
c0458dc1e2
commit
d15ef9da00
|
|
@ -399,6 +399,21 @@ const buildDecorations = (state, filePath) => {
|
|||
to: nodeTo,
|
||||
value: Decoration.mark({ class: 'cm-md-link-text' })
|
||||
})
|
||||
} else {
|
||||
// 编辑状态:给链接的语法标记添加样式,使其可见且颜色正确
|
||||
let cursor = node.node.cursor()
|
||||
if (cursor.firstChild()) {
|
||||
do {
|
||||
const subType = cursor.type.name
|
||||
if (subType === 'LinkMark' || subType === 'URL' || subType === 'LinkTitle') {
|
||||
decorations.push({
|
||||
from: cursor.from,
|
||||
to: cursor.to,
|
||||
value: Decoration.mark({ class: 'cm-md-syntax' })
|
||||
})
|
||||
}
|
||||
} while (cursor.nextSibling())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,299 +1,305 @@
|
|||
<template>
|
||||
<div class="live-editor">
|
||||
<EditorToolbar @action="handleToolbarAction" />
|
||||
<codemirror
|
||||
v-model="code"
|
||||
placeholder="请输入 Markdown 内容..."
|
||||
:style="{ height: '0', flex: 1, fontSize: 'var(--editor-font-size)' }"
|
||||
:autofocus="true"
|
||||
:indent-with-tab="true"
|
||||
:tab-size="2"
|
||||
:extensions="extensions"
|
||||
@ready="handleReady"
|
||||
@change="handleChange"
|
||||
/>
|
||||
<codemirror v-model="code" placeholder="请输入 Markdown 内容..." :style="{ height: '0', flex: 1, fontSize: 'var(--editor-font-size)', color: 'var(--primary-color)' }" :autofocus="true" :indent-with-tab="true" :tab-size="2" :extensions="extensions" @ready="handleReady" @change="handleChange" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onUnmounted, ref, watch, shallowRef } from 'vue'
|
||||
import { Codemirror } from 'vue-codemirror'
|
||||
import { markdown, markdownLanguage } from '@codemirror/lang-markdown'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { useTheme } from '../composables/useTheme'
|
||||
import { liveMarkdownDecorations } from '../codemirror/liveMarkdownDecorations'
|
||||
import { attachImagePaste } from '../codemirror/imagePaste'
|
||||
import { useEditor } from '../composables/useEditor'
|
||||
import { useMarkdownActions } from '../composables/useMarkdownActions'
|
||||
import { useTabs } from '../composables/useTabs'
|
||||
import EditorToolbar from './EditorToolbar.vue'
|
||||
import { computed, onUnmounted, ref, watch, shallowRef } from "vue";
|
||||
import { Codemirror } from "vue-codemirror";
|
||||
import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { useTheme } from "../composables/useTheme";
|
||||
import { liveMarkdownDecorations } from "../codemirror/liveMarkdownDecorations";
|
||||
import { attachImagePaste } from "../codemirror/imagePaste";
|
||||
import { useEditor } from "../composables/useEditor";
|
||||
import { useMarkdownActions } from "../composables/useMarkdownActions";
|
||||
import { useTabs } from "../composables/useTabs";
|
||||
import EditorToolbar from "./EditorToolbar.vue";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: String,
|
||||
filePath: String
|
||||
})
|
||||
filePath: String,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const code = ref(props.modelValue || '')
|
||||
const { isDark } = useTheme()
|
||||
const { onEditorAction } = useEditor()
|
||||
const { activeTab } = useTabs()
|
||||
const { applyHeading, wrapSelection, prefixLines, insertLink } = useMarkdownActions()
|
||||
const code = ref(props.modelValue || "");
|
||||
const { isDark } = useTheme();
|
||||
const { onEditorAction } = useEditor();
|
||||
const { activeTab } = useTabs();
|
||||
const { applyHeading, wrapSelection, prefixLines, insertLink } = useMarkdownActions();
|
||||
|
||||
// 配置 Markdown 扩展
|
||||
const markdownExtension = markdown({ base: markdownLanguage })
|
||||
const markdownExtension = markdown({ base: markdownLanguage });
|
||||
|
||||
const editorView = shallowRef(null)
|
||||
let detachPaste = null
|
||||
let removeActionListener = null
|
||||
const editorView = shallowRef(null);
|
||||
let detachPaste = null;
|
||||
let removeActionListener = null;
|
||||
|
||||
const liveEditorTheme = computed(() => {
|
||||
return EditorView.theme({
|
||||
'&': {
|
||||
backgroundColor: 'var(--card-background)',
|
||||
color: 'var(--text-color)'
|
||||
},
|
||||
'.cm-scroller': {
|
||||
fontFamily: 'var(--app-font-family)'
|
||||
},
|
||||
'.cm-content': {
|
||||
padding: '30px 40px',
|
||||
lineHeight: '1.8',
|
||||
maxWidth: '900px',
|
||||
margin: '0 auto',
|
||||
caretColor: 'var(--primary-color)'
|
||||
},
|
||||
// ... 其他保持不变,但需要确保 monospaceFont 的地方也改了
|
||||
return EditorView.theme(
|
||||
{
|
||||
"&": {
|
||||
backgroundColor: "var(--card-background)",
|
||||
color: "var(--text-color)",
|
||||
},
|
||||
".cm-scroller": {
|
||||
fontFamily: "var(--app-font-family)",
|
||||
},
|
||||
".cm-content": {
|
||||
padding: "30px 40px",
|
||||
lineHeight: "1.8",
|
||||
maxWidth: "900px",
|
||||
margin: "0 auto",
|
||||
caretColor: "var(--primary-color)",
|
||||
},
|
||||
// ... 其他保持不变,但需要确保 monospaceFont 的地方也改了
|
||||
|
||||
'.cm-selectionBackground, ::selection': {
|
||||
backgroundColor: 'var(--primary-color-bg)'
|
||||
},
|
||||
'&.cm-focused .cm-cursor': {
|
||||
borderLeftColor: 'var(--primary-color)'
|
||||
},
|
||||
'.cm-gutters': {
|
||||
display: 'none'
|
||||
},
|
||||
'.cm-activeLine': {
|
||||
backgroundColor: 'transparent'
|
||||
},
|
||||
'.cm-activeLineGutter': {
|
||||
backgroundColor: 'transparent'
|
||||
},
|
||||
|
||||
// Markdown 语法标记样式
|
||||
'.cm-md-syntax': {
|
||||
opacity: '0.3',
|
||||
transition: 'opacity 0.2s',
|
||||
color: 'var(--text-color-secondary)'
|
||||
},
|
||||
'.cm-md-hidden-mark': {
|
||||
opacity: 0,
|
||||
fontSize: '0'
|
||||
},
|
||||
'.cm-md-syntax-hidden': {
|
||||
display: 'none !important'
|
||||
},
|
||||
|
||||
// 标题样式
|
||||
'.cm-line.cm-md-heading': {
|
||||
fontWeight: '600',
|
||||
lineHeight: '1.4',
|
||||
marginTop: '24px',
|
||||
marginBottom: '8px',
|
||||
color: 'var(--text-color)'
|
||||
},
|
||||
'.cm-line.cm-md-h1': {
|
||||
fontSize: '2.2em',
|
||||
borderBottom: '1px solid var(--border-color)',
|
||||
paddingBottom: '0.3em'
|
||||
},
|
||||
'.cm-line.cm-md-h2': {
|
||||
fontSize: '1.75em',
|
||||
borderBottom: '1px solid var(--border-color)',
|
||||
paddingBottom: '0.3em'
|
||||
},
|
||||
'.cm-line.cm-md-h3': { fontSize: '1.4em' },
|
||||
'.cm-line.cm-md-h4': { fontSize: '1.2em' },
|
||||
'.cm-line.cm-md-h5': { fontSize: '1.1em' },
|
||||
'.cm-line.cm-md-h6': { fontSize: '1em', color: 'var(--text-color-secondary)' },
|
||||
|
||||
// 引用样式
|
||||
'.cm-line.cm-md-blockquote': {
|
||||
color: 'var(--text-color-secondary)',
|
||||
borderLeft: '4px solid var(--border-color)',
|
||||
paddingLeft: '16px',
|
||||
margin: '4px 0'
|
||||
},
|
||||
|
||||
// 列表样式
|
||||
'.cm-line.cm-md-list': {
|
||||
paddingLeft: '4px'
|
||||
},
|
||||
'.cm-line.cm-md-task': {
|
||||
paddingLeft: '4px'
|
||||
},
|
||||
|
||||
// 行内代码样式
|
||||
'.cm-md-monospace': {
|
||||
fontFamily: 'var(--editor-font-family)',
|
||||
backgroundColor: 'var(--hover-background)',
|
||||
padding: '2px 4px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '0.9em'
|
||||
},
|
||||
".cm-selectionBackground, ::selection": {
|
||||
backgroundColor: "var(--primary-color-bg)",
|
||||
},
|
||||
"&.cm-focused .cm-cursor": {
|
||||
borderLeftColor: "var(--primary-color)",
|
||||
},
|
||||
".cm-gutters": {
|
||||
display: "none",
|
||||
},
|
||||
".cm-activeLine": {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
".cm-activeLineGutter": {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
|
||||
// 代码块样式
|
||||
'.cm-line.cm-md-fenced-code': {
|
||||
fontFamily: 'var(--editor-font-family)',
|
||||
backgroundColor: 'var(--hover-background)',
|
||||
paddingLeft: '16px',
|
||||
fontSize: '0.9em'
|
||||
},
|
||||
// 激活状态的代码块(稍微深一点,或者保持一致)
|
||||
'.cm-line.cm-md-fenced-code.cm-active': {
|
||||
backgroundColor: 'var(--hover-background)',
|
||||
outline: '1px solid var(--border-color)'
|
||||
},
|
||||
'.cm-md-code-info': {
|
||||
opacity: 0.5,
|
||||
fontSize: '0.85em',
|
||||
fontFamily: 'var(--app-font-family)',
|
||||
float: 'right',
|
||||
marginRight: '8px'
|
||||
},
|
||||
|
||||
// 表格预览样式 (Widget)
|
||||
'.cm-md-table-preview': {
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
overflow: 'auto',
|
||||
marginBottom: '16px'
|
||||
},
|
||||
'.cm-md-table-preview table': {
|
||||
width: '100%',
|
||||
borderSpacing: 0,
|
||||
borderCollapse: 'collapse'
|
||||
},
|
||||
'.cm-md-table-preview th, .cm-md-table-preview td': {
|
||||
padding: '6px 13px',
|
||||
border: '1px solid var(--border-color)'
|
||||
},
|
||||
'.cm-md-table-preview tr:nth-child(2n)': {
|
||||
backgroundColor: 'var(--hover-background)'
|
||||
},
|
||||
// 表格源码编辑时的样式
|
||||
'.cm-line.cm-md-table-source': {
|
||||
fontFamily: 'var(--editor-font-family)',
|
||||
whiteSpace: 'pre'
|
||||
},
|
||||
// Markdown 语法标记样式
|
||||
".cm-md-syntax": {
|
||||
color: "var(--text-color-secondary)",
|
||||
},
|
||||
".cm-md-hidden-mark": {
|
||||
opacity: 0,
|
||||
fontSize: "0",
|
||||
},
|
||||
".cm-md-syntax-hidden": {
|
||||
display: "none !important",
|
||||
},
|
||||
|
||||
// 分割线预览样式
|
||||
'.cm-md-hr-preview': {
|
||||
border: 'none',
|
||||
borderBottom: '2px solid var(--border-color)',
|
||||
margin: '24px 0'
|
||||
},
|
||||
// 标题样式
|
||||
".cm-line.cm-md-heading": {
|
||||
fontWeight: "600",
|
||||
lineHeight: "1.4",
|
||||
marginTop: "24px",
|
||||
marginBottom: "8px",
|
||||
color: "var(--text-color)",
|
||||
},
|
||||
".cm-line.cm-md-h1": {
|
||||
fontSize: "2.2em",
|
||||
borderBottom: "1px solid var(--border-color)",
|
||||
paddingBottom: "0.3em",
|
||||
},
|
||||
".cm-line.cm-md-h2": {
|
||||
fontSize: "1.75em",
|
||||
borderBottom: "1px solid var(--border-color)",
|
||||
paddingBottom: "0.3em",
|
||||
},
|
||||
".cm-line.cm-md-h3": { fontSize: "1.4em" },
|
||||
".cm-line.cm-md-h4": { fontSize: "1.2em" },
|
||||
".cm-line.cm-md-h5": { fontSize: "1.1em" },
|
||||
".cm-line.cm-md-h6": { fontSize: "1em", color: "var(--text-color-secondary)" },
|
||||
|
||||
// 图片预览样式
|
||||
'.cm-md-image-preview': {
|
||||
maxWidth: '100%',
|
||||
maxHeight: '400px',
|
||||
display: 'block',
|
||||
margin: '8px auto',
|
||||
borderRadius: '4px',
|
||||
cursor: 'default'
|
||||
},
|
||||
// 引用样式
|
||||
".cm-line.cm-md-blockquote": {
|
||||
color: "var(--text-color-secondary)",
|
||||
borderLeft: "4px solid var(--border-color)",
|
||||
paddingLeft: "16px",
|
||||
margin: "4px 0",
|
||||
},
|
||||
|
||||
// 链接文本样式
|
||||
'.cm-md-link-text': {
|
||||
color: 'var(--primary-color)',
|
||||
textDecoration: 'underline',
|
||||
cursor: 'pointer'
|
||||
},
|
||||
// 列表样式
|
||||
".cm-line.cm-md-list": {
|
||||
paddingLeft: "4px",
|
||||
},
|
||||
".cm-line.cm-md-task": {
|
||||
paddingLeft: "4px",
|
||||
},
|
||||
|
||||
// 任务列表样式
|
||||
'.cm-md-task-checkbox': {
|
||||
display: 'inline-block',
|
||||
marginRight: '8px',
|
||||
verticalAlign: 'middle',
|
||||
cursor: 'default'
|
||||
// 行内代码样式
|
||||
".cm-md-monospace": {
|
||||
fontFamily: "var(--editor-font-family)",
|
||||
backgroundColor: "var(--hover-background)",
|
||||
padding: "2px 4px",
|
||||
borderRadius: "4px",
|
||||
fontSize: "0.9em",
|
||||
},
|
||||
|
||||
// 代码块样式
|
||||
".cm-line.cm-md-fenced-code": {
|
||||
fontFamily: "var(--editor-font-family)",
|
||||
backgroundColor: "var(--hover-background)",
|
||||
paddingLeft: "16px",
|
||||
fontSize: "0.9em",
|
||||
},
|
||||
// 激活状态的代码块(稍微深一点,或者保持一致)
|
||||
".cm-line.cm-md-fenced-code.cm-active": {
|
||||
backgroundColor: "var(--hover-background)",
|
||||
outline: "1px solid var(--border-color)",
|
||||
},
|
||||
".cm-md-code-info": {
|
||||
opacity: 0.5,
|
||||
fontSize: "0.85em",
|
||||
fontFamily: "var(--app-font-family)",
|
||||
float: "right",
|
||||
marginRight: "8px",
|
||||
},
|
||||
|
||||
// 表格预览样式 (Widget)
|
||||
".cm-md-table-preview": {
|
||||
display: "block",
|
||||
width: "100%",
|
||||
overflow: "auto",
|
||||
marginBottom: "16px",
|
||||
},
|
||||
".cm-md-table-preview table": {
|
||||
width: "100%",
|
||||
borderSpacing: 0,
|
||||
borderCollapse: "collapse",
|
||||
},
|
||||
".cm-md-table-preview th, .cm-md-table-preview td": {
|
||||
padding: "6px 13px",
|
||||
border: "1px solid var(--border-color)",
|
||||
},
|
||||
".cm-md-table-preview tr:nth-child(2n)": {
|
||||
backgroundColor: "var(--hover-background)",
|
||||
},
|
||||
// 表格源码编辑时的样式
|
||||
".cm-line.cm-md-table-source": {
|
||||
fontFamily: "var(--editor-font-family)",
|
||||
whiteSpace: "pre",
|
||||
},
|
||||
|
||||
// 分割线预览样式
|
||||
".cm-md-hr-preview": {
|
||||
border: "none",
|
||||
borderBottom: "2px solid var(--border-color)",
|
||||
margin: "24px 0",
|
||||
},
|
||||
|
||||
// 图片预览样式
|
||||
".cm-md-image-preview": {
|
||||
maxWidth: "100%",
|
||||
maxHeight: "400px",
|
||||
display: "block",
|
||||
margin: "8px auto",
|
||||
borderRadius: "4px",
|
||||
cursor: "default",
|
||||
},
|
||||
|
||||
// 链接文本样式
|
||||
".cm-md-link-text": {
|
||||
color: "var(--primary-color)",
|
||||
textDecoration: "underline",
|
||||
cursor: "pointer",
|
||||
},
|
||||
|
||||
// 任务列表样式
|
||||
".cm-md-task-checkbox": {
|
||||
display: "inline-block",
|
||||
marginRight: "8px",
|
||||
verticalAlign: "middle",
|
||||
cursor: "default",
|
||||
},
|
||||
|
||||
// 删除线样式
|
||||
".cm-md-strikethrough": {
|
||||
textDecoration: "line-through",
|
||||
color: "var(--text-color-secondary)",
|
||||
},
|
||||
},
|
||||
|
||||
// 删除线样式
|
||||
'.cm-md-strikethrough': {
|
||||
textDecoration: 'line-through',
|
||||
color: 'var(--text-color-secondary)'
|
||||
}
|
||||
}, { dark: isDark.value })
|
||||
})
|
||||
{ dark: isDark.value },
|
||||
);
|
||||
});
|
||||
|
||||
const extensions = computed(() => {
|
||||
return [
|
||||
markdownExtension,
|
||||
liveMarkdownDecorations(props.filePath),
|
||||
EditorView.lineWrapping,
|
||||
liveEditorTheme.value
|
||||
]
|
||||
})
|
||||
return [markdownExtension, liveMarkdownDecorations(props.filePath), EditorView.lineWrapping, liveEditorTheme.value];
|
||||
});
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if (val !== code.value) code.value = val || ''
|
||||
})
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (val !== code.value) code.value = val || "";
|
||||
},
|
||||
);
|
||||
|
||||
const handleChange = (val) => {
|
||||
emit('update:modelValue', val)
|
||||
}
|
||||
emit("update:modelValue", val);
|
||||
};
|
||||
|
||||
const handleReady = (payload) => {
|
||||
editorView.value = payload.view
|
||||
|
||||
if (typeof detachPaste === 'function') detachPaste()
|
||||
editorView.value = payload.view;
|
||||
|
||||
if (typeof detachPaste === "function") detachPaste();
|
||||
detachPaste = attachImagePaste({
|
||||
view: payload.view,
|
||||
filePath: props.filePath,
|
||||
onDocChange: (newDoc) => {
|
||||
code.value = newDoc
|
||||
emit('update:modelValue', newDoc)
|
||||
}
|
||||
})
|
||||
code.value = newDoc;
|
||||
emit("update:modelValue", newDoc);
|
||||
},
|
||||
});
|
||||
|
||||
// 注册事件监听
|
||||
if (removeActionListener) removeActionListener()
|
||||
if (removeActionListener) removeActionListener();
|
||||
removeActionListener = onEditorAction((action, payload) => {
|
||||
// 只有当自己是当前激活的 Tab 且编辑器实例存在时才响应
|
||||
if (activeTab.value?.filePath !== props.filePath || !editorView.value) return
|
||||
if (activeTab.value?.filePath !== props.filePath || !editorView.value) return;
|
||||
|
||||
const view = editorView.value
|
||||
executeAction(view, action, payload)
|
||||
})
|
||||
}
|
||||
const view = editorView.value;
|
||||
executeAction(view, action, payload);
|
||||
});
|
||||
};
|
||||
|
||||
const handleToolbarAction = (action, payload) => {
|
||||
if (!editorView.value) return
|
||||
executeAction(editorView.value, action, payload)
|
||||
}
|
||||
if (!editorView.value) return;
|
||||
executeAction(editorView.value, action, payload);
|
||||
};
|
||||
|
||||
const executeAction = (view, action, payload) => {
|
||||
switch (action) {
|
||||
case 'heading': applyHeading(view, payload); break;
|
||||
case 'bold': wrapSelection(view, '**', '**'); break;
|
||||
case 'italic': wrapSelection(view, '*', '*'); break;
|
||||
case 'strike': wrapSelection(view, '~~', '~~'); break;
|
||||
case 'inlineCode': wrapSelection(view, '`', '`'); break;
|
||||
case 'quote': prefixLines(view, () => '> '); break;
|
||||
case 'unorderedList': prefixLines(view, () => '- '); break;
|
||||
case 'orderedList': prefixLines(view, (i) => `${i + 1}. `); break;
|
||||
case 'link': insertLink(view); break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case "heading":
|
||||
applyHeading(view, payload);
|
||||
break;
|
||||
case "bold":
|
||||
wrapSelection(view, "**", "**");
|
||||
break;
|
||||
case "italic":
|
||||
wrapSelection(view, "*", "*");
|
||||
break;
|
||||
case "strike":
|
||||
wrapSelection(view, "~~", "~~");
|
||||
break;
|
||||
case "inlineCode":
|
||||
wrapSelection(view, "`", "`");
|
||||
break;
|
||||
case "quote":
|
||||
prefixLines(view, () => "> ");
|
||||
break;
|
||||
case "unorderedList":
|
||||
prefixLines(view, () => "- ");
|
||||
break;
|
||||
case "orderedList":
|
||||
prefixLines(view, (i) => `${i + 1}. `);
|
||||
break;
|
||||
case "link":
|
||||
insertLink(view);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
if (typeof detachPaste === 'function') detachPaste()
|
||||
if (removeActionListener) removeActionListener()
|
||||
})
|
||||
if (typeof detachPaste === "function") detachPaste();
|
||||
if (removeActionListener) removeActionListener();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -308,4 +314,12 @@ onUnmounted(() => {
|
|||
:deep(.ͼ7) {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
:deep(.ͼc) {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
:deep(.ͼe) {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@
|
|||
--border-color: #f0f0f0;
|
||||
--hover-background: rgba(0, 0, 0, 0.04);
|
||||
--scrollbar-thumb: rgba(0, 0, 0, 0.28);
|
||||
--app-font-size: 15px;
|
||||
--editor-font-size: 15px;
|
||||
--preview-font-size: 15px;
|
||||
--preview-code-font-size: 15px;
|
||||
--app-font-size: 16px;
|
||||
--editor-font-size: 17px;
|
||||
--preview-font-size: 16px;
|
||||
--preview-code-font-size: 16px;
|
||||
}
|
||||
|
||||
html,
|
||||
|
|
|
|||
Loading…
Reference in New Issue