884 lines
34 KiB
Vue
884 lines
34 KiB
Vue
<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">建议使用有意义的环境名称,如 dev、test、prod 等</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>
|