feat: 增加技能管理模块试验性功能,等待优化

This commit is contained in:
2026-06-08 16:52:41 +08:00
parent cb379e071c
commit 950148b3f7
68 changed files with 4901 additions and 135 deletions

View File

@@ -0,0 +1,164 @@
<script setup lang="ts">
import {onBeforeUnmount, onMounted, ref, shallowRef, watch} from 'vue';
import {defaultKeymap, history, historyKeymap, indentWithTab} from '@codemirror/commands';
import {javascript} from '@codemirror/lang-javascript';
import {python} from '@codemirror/lang-python';
import {
bracketMatching,
defaultHighlightStyle,
indentOnInput,
StreamLanguage,
syntaxHighlighting
} from '@codemirror/language';
import {shell} from '@codemirror/legacy-modes/mode/shell';
import {EditorState, type Extension} from '@codemirror/state';
import {
drawSelection,
EditorView,
highlightActiveLine,
keymap,
lineNumbers
} from '@codemirror/view';
type EditorLanguage = 'javascript' | 'markdown' | 'python' | 'shell' | 'text';
const props = withDefaults(
defineProps<{
modelValue: string;
language?: EditorLanguage;
readonly?: boolean;
}>(),
{
language: 'text',
readonly: false,
},
);
const emit = defineEmits<{
(event: 'update:modelValue', value: string): void;
}>();
const hostRef = ref<HTMLElement>();
const viewRef = shallowRef<EditorView>();
let syncingFromOutside = false;
function languageExtension(): Extension[] {
if (props.language === 'python') {
return [python()];
}
if (props.language === 'javascript') {
return [javascript({ jsx: true, typescript: true })];
}
if (props.language === 'shell') {
return [StreamLanguage.define(shell)];
}
return [];
}
function createState(doc: string) {
return EditorState.create({
doc,
extensions: [
lineNumbers(),
history(),
drawSelection(),
highlightActiveLine(),
indentOnInput(),
bracketMatching(),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap]),
EditorView.editable.of(!props.readonly),
EditorState.readOnly.of(props.readonly),
EditorView.lineWrapping,
EditorView.updateListener.of((update) => {
if (!update.docChanged || syncingFromOutside) return;
emit('update:modelValue', update.state.doc.toString());
}),
EditorView.theme({
'&': {
height: '100%',
fontSize: '13px',
backgroundColor: 'var(--el-bg-color)',
color: 'var(--el-text-color-primary)',
},
'.cm-scroller': {
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
},
'.cm-gutters': {
backgroundColor: 'var(--el-fill-color-lighter)',
borderRight: '1px solid var(--el-border-color-lighter)',
color: 'var(--el-text-color-secondary)',
},
'.cm-activeLine': {
backgroundColor: 'var(--el-fill-color-light)',
},
'.cm-activeLineGutter': {
backgroundColor: 'var(--el-fill-color-light)',
},
}),
...languageExtension(),
],
});
}
function mountEditor() {
if (!hostRef.value) return;
viewRef.value = new EditorView({
state: createState(props.modelValue || ''),
parent: hostRef.value,
});
}
function recreateEditor() {
const currentDoc = viewRef.value?.state.doc.toString() ?? props.modelValue ?? '';
viewRef.value?.destroy();
viewRef.value = undefined;
if (!hostRef.value) return;
viewRef.value = new EditorView({
state: createState(currentDoc),
parent: hostRef.value,
});
}
watch(
() => props.modelValue,
(value) => {
const view = viewRef.value;
if (!view) return;
const nextValue = value || '';
const currentValue = view.state.doc.toString();
if (nextValue === currentValue) return;
syncingFromOutside = true;
view.dispatch({
changes: { from: 0, to: currentValue.length, insert: nextValue },
});
syncingFromOutside = false;
},
);
watch(
() => [props.language, props.readonly],
() => recreateEditor(),
);
onMounted(mountEditor);
onBeforeUnmount(() => {
viewRef.value?.destroy();
});
</script>
<template>
<div ref="hostRef" class="code-mirror-editor"></div>
</template>
<style scoped>
.code-mirror-editor {
height: 100%;
min-height: 0;
overflow: hidden;
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
}
</style>