功能开发完成

This commit is contained in:
2025-12-31 16:23:40 +08:00
parent 2b51050ca8
commit 6f0598a859
28 changed files with 5463 additions and 118 deletions

440
web/index.html Normal file
View 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>