Files
toolbox/resources/js/components/EnvManagement.vue
2025-08-01 16:55:23 +08:00

884 lines
34 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="env-management h-full flex flex-col p-4 box-border">
<!-- 项目选择卡片 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 mb-4 flex-shrink-0">
<div class="flex items-center justify-between mb-2">
<h2 class="text-sm lg:text-base font-semibold text-gray-900 flex items-center">
<svg class="w-4 h-4 text-blue-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
</svg>
项目选择
</h2>
<div class="text-xs text-gray-500">
{{ projects.length }} 个项目
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-2 lg:gap-3">
<div>
<div class="relative">
<!-- 搜索输入框 -->
<div class="relative">
<input
v-model="projectSearchQuery"
@focus="handleProjectFocus"
@blur="handleProjectBlur"
@keydown="handleProjectKeydown"
placeholder="搜索项目..."
class="w-full px-4 py-2.5 pr-20 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors duration-200"
>
<div class="absolute inset-y-0 right-0 flex items-center pr-2 space-x-1">
<!-- 清除按钮 -->
<button
v-if="projectSearchQuery"
@mousedown="clearSearch"
class="p-1 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-full transition-colors duration-200"
title="清除搜索"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
<!-- 搜索图标 -->
<div class="p-1">
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</div>
</div>
</div>
<!-- 下拉选项列表 -->
<div
v-if="showProjectDropdown && filteredProjects.length > 0"
class="absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg max-h-80 overflow-y-auto"
>
<div
v-for="(project, index) in filteredProjects"
:key="project.name"
@mousedown="selectProject(project)"
class="px-3 py-1.5 cursor-pointer hover:bg-blue-50 transition-colors duration-200 flex items-center justify-between"
:class="{ 'bg-blue-100': index === highlightedIndex }"
>
<div class="font-medium text-gray-900 text-sm leading-tight truncate flex-1 mr-2">{{ project.name }}</div>
<div class="text-xs text-gray-500 flex-shrink-0">{{ project.envs.length }} 个环境</div>
</div>
</div>
<!-- 无搜索结果 -->
<div
v-if="showProjectDropdown && filteredProjects.length === 0 && projectSearchQuery"
class="absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg"
>
<div class="px-3 py-2 text-gray-500 text-center text-sm">
未找到匹配的项目
</div>
</div>
</div>
</div>
<div v-if="selectedProject" class="flex items-end space-x-2">
<button
@click="loadProjects"
class="px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200 flex items-center space-x-2 text-sm font-medium"
title="刷新项目列表"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
<span>刷新</span>
</button>
<button
@click="showCreateModal = true"
class="px-3 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors duration-200 flex items-center space-x-2 text-sm font-medium"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
<span>新增配置文件</span>
</button>
<button
@click="showImportModal = true"
class="px-3 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors duration-200 flex items-center space-x-2 text-sm font-medium"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"/>
</svg>
<span>导入当前.env</span>
</button>
</div>
</div>
</div>
<!-- 主要内容区域 -->
<div v-if="selectedProject" class="grid grid-cols-1 lg:grid-cols-10 gap-4 flex-1 min-h-0 overflow-hidden">
<!-- 左侧环境列表 (2/10 宽度) -->
<div class="lg:col-span-2 bg-white rounded-xl shadow-sm border border-gray-200 flex flex-col min-h-0">
<div class="p-2.5 lg:p-3 border-b border-gray-200">
<h3 class="text-sm lg:text-base font-semibold text-gray-900 flex items-center">
<svg class="w-4 h-4 text-green-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
环境配置列表
</h3>
<p class="text-xs text-gray-500 mt-0.5">项目: {{ selectedProject }}</p>
</div>
<div class="p-2.5 lg:p-3 flex-1 overflow-y-auto">
<div v-if="envs.length === 0" class="text-center py-12">
<svg class="w-12 h-12 text-gray-300 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
<p class="text-gray-500 text-sm">暂无环境配置文件</p>
<p class="text-gray-400 text-xs mt-1">点击"导入当前.env"开始创建</p>
</div>
<div v-else class="space-y-2">
<div
v-for="env in envs"
:key="env.name"
class="group border border-gray-200 rounded-lg p-3 hover:border-blue-300 hover:shadow-md transition-all duration-200"
:class="{ 'ring-2 ring-blue-500 border-blue-500': selectedEnv === env.name }"
>
<div class="flex items-center justify-between">
<div class="flex-1 cursor-pointer" @click="loadEnvContent(env.name)">
<div class="flex items-center space-x-2">
<div class="w-2.5 h-2.5 bg-green-400 rounded-full"></div>
<h4 class="font-medium text-gray-900 hover:text-blue-600 transition-colors duration-200 text-sm">{{ env.name }}</h4>
</div>
<div class="text-xs text-gray-500 mt-0.5 space-x-3 ml-4.5">
<span>{{ formatFileSize(env.size) }}</span>
<span>{{ env.modified_at }}</span>
</div>
</div>
<div class="flex space-x-1">
<button
@click.stop="applyEnv(env.name)"
class="p-1.5 text-green-600 hover:bg-green-50 rounded-md transition-colors duration-200"
title="应用"
>
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
</button>
<button
@click.stop="deleteEnv(env.name)"
class="p-1.5 text-red-600 hover:bg-red-50 rounded-md transition-colors duration-200"
title="删除"
>
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧环境编辑器 (8/10 宽度) -->
<div class="lg:col-span-8 bg-white rounded-xl shadow-sm border border-gray-200 flex flex-col min-h-0">
<div class="p-3 lg:p-4 border-b border-gray-200">
<div class="flex items-center justify-between">
<h3 class="text-base lg:text-lg font-semibold text-gray-900 flex items-center">
<svg class="w-4 h-4 lg:w-5 lg:h-5 text-purple-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
</svg>
{{ selectedEnv ? `编辑: ${selectedEnv}` : '环境配置编辑器' }}
</h3>
<div class="flex items-center space-x-2">
<input
v-model="newEnvName"
placeholder="新环境名称"
class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<button
@click="saveEnv"
:disabled="!envContent || !envContent.trim()"
class="bg-gradient-to-r from-blue-500 to-blue-600 text-white px-3 py-1.5 rounded-lg hover:from-blue-600 hover:to-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 flex items-center space-x-1"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3-3m0 0l-3 3m3-3v12"/>
</svg>
<span>保存</span>
</button>
</div>
</div>
</div>
<div class="p-4 flex-1 flex flex-col min-h-0 overflow-hidden">
<!-- 代码编辑器工具栏 -->
<div class="mb-2 flex items-center justify-between flex-shrink-0">
<div class="flex items-center space-x-3">
<div class="flex items-center space-x-2">
<label class="text-xs lg:text-sm font-medium text-gray-700">语法高亮:</label>
<select
v-model="editorLanguage"
class="text-xs lg:text-sm border border-gray-300 rounded px-2 py-1 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="env">.env 文件</option>
<option value="javascript">JavaScript</option>
<option value="php">PHP</option>
</select>
</div>
<div class="flex items-center space-x-2">
<label class="text-xs lg:text-sm font-medium text-gray-700">主题:</label>
<select
v-model="editorTheme"
class="text-xs lg:text-sm border border-gray-300 rounded px-2 py-1 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="light">浅色</option>
<option value="dark">深色</option>
</select>
</div>
</div>
<div class="text-xs lg:text-sm text-gray-500">
行数: {{ envContent ? envContent.split('\n').length : 1 }} | 字符数: {{ envContent ? envContent.length : 0 }}
</div>
</div>
<!-- 代码编辑器 -->
<div class="border border-gray-300 rounded-lg overflow-hidden flex-1 min-h-0">
<code-editor
v-model="envContent"
:language="editorLanguage"
:theme="editorTheme"
placeholder="在此输入环境配置内容..."
@change="onEditorChange"
@language-change="handleLanguageChange"
class="h-full"
/>
</div>
<div class="mt-1 flex items-center justify-between text-xs text-gray-500">
<div class="flex items-center space-x-3">
<span>支持的格式: .env, JavaScript, PHP</span>
<span>编码: UTF-8</span>
</div>
<div class="flex items-center space-x-2">
<svg class="w-3 h-3 lg:w-4 lg:h-4 text-green-500" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
<span class="text-green-600">语法高亮已启用</span>
</div>
</div>
</div>
</div>
</div>
<!-- 新增配置文件模态框 -->
<div v-if="showCreateModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div class="bg-white rounded-xl shadow-xl w-full max-w-md">
<div class="p-6 border-b border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 flex items-center">
<svg class="w-5 h-5 text-purple-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
新增配置文件
</h3>
<p class="text-sm text-gray-600 mt-1">为项目 {{ selectedProject }} 创建新的环境配置文件</p>
</div>
<div class="p-6">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">配置文件名称</label>
<input
v-model="createEnvName"
type="text"
placeholder="例如: production, staging, development"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors duration-200"
@keyup.enter="createNewEnv"
/>
<p class="text-xs text-gray-500 mt-1">文件将保存为 {{ createEnvName }}.env</p>
</div>
</div>
<div class="p-6 border-t border-gray-200 flex justify-end space-x-3">
<button
@click="showCreateModal = false"
class="px-4 py-2 text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors duration-200"
>
取消
</button>
<button
@click="createNewEnv"
:disabled="!createEnvName.trim()"
class="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors duration-200"
>
创建
</button>
</div>
</div>
</div>
<!-- 导入模态框 -->
<div v-if="showImportModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div class="bg-white rounded-xl shadow-xl w-full max-w-md">
<div class="p-6 border-b border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 flex items-center">
<svg class="w-5 h-5 text-green-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"/>
</svg>
导入当前.env文件
</h3>
</div>
<div class="p-6">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">环境名称:</label>
<input
v-model="importEnvName"
placeholder="例如: production, development, staging"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<p class="text-xs text-gray-500 mt-1">建议使用有意义的环境名称 devtestprod </p>
</div>
</div>
<div class="p-6 border-t border-gray-200 flex justify-end space-x-3">
<button
@click="showImportModal = false"
class="px-4 py-2 text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors duration-200"
>
取消
</button>
<button
@click="importEnv"
:disabled="!importEnvName.trim()"
class="px-4 py-2 bg-gradient-to-r from-green-500 to-green-600 text-white rounded-lg hover:from-green-600 hover:to-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
>
导入
</button>
</div>
</div>
</div>
<!-- 消息提示 -->
<div
v-if="message.text"
class="fixed top-4 right-4 p-4 rounded-lg shadow-lg z-50 max-w-sm"
:class="{
'bg-green-50 text-green-800 border border-green-200': message.type === 'success',
'bg-red-50 text-red-800 border border-red-200': message.type === 'error',
'bg-blue-50 text-blue-800 border border-blue-200': message.type === 'info'
}"
>
<div class="flex items-center">
<svg v-if="message.type === 'success'" class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
<svg v-else-if="message.type === 'error'" class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
</svg>
<span>{{ message.text }}</span>
</div>
</div>
</div>
</template>
<script>
import CodeEditor from './CodeEditor.vue';
export default {
name: 'EnvManagement',
components: {
CodeEditor
},
data() {
return {
projects: [],
selectedProject: '',
projectSearchQuery: '',
showProjectDropdown: false,
showAllProjectsOnFocus: false,
highlightedIndex: -1,
envs: [],
selectedEnv: '',
envContent: '',
newEnvName: '',
showCreateModal: false,
createEnvName: '',
showImportModal: false,
importEnvName: '',
editorLanguage: 'env',
editorTheme: 'light',
message: {
text: '',
type: ''
}
}
},
computed: {
filteredProjects() {
// 如果是点击输入框时显示下拉框,总是显示所有项目
if (this.showAllProjectsOnFocus) {
return this.projects;
}
// 如果没有搜索内容,显示所有项目
if (!this.projectSearchQuery) {
return this.projects;
}
// 有搜索内容时进行过滤
const query = this.projectSearchQuery.toLowerCase();
return this.projects.filter(project =>
project.name.toLowerCase().includes(query)
);
}
},
watch: {
projectSearchQuery(newValue) {
// 当搜索内容变化时,重置高亮索引
this.highlightedIndex = -1;
// 如果用户开始输入,关闭"显示所有项目"的标志,启用过滤
if (newValue && this.showAllProjectsOnFocus) {
this.showAllProjectsOnFocus = false;
}
// 如果搜索框被清空且没有选中项目,清空选中状态
if (!newValue && !this.selectedProject) {
this.selectedProject = '';
this.envs = [];
}
},
projects() {
// 当项目列表更新时,重置搜索状态
this.highlightedIndex = -1;
}
},
mounted() {
console.log('EnvManagement mounted');
this.loadProjects();
},
methods: {
async loadProjects() {
try {
const response = await fetch('/env/api/projects');
const data = await response.json();
if (data.success) {
this.projects = data.data;
} else {
this.showMessage('加载项目列表失败', 'error');
}
} catch (error) {
this.showMessage('网络错误', 'error');
}
},
async loadEnvs() {
if (!this.selectedProject) {
this.envs = [];
return;
}
try {
const response = await fetch(`/env/api/projects/${this.selectedProject}/envs`);
const data = await response.json();
if (data.success) {
this.envs = data.data;
} else {
this.showMessage('加载环境列表失败', 'error');
}
} catch (error) {
this.showMessage('网络错误', 'error');
}
},
async loadEnvContent(envName) {
this.selectedEnv = envName;
this.newEnvName = envName;
try {
const response = await fetch(`/env/api/projects/${this.selectedProject}/envs/${envName}`);
const data = await response.json();
if (data.success) {
this.envContent = data.data.content;
// 根据环境名称自动检测语言
this.autoDetectLanguage(envName);
// 自动进入编辑状态
this.isEditing = true;
} else {
this.showMessage('加载环境配置失败', 'error');
}
} catch (error) {
this.showMessage('网络错误', 'error');
}
},
async saveEnv() {
const envName = this.newEnvName || this.selectedEnv;
if (!envName.trim()) {
this.showMessage('请输入环境名称', 'error');
return;
}
try {
const response = await fetch('/env/api/envs/save', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
body: JSON.stringify({
project: this.selectedProject,
env: envName,
content: this.envContent
})
});
const data = await response.json();
if (data.success) {
this.showMessage('环境配置保存成功', 'success');
this.selectedEnv = envName;
this.newEnvName = '';
await this.loadEnvs();
} else {
this.showMessage(data.message || '保存失败', 'error');
}
} catch (error) {
this.showMessage('网络错误', 'error');
}
},
async applyEnv(envName) {
if (!confirm(`确定要将 ${envName} 环境应用到项目 ${this.selectedProject} 吗?`)) {
return;
}
try {
const response = await fetch('/env/api/envs/apply', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
body: JSON.stringify({
project: this.selectedProject,
env: envName
})
});
const data = await response.json();
if (data.success) {
this.showMessage('环境配置应用成功', 'success');
} else {
this.showMessage(data.message || '应用失败', 'error');
}
} catch (error) {
this.showMessage('网络错误', 'error');
}
},
async deleteEnv(envName) {
if (!confirm(`确定要删除环境配置 ${envName} 吗?`)) {
return;
}
try {
const response = await fetch(`/env/api/projects/${this.selectedProject}/envs/${envName}`, {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
}
});
const data = await response.json();
if (data.success) {
this.showMessage('环境配置删除成功', 'success');
if (this.selectedEnv === envName) {
this.selectedEnv = '';
this.envContent = '';
}
await this.loadEnvs();
} else {
this.showMessage(data.message || '删除失败', 'error');
}
} catch (error) {
this.showMessage('网络错误', 'error');
}
},
async importEnv() {
if (!this.importEnvName.trim()) {
this.showMessage('请输入环境名称', 'error');
return;
}
try {
const response = await fetch('/env/api/envs/import', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
body: JSON.stringify({
project: this.selectedProject,
env: this.importEnvName
})
});
const data = await response.json();
if (data.success) {
this.showMessage('环境配置导入成功', 'success');
this.showImportModal = false;
this.importEnvName = '';
await this.loadEnvs();
} else {
this.showMessage(data.message || '导入失败', 'error');
}
} catch (error) {
this.showMessage('网络错误', 'error');
}
},
async createNewEnv() {
if (!this.createEnvName.trim()) {
this.showMessage('请输入配置文件名称', 'error');
return;
}
try {
const response = await fetch('/env/api/envs/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
body: JSON.stringify({
project: this.selectedProject,
env_name: this.createEnvName
})
});
const data = await response.json();
if (data.success) {
this.showMessage('配置文件创建成功', 'success');
this.showCreateModal = false;
// 保存环境名称用于后续加载
const envName = this.createEnvName;
this.createEnvName = '';
await this.loadEnvs();
// 自动选择新创建的配置文件
setTimeout(() => {
this.loadEnvContent(envName);
}, 500);
} else {
this.showMessage(data.message || '创建失败', 'error');
}
} catch (error) {
this.showMessage('网络错误', 'error');
}
},
// 项目搜索相关方法
clearSearch() {
this.projectSearchQuery = '';
this.showProjectDropdown = false;
this.highlightedIndex = -1;
// 不重置 selectedProject、envs 和编辑器内容,保持当前状态
// 如果有选中的项目,在清除搜索后恢复显示项目名称
setTimeout(() => {
if (this.selectedProject && !this.showProjectDropdown) {
this.projectSearchQuery = this.selectedProject;
}
}, 100);
},
selectProject(project) {
this.selectedProject = project.name;
// 显示选中的项目名称在搜索框中
this.projectSearchQuery = project.name;
this.showProjectDropdown = false;
this.highlightedIndex = -1;
this.loadEnvs();
},
handleProjectFocus() {
// 点击输入框时显示下拉框,并设置显示所有项目的标志
this.showProjectDropdown = true;
this.showAllProjectsOnFocus = true;
this.highlightedIndex = -1;
// 如果当前显示的是选中的项目名称,清空搜索框以便用户重新搜索
if (this.selectedProject && this.projectSearchQuery === this.selectedProject) {
this.projectSearchQuery = '';
}
},
handleProjectBlur() {
// 延迟隐藏下拉框,以便点击选项能够正常工作
setTimeout(() => {
this.showProjectDropdown = false;
this.showAllProjectsOnFocus = false;
this.highlightedIndex = -1;
// 如果有选中的项目,始终在搜索框中显示项目名称
if (this.selectedProject) {
this.projectSearchQuery = this.selectedProject;
} else {
// 如果没有选择项目,清空搜索框
this.projectSearchQuery = '';
}
}, 200);
},
handleProjectKeydown(event) {
if (!this.showProjectDropdown) {
this.showProjectDropdown = true;
return;
}
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
this.highlightedIndex = Math.min(
this.highlightedIndex + 1,
this.filteredProjects.length - 1
);
break;
case 'ArrowUp':
event.preventDefault();
this.highlightedIndex = Math.max(this.highlightedIndex - 1, -1);
break;
case 'Enter':
event.preventDefault();
if (this.highlightedIndex >= 0 && this.filteredProjects[this.highlightedIndex]) {
this.selectProject(this.filteredProjects[this.highlightedIndex]);
}
break;
case 'Escape':
this.showProjectDropdown = false;
this.highlightedIndex = -1;
break;
}
},
formatFileSize(bytes) {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${Math.round(size * 100) / 100} ${units[unitIndex]}`;
},
showMessage(text, type) {
this.message = { text, type };
setTimeout(() => {
this.message = { text: '', type: '' };
}, 3000);
},
onEditorChange(content) {
// 编辑器内容变化时的处理
// 可以在这里添加实时保存、语法检查等功能
console.log('Editor content changed, length:', content.length);
},
handleLanguageChange(newLanguage) {
// 处理编辑器语言变化
this.editorLanguage = newLanguage;
},
// 根据文件名自动检测语言
autoDetectLanguage(filename) {
if (!filename) return;
const name = filename.toLowerCase();
// .env 文件检测
if (name === 'env' || name === '.env' || name.startsWith('env.') || name.endsWith('.env')) {
this.editorLanguage = 'env';
return;
}
// JavaScript 文件检测
if (name.endsWith('.js') || name.endsWith('.jsx') || name.endsWith('.ts') || name.endsWith('.tsx') || name.endsWith('.mjs')) {
this.editorLanguage = 'javascript';
return;
}
// PHP 文件检测
if (name.endsWith('.php') || name.endsWith('.phtml') || name.endsWith('.php3') || name.endsWith('.php4') || name.endsWith('.php5')) {
this.editorLanguage = 'php';
return;
}
// 根据内容特征检测
if (this.envContent) {
const content = this.envContent.toLowerCase();
// 检测是否包含典型的环境变量模式
if (content.includes('app_name=') || content.includes('db_host=') || content.includes('app_env=')) {
this.editorLanguage = 'env';
return;
}
// 检测是否包含PHP标签
if (content.includes('<?php') || content.includes('<?=')) {
this.editorLanguage = 'php';
return;
}
// 检测是否包含JavaScript关键字
if (content.includes('function') || content.includes('const ') || content.includes('let ') || content.includes('var ')) {
this.editorLanguage = 'javascript';
return;
}
}
// 默认使用 env
this.editorLanguage = 'env';
}
}
}
</script>
<style scoped>
.env-management {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
height: 100%;
box-sizing: border-box;
}
/* 自定义滚动条 */
textarea::-webkit-scrollbar {
width: 8px;
}
textarea::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
textarea::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
textarea::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
</style>