1:子组件
<script setup lang="ts">
import type { CSSProperties } from 'vue'
import { Codemirror } from 'vue-codemirror'
import { vue } from '@codemirror/lang-vue'
import { python } from '@codemirror/lang-python'
import { javascript } from '@codemirror/lang-javascript'
import { css } from '@codemirror/lang-css'
import { oneDark } from '@codemirror/theme-one-dark'
import { EditorView } from '@codemirror/view'
import prettier from 'prettier/standalone'
import parserBabel from 'prettier/plugins/babel'
import parserEstree from 'prettier/plugins/estree'
import parserPostcss from 'prettier/plugins/postcss'
import parserHtml from 'prettier/plugins/html'
import parserTypescript from 'prettier/plugins/typescript'
interface Props {
codeStyle?: CSSProperties // 代码样式
dark?: boolean // 是否暗黑主题
code?: string // 代码字符串
showFormatButton?: boolean // 是否显示格式化按钮
// placeholder?: string // 占位文本
// autofocus?: boolean // 自动聚焦
// disabled?: boolean // 禁用输入行为和更改状态
// indentWithTab?: boolean // 启用 tab 按键
// tabSize?: number // tab 按键缩进空格数
// autoDestroy?: boolean // 组件销毁时是否自动销毁代码编辑器实例
}
const props = withDefaults(defineProps<Props>(), {
codeStyle: () => ({}),
dark: false,
code: '',
showFormatButton: true,
// placeholder: 'Code goes here...',
// autofocus: false,
// disabled: false,
// indentWithTab: true,
// tabSize: 2
})
// 检测代码语言类型
function detectLanguage(code: string): string {
const trimmedCode = code.trim()
// Python 检测
if (
trimmedCode.includes('def ') ||
trimmedCode.includes('import ') ||
trimmedCode.includes('from ') ||
trimmedCode.includes('class ') ||
/^#.*/.test(trimmedCode) ||
trimmedCode.includes('print(') ||
/:$\s*\n/.test(trimmedCode) // Python 的冒号换行语法
) {
return 'python'
}
// Vue 检测
if (trimmedCode.includes('<template>') || trimmedCode.includes('<script>')) {
return 'vue'
}
// TypeScript 检测
if (trimmedCode.includes('interface ') || trimmedCode.includes(': string') || trimmedCode.includes(': number')) {
return 'typescript'
}
// CSS 检测
if (trimmedCode.includes('{') && trimmedCode.includes('}') && !trimmedCode.includes('function')) {
return 'css'
}
return 'javascript'
}
const codeValue = ref('')
const isFormatting = ref(false)
// 根据代码内容动态选择语法高亮扩展
const extensions = computed(() => {
const language = detectLanguage(codeValue.value || props.code)
let langExtension
switch (language) {
case 'python':
langExtension = python()
break
case 'vue':
langExtension = vue()
break
case 'javascript':
case 'typescript':
langExtension = javascript()
break
case 'css':
langExtension = css()
break
default:
langExtension = javascript() // 默认使用 JavaScript 高亮
}
const baseExtensions = [langExtension, EditorView.lineWrapping]
return props.dark ? [...baseExtensions, oneDark] : baseExtensions
})
watchEffect(() => {
codeValue.value = props.code
})
const emits = defineEmits(['update:code', 'ready', 'change', 'focus', 'blur'])
function handleReady(payload: any) {
// console.log('ready')
emits('ready', payload)
}
function onChange(value: string, viewUpdate: any) {
emits('change', value, viewUpdate)
emits('update:code', value)
}
function onFocus(viewUpdate: any) {
emits('focus', viewUpdate)
}
function onBlur(viewUpdate: any) {
emits('blur', viewUpdate)
}
// Python 代码格式化函数
function formatPythonCode(code: string): string {
const lines = code.split('\n')
const formattedLines: string[] = []
let indentLevel = 0
const indentSize = 4 // Python 标准缩进
for (let i = 0; i < lines.length; i++) {
let line = lines[i].trim()
// 跳过空行
if (!line) {
formattedLines.push('')
continue
}
// 减少缩进的关键字
if (
line.startsWith('except') ||
line.startsWith('elif') ||
line.startsWith('else') ||
line.startsWith('finally') ||
line.startsWith('case') ||
line.startsWith('match')
) {
indentLevel = Math.max(0, indentLevel - 1)
}
// 先将所有多余空格标准化为单个空格
line = line.replace(/\s+/g, ' ').trim()
// 检查是否是注释行
if (line.startsWith('#')) {
// 注释行只处理开头空格
line = line.replace(/^(#+)\s+/, '$1 ')
} else {
// 简单直接的操作符格式化,先清理后添加
line = line
// 清理操作符周围的所有空格
.replace(/\s*=\s*/g, '=')
.replace(/\s*==\s*/g, '==')
.replace(/\s*!=\s*/g, '!=')
.replace(/\s*<=\s*/g, '<=')
.replace(/\s*>=\s*/g, '>=')
.replace(/\s*<\s*/g, '<')
.replace(/\s*>\s*/g, '>')
.replace(/\s*,\s*/g, ',')
.replace(/\s*\(\s*/g, '(')
.replace(/\s*\)\s*/g, ')')
.replace(/\s*\[\s*/g, '[')
.replace(/\s*\]\s*/g, ']')
// 重新添加标准空格
.replace(/([^=!<>])=([^=])/g, '$1 = $2')
.replace(/([^=!<>])==([^=])/g, '$1 == $2')
.replace(/([^=!<>])!=([^=])/g, '$1 != $2')
.replace(/([^<>])<=([^=])/g, '$1 <= $2')
.replace(/([^<>])>=([^=])/g, '$1 >= $2')
.replace(/([^<>])<([^<=])/g, '$1 < $2')
.replace(/([^<>])>([^>=])/g, '$1 > $2')
.replace(/,([^\s])/g, ', $1')
}
// 格式化函数定义和类定义
if (line.match(/^(def|class|if|elif|else|for|while|try|except|finally|with|match|case)\s/)) {
line = line.replace(/:\s*$/, ':')
}
// 应用当前缩进
const indent = ' '.repeat(indentLevel * indentSize)
formattedLines.push(indent + line)
// 增加缩进的情况
if (line.endsWith(':') && !line.startsWith('#')) {
indentLevel++
}
}
return formattedLines.join('\n')
}
// 格式化代码函数
async function formatCode() {
if (!codeValue.value.trim()) return
isFormatting.value = true
try {
const language = detectLanguage(codeValue.value)
const originalCode = codeValue.value
let formatted = ''
if (language === 'python') {
// 使用自定义的 Python 格式化
formatted = formatPythonCode(originalCode)
} else {
// 使用 Prettier 格式化其他语言
let parser = 'babel'
let plugins = [parserBabel, parserEstree]
switch (language) {
case 'vue':
parser = 'vue'
plugins = [parserHtml, parserBabel, parserEstree, parserTypescript]
break
case 'typescript':
parser = 'typescript'
plugins = [parserTypescript, parserEstree]
break
case 'css':
parser = 'css'
plugins = [parserPostcss]
break
default:
parser = 'babel'
plugins = [parserBabel, parserEstree]
}
formatted = await prettier.format(originalCode, {
parser,
plugins,
semi: true,
singleQuote: true,
tabWidth: 2,
trailingComma: 'es5',
printWidth: 80,
})
}
// 只要格式化结果与原代码不同就更新,确保能清理多余空格
if (formatted !== originalCode) {
codeValue.value = formatted
emits('update:code', formatted)
}
} catch (error) {
console.error('代码格式化失败:', error)
// 可以在这里添加错误提示
} finally {
isFormatting.value = false
}
}
</script>
<template>
<div class="codemirror-container">
<!-- 格式化按钮 -->
<div v-if="showFormatButton" class="format-button-container">
<button
@click="formatCode"
:disabled="isFormatting"
class="format-button"
:class="{ 'format-button-dark': dark }"
>
<span v-if="!isFormatting">格式化代码</span>
<span v-else>格式化中...</span>
</button>
</div>
<Codemirror
v-model="codeValue"
:style="codeStyle"
:extensions="extensions"
@ready="handleReady"
@change="onChange"
@focus="onFocus"
@blur="onBlur"
v-bind="$attrs"
/>
</div>
</template>
<style lang="scss" scoped>
.codemirror-container {
position: relative;
user-select: text; // 确保容器允许文本选择
// 确保所有文本都可以被选择
* {
user-select: text;
}
// 按钮除外,保持正常的按钮行为
button {
user-select: none;
}
}
.format-button-container {
position: absolute;
top: 8px;
right: 8px;
z-index: 10;
pointer-events: auto; // 确保按钮可以点击
// 确保按钮不会阻挡编辑器的文本选择
& ~ * {
pointer-events: auto;
}
}
.format-button {
padding: 4px 8px;
font-size: 12px;
background-color: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
&:hover:not(:disabled) {
background-color: #e9e9e9;
border-color: #ccc;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.format-button-dark {
background-color: #333;
border-color: #555;
color: #fff;
&:hover:not(:disabled) {
background-color: #444;
border-color: #666;
}
}
:deep(.cm-editor) {
border-radius: 8px;
outline: none;
border: 1px solid transparent;
user-select: text !important;
.cm-scroller {
border-radius: 8px;
user-select: text !important;
}
.cm-content {
user-select: text !important;
}
.cm-line {
user-select: text !important;
}
}
:deep(.cm-focused) {
border: 1px solid #3498db !important;
}
</style>
2:父组件
<script>
const code = ref(`import os
import uuid
def create_session_output_dir(base_output_dir,user_input: str) -> str:
"""为本次分析创建独立的输出目录"""
# 使用UUID创建唯一的会话目录名(16进制格式,去掉连字符)
session_id = uuid.uuid4().hex
dir_name = f"session_{session_id}"
session_dir = os.path.join(base_output_dir, dir_name)
os.makedirs(session_dir, exist_ok=True)
return session_dir`)
function onReady(payload: any) {
console.log('ready', payload)
}
function onChange(value: string, viewUpdate: any) {
console.log('change', value)
console.log('change', viewUpdate)
}
function onFocus(viewUpdate: any) {
console.log('focus', viewUpdate)
}
function onBlur(viewUpdate: any) {
console.log('blur', viewUpdate)
}```
</script>
<template>
<CodeMirror
v-model:code="code"
:dark="false"
:codeStyle="{ width: '100%', height: '400px', fontSize: '16px' }"
@ready="onReady"
@change="onChange"
@focus="onFocus"
@blur="onBlur"
/>
</template>
3926

被折叠的 条评论
为什么被折叠?



