feat(editor): 调整字体大小并增强链接语法高亮

- 将编辑器字体大小从15px调整为16-17px,提升可读性
- 在编辑状态为链接语法标记添加.cm-md-syntax样式,使其可见且颜色正确
- 修复编辑器内链接和光标颜色,统一使用主题主色
This commit is contained in:
cfq 2026-01-27 09:35:58 +08:00
parent c0458dc1e2
commit d15ef9da00
3 changed files with 283 additions and 254 deletions

View File

@ -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())
}
}
}

View File

@ -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>

View File

@ -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,