vue3代码展示vue-codemirror

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>
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值