|
@@ -0,0 +1,214 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="bash-editor" :style="{ height: height }">
|
|
|
|
|
+ <div class="editor-wrapper">
|
|
|
|
|
+ <div class="editor-header">
|
|
|
|
|
+ <span class="file-type">
|
|
|
|
|
+ <el-icon><Monitor /></el-icon>
|
|
|
|
|
+ Bash Script
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <el-button class="copy-btn" size="small" :icon="DocumentCopy" @click="handleCopy">
|
|
|
|
|
+ {{ t('复制') }}
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Codemirror
|
|
|
|
|
+ v-model="localValue"
|
|
|
|
|
+ :style="{ height: editorHeight }"
|
|
|
|
|
+ :extensions="extensions"
|
|
|
|
|
+ :autofocus="autofocus"
|
|
|
|
|
+ :indent-with-tab="true"
|
|
|
|
|
+ :tab-size="2"
|
|
|
|
|
+ :placeholder="placeholder"
|
|
|
|
|
+ @change="handleChange"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import { ref, computed, watch } from 'vue'
|
|
|
|
|
+import { Codemirror } from 'vue-codemirror'
|
|
|
|
|
+import { EditorView } from '@codemirror/view'
|
|
|
|
|
+import { DocumentCopy, Monitor } from '@element-plus/icons-vue'
|
|
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
|
|
+import { useI18n } from 'vue-i18n'
|
|
|
|
|
+
|
|
|
|
|
+const { t } = useI18n({ useScope: 'global' })
|
|
|
|
|
+
|
|
|
|
|
+interface Props {
|
|
|
|
|
+ modelValue: string
|
|
|
|
|
+ height?: string
|
|
|
|
|
+ autofocus?: boolean
|
|
|
|
|
+ placeholder?: string
|
|
|
|
|
+ readonly?: boolean
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const props = withDefaults(defineProps<Props>(), {
|
|
|
|
|
+ modelValue: '',
|
|
|
|
|
+ height: '400px',
|
|
|
|
|
+ autofocus: false,
|
|
|
|
|
+ placeholder: '#!/bin/bash\n# Enter your script here...',
|
|
|
|
|
+ readonly: false
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const emit = defineEmits<{
|
|
|
|
|
+ (e: 'update:modelValue', value: string): void
|
|
|
|
|
+}>()
|
|
|
|
|
+
|
|
|
|
|
+const localValue = ref(props.modelValue)
|
|
|
|
|
+
|
|
|
|
|
+// Calculate editor height (minus header)
|
|
|
|
|
+const editorHeight = computed(() => {
|
|
|
|
|
+ const totalHeight = parseInt(props.height)
|
|
|
|
|
+ return `${totalHeight - 40}px`
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// Watch for external changes
|
|
|
|
|
+watch(
|
|
|
|
|
+ () => props.modelValue,
|
|
|
|
|
+ (newVal) => {
|
|
|
|
|
+ if (newVal !== localValue.value) {
|
|
|
|
|
+ localValue.value = newVal
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+// Editor extensions with light theme (similar to JsonEditor)
|
|
|
|
|
+const extensions = computed(() => {
|
|
|
|
|
+ const exts = [
|
|
|
|
|
+ EditorView.lineWrapping,
|
|
|
|
|
+ EditorView.theme({
|
|
|
|
|
+ '&': {
|
|
|
|
|
+ fontSize: '13px',
|
|
|
|
|
+ border: '1px solid #dcdfe6',
|
|
|
|
|
+ borderRadius: '4px'
|
|
|
|
|
+ },
|
|
|
|
|
+ '.cm-content': {
|
|
|
|
|
+ fontFamily: "'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace",
|
|
|
|
|
+ padding: '8px 0'
|
|
|
|
|
+ },
|
|
|
|
|
+ '.cm-gutters': {
|
|
|
|
|
+ backgroundColor: '#f5f7fa',
|
|
|
|
|
+ color: '#909399',
|
|
|
|
|
+ border: 'none',
|
|
|
|
|
+ borderRight: '1px solid #e4e7ed'
|
|
|
|
|
+ },
|
|
|
|
|
+ '.cm-activeLineGutter': {
|
|
|
|
|
+ backgroundColor: '#e8eaed'
|
|
|
|
|
+ },
|
|
|
|
|
+ '.cm-activeLine': {
|
|
|
|
|
+ backgroundColor: '#f5f7fa'
|
|
|
|
|
+ },
|
|
|
|
|
+ '&.cm-focused': {
|
|
|
|
|
+ outline: 'none'
|
|
|
|
|
+ },
|
|
|
|
|
+ '&.cm-focused .cm-selectionBackground, ::selection': {
|
|
|
|
|
+ backgroundColor: '#d7d4f0 !important'
|
|
|
|
|
+ },
|
|
|
|
|
+ '.cm-placeholder': {
|
|
|
|
|
+ color: '#909399'
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ if (props.readonly) {
|
|
|
|
|
+ exts.push(EditorView.editable.of(false))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return exts
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// Handle value change
|
|
|
|
|
+function handleChange(value: string) {
|
|
|
|
|
+ localValue.value = value
|
|
|
|
|
+ emit('update:modelValue', value)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Copy to clipboard
|
|
|
|
|
+async function handleCopy() {
|
|
|
|
|
+ if (!localValue.value) return
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ await navigator.clipboard.writeText(localValue.value)
|
|
|
|
|
+ ElMessage.success(t('已复制到剪贴板'))
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ ElMessage.error(t('复制失败'))
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Expose methods
|
|
|
|
|
+defineExpose({
|
|
|
|
|
+ getValue: () => localValue.value,
|
|
|
|
|
+ setValue: (val: string) => {
|
|
|
|
|
+ localValue.value = val
|
|
|
|
|
+ emit('update:modelValue', val)
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style lang="scss" scoped>
|
|
|
|
|
+.bash-editor {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.editor-wrapper {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ background-color: #fff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.editor-header {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ height: 32px;
|
|
|
|
|
+ padding: 0 8px;
|
|
|
|
|
+ background-color: #f5f7fa;
|
|
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
|
|
+ border-bottom: none;
|
|
|
|
|
+ border-radius: 4px 4px 0 0;
|
|
|
|
|
+
|
|
|
|
|
+ .file-type {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
|
|
|
+
|
|
|
|
|
+ .el-icon {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #67c23a;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .copy-btn {
|
|
|
|
|
+ background-color: transparent;
|
|
|
|
|
+ border-color: #dcdfe6;
|
|
|
|
|
+ color: #606266;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ background-color: #ecf5ff;
|
|
|
|
|
+ border-color: #c6e2ff;
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.cm-editor) {
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.cm-scroller) {
|
|
|
|
|
+ overflow: auto;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.cm-line) {
|
|
|
|
|
+ padding-left: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|