#feature: add project&cronjob management
This commit is contained in:
653
resources/js/components/admin/ProjectManagement.vue
Normal file
653
resources/js/components/admin/ProjectManagement.vue
Normal file
@@ -0,0 +1,653 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between bg-white p-4 rounded-lg shadow-sm border border-gray-200">
|
||||
<div class="flex items-center gap-2">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold text-gray-800">项目配置管理</h3>
|
||||
<p class="text-sm text-gray-500">统一管理项目路径、Git监控、日志分析等配置</p>
|
||||
</div>
|
||||
<!-- 帮助气泡 -->
|
||||
<div class="relative" @mouseenter="showHelp = true" @mouseleave="showHelp = false">
|
||||
<button class="p-1 text-gray-400 hover:text-blue-500 rounded-full hover:bg-blue-50">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM8.94 6.94a.75.75 0 11-1.061-1.061 3 3 0 112.871 5.026v.345a.75.75 0 01-1.5 0v-.5c0-.72.57-1.172 1.081-1.287A1.5 1.5 0 108.94 6.94zM10 15a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
<div v-show="showHelp" class="absolute left-0 top-8 z-50 w-80 bg-white rounded-lg shadow-lg border border-gray-200 p-4">
|
||||
<h4 class="font-medium text-gray-800 mb-2">功能说明</h4>
|
||||
<ul class="text-sm text-gray-600 space-y-1.5">
|
||||
<li><strong class="text-gray-700">一键同步</strong>:同步重要项目的 Git 版本信息</li>
|
||||
<li><strong class="text-gray-700">发现项目</strong>:扫描项目根目录,自动发现新项目</li>
|
||||
<li><strong class="text-gray-700">有效/无效</strong>:项目目录是否存在于磁盘上</li>
|
||||
<li><strong class="text-gray-700">星标</strong>:标记重要项目,优先显示并参与一键同步</li>
|
||||
<li><strong class="text-gray-700">Git 监控</strong>:启用后监控项目的 Git 提交变化</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
@click="syncAllProjects"
|
||||
:disabled="syncingAll"
|
||||
class="px-3 py-2 bg-green-600 text-white text-sm font-medium rounded hover:bg-green-700 disabled:opacity-50 flex items-center gap-1"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4" :class="{'animate-spin': syncingAll}">
|
||||
<path fill-rule="evenodd" d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0v2.433l-.31-.31a7 7 0 00-11.712 3.138.75.75 0 001.449.39 5.5 5.5 0 019.201-2.466l.312.312h-2.433a.75.75 0 000 1.5h4.185a.75.75 0 00.53-.219z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
{{ syncingAll ? '同步中...' : '一键同步' }}
|
||||
</button>
|
||||
<button
|
||||
@click="discoverProjects"
|
||||
:disabled="discovering"
|
||||
class="px-3 py-2 bg-gray-100 text-gray-700 text-sm font-medium rounded hover:bg-gray-200 disabled:opacity-50 flex items-center gap-1"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4">
|
||||
<path d="M9 6a.75.75 0 01.75.75v1.5h1.5a.75.75 0 010 1.5h-1.5v1.5a.75.75 0 01-1.5 0v-1.5h-1.5a.75.75 0 010-1.5h1.5v-1.5A.75.75 0 019 6z" />
|
||||
<path fill-rule="evenodd" d="M2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9zm7-5.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
发现项目
|
||||
</button>
|
||||
<button
|
||||
@click="openCreateModal"
|
||||
class="px-3 py-2 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700 flex items-center gap-1"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4">
|
||||
<path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" />
|
||||
</svg>
|
||||
添加项目
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
<div v-if="message" class="text-sm text-green-600 bg-green-50 px-4 py-3 rounded border border-green-100">{{ message }}</div>
|
||||
<div v-if="error" class="text-sm text-red-600 bg-red-50 px-4 py-3 rounded border border-red-100">{{ error }}</div>
|
||||
|
||||
<!-- Projects Path -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="text-sm font-medium text-gray-600 whitespace-nowrap">项目根目录:</label>
|
||||
<span class="font-mono text-sm text-gray-800 bg-gray-50 px-3 py-1.5 rounded border">{{ projectsPath || '未配置' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Git 监控项目 -->
|
||||
<div v-if="gitMonitorProjects.length > 0" class="space-y-3">
|
||||
<h4 class="text-sm font-semibold text-gray-700 flex items-center gap-2">
|
||||
<span class="w-2 h-2 bg-green-500 rounded-full"></span>
|
||||
Git 监控项目 ({{ gitMonitorProjects.length }})
|
||||
</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
<project-card
|
||||
v-for="project in gitMonitorProjects"
|
||||
:key="project.slug"
|
||||
:project="project"
|
||||
@sync="syncProject"
|
||||
@edit="openEditModal"
|
||||
@delete="deleteProject"
|
||||
@toggle-field="toggleField"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 重要项目 -->
|
||||
<div v-if="importantProjects.length > 0" class="space-y-3">
|
||||
<h4 class="text-sm font-semibold text-gray-700 flex items-center gap-2">
|
||||
<span class="w-2 h-2 bg-yellow-500 rounded-full"></span>
|
||||
重要项目 ({{ importantProjects.length }})
|
||||
</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
<project-card
|
||||
v-for="project in importantProjects"
|
||||
:key="project.slug"
|
||||
:project="project"
|
||||
@sync="syncProject"
|
||||
@edit="openEditModal"
|
||||
@delete="deleteProject"
|
||||
@toggle-field="toggleField"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 其他项目 -->
|
||||
<div v-if="otherProjects.length > 0" class="space-y-3">
|
||||
<h4 class="text-sm font-semibold text-gray-700 flex items-center gap-2">
|
||||
<span class="w-2 h-2 bg-gray-400 rounded-full"></span>
|
||||
其他项目 ({{ otherProjects.length }})
|
||||
</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
<project-card
|
||||
v-for="project in otherProjects"
|
||||
:key="project.slug"
|
||||
:project="project"
|
||||
@sync="syncProject"
|
||||
@edit="openEditModal"
|
||||
@delete="deleteProject"
|
||||
@toggle-field="toggleField"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="!loading && projects.length === 0" class="text-center py-12 text-gray-400 bg-white rounded-lg border border-gray-200">
|
||||
暂无项目配置,点击"发现项目"或"添加项目"开始
|
||||
</div>
|
||||
|
||||
<!-- Edit/Create Modal -->
|
||||
<div v-if="showModal" class="fixed inset-0 backdrop-blur-sm bg-white/30 flex items-center justify-center z-50" @click.self="closeModal">
|
||||
<div class="bg-white rounded-lg shadow-xl w-full max-w-lg mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<h4 class="text-lg font-semibold text-gray-800">{{ editingProject ? '编辑项目' : '添加项目' }}</h4>
|
||||
<button @click="closeModal" class="text-gray-400 hover:text-gray-600">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
||||
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-6 space-y-4">
|
||||
<!-- Basic Info -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Slug (唯一标识)</label>
|
||||
<input v-model="form.slug" type="text" :disabled="!!editingProject" class="w-full px-3 py-2 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 disabled:bg-gray-100" placeholder="如: portal-be" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">显示名称</label>
|
||||
<input v-model="form.name" type="text" class="w-full px-3 py-2 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500" placeholder="如: Portal Backend" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">目录名 (相对路径)</label>
|
||||
<input v-model="form.directory" type="text" class="w-full px-3 py-2 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500" placeholder="如: portal-be" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">绝对路径 (可选)</label>
|
||||
<input v-model="form.absolute_path" type="text" class="w-full px-3 py-2 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500" placeholder="覆盖默认路径" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">JIRA 项目代码</label>
|
||||
<input v-model="form.jira_project_code" type="text" class="w-full px-3 py-2 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500" placeholder="如: PORTAL" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input v-model="form.git_monitor_enabled" type="checkbox" id="git_monitor" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
|
||||
<label for="git_monitor" class="text-sm text-gray-700">启用 Git 监控</label>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input v-model="form.auto_create_release_branch" type="checkbox" id="auto_create_release_branch" class="rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
|
||||
<label for="auto_create_release_branch" class="text-sm text-gray-700">自动创建 Release 分支</label>
|
||||
<span class="text-xs text-gray-400" title="当 Jira 中有新版本且远程没有对应分支时,自动从 master 创建并推送">(?)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input v-model="form.is_important" type="checkbox" id="is_important" class="rounded border-gray-300 text-yellow-500 focus:ring-yellow-500" />
|
||||
<label for="is_important" class="text-sm text-gray-700">标记为重要项目</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">日志 App 名称 (逗号分隔)</label>
|
||||
<input v-model="form.log_app_names_text" type="text" class="w-full px-3 py-2 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500" placeholder="如: portal-api, portal-worker" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">日志环境</label>
|
||||
<input v-model="form.log_env" type="text" class="w-full px-3 py-2 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500" placeholder="如: production" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-4 border-t border-gray-200 flex justify-end gap-2">
|
||||
<button @click="closeModal" class="px-4 py-2 text-sm text-gray-700 bg-gray-100 rounded hover:bg-gray-200">取消</button>
|
||||
<button @click="saveProject" :disabled="saving" class="px-4 py-2 text-sm text-white bg-blue-600 rounded hover:bg-blue-700 disabled:opacity-50">
|
||||
{{ saving ? '保存中...' : '保存' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Discover Modal -->
|
||||
<div v-if="showDiscoverModal" class="fixed inset-0 backdrop-blur-sm bg-white/30 flex items-center justify-center z-50" @click.self="showDiscoverModal = false">
|
||||
<div class="bg-white rounded-lg shadow-xl w-full max-w-2xl mx-4">
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<h4 class="text-lg font-semibold text-gray-800">发现的项目</h4>
|
||||
<label v-if="discoveredProjects.length > 0" class="flex items-center gap-2 text-sm text-gray-600 cursor-pointer">
|
||||
<input type="checkbox" :checked="isAllSelected" @change="toggleSelectAll" class="rounded border-gray-300 text-blue-600" />
|
||||
全选
|
||||
</label>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div v-if="discoveredProjects.length === 0" class="text-center text-gray-400 py-8">
|
||||
未发现新项目
|
||||
</div>
|
||||
<div v-else class="overflow-x-auto max-h-80">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50 sticky top-0">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase w-10"></th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Slug</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">名称</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Git</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<tr v-for="project in discoveredProjects" :key="project.slug" class="hover:bg-gray-50 cursor-pointer" @click="toggleProjectSelection(project.slug)">
|
||||
<td class="px-3 py-2">
|
||||
<input type="checkbox" :checked="selectedDiscovered.includes(project.slug)" @click.stop @change="toggleProjectSelection(project.slug)" class="rounded border-gray-300 text-blue-600" />
|
||||
</td>
|
||||
<td class="px-3 py-2 font-mono text-sm text-gray-900">{{ project.slug }}</td>
|
||||
<td class="px-3 py-2 text-sm text-gray-600">{{ project.name }}</td>
|
||||
<td class="px-3 py-2">
|
||||
<span v-if="project.is_git_repo" class="text-green-600 text-xs">Git</span>
|
||||
<span v-else class="text-gray-400 text-xs">-</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-6 py-4 border-t border-gray-200 flex justify-end gap-2">
|
||||
<button @click="showDiscoverModal = false" class="px-4 py-2 text-sm text-gray-700 bg-gray-100 rounded hover:bg-gray-200">取消</button>
|
||||
<button @click="addDiscoveredProjects" :disabled="selectedDiscovered.length === 0 || addingDiscovered" class="px-4 py-2 text-sm text-white bg-blue-600 rounded hover:bg-blue-700 disabled:opacity-50">
|
||||
添加选中 ({{ selectedDiscovered.length }})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// ProjectCard 子组件
|
||||
const ProjectCard = {
|
||||
name: 'ProjectCard',
|
||||
props: {
|
||||
project: { type: Object, required: true }
|
||||
},
|
||||
emits: ['sync', 'edit', 'delete', 'toggle-field'],
|
||||
template: `
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 hover:shadow-md transition-shadow">
|
||||
<div class="p-4">
|
||||
<!-- Header -->
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-mono font-semibold text-gray-800">{{ project.slug }}</span>
|
||||
<span
|
||||
:class="project.path_valid ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'"
|
||||
class="text-xs px-1.5 py-0.5 rounded"
|
||||
:title="project.path_valid ? '项目目录存在' : '项目目录不存在或已被移动'"
|
||||
>
|
||||
{{ project.path_valid ? '有效' : '无效' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 mt-0.5">{{ project.name }}</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<!-- 同步 -->
|
||||
<button @click="$emit('sync', project)" :disabled="project._syncing" class="p-1.5 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded" title="同步Git信息">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4" :class="{'animate-spin': project._syncing}">
|
||||
<path fill-rule="evenodd" d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0v2.433l-.31-.31a7 7 0 00-11.712 3.138.75.75 0 001.449.39 5.5 5.5 0 019.201-2.466l.312.312h-2.433a.75.75 0 000 1.5h4.185a.75.75 0 00.53-.219z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
<!-- 编辑 -->
|
||||
<button @click="$emit('edit', project)" class="p-1.5 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded" title="编辑">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4">
|
||||
<path d="M2.695 14.763l-1.262 3.154a.5.5 0 00.65.65l3.155-1.262a4 4 0 001.343-.885L17.5 5.5a2.121 2.121 0 00-3-3L3.58 13.42a4 4 0 00-.885 1.343z" />
|
||||
</svg>
|
||||
</button>
|
||||
<!-- 删除 -->
|
||||
<button @click="$emit('delete', project)" :disabled="project._deleting" class="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded" title="删除">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4">
|
||||
<path fill-rule="evenodd" d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 002.742-2.53l.841-10.52.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="space-y-2 text-sm">
|
||||
<div v-if="project.jira_project_code" class="flex items-center gap-2">
|
||||
<span class="text-gray-500">JIRA:</span>
|
||||
<span class="font-mono bg-blue-50 text-blue-700 px-1.5 py-0.5 rounded text-xs">{{ project.jira_project_code }}</span>
|
||||
</div>
|
||||
<div v-if="project.git_current_version" class="flex items-center gap-2">
|
||||
<span class="text-gray-500">版本:</span>
|
||||
<span class="font-mono text-gray-700">{{ project.git_current_version }}</span>
|
||||
</div>
|
||||
<div v-if="project.log_app_names?.length" class="flex items-center gap-2">
|
||||
<span class="text-gray-500">App:</span>
|
||||
<span class="text-gray-700">{{ project.log_app_names.join(', ') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Switches -->
|
||||
<div class="mt-3 pt-3 border-t border-gray-100 flex flex-wrap gap-3">
|
||||
<label class="inline-flex items-center gap-1.5 cursor-pointer" @click.prevent="$emit('toggle-field', project, 'git_monitor_enabled')">
|
||||
<span class="relative inline-block">
|
||||
<input type="checkbox" :checked="project.git_monitor_enabled" class="sr-only peer" />
|
||||
<span class="block w-8 h-4 bg-gray-200 rounded-full peer peer-checked:bg-blue-500 transition-colors"></span>
|
||||
<span class="absolute left-0.5 top-0.5 w-3 h-3 bg-white rounded-full transition-transform peer-checked:translate-x-4"></span>
|
||||
</span>
|
||||
<span class="text-xs" :class="project.git_monitor_enabled ? 'text-blue-600' : 'text-gray-500'">Git监控</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-1.5 cursor-pointer" @click.prevent="$emit('toggle-field', project, 'auto_create_release_branch')">
|
||||
<span class="relative inline-block">
|
||||
<input type="checkbox" :checked="project.auto_create_release_branch" class="sr-only peer" />
|
||||
<span class="block w-8 h-4 bg-gray-200 rounded-full peer peer-checked:bg-purple-500 transition-colors"></span>
|
||||
<span class="absolute left-0.5 top-0.5 w-3 h-3 bg-white rounded-full transition-transform peer-checked:translate-x-4"></span>
|
||||
</span>
|
||||
<span class="text-xs" :class="project.auto_create_release_branch ? 'text-purple-600' : 'text-gray-500'">自动创建分支</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-1.5 cursor-pointer" @click.prevent="$emit('toggle-field', project, 'is_important')">
|
||||
<span class="relative inline-block">
|
||||
<input type="checkbox" :checked="project.is_important" class="sr-only peer" />
|
||||
<span class="block w-8 h-4 bg-gray-200 rounded-full peer peer-checked:bg-yellow-500 transition-colors"></span>
|
||||
<span class="absolute left-0.5 top-0.5 w-3 h-3 bg-white rounded-full transition-transform peer-checked:translate-x-4"></span>
|
||||
</span>
|
||||
<span class="text-xs" :class="project.is_important ? 'text-yellow-600' : 'text-gray-500'">重要</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'ProjectManagement',
|
||||
components: {
|
||||
ProjectCard
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
saving: false,
|
||||
discovering: false,
|
||||
addingDiscovered: false,
|
||||
syncingAll: false,
|
||||
showHelp: false,
|
||||
projects: [],
|
||||
projectsPath: '',
|
||||
message: '',
|
||||
error: '',
|
||||
showModal: false,
|
||||
showDiscoverModal: false,
|
||||
editingProject: null,
|
||||
form: this.getEmptyForm(),
|
||||
discoveredProjects: [],
|
||||
selectedDiscovered: []
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadProjects();
|
||||
},
|
||||
computed: {
|
||||
isAllSelected() {
|
||||
return this.discoveredProjects.length > 0 && this.selectedDiscovered.length === this.discoveredProjects.length;
|
||||
},
|
||||
// Git 监控项目(最高优先级)
|
||||
gitMonitorProjects() {
|
||||
return this.projects.filter(p => p.git_monitor_enabled);
|
||||
},
|
||||
// 重要项目(不包含已开启 Git 监控的)
|
||||
importantProjects() {
|
||||
return this.projects.filter(p => p.is_important && !p.git_monitor_enabled);
|
||||
},
|
||||
// 其他项目
|
||||
otherProjects() {
|
||||
return this.projects.filter(p => !p.git_monitor_enabled && !p.is_important);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleSelectAll() {
|
||||
if (this.isAllSelected) {
|
||||
this.selectedDiscovered = [];
|
||||
} else {
|
||||
this.selectedDiscovered = this.discoveredProjects.map(p => p.slug);
|
||||
}
|
||||
},
|
||||
toggleProjectSelection(slug) {
|
||||
const idx = this.selectedDiscovered.indexOf(slug);
|
||||
if (idx === -1) {
|
||||
this.selectedDiscovered.push(slug);
|
||||
} else {
|
||||
this.selectedDiscovered.splice(idx, 1);
|
||||
}
|
||||
},
|
||||
getEmptyForm() {
|
||||
return {
|
||||
slug: '',
|
||||
name: '',
|
||||
directory: '',
|
||||
absolute_path: '',
|
||||
jira_project_code: '',
|
||||
git_monitor_enabled: false,
|
||||
auto_create_release_branch: false,
|
||||
is_important: false,
|
||||
log_app_names_text: '',
|
||||
log_env: 'production'
|
||||
};
|
||||
},
|
||||
async loadProjects() {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
try {
|
||||
const res = await fetch('/api/admin/projects');
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
this.projects = (data.data.projects || []).map(p => ({ ...p, _syncing: false, _deleting: false }));
|
||||
this.projectsPath = data.data.projects_path || '';
|
||||
} else {
|
||||
this.error = data.message || '加载失败';
|
||||
}
|
||||
} catch (e) {
|
||||
this.error = e.message;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
openCreateModal() {
|
||||
this.editingProject = null;
|
||||
this.form = this.getEmptyForm();
|
||||
this.showModal = true;
|
||||
},
|
||||
openEditModal(project) {
|
||||
this.editingProject = project;
|
||||
this.form = {
|
||||
slug: project.slug,
|
||||
name: project.name,
|
||||
directory: project.directory || '',
|
||||
absolute_path: project.absolute_path || '',
|
||||
jira_project_code: project.jira_project_code || '',
|
||||
git_monitor_enabled: project.git_monitor_enabled || false,
|
||||
auto_create_release_branch: project.auto_create_release_branch || false,
|
||||
is_important: project.is_important || false,
|
||||
log_app_names_text: (project.log_app_names || []).join(', '),
|
||||
log_env: project.log_env || 'production'
|
||||
};
|
||||
this.showModal = true;
|
||||
},
|
||||
closeModal() {
|
||||
this.showModal = false;
|
||||
this.editingProject = null;
|
||||
},
|
||||
async saveProject() {
|
||||
if (!this.form.slug || !this.form.name) {
|
||||
this.error = 'Slug 和名称不能为空';
|
||||
return;
|
||||
}
|
||||
this.saving = true;
|
||||
this.error = '';
|
||||
this.message = '';
|
||||
|
||||
const payload = {
|
||||
slug: this.form.slug,
|
||||
name: this.form.name,
|
||||
directory: this.form.directory || null,
|
||||
absolute_path: this.form.absolute_path || null,
|
||||
jira_project_code: this.form.jira_project_code || null,
|
||||
git_monitor_enabled: this.form.git_monitor_enabled,
|
||||
auto_create_release_branch: this.form.auto_create_release_branch,
|
||||
is_important: this.form.is_important,
|
||||
log_app_names: this.form.log_app_names_text ? this.form.log_app_names_text.split(',').map(s => s.trim()).filter(Boolean) : null,
|
||||
log_env: this.form.log_env || null
|
||||
};
|
||||
|
||||
try {
|
||||
const url = this.editingProject ? `/api/admin/projects/${this.editingProject.slug}` : '/api/admin/projects';
|
||||
const method = this.editingProject ? 'PUT' : 'POST';
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
this.message = this.editingProject ? '项目已更新' : '项目已创建';
|
||||
this.closeModal();
|
||||
await this.loadProjects();
|
||||
} else {
|
||||
this.error = data.message || '保存失败';
|
||||
}
|
||||
} catch (e) {
|
||||
this.error = e.message;
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
async deleteProject(project) {
|
||||
if (!confirm(`确认删除项目 ${project.slug} 吗?`)) return;
|
||||
project._deleting = true;
|
||||
this.error = '';
|
||||
try {
|
||||
const res = await fetch(`/api/admin/projects/${project.slug}`, { method: 'DELETE' });
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
this.message = '项目已删除';
|
||||
this.projects = this.projects.filter(p => p.slug !== project.slug);
|
||||
} else {
|
||||
this.error = data.message || '删除失败';
|
||||
}
|
||||
} catch (e) {
|
||||
this.error = e.message;
|
||||
} finally {
|
||||
project._deleting = false;
|
||||
}
|
||||
},
|
||||
async syncProject(project) {
|
||||
project._syncing = true;
|
||||
this.error = '';
|
||||
try {
|
||||
const res = await fetch(`/api/admin/projects/${project.slug}/sync`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
Object.assign(project, data.data.project);
|
||||
this.message = `${project.slug} 同步完成`;
|
||||
} else {
|
||||
this.error = data.message || '同步失败';
|
||||
}
|
||||
} catch (e) {
|
||||
this.error = e.message;
|
||||
} finally {
|
||||
project._syncing = false;
|
||||
}
|
||||
},
|
||||
async syncAllProjects() {
|
||||
this.syncingAll = true;
|
||||
this.error = '';
|
||||
this.message = '';
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
// 只同步重要项目(Git 监控项目 + 标记为重要的项目)
|
||||
const importantProjects = this.projects.filter(p => p.git_monitor_enabled || p.is_important);
|
||||
|
||||
if (importantProjects.length === 0) {
|
||||
this.message = '没有需要同步的重要项目,请先标记重要项目或启用 Git 监控';
|
||||
this.syncingAll = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
for (const project of importantProjects) {
|
||||
if (!project.path_valid) continue;
|
||||
project._syncing = true;
|
||||
try {
|
||||
const res = await fetch(`/api/admin/projects/${project.slug}/sync`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
Object.assign(project, data.data.project);
|
||||
successCount++;
|
||||
} else {
|
||||
failCount++;
|
||||
}
|
||||
} catch (e) {
|
||||
failCount++;
|
||||
} finally {
|
||||
project._syncing = false;
|
||||
}
|
||||
}
|
||||
this.message = `同步完成:成功 ${successCount} 个${failCount > 0 ? `,失败 ${failCount} 个` : ''}`;
|
||||
} finally {
|
||||
this.syncingAll = false;
|
||||
}
|
||||
},
|
||||
async toggleField(project, field) {
|
||||
this.error = '';
|
||||
try {
|
||||
const newValue = !project[field];
|
||||
const res = await fetch(`/api/admin/projects/${project.slug}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ [field]: newValue })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
project[field] = newValue;
|
||||
} else {
|
||||
this.error = data.message || '操作失败';
|
||||
}
|
||||
} catch (e) {
|
||||
this.error = e.message;
|
||||
}
|
||||
},
|
||||
async discoverProjects() {
|
||||
this.discovering = true;
|
||||
this.error = '';
|
||||
try {
|
||||
const res = await fetch('/api/admin/projects/discover');
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
this.discoveredProjects = data.data.discovered || [];
|
||||
this.selectedDiscovered = [];
|
||||
this.showDiscoverModal = true;
|
||||
} else {
|
||||
this.error = data.message || '发现失败';
|
||||
}
|
||||
} catch (e) {
|
||||
this.error = e.message;
|
||||
} finally {
|
||||
this.discovering = false;
|
||||
}
|
||||
},
|
||||
async addDiscoveredProjects() {
|
||||
if (this.selectedDiscovered.length === 0) return;
|
||||
this.addingDiscovered = true;
|
||||
this.error = '';
|
||||
try {
|
||||
const res = await fetch('/api/admin/projects/add-discovered', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ slugs: this.selectedDiscovered })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
this.message = `已添加 ${data.data.added?.length || 0} 个项目`;
|
||||
this.showDiscoverModal = false;
|
||||
await this.loadProjects();
|
||||
} else {
|
||||
this.error = data.message || '添加失败';
|
||||
}
|
||||
} catch (e) {
|
||||
this.error = e.message;
|
||||
} finally {
|
||||
this.addingDiscovered = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user