Files
toolbox/resources/js/components/admin/ProjectManagement.vue

654 lines
31 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="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>