功能开发完成
This commit is contained in:
440
web/index.html
Normal file
440
web/index.html
Normal file
@@ -0,0 +1,440 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>GitCodeStatic - Git 仓库统计与缓存系统</title>
|
||||
<link rel="stylesheet" href="/static/lib/element-plus.css">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,.12);
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
.header p {
|
||||
margin: 5px 0 0 0;
|
||||
opacity: 0.9;
|
||||
font-size: 14px;
|
||||
}
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 20px auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
.page-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.stats-card {
|
||||
text-align: center;
|
||||
}
|
||||
.stats-value {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #409EFF;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.code-block {
|
||||
background: #f5f7fa;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.task-status {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.task-status.pending { background: #e6a23c; color: white; }
|
||||
.task-status.running { background: #409eff; color: white; }
|
||||
.task-status.completed { background: #67c23a; color: white; }
|
||||
.task-status.failed { background: #f56c6c; color: white; }
|
||||
|
||||
.cache-card {
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
.cache-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
border-color: #409eff;
|
||||
}
|
||||
.cache-card .el-card__body {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="header">
|
||||
<h1>🚀 GitCodeStatic</h1>
|
||||
<p>Git 仓库统计与缓存系统 - 高性能代码仓库数据分析平台</p>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<el-tabs v-model="activeTab" type="border-card">
|
||||
<!-- 仓库管理 -->
|
||||
<el-tab-pane label="仓库管理" name="repos">
|
||||
<el-card class="page-card">
|
||||
<template #header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>仓库列表</span>
|
||||
<div>
|
||||
<el-button @click="loadRepos" size="small">刷新</el-button>
|
||||
<el-button type="primary" @click="showAddRepoDialog" size="small">批量添加</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="repos" border stripe v-loading="reposLoading">
|
||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||
<el-table-column prop="url" label="仓库URL" min-width="300"></el-table-column>
|
||||
<el-table-column prop="current_branch" label="当前分支" width="120"></el-table-column>
|
||||
<el-table-column label="认证" width="80">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.has_credentials" type="success" size="small">已配置</el-tag>
|
||||
<el-tag v-else type="info" size="small">无</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getRepoStatusType(scope.row.status)">
|
||||
{{ scope.row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="local_path" label="本地路径" min-width="200"></el-table-column>
|
||||
<el-table-column label="操作" width="320">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="switchBranch(scope.row)">切换分支</el-button>
|
||||
<el-button size="small" type="warning" @click="updateRepo(scope.row.id)">更新</el-button>
|
||||
<el-button size="small" type="info" @click="resetRepo(scope.row.id)">重置</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteRepo(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 统计管理 -->
|
||||
<el-tab-pane label="统计管理" name="stats">
|
||||
<!-- 统计计算 -->
|
||||
<el-row :gutter="20" style="margin-bottom: 20px;">
|
||||
<el-col :span="8">
|
||||
<el-card>
|
||||
<template #header>新建统计任务</template>
|
||||
<el-form :model="statsForm" size="small">
|
||||
<el-form-item label="仓库">
|
||||
<el-select v-model="statsForm.repo_id" placeholder="选择仓库" style="width: 100%" @change="onRepoChange">
|
||||
<el-option
|
||||
v-for="repo in repos"
|
||||
:key="repo.id"
|
||||
:label="getRepoDisplayName(repo)"
|
||||
:value="repo.id">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="分支">
|
||||
<el-select
|
||||
v-model="statsForm.branch"
|
||||
placeholder="选择分支"
|
||||
style="width: 100%"
|
||||
filterable
|
||||
allow-create
|
||||
:loading="statsFormBranchesLoading">
|
||||
<el-option
|
||||
v-for="branch in statsFormBranches"
|
||||
:key="branch"
|
||||
:label="branch"
|
||||
:value="branch">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="约束">
|
||||
<el-radio-group v-model="statsForm.constraint_type" size="small">
|
||||
<el-radio label="commit_limit">提交数</el-radio>
|
||||
<el-radio label="date_range">日期</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="statsForm.constraint_type === 'date_range'" label="日期范围">
|
||||
<el-date-picker
|
||||
v-model="statsDateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="statsForm.constraint_type === 'commit_limit'" label="提交数">
|
||||
<el-input-number v-model="statsForm.limit" :min="1" :max="10000" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="calculateStats" :disabled="!statsForm.repo_id || !statsForm.branch" block>开始计算</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>统计结果列表</span>
|
||||
<el-button @click="loadCaches" size="small">刷新</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-loading="cachesLoading" style="max-height: 400px; overflow-y: auto;">
|
||||
<el-empty v-if="!caches || caches.length === 0" description="暂无统计数据" :image-size="100"></el-empty>
|
||||
<div v-else>
|
||||
<div v-for="cache in caches" :key="cache.id"
|
||||
style="border: 1px solid #ebeef5; border-radius: 4px; margin-bottom: 12px; padding: 12px; cursor: pointer; transition: all 0.3s;"
|
||||
@click="viewStatsCache(cache)"
|
||||
@mouseenter="$event.target.style.borderColor='#409eff'; $event.target.style.boxShadow='0 2px 12px rgba(64,158,255,0.15)'"
|
||||
@mouseleave="$event.target.style.borderColor='#ebeef5'; $event.target.style.boxShadow='none'">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
||||
<div style="font-weight: bold; color: #303133;">
|
||||
{{ getRepoName(cache.repo_id) }} / {{ cache.branch }}
|
||||
</div>
|
||||
<div>
|
||||
<el-tag size="small" :type="cache.constraint_type === 'date_range' ? 'success' : 'primary'">
|
||||
{{ cache.constraint_type === 'date_range' ? '日期范围' : '提交数限制' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div style="color: #606266; font-size: 13px; margin-bottom: 6px;">
|
||||
{{ getConstraintText(cache) }}
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; font-size: 12px; color: #909399;">
|
||||
<span>{{ formatDate(cache.created_at) }}</span>
|
||||
<span>{{ formatFileSize(cache.result_size) }} | {{ cache.hit_count }}次命中</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 统计详情显示 -->
|
||||
<div v-if="selectedStatsResult" v-loading="statsLoading">
|
||||
<el-row :gutter="16" style="margin-bottom: 16px;">
|
||||
<el-col :span="6">
|
||||
<el-card class="stats-card">
|
||||
<div>总提交数</div>
|
||||
<div class="stats-value">{{ selectedStatsResult.summary.total_commits }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stats-card">
|
||||
<div>贡献者数</div>
|
||||
<div class="stats-value">{{ selectedStatsResult.summary.total_contributors }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stats-card">
|
||||
<div>增加行数</div>
|
||||
<div class="stats-value" style="color: #67c23a;">+{{ selectedStatsResult.summary.total_additions }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stats-card">
|
||||
<div>删除行数</div>
|
||||
<div class="stats-value" style="color: #f56c6c;">-{{ selectedStatsResult.summary.total_deletions }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card>
|
||||
<template #header>贡献者详情</template>
|
||||
<el-table :data="selectedStatsResult.contributors" border stripe max-height="400">
|
||||
<el-table-column prop="name" label="姓名" width="150"></el-table-column>
|
||||
<el-table-column prop="email" label="邮箱" width="200"></el-table-column>
|
||||
<el-table-column prop="commit_count" label="提交数" width="80" sortable></el-table-column>
|
||||
<el-table-column prop="additions" label="增加" width="80" sortable>
|
||||
<template #default="scope">
|
||||
<span style="color: #67c23a;">+{{ scope.row.additions }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="deletions" label="删除" width="80" sortable>
|
||||
<template #default="scope">
|
||||
<span style="color: #f56c6c;">-{{ scope.row.deletions }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="first_commit_date" label="首次提交" width="160"></el-table-column>
|
||||
<el-table-column prop="last_commit_date" label="最后提交" width="160"></el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 任务监控 -->
|
||||
<el-tab-pane label="任务监控" name="tasks">
|
||||
<el-card class="page-card">
|
||||
<template #header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>任务列表</span>
|
||||
<div>
|
||||
<el-button @click="loadTasks" size="small">刷新</el-button>
|
||||
<el-button @click="clearCompletedTasks" size="small" type="warning">清理已完成</el-button>
|
||||
<el-button @click="clearAllTasks" size="small" type="danger">清空所有</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="tasks" border stripe v-loading="tasksLoading">
|
||||
<el-table-column prop="id" label="任务ID" width="80"></el-table-column>
|
||||
<el-table-column prop="task_type" label="任务类型" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag size="small">{{ scope.row.task_type }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="repo_id" label="仓库ID" width="100"></el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getTaskStatusType(scope.row.status)">
|
||||
{{ scope.row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="priority" label="优先级" width="100"></el-table-column>
|
||||
<el-table-column prop="error_message" label="错误信息" min-width="200"></el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="180"></el-table-column>
|
||||
<el-table-column prop="updated_at" label="更新时间" width="180"></el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 缓存管理 -->
|
||||
<el-tab-pane label="缓存管理" name="caches">
|
||||
<el-card class="page-card">
|
||||
<template #header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>统计缓存列表</span>
|
||||
<div>
|
||||
<el-button @click="loadCaches" size="small">刷新</el-button>
|
||||
<el-button @click="clearAllCaches" size="small" type="danger">清空缓存</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="caches" border stripe v-loading="cachesLoading">
|
||||
<el-table-column prop="id" label="缓存ID" width="80"></el-table-column>
|
||||
<el-table-column prop="repo_id" label="仓库ID" width="100"></el-table-column>
|
||||
<el-table-column prop="branch" label="分支" width="120"></el-table-column>
|
||||
<el-table-column prop="constraint_type" label="约束类型" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag size="small" :type="scope.row.constraint_type === 'commit_limit' ? 'primary' : 'success'">
|
||||
{{ scope.row.constraint_type === 'commit_limit' ? '提交限制' : '日期范围' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="result_size" label="文件大小" width="120">
|
||||
<template #default="scope">
|
||||
{{ formatFileSize(scope.row.result_size) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="hit_count" label="命中次数" width="100"></el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="180"></el-table-column>
|
||||
<el-table-column prop="last_hit_at" label="最后命中" width="180">
|
||||
<template #default="scope">
|
||||
{{ scope.row.last_hit_at || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- API 文档 -->
|
||||
<el-tab-pane label="API 文档" name="api">
|
||||
<el-card class="page-card">
|
||||
<template #header>Swagger API 文档</template>
|
||||
<p>访问 <a href="/swagger/index.html" target="_blank" style="color: #409EFF;">/swagger/index.html</a> 查看完整的 API 文档</p>
|
||||
<el-divider></el-divider>
|
||||
<h3>快速开始</h3>
|
||||
<div class="code-block">
|
||||
curl -X POST http://localhost:8080/api/v1/repos/batch \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"repos": [
|
||||
{"url": "https://github.com/user/repo.git", "branch": "main"}
|
||||
]
|
||||
}'
|
||||
</div>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 添加仓库对话框 -->
|
||||
<el-dialog v-model="addRepoVisible" title="批量添加仓库" width="600px">
|
||||
<el-form :model="addRepoForm" label-width="100px">
|
||||
<el-form-item label="仓库URL">
|
||||
<el-input
|
||||
v-model="addRepoForm.urls"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="每行一个仓库URL,格式:https://github.com/user/repo.git">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="默认分支">
|
||||
<el-input v-model="addRepoForm.branch" placeholder="main"></el-input>
|
||||
</el-form-item>
|
||||
<el-divider content-position="left">认证信息(可选)</el-divider>
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="addRepoForm.username" placeholder="如需认证,请输入用户名" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="密码/Token">
|
||||
<el-input v-model="addRepoForm.password" type="password" placeholder="如需认证,请输入密码或Token" show-password clearable></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="addRepoVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="addRepos">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 切换分支对话框 -->
|
||||
<el-dialog v-model="switchBranchVisible" title="切换分支" width="500px">
|
||||
<el-form :model="switchBranchForm" label-width="100px" v-loading="branchesLoading">
|
||||
<el-form-item label="仓库">
|
||||
<el-input :value="switchBranchForm.repoUrl" disabled></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="当前分支">
|
||||
<el-tag>{{ switchBranchForm.currentBranch }}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="选择分支">
|
||||
<el-select v-model="switchBranchForm.branch" placeholder="选择分支" style="width: 100%;" filterable allow-create>
|
||||
<el-option
|
||||
v-for="branch in branches"
|
||||
:key="branch"
|
||||
:label="branch"
|
||||
:value="branch">
|
||||
</el-option>
|
||||
</el-select>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: #909399;">
|
||||
可以从列表选择或手动输入分支名
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="switchBranchVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmSwitchBranch" :disabled="!switchBranchForm.branch">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<script src="/static/lib/vue.global.prod.js"></script>
|
||||
<script src="/static/lib/element-plus.min.js"></script>
|
||||
<script src="/static/lib/axios.min.js"></script>
|
||||
<script src="/static/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
522
web/static/app.js
Normal file
522
web/static/app.js
Normal file
@@ -0,0 +1,522 @@
|
||||
const { createApp } = Vue;
|
||||
const { ElMessage, ElMessageBox } = ElementPlus;
|
||||
|
||||
const API_BASE = '/api/v1';
|
||||
|
||||
createApp({
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'repos',
|
||||
repos: [],
|
||||
reposLoading: false,
|
||||
statsLoading: false,
|
||||
addRepoVisible: false,
|
||||
switchBranchVisible: false,
|
||||
addRepoForm: {
|
||||
urls: '',
|
||||
branch: 'main',
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
switchBranchForm: {
|
||||
branch: '',
|
||||
repoId: null,
|
||||
repoUrl: '',
|
||||
currentBranch: ''
|
||||
},
|
||||
branches: [],
|
||||
branchesLoading: false,
|
||||
statsForm: {
|
||||
repo_id: null,
|
||||
branch: 'main',
|
||||
constraint_type: 'commit_limit',
|
||||
from: '',
|
||||
to: '',
|
||||
limit: 100
|
||||
},
|
||||
statsDateRange: null,
|
||||
statsFormBranches: [],
|
||||
statsFormBranchesLoading: false,
|
||||
selectedStatsResult: null,
|
||||
tasks: [],
|
||||
tasksLoading: false,
|
||||
caches: [],
|
||||
cachesLoading: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.loadRepos();
|
||||
this.loadCaches();
|
||||
},
|
||||
watch: {
|
||||
activeTab(newTab) {
|
||||
if (newTab === 'tasks') {
|
||||
this.loadTasks();
|
||||
} else if (newTab === 'caches') {
|
||||
this.loadCaches();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadRepos() {
|
||||
this.reposLoading = true;
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE}/repos`);
|
||||
if (response.data.code === 0) {
|
||||
this.repos = response.data.data.repositories || [];
|
||||
} else {
|
||||
ElMessage.error(response.data.message || '加载仓库列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('网络请求失败: ' + error.message);
|
||||
} finally {
|
||||
this.reposLoading = false;
|
||||
}
|
||||
},
|
||||
showAddRepoDialog() {
|
||||
this.addRepoForm = {
|
||||
urls: '',
|
||||
branch: 'main',
|
||||
username: '',
|
||||
password: ''
|
||||
};
|
||||
this.addRepoVisible = true;
|
||||
},
|
||||
async onRepoChange() {
|
||||
this.statsForm.branch = 'main';
|
||||
this.statsFormBranches = [];
|
||||
if (this.statsForm.repo_id) {
|
||||
await this.loadStatsFormBranches();
|
||||
}
|
||||
},
|
||||
async loadStatsFormBranches() {
|
||||
if (!this.statsForm.repo_id) return;
|
||||
|
||||
this.statsFormBranchesLoading = true;
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE}/repos/${this.statsForm.repo_id}/branches`);
|
||||
if (response.data.code === 0) {
|
||||
this.statsFormBranches = response.data.data.branches || ['main'];
|
||||
// 如果当前分支不在列表中,添加进去
|
||||
if (this.statsForm.branch && !this.statsFormBranches.includes(this.statsForm.branch)) {
|
||||
this.statsFormBranches.unshift(this.statsForm.branch);
|
||||
}
|
||||
} else {
|
||||
this.statsFormBranches = ['main', 'master', 'develop'];
|
||||
}
|
||||
} catch (error) {
|
||||
this.statsFormBranches = ['main', 'master', 'develop'];
|
||||
} finally {
|
||||
this.statsFormBranchesLoading = false;
|
||||
}
|
||||
},
|
||||
getRepoDisplayName(repo) {
|
||||
const url = repo.url;
|
||||
const parts = url.split('/');
|
||||
return parts[parts.length - 1].replace('.git', '');
|
||||
},
|
||||
async addRepos() {
|
||||
const urls = this.addRepoForm.urls.split('\n')
|
||||
.map(u => u.trim())
|
||||
.filter(u => u);
|
||||
|
||||
if (urls.length === 0) {
|
||||
ElMessage.warning('请输入至少一个仓库URL');
|
||||
return;
|
||||
}
|
||||
|
||||
const repos = urls.map(url => ({
|
||||
url,
|
||||
branch: this.addRepoForm.branch || 'main'
|
||||
}));
|
||||
|
||||
const requestData = { repos };
|
||||
|
||||
// 如果提供了认证信息
|
||||
if (this.addRepoForm.username && this.addRepoForm.password) {
|
||||
requestData.username = this.addRepoForm.username;
|
||||
requestData.password = this.addRepoForm.password;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE}/repos/batch`, requestData);
|
||||
if (response.data.code === 0) {
|
||||
const result = response.data.data;
|
||||
ElMessage.success(`成功添加 ${result.success_count} 个仓库`);
|
||||
if (result.failure_count > 0) {
|
||||
ElMessage.warning(`${result.failure_count} 个仓库添加失败`);
|
||||
}
|
||||
this.addRepoVisible = false;
|
||||
this.loadRepos();
|
||||
} else {
|
||||
ElMessage.error(response.data.message || '添加仓库失败');
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('网络请求失败: ' + error.message);
|
||||
}
|
||||
},
|
||||
async switchBranch(repo) {
|
||||
this.switchBranchForm = {
|
||||
branch: '',
|
||||
repoId: repo.id,
|
||||
repoUrl: repo.url,
|
||||
currentBranch: repo.current_branch || '未知'
|
||||
};
|
||||
this.branches = [];
|
||||
this.switchBranchVisible = true;
|
||||
|
||||
// 获取分支列表
|
||||
await this.loadBranches(repo.id);
|
||||
},
|
||||
async loadBranches(repoId) {
|
||||
this.branchesLoading = true;
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE}/repos/${repoId}/branches`);
|
||||
if (response.data.code === 0) {
|
||||
this.branches = response.data.data.branches || [];
|
||||
} else {
|
||||
ElMessage.warning('获取分支列表失败: ' + (response.data.message || ''));
|
||||
this.branches = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('无法获取分支列表:', error.message);
|
||||
this.branches = [];
|
||||
} finally {
|
||||
this.branchesLoading = false;
|
||||
}
|
||||
},
|
||||
async confirmSwitchBranch() {
|
||||
if (!this.switchBranchForm.branch) {
|
||||
ElMessage.warning('请输入分支名称');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${API_BASE}/repos/${this.switchBranchForm.repoId}/switch-branch`,
|
||||
{ branch: this.switchBranchForm.branch }
|
||||
);
|
||||
if (response.data.code === 0) {
|
||||
ElMessage.success('切换分支任务已提交');
|
||||
this.switchBranchVisible = false;
|
||||
this.loadRepos();
|
||||
} else {
|
||||
ElMessage.error(response.data.message || '切换分支失败');
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('网络请求失败: ' + error.message);
|
||||
}
|
||||
},
|
||||
async updateRepo(repoId) {
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE}/repos/${repoId}/update`);
|
||||
if (response.data.code === 0) {
|
||||
ElMessage.success('更新任务已提交');
|
||||
this.loadRepos();
|
||||
} else {
|
||||
ElMessage.error(response.data.message || '更新仓库失败');
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('网络请求失败: ' + error.message);
|
||||
}
|
||||
},
|
||||
async resetRepo(repoId) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要重置该仓库吗?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
});
|
||||
|
||||
const response = await axios.post(`${API_BASE}/repos/${repoId}/reset`);
|
||||
if (response.data.code === 0) {
|
||||
ElMessage.success('重置任务已提交');
|
||||
this.loadRepos();
|
||||
} else {
|
||||
ElMessage.error(response.data.message || '重置仓库失败');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('网络请求失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
async deleteRepo(repoId) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该仓库吗?此操作不可恢复!', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
});
|
||||
|
||||
const response = await axios.delete(`${API_BASE}/repos/${repoId}`);
|
||||
if (response.data.code === 0) {
|
||||
ElMessage.success('删除成功');
|
||||
this.loadRepos();
|
||||
} else {
|
||||
ElMessage.error(response.data.message || '删除仓库失败');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('网络请求失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
async calculateStats() {
|
||||
if (!this.statsForm.repo_id) {
|
||||
ElMessage.warning('请选择仓库');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.statsForm.branch) {
|
||||
ElMessage.warning('请输入分支名称');
|
||||
return;
|
||||
}
|
||||
|
||||
const constraint = {
|
||||
type: this.statsForm.constraint_type
|
||||
};
|
||||
|
||||
if (this.statsForm.constraint_type === 'date_range') {
|
||||
if (!this.statsDateRange || this.statsDateRange.length !== 2) {
|
||||
ElMessage.warning('请选择日期范围');
|
||||
return;
|
||||
}
|
||||
constraint.from = this.statsDateRange[0];
|
||||
constraint.to = this.statsDateRange[1];
|
||||
} else {
|
||||
constraint.limit = this.statsForm.limit;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE}/stats/calculate`, {
|
||||
repo_id: this.statsForm.repo_id,
|
||||
branch: this.statsForm.branch,
|
||||
constraint
|
||||
});
|
||||
|
||||
if (response.data.code === 0) {
|
||||
ElMessage.success('统计任务已提交,请稍后查看结果');
|
||||
// 3秒后刷新缓存列表
|
||||
setTimeout(() => {
|
||||
this.loadCaches();
|
||||
}, 3000);
|
||||
} else {
|
||||
ElMessage.error(response.data.message || '提交统计任务失败');
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('网络请求失败: ' + error.message);
|
||||
}
|
||||
},
|
||||
async loadTasks() {
|
||||
this.tasksLoading = true;
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE}/tasks`);
|
||||
if (response.data.code === 0) {
|
||||
this.tasks = response.data.data.tasks || [];
|
||||
} else {
|
||||
ElMessage.error(response.data.message || '加载任务列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('网络请求失败: ' + error.message);
|
||||
} finally {
|
||||
this.tasksLoading = false;
|
||||
}
|
||||
},
|
||||
async loadCaches() {
|
||||
this.cachesLoading = true;
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE}/stats/caches`);
|
||||
if (response.data.code === 0) {
|
||||
this.caches = response.data.data.caches || [];
|
||||
} else {
|
||||
ElMessage.error(response.data.message || '加载统计缓存列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('网络请求失败: ' + error.message);
|
||||
} finally {
|
||||
this.cachesLoading = false;
|
||||
}
|
||||
},
|
||||
async clearAllCaches() {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要清空所有统计缓存吗?此操作不可恢复!', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
});
|
||||
|
||||
const response = await axios.delete(`${API_BASE}/stats/caches/clear`);
|
||||
if (response.data.code === 0) {
|
||||
ElMessage.success('所有统计缓存已清除');
|
||||
this.loadCaches();
|
||||
} else {
|
||||
ElMessage.error(response.data.message || '清除缓存失败');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('网络请求失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
async clearAllTasks() {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要清空所有任务记录吗?包括正在执行的任务!', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
});
|
||||
|
||||
const response = await axios.delete(`${API_BASE}/tasks/clear`);
|
||||
if (response.data.code === 0) {
|
||||
ElMessage.success('所有任务记录已清除');
|
||||
this.loadTasks();
|
||||
} else {
|
||||
ElMessage.error(response.data.message || '清除任务失败');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('网络请求失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
async clearCompletedTasks() {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要清除所有已完成的任务记录吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
});
|
||||
|
||||
const response = await axios.delete(`${API_BASE}/tasks/clear-completed`);
|
||||
if (response.data.code === 0) {
|
||||
ElMessage.success('已完成的任务记录已清除');
|
||||
this.loadTasks();
|
||||
} else {
|
||||
ElMessage.error(response.data.message || '清除已完成任务失败');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('网络请求失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
async viewStatsCache(cache) {
|
||||
this.statsLoading = true;
|
||||
try {
|
||||
const params = {
|
||||
repo_id: cache.repo_id,
|
||||
branch: cache.branch,
|
||||
constraint_type: cache.constraint_type
|
||||
};
|
||||
|
||||
// 根据缓存的constraint_value添加参数
|
||||
if (cache.constraint_type === 'date_range' && cache.constraint_value) {
|
||||
try {
|
||||
const constraint = JSON.parse(cache.constraint_value);
|
||||
if (constraint.from) params.from = constraint.from;
|
||||
if (constraint.to) params.to = constraint.to;
|
||||
} catch (e) {
|
||||
console.error('Failed to parse constraint_value:', e);
|
||||
}
|
||||
} else if (cache.constraint_type === 'commit_limit' && cache.constraint_value) {
|
||||
try {
|
||||
const constraint = JSON.parse(cache.constraint_value);
|
||||
if (constraint.limit) params.limit = constraint.limit;
|
||||
} catch (e) {
|
||||
params.limit = 100; // 默认值
|
||||
}
|
||||
}
|
||||
|
||||
const response = await axios.get(`${API_BASE}/stats/result`, { params });
|
||||
|
||||
if (response.data.code === 0) {
|
||||
// 适配后端返回的数据结构
|
||||
const data = response.data.data;
|
||||
const stats = data.statistics;
|
||||
|
||||
this.selectedStatsResult = {
|
||||
summary: {
|
||||
total_commits: stats.summary.total_commits || 0,
|
||||
total_contributors: stats.summary.total_contributors || 0,
|
||||
total_additions: stats.by_contributor.reduce((sum, c) => sum + (c.additions || 0), 0),
|
||||
total_deletions: stats.by_contributor.reduce((sum, c) => sum + (c.deletions || 0), 0)
|
||||
},
|
||||
date_range: stats.summary.date_range || { from: '未指定', to: '未指定' },
|
||||
contributors: stats.by_contributor.map(c => ({
|
||||
name: c.author,
|
||||
email: c.email,
|
||||
commit_count: c.commits,
|
||||
additions: c.additions,
|
||||
deletions: c.deletions,
|
||||
first_commit_date: c.first_commit_date || '-',
|
||||
last_commit_date: c.last_commit_date || '-'
|
||||
}))
|
||||
};
|
||||
ElMessage.success('查看统计结果成功');
|
||||
} else {
|
||||
ElMessage.error(response.data.message || '查询统计结果失败');
|
||||
this.selectedStatsResult = null;
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('网络请求失败: ' + error.message);
|
||||
this.selectedStatsResult = null;
|
||||
} finally {
|
||||
this.statsLoading = false;
|
||||
}
|
||||
},
|
||||
getRepoStatusType(status) {
|
||||
const statusMap = {
|
||||
'pending': 'info',
|
||||
'cloning': 'warning',
|
||||
'ready': 'success',
|
||||
'error': 'danger'
|
||||
};
|
||||
return statusMap[status] || 'info';
|
||||
},
|
||||
getTaskStatusType(status) {
|
||||
const statusMap = {
|
||||
'pending': 'info',
|
||||
'running': 'warning',
|
||||
'completed': 'success',
|
||||
'failed': 'danger'
|
||||
};
|
||||
return statusMap[status] || 'info';
|
||||
},
|
||||
getConstraintText(cache) {
|
||||
if (!cache.constraint_value) return '未指定';
|
||||
try {
|
||||
const constraint = JSON.parse(cache.constraint_value);
|
||||
if (cache.constraint_type === 'date_range') {
|
||||
return `${constraint.from || ''} ~ ${constraint.to || ''}`;
|
||||
} else if (cache.constraint_type === 'commit_limit') {
|
||||
return `最近 ${constraint.limit || 100} 次提交`;
|
||||
}
|
||||
} catch (e) {
|
||||
return '解析失败';
|
||||
}
|
||||
return '未知';
|
||||
},
|
||||
getRepoName(repoId) {
|
||||
const repo = this.repos.find(r => r.id === repoId);
|
||||
if (repo) {
|
||||
const url = repo.url;
|
||||
const parts = url.split('/');
|
||||
return parts[parts.length - 1].replace('.git', '');
|
||||
}
|
||||
return `仓库 #${repoId}`;
|
||||
},
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString('zh-CN');
|
||||
},
|
||||
formatFileSize(bytes) {
|
||||
if (!bytes) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||||
}
|
||||
}
|
||||
}).use(ElementPlus).mount('#app');
|
||||
2
web/static/lib/axios.min.js
vendored
Normal file
2
web/static/lib/axios.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/static/lib/element-plus.css
Normal file
1
web/static/lib/element-plus.css
Normal file
File diff suppressed because one or more lines are too long
78
web/static/lib/element-plus.min.js
vendored
Normal file
78
web/static/lib/element-plus.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
11
web/static/lib/vue.global.prod.js
Normal file
11
web/static/lib/vue.global.prod.js
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user