#add env management

This commit is contained in:
2025-08-01 16:55:23 +08:00
parent 57a4d7d97e
commit 5c4492d8f8
16 changed files with 5524 additions and 5 deletions

View File

@@ -0,0 +1,969 @@
<template>
<div class="simple-code-editor">
<!-- 编辑器头部工具栏 -->
<div class="editor-header">
<div class="editor-tabs">
<div class="tab active">
<div class="tab-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/>
</svg>
</div>
<span class="tab-name">{{ getFileName() }}</span>
<div class="tab-close">×</div>
</div>
</div>
<div class="editor-controls">
<button class="control-btn" @click="formatCode" title="格式化代码">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.22,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.22,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.68 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z"/>
</svg>
</button>
<button class="control-btn" @click="toggleWrap" title="切换自动换行">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M4,6H20V8H4V6M4,18V16H20V18H4M4,13H16L13,16H15L19,12L15,8H13L16,11H4V13Z"/>
</svg>
</button>
</div>
</div>
<!-- 编辑器主体 - 单层渲染方案 -->
<div class="editor-body" :class="{ 'dark-theme': theme === 'dark', 'word-wrap': wordWrap }">
<!-- 行号区域 -->
<div class="line-numbers-area" ref="lineNumbers">
<div
v-for="(line, index) in lines"
:key="index"
class="line-number"
:class="{ 'active': index + 1 === currentLine }"
>
{{ String(index + 1).padStart(3, ' ') }}
</div>
</div>
<!-- 代码区域 - 只使用一个带语法高亮的可编辑div -->
<div class="code-area">
<div
ref="codeEditor"
class="code-editor-div"
:contenteditable="!readonly"
@input="handleDivInput"
@scroll="handleScroll"
@keydown="handleKeydown"
@click="updateCursor"
@keyup="updateCursor"
@focus="handleFocus"
@blur="handleBlur"
@paste="handlePaste"
spellcheck="false"
v-html="highlightedContent"
></div>
</div>
</div>
<!-- 编辑器底部状态栏 -->
<div class="editor-footer">
<div class="status-left">
<span class="status-item"> {{ currentLine }}</span>
<span class="status-item"> {{ currentColumn }}</span>
<span class="status-item">{{ lines.length }} </span>
<span class="status-item">{{ content.length }} 字符</span>
</div>
<div class="status-right">
<span class="status-item">{{ language.toUpperCase() }}</span>
<span class="status-item">UTF-8</span>
<span class="status-item" :class="{ 'status-success': !readonly }">
{{ readonly ? '只读' : '可编辑' }}
</span>
</div>
</div>
</div>
</template>
<script>
// 纯原生实现,无外部依赖
export default {
name: 'CodeEditor',
props: {
modelValue: {
type: String,
default: ''
},
language: {
type: String,
default: 'env' // 'env', 'javascript', 'php'
},
theme: {
type: String,
default: 'light' // 'light', 'dark'
},
readonly: {
type: Boolean,
default: false
},
placeholder: {
type: String,
default: ''
}
},
emits: ['update:modelValue', 'change'],
data() {
return {
content: this.modelValue,
currentLine: 1,
currentColumn: 1,
focused: false,
wordWrap: false
}
},
computed: {
lines() {
return this.content.split('\n');
},
highlightedContent() {
if (!this.content) return '';
if (this.language === 'env') {
return this.highlightEnvContent(this.content);
} else if (this.language === 'javascript') {
return this.highlightJavaScriptContent(this.content);
} else if (this.language === 'php') {
return this.highlightPhpContent(this.content);
}
return this.escapeHtml(this.content);
}
},
mounted() {
// 初始化时自动检测语言
this.autoDetectLanguage();
// 初始化编辑器
this.initEditor();
},
watch: {
modelValue(newValue) {
if (newValue !== this.content) {
this.content = newValue;
this.$nextTick(() => {
this.initEditor();
});
}
},
content() {
this.$nextTick(() => {
this.initEditor();
});
},
language() {
// 语言变化时重新渲染语法高亮
this.$nextTick(() => {
this.initEditor();
});
}
},
methods: {
handleInput() {
this.$emit('update:modelValue', this.content);
this.$emit('change', this.content);
this.updateCursor();
},
handleDivInput(event) {
// 获取纯文本内容
const text = this.getPlainTextFromDiv(event.target);
this.content = text;
this.$emit('update:modelValue', text);
this.$emit('change', text);
// 延迟更新语法高亮,避免光标跳动
this.$nextTick(() => {
this.updateSyntaxHighlight();
this.updateCursor();
});
},
handleScroll() {
// 同步行号滚动
const codeEditor = this.$refs.codeEditor;
const lineNumbers = this.$refs.lineNumbers;
if (codeEditor && lineNumbers) {
lineNumbers.scrollTop = codeEditor.scrollTop;
}
},
handleFocus() {
this.focused = true;
},
handleBlur() {
this.focused = false;
},
handleKeydown(event) {
// Tab键处理
if (event.key === 'Tab') {
event.preventDefault();
this.insertText(' ');
}
},
handlePaste(event) {
event.preventDefault();
const text = (event.clipboardData || window.clipboardData).getData('text');
this.insertText(text);
},
insertText(text) {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
range.deleteContents();
range.insertNode(document.createTextNode(text));
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
// 触发input事件
this.handleDivInput({ target: this.$refs.codeEditor });
}
},
updateCursor() {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const beforeCursor = this.getTextBeforeCursor(range);
const lines = beforeCursor.split('\n');
this.currentLine = lines.length;
this.currentColumn = lines[lines.length - 1].length + 1;
}
},
initEditor() {
// 初始化编辑器内容
const codeEditor = this.$refs.codeEditor;
if (codeEditor && this.content) {
codeEditor.innerHTML = this.highlightedContent;
}
},
getPlainTextFromDiv(div) {
// 从contenteditable div中提取纯文本
return div.innerText || div.textContent || '';
},
getTextBeforeCursor(range) {
// 获取光标前的文本
const container = this.$refs.codeEditor;
if (!container) return '';
const tempRange = document.createRange();
tempRange.setStart(container, 0);
tempRange.setEnd(range.startContainer, range.startOffset);
return tempRange.toString();
},
updateSyntaxHighlight() {
const codeEditor = this.$refs.codeEditor;
if (!codeEditor) return;
// 保存当前光标位置
const selection = window.getSelection();
let savedRange = null;
let cursorOffset = 0;
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
cursorOffset = this.getTextBeforeCursor(range).length;
}
// 更新语法高亮内容
codeEditor.innerHTML = this.highlightedContent;
// 恢复光标位置
this.$nextTick(() => {
this.restoreCursorPosition(cursorOffset);
});
},
restoreCursorPosition(offset) {
const codeEditor = this.$refs.codeEditor;
if (!codeEditor) return;
const selection = window.getSelection();
const range = document.createRange();
try {
let currentOffset = 0;
const walker = document.createTreeWalker(
codeEditor,
NodeFilter.SHOW_TEXT,
null,
false
);
let node;
while (node = walker.nextNode()) {
const nodeLength = node.textContent.length;
if (currentOffset + nodeLength >= offset) {
range.setStart(node, offset - currentOffset);
range.setEnd(node, offset - currentOffset);
break;
}
currentOffset += nodeLength;
}
selection.removeAllRanges();
selection.addRange(range);
} catch (e) {
// 如果恢复失败,将光标放到末尾
range.selectNodeContents(codeEditor);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
}
},
formatCode() {
// 简单的代码格式化
if (this.language === 'env') {
const lines = this.content.split('\n');
const formatted = lines.map(line => {
line = line.trim();
if (line.includes('=') && !line.startsWith('#')) {
const [key, ...valueParts] = line.split('=');
const value = valueParts.join('=');
return `${key.trim()}=${value}`;
}
return line;
}).join('\n');
this.content = formatted;
this.handleInput();
}
},
toggleWrap() {
this.wordWrap = !this.wordWrap;
},
getFileName() {
const fileNames = {
'env': '.env',
'javascript': 'script.js',
'php': 'config.php'
};
return fileNames[this.language] || '.env';
},
// 根据文件名自动检测语言
detectLanguageFromFilename(filename) {
if (!filename) return 'env';
const ext = filename.toLowerCase();
// .env 文件检测
if (ext === '.env' || ext.startsWith('.env.') || ext.endsWith('.env')) {
return 'env';
}
// JavaScript 文件检测
if (ext.endsWith('.js') || ext.endsWith('.jsx') || ext.endsWith('.ts') || ext.endsWith('.tsx') || ext.endsWith('.mjs')) {
return 'javascript';
}
// PHP 文件检测
if (ext.endsWith('.php') || ext.endsWith('.phtml') || ext.endsWith('.php3') || ext.endsWith('.php4') || ext.endsWith('.php5')) {
return 'php';
}
// 默认返回 env
return 'env';
},
// 自动设置语言
autoDetectLanguage() {
const filename = this.getFileName();
const detectedLanguage = this.detectLanguageFromFilename(filename);
if (detectedLanguage !== this.language) {
this.$emit('language-change', detectedLanguage);
}
},
highlightEnvContent(content) {
return content
.split('\n')
.map(line => {
if (line.trim().startsWith('#')) {
// 注释
return `<span class="env-comment">${this.escapeHtml(line)}</span>`;
} else if (line.includes('=')) {
// 键值对
const equalIndex = line.indexOf('=');
const key = line.substring(0, equalIndex);
const value = line.substring(equalIndex + 1);
return `<span class="env-key">${this.escapeHtml(key)}</span><span class="env-operator">=</span><span class="env-value">${this.escapeHtml(value)}</span>`;
} else {
return this.escapeHtml(line);
}
})
.join('\n');
},
highlightJavaScriptContent(content) {
// 简单的JavaScript语法高亮
const keywords = ['function', 'const', 'let', 'var', 'if', 'else', 'for', 'while', 'return', 'class', 'import', 'export', 'default'];
const keywordRegex = new RegExp(`\\b(${keywords.join('|')})\\b`, 'g');
return content
.split('\n')
.map(line => {
let highlightedLine = this.escapeHtml(line);
// 高亮关键字
highlightedLine = highlightedLine.replace(keywordRegex, '<span class="js-keyword">$1</span>');
// 高亮字符串
highlightedLine = highlightedLine.replace(/(["'`])((?:\\.|(?!\1)[^\\])*?)\1/g, '<span class="js-string">$1$2$1</span>');
// 高亮注释
highlightedLine = highlightedLine.replace(/(\/\/.*$)/g, '<span class="js-comment">$1</span>');
return highlightedLine;
})
.join('\n');
},
highlightPhpContent(content) {
// 简单的PHP语法高亮
const keywords = ['function', 'class', 'if', 'else', 'elseif', 'while', 'for', 'foreach', 'return', 'public', 'private', 'protected', 'static'];
const keywordRegex = new RegExp(`\\b(${keywords.join('|')})\\b`, 'g');
return content
.split('\n')
.map(line => {
let highlightedLine = this.escapeHtml(line);
// 高亮PHP标签
highlightedLine = highlightedLine.replace(/(&lt;\?php|&lt;\?=|\?&gt;)/g, '<span class="php-tag">$1</span>');
// 高亮关键字
highlightedLine = highlightedLine.replace(keywordRegex, '<span class="php-keyword">$1</span>');
// 高亮变量
highlightedLine = highlightedLine.replace(/(\$\w+)/g, '<span class="php-variable">$1</span>');
// 高亮字符串
highlightedLine = highlightedLine.replace(/(["'])((?:\\.|(?!\1)[^\\])*?)\1/g, '<span class="php-string">$1$2$1</span>');
// 高亮注释
highlightedLine = highlightedLine.replace(/(\/\/.*$|\/\*.*?\*\/)/g, '<span class="php-comment">$1</span>');
return highlightedLine;
})
.join('\n');
},
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
},
focus() {
if (this.$refs.textarea) {
this.$refs.textarea.focus();
}
},
getContent() {
return this.content;
},
}
}
</script>
<style scoped>
.simple-code-editor {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
min-height: 0;
font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', 'Monaco', 'Inconsolata', 'Consolas', monospace;
background: #ffffff;
border-radius: 12px;
overflow: visible; /* 移除容器级别的滚动条限制 */
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
border: 1px solid #e5e7eb;
}
/* 编辑器头部 */
.editor-header {
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-bottom: 1px solid #e2e8f0;
padding: 0;
min-height: 40px;
}
.editor-tabs {
display: flex;
align-items: center;
}
.tab {
display: flex;
align-items: center;
padding: 8px 16px;
background: #ffffff;
border-right: 1px solid #e2e8f0;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
}
.tab.active {
background: #ffffff;
border-bottom: 2px solid #3b82f6;
}
.tab:hover {
background: #f8fafc;
}
.tab-icon {
margin-right: 8px;
color: #6b7280;
display: flex;
align-items: center;
}
.tab-name {
font-size: 13px;
font-weight: 500;
color: #374151;
margin-right: 8px;
}
.tab-close {
color: #9ca3af;
font-size: 16px;
line-height: 1;
cursor: pointer;
padding: 2px;
border-radius: 2px;
transition: all 0.2s ease;
}
.tab-close:hover {
background: #f3f4f6;
color: #374151;
}
.editor-controls {
display: flex;
align-items: center;
padding: 0 12px;
gap: 4px;
}
.control-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
background: transparent;
color: #6b7280;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
}
.control-btn:hover {
background: #f3f4f6;
color: #374151;
}
/* 编辑器主体 */
.editor-body {
display: flex;
flex: 1;
min-height: 0;
background: #ffffff;
overflow: visible; /* 允许内容正常显示,不被截断 */
}
.editor-body.dark-theme {
background: #1e1e1e;
}
/* 行号区域 */
.line-numbers-area {
background: linear-gradient(180deg, #fafbfc 0%, #f4f6f8 100%);
border-right: 1px solid #e2e8f0;
padding: 16px 0;
min-width: 60px;
overflow: hidden; /* 行号区域保持隐藏溢出,避免水平滚动 */
user-select: none;
font-size: 13px;
line-height: 1.6;
color: #8b949e;
position: relative;
flex-shrink: 0; /* 防止行号区域被压缩 */
}
.dark-theme .line-numbers-area {
background: linear-gradient(180deg, #2d3748 0%, #1a202c 100%);
border-right-color: #4a5568;
color: #718096;
}
.line-number {
text-align: right;
padding: 0 12px;
white-space: nowrap;
font-variant-numeric: tabular-nums;
transition: all 0.2s ease;
}
.line-number.active {
color: #3b82f6;
font-weight: 600;
background: rgba(59, 130, 246, 0.1);
}
.dark-theme .line-number.active {
color: #60a5fa;
background: rgba(96, 165, 250, 0.1);
}
/* 代码区域 */
.code-area {
flex: 1;
min-height: 0;
position: relative;
overflow: visible; /* 确保代码区域不被截断 */
display: flex;
flex-direction: column;
}
/* 可编辑的代码编辑器div - 单层方案,无重影 */
.code-editor-div {
width: 100%;
height: 100%; /* 改为100%高度以充分利用父容器空间 */
min-height: 200px; /* 设置最小高度确保可见 */
padding: 16px 20px;
margin: 0;
font-size: 14px;
line-height: 1.6;
font-family: inherit;
color: #24292f;
background: transparent;
border: none;
outline: none;
overflow: auto; /* 确保编辑器内部滚动条正常工作 */
white-space: pre;
caret-color: #3b82f6;
font-variant-ligatures: common-ligatures;
/* 确保可编辑性 */
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
/* 优化渲染性能 */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
/* 防止内容溢出 */
word-wrap: break-word;
overflow-wrap: break-word;
/* 确保编辑器能够自适应父容器 */
box-sizing: border-box;
}
.word-wrap .code-editor-div {
white-space: pre-wrap;
word-wrap: break-word;
}
/* 确保编辑器在不同容器中都能正确显示 */
.code-editor-div:focus {
outline: none;
}
/* 自适应高度的额外样式 */
.simple-code-editor {
max-height: 100vh; /* 防止超出视口高度 */
}
.editor-body {
min-height: 300px; /* 确保编辑器有足够的最小高度 */
}
.dark-theme .code-editor-div {
color: #f0f6fc;
caret-color: #60a5fa;
}
/* 编辑器底部状态栏 */
.editor-footer {
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-top: 1px solid #e2e8f0;
padding: 6px 16px;
font-size: 12px;
color: #6b7280;
min-height: 32px;
}
.dark-theme .editor-footer {
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
border-top-color: #4a5568;
color: #a0aec0;
}
.status-left,
.status-right {
display: flex;
align-items: center;
gap: 16px;
}
.status-item {
font-weight: 500;
font-variant-numeric: tabular-nums;
}
.status-success {
color: #059669;
}
.dark-theme .status-success {
color: #68d391;
}
/* 语法高亮样式 */
.code-editor-div :deep(.env-comment) {
color: #6a737d;
font-style: italic;
}
.code-editor-div :deep(.env-key) {
color: #005cc5;
font-weight: 600;
}
.code-editor-div :deep(.env-operator) {
color: #d73a49;
font-weight: 600;
}
.code-editor-div :deep(.env-value) {
color: #032f62;
}
/* 深色主题语法高亮 */
.dark-theme .code-editor-div :deep(.env-comment) {
color: #8b949e;
}
.dark-theme .code-editor-div :deep(.env-key) {
color: #79c0ff;
font-weight: 600;
}
.dark-theme .code-editor-div :deep(.env-operator) {
color: #ff7b72;
font-weight: 600;
}
.dark-theme .code-editor-div :deep(.env-value) {
color: #a5d6ff;
}
/* JavaScript 语法高亮样式 */
.code-editor-div :deep(.js-keyword) {
color: #d73a49;
font-weight: 600;
}
.code-editor-div :deep(.js-string) {
color: #032f62;
}
.code-editor-div :deep(.js-comment) {
color: #6a737d;
font-style: italic;
}
/* PHP 语法高亮样式 */
.code-editor-div :deep(.php-tag) {
color: #d73a49;
font-weight: 600;
}
.code-editor-div :deep(.php-keyword) {
color: #6f42c1;
font-weight: 600;
}
.code-editor-div :deep(.php-variable) {
color: #e36209;
}
.code-editor-div :deep(.php-string) {
color: #032f62;
}
.code-editor-div :deep(.php-comment) {
color: #6a737d;
font-style: italic;
}
/* 深色主题 JavaScript */
.dark-theme .code-editor-div :deep(.js-keyword) {
color: #ff7b72;
font-weight: 600;
}
.dark-theme .code-editor-div :deep(.js-string) {
color: #a5d6ff;
}
.dark-theme .code-editor-div :deep(.js-comment) {
color: #8b949e;
font-style: italic;
}
/* 深色主题 PHP */
.dark-theme .code-editor-div :deep(.php-tag) {
color: #ff7b72;
font-weight: 600;
}
.dark-theme .code-editor-div :deep(.php-keyword) {
color: #d2a8ff;
font-weight: 600;
}
.dark-theme .code-editor-div :deep(.php-variable) {
color: #ffa657;
}
.dark-theme .code-editor-div :deep(.php-string) {
color: #a5d6ff;
}
.dark-theme .code-editor-div :deep(.php-comment) {
color: #8b949e;
font-style: italic;
}
/* 滚动条样式 - 只为代码编辑器设置滚动条 */
.code-editor-div::-webkit-scrollbar {
width: 12px;
height: 12px;
}
.code-editor-div::-webkit-scrollbar-track {
background: #f6f8fa;
border-radius: 6px;
}
.code-editor-div::-webkit-scrollbar-thumb {
background: #d0d7de;
border-radius: 6px;
border: 2px solid #f6f8fa;
}
.code-editor-div::-webkit-scrollbar-thumb:hover {
background: #a8b3c1;
}
/* 深色主题滚动条样式 */
.dark-theme .code-editor-div::-webkit-scrollbar-track {
background: #21262d;
}
.dark-theme .code-editor-div::-webkit-scrollbar-thumb {
background: #484f58;
border-color: #21262d;
}
.dark-theme .code-editor-div::-webkit-scrollbar-thumb:hover {
background: #6e7681;
}
/* 选择文本样式 */
.code-editor-div::selection {
background: rgba(59, 130, 246, 0.2);
}
.dark-theme .code-editor-div::selection {
background: rgba(96, 165, 250, 0.3);
}
/* placeholder样式 */
.code-editor-div:empty:before {
content: attr(data-placeholder);
color: #8b949e;
font-style: italic;
pointer-events: none;
}
.dark-theme .code-editor-div:empty:before {
color: #6e7681;
}
/* 响应式设计 */
@media (max-width: 768px) {
.editor-header {
padding: 0 8px;
}
.tab {
padding: 6px 12px;
}
.tab-name {
font-size: 12px;
}
.line-numbers-area {
min-width: 50px;
}
.line-number {
padding: 0 8px;
}
.code-editor-div {
padding: 12px 16px;
font-size: 13px;
}
.status-left,
.status-right {
gap: 12px;
}
}
</style>