feat: add svn preset management and optimize docker builds
This commit is contained in:
@@ -14,7 +14,11 @@
|
||||
</router-link>
|
||||
<router-link class="sidebar-link" to="/svn-fetch" active-class="active" aria-label="SVN 日志抓取">
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
<span>SVN 日志抓取</span>
|
||||
<span>日志抓取</span>
|
||||
</router-link>
|
||||
<router-link class="sidebar-link" to="/presets" active-class="active" aria-label="仓库管理">
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
||||
<span>仓库管理</span>
|
||||
</router-link>
|
||||
<router-link class="sidebar-link" to="/history" active-class="active" aria-label="任务历史">
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
|
||||
@@ -5,13 +5,15 @@ import DashboardView from './views/DashboardView.vue'
|
||||
import SvnFetchView from './views/SvnFetchView.vue'
|
||||
import HistoryView from './views/HistoryView.vue'
|
||||
import SettingsView from './views/SettingsView.vue'
|
||||
import SvnPresetsView from './views/SvnPresetsView.vue'
|
||||
import './styles/main.css'
|
||||
|
||||
const routes = [
|
||||
{ path: '/', redirect: '/dashboard' },
|
||||
{ path: '/dashboard', name: 'dashboard', component: DashboardView, meta: { title: '工作台', desc: '查看系统状态与最近产物' } },
|
||||
{ path: '/svn-fetch', name: 'svn-fetch', component: SvnFetchView, meta: { title: 'SVN 日志抓取', desc: '一键抓取 SVN 日志并导出工作量 Excel' } },
|
||||
{ path: '/svn-fetch', name: 'svn-fetch', component: SvnFetchView, meta: { title: '日志抓取', desc: '一键抓取 SVN 日志并导出工作量 Excel' } },
|
||||
{ path: '/history', name: 'history', component: HistoryView, meta: { title: '任务历史', desc: '查看任务执行状态、日志与产物' } },
|
||||
{ path: '/presets', name: 'presets', component: SvnPresetsView, meta: { title: '仓库管理', desc: '管理 SVN 仓库预设' } },
|
||||
{ path: '/settings', name: 'settings', component: SettingsView, meta: { title: '系统设置', desc: '配置 API Key 与输出目录' } },
|
||||
]
|
||||
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
<template>
|
||||
<div class="svn-fetch">
|
||||
<div class="card">
|
||||
<!-- 空状态:无预设时引导去仓库管理新增 -->
|
||||
<div v-if="!loading && !presets.length" class="card" style="text-align:center;padding:var(--space-2xl) var(--space-lg);">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="color:var(--c-text-muted);margin-bottom:var(--space-md);" aria-hidden="true"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
||||
<h3 style="font-size:16px;font-weight:600;margin-bottom:8px;color:var(--c-text);">尚未配置 SVN 仓库</h3>
|
||||
<p style="font-size:13px;color:var(--c-text-muted);margin-bottom:var(--space-lg);">
|
||||
请先在<a href="#/presets" style="color:var(--c-primary);font-weight:500;">仓库管理</a>页面新增仓库后再来抓取日志。
|
||||
</p>
|
||||
<router-link to="/presets" class="btn btn-primary">前往仓库管理</router-link>
|
||||
</div>
|
||||
|
||||
<div v-else class="card">
|
||||
<div class="card-title">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
SVN 批量抓取参数
|
||||
</div>
|
||||
<div class="alert alert-info" style="margin-bottom:var(--space-md);">
|
||||
默认已填充 3 个常用项目路径,可选择月份自动填充版本号,或手动填写。
|
||||
支持多个项目批量抓取,可先选月份自动填充版本号,或手动填写。
|
||||
</div>
|
||||
|
||||
<div style="padding:var(--space-md);background:rgba(15,23,42,0.4);border-radius:var(--radius-md);margin-bottom:var(--space-lg);border:1px solid var(--c-border-light);">
|
||||
@@ -114,6 +124,7 @@ const { toast } = useToast()
|
||||
|
||||
const presets = ref([])
|
||||
const defaultPresetId = ref('')
|
||||
const loading = ref(true)
|
||||
const testing = ref(false)
|
||||
const running = ref(false)
|
||||
const autoFillLoading = ref(false)
|
||||
@@ -166,7 +177,8 @@ onMounted(async () => {
|
||||
startRevision: '',
|
||||
endRevision: '',
|
||||
}))
|
||||
} catch (err) { toast(err.message, true) }
|
||||
loading.value = false
|
||||
} catch (err) { loading.value = false; toast(err.message, true) }
|
||||
|
||||
const now = new Date()
|
||||
const y = now.getFullYear()
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
<template>
|
||||
<div class="presets-manage">
|
||||
<!-- 空状态:无预设时显示引导 -->
|
||||
<div v-if="!loading && !items.length" class="card" style="text-align:center;padding:var(--space-2xl) var(--space-lg);">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="color:var(--c-text-muted);margin-bottom:var(--space-md);" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
<h3 style="font-size:16px;font-weight:600;margin-bottom:8px;color:var(--c-text);">暂无 SVN 仓库</h3>
|
||||
<p style="font-size:13px;color:var(--c-text-muted);margin-bottom:var(--space-lg);max-width:400px;margin-left:auto;margin-right:auto;">
|
||||
请新增一个仓库,填写 SVN 地址和可选的账号密码以开始使用。
|
||||
</p>
|
||||
<button type="button" class="btn btn-primary" @click="showAddForm = true">新增仓库</button>
|
||||
</div>
|
||||
|
||||
<!-- 顶部操作栏:有预设时显示 -->
|
||||
<div v-if="items.length" class="toolbar" style="justify-content:space-between;">
|
||||
<span style="font-size:13px;color:var(--c-text-secondary);">共 {{ items.length }} 个仓库</span>
|
||||
<button type="button" class="btn btn-primary btn-sm" @click="showAddForm = true">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
新增仓库
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 新增表单弹窗 -->
|
||||
<div v-if="showAddForm" class="modal-overlay" @click.self="cancelAdd">
|
||||
<div class="modal" role="dialog" aria-label="新增 SVN 仓库">
|
||||
<h3 style="font-size:15px;font-weight:600;margin-bottom:var(--space-md);">新增 SVN 仓库</h3>
|
||||
<form @submit.prevent="onAdd">
|
||||
<div class="form-grid">
|
||||
<div class="form-group span-all">
|
||||
<label for="add-name">名称</label>
|
||||
<input id="add-name" class="form-input" v-model="addForm.name" placeholder="例如 PRS-7050场站智慧管控" required autocomplete="off" />
|
||||
</div>
|
||||
<div class="form-group span-all">
|
||||
<label for="add-url">SVN 地址</label>
|
||||
<input id="add-url" class="form-input" v-model="addForm.url" placeholder="https://svn.example.com/svn/project" required autocomplete="off" spellcheck="false" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="add-username">SVN 用户名(可选)</label>
|
||||
<input id="add-username" class="form-input" v-model="addForm.svnUsername" placeholder="预设专用" autocomplete="off" spellcheck="false" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="add-password">SVN 密码(可选)</label>
|
||||
<input id="add-password" class="form-input" type="password" v-model="addForm.svnPassword" placeholder="预设专用" autocomplete="new-password" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group" style="margin-top:var(--space-lg);">
|
||||
<button type="submit" class="btn btn-primary" :disabled="adding">
|
||||
<span v-if="adding" class="spinner"></span>
|
||||
创建
|
||||
</button>
|
||||
<button type="button" class="btn" @click="cancelAdd">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<div v-if="editTarget" class="modal-overlay" @click.self="editTarget = null">
|
||||
<div class="modal" role="dialog" aria-label="编辑 SVN 仓库">
|
||||
<h3 style="font-size:15px;font-weight:600;margin-bottom:var(--space-md);">编辑仓库</h3>
|
||||
<form @submit.prevent="onEdit">
|
||||
<div class="form-grid">
|
||||
<div class="form-group span-all">
|
||||
<label for="edit-name">名称</label>
|
||||
<input id="edit-name" class="form-input" v-model="editForm.name" required autocomplete="off" />
|
||||
</div>
|
||||
<div class="form-group span-all">
|
||||
<label for="edit-url">SVN 地址</label>
|
||||
<input id="edit-url" class="form-input" v-model="editForm.url" required autocomplete="off" spellcheck="false" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-username">SVN 用户名</label>
|
||||
<input id="edit-username" class="form-input" v-model="editForm.svnUsername" placeholder="留空不覆盖" autocomplete="off" spellcheck="false" />
|
||||
<span v-if="editTarget.svnCredentialsConfigured" style="font-size:11px;color:var(--c-warning);">已有凭据,留空则保留原值</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-password">SVN 密码</label>
|
||||
<input id="edit-password" class="form-input" type="password" v-model="editForm.svnPassword" placeholder="留空不覆盖" autocomplete="new-password" />
|
||||
</div>
|
||||
<div class="form-group span-all">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="editForm.clearSvnPassword" />
|
||||
清空已保存的 SVN 密码
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group span-all">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="editForm.enabled" />
|
||||
启用
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group" style="margin-top:var(--space-lg);">
|
||||
<button type="submit" class="btn btn-primary" :disabled="saving">
|
||||
<span v-if="saving" class="spinner"></span>
|
||||
保存
|
||||
</button>
|
||||
<button type="button" class="btn" @click="editTarget = null">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 删除确认弹窗 -->
|
||||
<div v-if="deleteTarget" class="modal-overlay" @click.self="deleteTarget = null">
|
||||
<div class="modal" style="max-width:400px;" role="dialog" aria-label="确认删除">
|
||||
<h3 style="font-size:15px;font-weight:600;margin-bottom:var(--space-md);">确认删除</h3>
|
||||
<p style="font-size:13px;color:var(--c-text-secondary);margin-bottom:var(--space-lg);">
|
||||
确定要将仓库 <strong style="color:var(--c-text);">{{ deleteTarget.name }}</strong> 停用吗?可随时在编辑中重新启用。
|
||||
</p>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-danger" :disabled="deleting" @click="onDelete">
|
||||
<span v-if="deleting" class="spinner"></span>
|
||||
停用仓库
|
||||
</button>
|
||||
<button type="button" class="btn" @click="deleteTarget = null">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格列表 -->
|
||||
<div v-if="items.length" class="table-wrap" style="margin-top:var(--space-md);">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>SVN 地址</th>
|
||||
<th>SVN 账号</th>
|
||||
<th>凭据</th>
|
||||
<th>最后使用</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in items" :key="item.id" :style="{ opacity: item.enabled ? 1 : 0.5 }">
|
||||
<td><strong>{{ item.name }}</strong></td>
|
||||
<td style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--font-mono);font-size:12px;" :title="item.url">{{ item.url }}</td>
|
||||
<td style="font-family:var(--font-mono);font-size:12px;">{{ item.svnUsername || '—' }}</td>
|
||||
<td>
|
||||
<span :class="['tag', item.svnCredentialsConfigured ? 'tag-success' : 'tag-muted']">{{ item.svnCredentialsConfigured ? '已配置' : '未配置' }}</span>
|
||||
</td>
|
||||
<td style="font-size:12px;color:var(--c-text-muted);font-family:var(--font-mono);">{{ formatTime(item.lastUsedAt) }}</td>
|
||||
<td>
|
||||
<span :class="['tag', item.enabled ? 'tag-success' : 'tag-muted']">{{ item.enabled ? '启用' : '停用' }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group" style="gap:4px;">
|
||||
<button type="button" class="btn btn-sm" @click="startEdit(item)" title="编辑">编辑</button>
|
||||
<button type="button" class="btn btn-sm" :disabled="testingId === item.id" @click="onTest(item)" title="测试连接">
|
||||
<span v-if="testingId === item.id" class="spinner"></span>
|
||||
测试
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-danger" @click="deleteTarget = item" title="停用">停用</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 加载态 -->
|
||||
<div v-if="loading" class="card" style="text-align:center;padding:var(--space-2xl);">
|
||||
<span class="spinner"></span>
|
||||
<p style="margin-top:var(--space-sm);font-size:13px;color:var(--c-text-muted);">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useApi, useToast } from '../composables/useApi'
|
||||
|
||||
const { apiFetch } = useApi()
|
||||
const { toast } = useToast()
|
||||
|
||||
const items = ref([])
|
||||
const loading = ref(true)
|
||||
const adding = ref(false)
|
||||
const saving = ref(false)
|
||||
const deleting = ref(false)
|
||||
const testingId = ref(null)
|
||||
|
||||
const showAddForm = ref(false)
|
||||
const addForm = ref({ name: '', url: '', svnUsername: '', svnPassword: '' })
|
||||
|
||||
const editTarget = ref(null)
|
||||
const editForm = ref({ name: '', url: '', svnUsername: '', svnPassword: '', clearSvnPassword: false, enabled: true })
|
||||
|
||||
const deleteTarget = ref(null)
|
||||
|
||||
onMounted(async () => {
|
||||
await loadItems()
|
||||
})
|
||||
|
||||
async function loadItems() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await apiFetch('/api/svn/presets/manage')
|
||||
items.value = data || []
|
||||
} catch (err) {
|
||||
toast(err.message, true)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function cancelAdd() {
|
||||
showAddForm.value = false
|
||||
addForm.value = { name: '', url: '', svnUsername: '', svnPassword: '' }
|
||||
}
|
||||
|
||||
async function onAdd() {
|
||||
if (!addForm.value.name.trim() || !addForm.value.url.trim()) {
|
||||
toast('名称和地址为必填项', true)
|
||||
return
|
||||
}
|
||||
adding.value = true
|
||||
try {
|
||||
await apiFetch('/api/svn/presets', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(addForm.value),
|
||||
})
|
||||
toast('仓库创建成功')
|
||||
showAddForm.value = false
|
||||
addForm.value = { name: '', url: '', svnUsername: '', svnPassword: '' }
|
||||
await loadItems()
|
||||
} catch (err) {
|
||||
toast(err.message, true)
|
||||
} finally {
|
||||
adding.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function startEdit(item) {
|
||||
editTarget.value = item
|
||||
editForm.value = {
|
||||
name: item.name,
|
||||
url: item.url,
|
||||
svnUsername: '',
|
||||
svnPassword: '',
|
||||
clearSvnPassword: false,
|
||||
enabled: item.enabled,
|
||||
}
|
||||
}
|
||||
|
||||
async function onEdit() {
|
||||
if (!editTarget.value) return
|
||||
saving.value = true
|
||||
try {
|
||||
await apiFetch(`/api/svn/presets/${encodeURIComponent(editTarget.value.id)}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(editForm.value),
|
||||
})
|
||||
toast('保存成功')
|
||||
editTarget.value = null
|
||||
await loadItems()
|
||||
} catch (err) {
|
||||
toast(err.message, true)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onTest(item) {
|
||||
testingId.value = item.id
|
||||
try {
|
||||
await apiFetch(`/api/svn/presets/${encodeURIComponent(item.id)}/test`, { method: 'POST' })
|
||||
toast(`连接成功: ${item.name}`)
|
||||
await loadItems()
|
||||
} catch (err) {
|
||||
toast(err.message, true)
|
||||
} finally {
|
||||
testingId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function onDelete() {
|
||||
if (!deleteTarget.value) return
|
||||
deleting.value = true
|
||||
try {
|
||||
await apiFetch(`/api/svn/presets/${encodeURIComponent(deleteTarget.value.id)}`, { method: 'DELETE' })
|
||||
toast(`已停用: ${deleteTarget.value.name}`)
|
||||
deleteTarget.value = null
|
||||
await loadItems()
|
||||
} catch (err) {
|
||||
toast(err.message, true)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(ts) {
|
||||
if (!ts) return '从未使用'
|
||||
const d = new Date(ts)
|
||||
const pad = n => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 10000;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-md);
|
||||
}
|
||||
.modal {
|
||||
background: var(--c-surface);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-lg);
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
box-shadow: var(--shadow-elevated);
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.checkbox-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--c-text-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
.checkbox-label input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: var(--c-primary);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user