970 lines
24 KiB
Vue
970 lines
24 KiB
Vue
<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(/(<\?php|<\?=|\?>)/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>
|