release: v0.4.1 - GitLab支持、迁移优化、Bug修复
Some checks failed
Release / build-windows (push) Has been cancelled

 新功能:
- GitLab 平台完整支持(16+7 个 API 方法、前端图标/选择器)
- Release 同步支持手动输入 Token

🐛 Bug 修复:
- 自建平台凭证匹配(BaseURL 优先精确匹配)
- Gitea 迁移 API service 字段类型修复(HTTP 422)
- 构建时不再覆盖已有 config.yaml(数据丢失问题)
- 错误信息显示实际 URL

🔨 改进:
- 源平台开启代理时自动跳过 Gitea 原生迁移,改用本地代理中转
- Push 超时从 30s 提升到 10 分钟
- 设置 http.postBuffer=500MB,push --all 失败自动逐分支推送
- 新增 RunWithProxyTimeout 方法
This commit is contained in:
zyj
2026-03-12 15:07:41 +08:00
parent c787c99622
commit 3ebcd10a23
16 changed files with 5082 additions and 64 deletions

View File

@@ -85,6 +85,17 @@ const newTagName = ref('')
const newTagMessage = ref('')
const createTagLoading = ref(false)
// ---- Release 同步 ----
const releases = ref([])
const releasesLoading = ref(false)
const selectedRelease = ref(null)
const releaseSyncSource = ref('')
const releaseSyncTarget = ref('')
const releaseSyncLoading = ref(false)
const releaseSyncResults = ref([])
const releaseSrcToken = ref('')
const releaseTgtToken = ref('')
// ---- 分支管理 ----
const newBranchName = ref('')
const createBranchLoading = ref(false)
@@ -1256,8 +1267,57 @@ watch(activeTab, (tab) => {
if (tab === 'tags' && !tags.value.length && !tagsLoading.value) {
loadTags()
}
if (tab === 'releases' && !releases.value.length && releaseSyncSource.value) {
loadReleases()
}
})
// ---- Release 同步 ----
async function loadReleases() {
if (!props.project?.path || !releaseSyncSource.value) return
releasesLoading.value = true
selectedRelease.value = null
try {
const list = await AppService.GetRemoteReleases(props.project.path, releaseSyncSource.value, releaseSrcToken.value)
releases.value = Array.isArray(list) ? list : []
} catch (e) {
console.error('获取 Release 失败:', e)
releases.value = []
message.error('获取 Release 失败: ' + String(e))
} finally {
releasesLoading.value = false
}
}
async function doSyncReleases() {
if (!props.project?.path || !releaseSyncSource.value || !releaseSyncTarget.value) return
releaseSyncLoading.value = true
releaseSyncResults.value = []
try {
const results = await AppService.SyncReleases(props.project.path, releaseSyncSource.value, releaseSyncTarget.value, releaseSrcToken.value, releaseTgtToken.value)
releaseSyncResults.value = Array.isArray(results) ? results : []
const successCount = releaseSyncResults.value.filter(r => r.success).length
message.info(`Release 同步完成:${successCount}/${releaseSyncResults.value.length}`)
} catch (e) {
console.error('Release 同步失败:', e)
Modal.error({ title: 'Release 同步失败', content: String(e) })
} finally {
releaseSyncLoading.value = false
}
}
function formatFileSize(bytes) {
if (!bytes || bytes <= 0) return ''
const units = ['B', 'KB', 'MB', 'GB']
let i = 0
let size = bytes
while (size >= 1024 && i < units.length - 1) {
size /= 1024
i++
}
return size.toFixed(i === 0 ? 0 : 1) + ' ' + units[i]
}
// ---- Remote 远程仓库管理 ----
async function switchRemote(remoteName) {
currentRemote.value = remoteName
@@ -1549,6 +1609,9 @@ const currentRemoteUrl = computed(() => {
<div class="file-list-tab" :class="{ active: activeTab === 'tags' }" @click="activeTab = 'tags'">
<TagsOutlined /> 标签
</div>
<div class="file-list-tab" :class="{ active: activeTab === 'releases' }" @click="activeTab = 'releases'">
<DownloadOutlined /> 发布
</div>
</div>
<!-- ===== 变更面板 ===== -->
@@ -1962,6 +2025,94 @@ const currentRemoteUrl = computed(() => {
</div>
</div>
</template>
<!-- ===== 发布同步面板 ===== -->
<template v-else-if="activeTab === 'releases'">
<div class="file-list-content tags-panel">
<!-- /目标 remote 选择 -->
<div style="padding: 8px 10px; border-bottom: 1px solid var(--border-color);">
<div style="display: flex; gap: 6px; align-items: center; margin-bottom: 6px;">
<span style="font-size: 11px; color: var(--text-muted); width: 30px; flex-shrink: 0;"></span>
<a-select v-model:value="releaseSyncSource" size="small" style="flex: 1;" placeholder="源 remote">
<a-select-option v-for="r in remotes" :key="r.name" :value="r.name">{{ r.name }}</a-select-option>
</a-select>
<a-button size="small" :loading="releasesLoading" @click="loadReleases" :disabled="!releaseSyncSource">
<template #icon><ReloadOutlined /></template>
</a-button>
</div>
<div style="display: flex; gap: 6px; align-items: center; margin-bottom: 6px;">
<span style="font-size: 11px; color: var(--text-muted); width: 30px; flex-shrink: 0;">目标</span>
<a-select v-model:value="releaseSyncTarget" size="small" style="flex: 1;" placeholder="目标 remote">
<a-select-option v-for="r in remotes" :key="r.name" :value="r.name" :disabled="r.name === releaseSyncSource">{{ r.name }}</a-select-option>
</a-select>
<a-button
size="small"
type="primary"
:loading="releaseSyncLoading"
:disabled="!releaseSyncSource || !releaseSyncTarget || releaseSyncSource === releaseSyncTarget"
@click="doSyncReleases"
>
<template #icon><SyncOutlined /></template>
同步
</a-button>
</div>
<div style="display: flex; gap: 6px; align-items: center; margin-bottom: 6px;">
<span style="font-size: 11px; color: var(--text-muted); width: 30px; flex-shrink: 0;">认证</span>
<a-input-password v-model:value="releaseSrcToken" size="small" style="flex: 1;" placeholder="源 Token可选" />
<a-input-password v-model:value="releaseTgtToken" size="small" style="flex: 1;" placeholder="目标 Token可选" />
</div>
</div>
<!-- Release 列表 -->
<div class="tag-list-section">
<template v-if="releasesLoading">
<div v-for="i in 4" :key="i" style="padding: 10px 12px;">
<div style="height: 14px; background: var(--bg-hover, #333); border-radius: 4px; animation: pulse 1.5s infinite;" :style="{ width: (40 + i * 8) + '%' }"></div>
</div>
</template>
<div v-else-if="!releases.length && releaseSyncSource" style="padding: 24px; text-align: center; color: var(--text-muted);">
<DownloadOutlined :style="{ fontSize: '28px', marginBottom: '8px' }" />
<div>暂无 Release</div>
</div>
<div v-else-if="!releaseSyncSource" style="padding: 24px; text-align: center; color: var(--text-muted);">
<DownloadOutlined :style="{ fontSize: '28px', marginBottom: '8px' }" />
<div>选择源 remote 以查看 Release</div>
</div>
<template v-else>
<div
v-for="rel in releases"
:key="rel.tagName"
class="tag-item"
:class="{ active: selectedRelease?.tagName === rel.tagName }"
@click="selectedRelease = rel"
>
<div class="tag-main-row">
<TagOutlined class="tag-icon" />
<span class="tag-name">{{ rel.name || rel.tagName }}</span>
<a-tag v-if="rel.draft" size="small" color="orange">草稿</a-tag>
<a-tag v-if="rel.prerelease" size="small" color="blue">预发布</a-tag>
<a-badge v-if="rel.assets?.length" :count="rel.assets.length" :number-style="{ backgroundColor: 'var(--accent)', fontSize: '10px', height: '16px', lineHeight: '16px', minWidth: '16px', padding: '0 4px' }" />
</div>
<div class="tag-meta">
<span class="tag-hash">{{ rel.tagName }}</span>
<span v-if="rel.publishedAt || rel.createdAt" class="tag-time">{{ formatTime(rel.publishedAt || rel.createdAt) }}</span>
</div>
</div>
</template>
</div>
<!-- 同步结果 -->
<div v-if="releaseSyncResults.length" style="padding: 8px 10px; border-top: 1px solid var(--border-color);">
<div style="font-size: 11px; color: var(--text-muted); margin-bottom: 4px;">同步结果</div>
<div v-for="r in releaseSyncResults" :key="r.tagName" style="display: flex; align-items: center; gap: 4px; padding: 2px 0; font-size: 11px;">
<CheckCircleOutlined v-if="r.success" style="color: #a6e3a1; flex-shrink: 0;" />
<CloseCircleOutlined v-else style="color: #f38ba8; flex-shrink: 0;" />
<span style="font-weight: 500;">{{ r.tagName }}</span>
<span style="color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ r.message }}</span>
</div>
</div>
</div>
</template>
</div>
<!-- 拖拽分隔条 -->
@@ -2050,6 +2201,42 @@ const currentRemoteUrl = computed(() => {
<span style="color: var(--text-muted); font-size: 12px; margin-top: 4px;"> {{ tags.length }} 个标签</span>
</div>
</template>
<!-- Release 详情模式 -->
<template v-else-if="activeTab === 'releases'">
<div v-if="selectedRelease" class="release-detail-panel">
<div class="release-detail-header">
<h3 style="margin: 0; font-size: 16px;">{{ selectedRelease.name || selectedRelease.tagName }}</h3>
<div style="display: flex; gap: 6px; margin-top: 4px;">
<a-tag color="green">{{ selectedRelease.tagName }}</a-tag>
<a-tag v-if="selectedRelease.draft" color="orange">草稿</a-tag>
<a-tag v-if="selectedRelease.prerelease" color="blue">预发布</a-tag>
<span v-if="selectedRelease.publishedAt || selectedRelease.createdAt" style="color: var(--text-muted); font-size: 12px; line-height: 22px;">
{{ formatTime(selectedRelease.publishedAt || selectedRelease.createdAt) }}
</span>
</div>
</div>
<div v-if="selectedRelease.body" class="release-detail-body">
<pre style="white-space: pre-wrap; font-size: 13px; line-height: 1.6; margin: 0; font-family: inherit;">{{ selectedRelease.body }}</pre>
</div>
<div v-if="selectedRelease.assets?.length" class="release-detail-assets">
<div style="font-weight: 500; font-size: 13px; margin-bottom: 8px;">附件 ({{ selectedRelease.assets.length }})</div>
<div
v-for="asset in selectedRelease.assets"
:key="asset.name"
class="release-asset-item"
>
<DownloadOutlined style="color: var(--accent); flex-shrink: 0;" />
<span style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ asset.name }}</span>
<span style="color: var(--text-muted); font-size: 11px; flex-shrink: 0;">{{ formatFileSize(asset.size) }}</span>
</div>
</div>
</div>
<div v-else class="empty-state small">
<DownloadOutlined :style="{ fontSize: '48px', color: 'var(--text-muted)', marginBottom: '8px' }" />
<span style="color: var(--text-muted)">选择一个 Release 查看详情</span>
<span style="color: var(--text-muted); font-size: 12px; margin-top: 4px;"> {{ releases.length }} Release</span>
</div>
</template>
</div>
</div>
</template>
@@ -3059,4 +3246,43 @@ const currentRemoteUrl = computed(() => {
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
/* Release 详情面板 */
.release-detail-panel {
padding: 16px;
overflow-y: auto;
height: 100%;
}
.release-detail-header {
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-color);
}
.release-detail-body {
margin-bottom: 16px;
padding: 12px;
background: var(--bg-secondary, #181825);
border-radius: 6px;
border: 1px solid var(--border-color);
}
.release-detail-assets {
padding-top: 12px;
border-top: 1px solid var(--border-color);
}
.release-asset-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: 4px;
font-size: 13px;
}
.release-asset-item:hover {
background: var(--bg-hover, #313244);
}
</style>