feat: 多 remote 远程仓库支持 (v0.3.1)\n\n后端:\n- 新增 RemoteList/AddRemote/RemoveRemote 远程仓库管理\n- 所有远程操作(Push/Pull/Fetch/PushTag/DeleteTag/DeleteRemoteBranch等)支持指定 remote\n- 新增 PushTo/PullFrom/FetchRemote/PushTagTo/DeleteRemoteTagFrom 等方法\n- BatchPull/BatchPush 支持 remote 参数\n\n前端:\n- 顶部信息栏新增 remote 选择器下拉菜单\n- 支持切换当前操作的远程仓库\n- 支持添加/删除远程仓库\n- Pull/Push/Fetch/标签推送/远程分支删除等操作均使用选中的 remote
Some checks failed
Release / build-windows (push) Has been cancelled

This commit is contained in:
zyj
2026-03-12 10:57:27 +08:00
parent 3d6874f79e
commit 3ec453cc38
10 changed files with 536 additions and 78 deletions

View File

@@ -90,6 +90,15 @@ const newBranchName = ref('')
const createBranchLoading = ref(false)
const remoteBranches = ref([])
// ---- Remote 远程仓库 ----
const remotes = ref([])
const currentRemote = ref('origin')
const showRemoteDropdown = ref(false)
const showAddRemote = ref(false)
const newRemoteName = ref('')
const newRemoteUrl = ref('')
const addRemoteLoading = ref(false)
// ---- Stash 贮藏管理 ----
const stashList = ref([])
const stashLoading = ref(false)
@@ -248,8 +257,15 @@ async function loadStatus() {
status.value = {
branch: result.branch || '',
remoteUrl: result.remoteUrl || result.remoteURL || '',
remotes: Array.isArray(result.remotes) ? result.remotes : [],
currentRemote: result.currentRemote || 'origin',
changedFiles: [],
}
// 同步 remotes
remotes.value = status.value.remotes
if (remotes.value.length && !remotes.value.find(r => r.name === currentRemote.value)) {
currentRemote.value = remotes.value[0].name
}
} else {
errorMsg.value = '获取项目状态返回空值: ' + JSON.stringify(result)
loadingBase.value = false
@@ -512,11 +528,11 @@ async function gitAction(action) {
actionLoading.value = action
try {
if (action === 'pull') {
await AppService.PullProject(props.project.path)
await AppService.PullProject(props.project.path, currentRemote.value)
} else if (action === 'push') {
await AppService.PushProject(props.project.path)
await AppService.PushProject(props.project.path, currentRemote.value)
} else {
await AppService.FetchProject(props.project.path)
await AppService.FetchProject(props.project.path, currentRemote.value)
}
await loadStatus()
} catch (e) {
@@ -675,7 +691,7 @@ async function mergeBranch(branchName) {
async function loadRemoteBranches() {
if (!props.project?.path) return
try {
const list = await AppService.GetRemoteBranches(props.project.path)
const list = await AppService.GetRemoteBranches(props.project.path, currentRemote.value)
remoteBranches.value = Array.isArray(list) ? list : []
} catch (e) {
console.error('获取远程分支失败:', e)
@@ -709,7 +725,7 @@ async function deleteRemoteBranch(remoteBranch) {
okButtonProps: { danger: true },
async onOk() {
try {
await AppService.DeleteRemoteBranch(props.project.path, remoteBranch)
await AppService.DeleteRemoteBranch(props.project.path, remoteBranch, currentRemote.value)
await loadRemoteBranches()
message.success(`远程分支 ${remoteBranch} 已删除`)
} catch (e) {
@@ -1130,7 +1146,7 @@ async function deleteTag(tag) {
okButtonProps: { danger: true },
async onOk() {
try {
await AppService.DeleteTag(props.project.path, tag.name)
await AppService.DeleteTag(props.project.path, tag.name, currentRemote.value)
await loadTags()
} catch (e) {
console.error('删除标签失败:', e)
@@ -1143,7 +1159,7 @@ async function deleteTag(tag) {
async function pushTag(tag) {
if (!props.project?.path) return
try {
await AppService.PushTag(props.project.path, tag.name)
await AppService.PushTag(props.project.path, tag.name, currentRemote.value)
Modal.success({ title: '推送成功', content: `标签 ${tag.name} 已推送到远程` })
} catch (e) {
console.error('推送标签失败:', e)
@@ -1157,6 +1173,63 @@ watch(activeTab, (tab) => {
loadTags()
}
})
// ---- Remote 远程仓库管理 ----
async function switchRemote(remoteName) {
currentRemote.value = remoteName
showRemoteDropdown.value = false
// 切换 remote 后刷新远程分支
await loadRemoteBranches()
}
async function addRemote() {
if (!props.project?.path || !newRemoteName.value.trim() || !newRemoteUrl.value.trim()) return
addRemoteLoading.value = true
try {
await AppService.AddRemote(props.project.path, newRemoteName.value.trim(), newRemoteUrl.value.trim())
newRemoteName.value = ''
newRemoteUrl.value = ''
showAddRemote.value = false
message.success('远程仓库添加成功')
await loadStatus()
} catch (e) {
Modal.error({ title: '添加远程仓库失败', content: String(e) })
} finally {
addRemoteLoading.value = false
}
}
async function removeRemote(remoteName) {
if (!props.project?.path) return
Modal.confirm({
title: '确认删除远程仓库',
icon: h(ExclamationCircleOutlined),
content: h('div', [
h('p', '确定要删除远程仓库吗?'),
h('p', { style: 'font-family: monospace; color: #f38ba8; font-size: 15px;' }, remoteName),
]),
okText: '删除',
cancelText: '取消',
okButtonProps: { danger: true },
async onOk() {
try {
await AppService.RemoveRemote(props.project.path, remoteName)
message.success(`远程仓库 ${remoteName} 已删除`)
if (currentRemote.value === remoteName) {
currentRemote.value = 'origin'
}
await loadStatus()
} catch (e) {
Modal.error({ title: '删除远程仓库失败', content: String(e) })
}
},
})
}
const currentRemoteUrl = computed(() => {
const r = remotes.value.find(r => r.name === currentRemote.value)
return r ? r.url : status.value?.remoteUrl || ''
})
</script>
<template>
@@ -1271,7 +1344,58 @@ watch(activeTab, (tab) => {
</div>
</template>
</a-dropdown>
<span v-if="status.remoteUrl" class="remote-url" :title="status.remoteUrl">{{ status.remoteUrl }}</span>
<span v-if="status.remoteUrl" class="remote-url" :title="currentRemoteUrl">
<a-dropdown :open="showRemoteDropdown" @openChange="v => showRemoteDropdown = v" :trigger="['click']">
<span class="remote-selector" @click.prevent="showRemoteDropdown = !showRemoteDropdown">
<GlobalOutlined style="margin-right: 4px;" />
{{ currentRemote }}
<CaretDownOutlined style="font-size: 10px; margin-left: 2px;" />
</span>
<template #overlay>
<div class="remote-dropdown">
<div class="branch-dropdown-title">远程仓库</div>
<div class="branch-dropdown-list">
<div
v-for="r in remotes"
:key="r.name"
class="branch-dropdown-item"
:class="{ active: r.name === currentRemote }"
>
<div class="branch-item-main" @click="switchRemote(r.name)">
<GlobalOutlined style="font-size: 12px; margin-right: 6px;" />
<span class="branch-item-name">{{ r.name }}</span>
<span v-if="r.name === currentRemote" style="margin-left: auto; color: var(--success, #a6e3a1);"></span>
</div>
<div class="branch-item-actions" @click.stop>
<a-tooltip :title="r.url">
<span class="branch-action-btn" style="cursor: default; opacity: 0.6; font-size: 10px;">URL</span>
</a-tooltip>
<a-tooltip title="删除远程仓库" v-if="r.name !== 'origin'">
<span class="branch-action-btn danger" @click="removeRemote(r.name)"><DeleteOutlined /></span>
</a-tooltip>
</div>
</div>
<div v-if="!remotes.length" style="padding: 12px; text-align: center; color: var(--text-muted);">
无远程仓库
</div>
</div>
<!-- 添加远程仓库 -->
<div v-if="!showAddRemote" class="remote-add-btn" @click="showAddRemote = true">
<PlusOutlined /> 添加远程仓库
</div>
<div v-else class="remote-add-form">
<a-input v-model:value="newRemoteName" placeholder="名称 (如 upstream)" size="small" style="margin-bottom: 4px;" />
<a-input v-model:value="newRemoteUrl" placeholder="仓库地址" size="small" style="margin-bottom: 4px;" />
<div style="display: flex; gap: 4px;">
<a-button size="small" type="primary" :loading="addRemoteLoading" :disabled="!newRemoteName.trim() || !newRemoteUrl.trim()" @click="addRemote" style="flex: 1;">添加</a-button>
<a-button size="small" @click="showAddRemote = false; newRemoteName = ''; newRemoteUrl = ''">取消</a-button>
</div>
</div>
</div>
</template>
</a-dropdown>
<span class="remote-url-text" :title="currentRemoteUrl">{{ currentRemoteUrl }}</span>
</span>
</template>
</div>
<a-space class="project-actions">
@@ -1898,6 +2022,56 @@ watch(activeTab, (tab) => {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-flex;
align-items: center;
gap: 4px;
}
.remote-selector {
display: inline-flex;
align-items: center;
cursor: pointer;
padding: 1px 6px;
border-radius: 4px;
color: var(--accent, #89b4fa);
font-weight: 500;
transition: background 0.2s;
}
.remote-selector:hover {
background: var(--hover-bg, rgba(137, 180, 250, 0.1));
}
.remote-url-text {
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.remote-dropdown {
background: var(--dropdown-bg, #1e1e2e);
border: 1px solid var(--border-color, #313244);
border-radius: 8px;
padding: 8px 0;
min-width: 280px;
max-width: 400px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
}
.remote-add-btn {
padding: 8px 12px;
color: var(--accent, #89b4fa);
cursor: pointer;
font-size: 12px;
border-top: 1px solid var(--border-color, #313244);
margin-top: 4px;
}
.remote-add-btn:hover {
background: var(--hover-bg, rgba(137, 180, 250, 0.08));
}
.remote-add-form {
padding: 8px 12px;
border-top: 1px solid var(--border-color, #313244);
margin-top: 4px;
}
.project-actions {

View File

@@ -457,7 +457,7 @@ async function doBatchPull() {
if (!selectedBatchPaths.value.length) return
batchActionLoading.value = 'pull'
try {
const results = await AppService.BatchPull(selectedBatchPaths.value)
const results = await AppService.BatchPull(selectedBatchPaths.value, 'origin')
batchResults.value = Array.isArray(results) ? results : []
const successCount = batchResults.value.filter(r => r.success).length
message.info(`批量 Pull 完成:${successCount}/${batchResults.value.length} 成功`)
@@ -473,7 +473,7 @@ async function doBatchPush() {
if (!selectedBatchPaths.value.length) return
batchActionLoading.value = 'push'
try {
const results = await AppService.BatchPush(selectedBatchPaths.value)
const results = await AppService.BatchPush(selectedBatchPaths.value, 'origin')
batchResults.value = Array.isArray(results) ? results : []
const successCount = batchResults.value.filter(r => r.success).length
message.info(`批量 Push 完成:${successCount}/${batchResults.value.length} 成功`)

View File

@@ -53,6 +53,17 @@ export function AddProject(platform, username, name, path) {
return $Call.ByID(2299402672, platform, username, name, path);
}
/**
* AddRemote 添加远程仓库
* @param {string} path
* @param {string} name
* @param {string} url
* @returns {$CancellablePromise<void>}
*/
export function AddRemote(path, name, url) {
return $Call.ByID(3697916441, path, name, url);
}
/**
* AddUser 添加用户到指定平台
* @param {string} platform
@@ -67,10 +78,11 @@ export function AddUser(platform, username, token) {
/**
* BatchPull 批量拉取指定项目
* @param {string[]} paths
* @param {string} remote
* @returns {$CancellablePromise<$models.BatchPullResult[]>}
*/
export function BatchPull(paths) {
return $Call.ByID(758996647, paths).then(/** @type {($result: any) => any} */(($result) => {
export function BatchPull(paths, remote) {
return $Call.ByID(758996647, paths, remote).then(/** @type {($result: any) => any} */(($result) => {
return $$createType1($result);
}));
}
@@ -78,10 +90,11 @@ export function BatchPull(paths) {
/**
* BatchPush 批量推送指定项目
* @param {string[]} paths
* @param {string} remote
* @returns {$CancellablePromise<$models.BatchPullResult[]>}
*/
export function BatchPush(paths) {
return $Call.ByID(794082076, paths).then(/** @type {($result: any) => any} */(($result) => {
export function BatchPush(paths, remote) {
return $Call.ByID(794082076, paths, remote).then(/** @type {($result: any) => any} */(($result) => {
return $$createType1($result);
}));
}
@@ -165,20 +178,22 @@ export function DeleteBranch(path, name, force) {
* DeleteRemoteBranch 删除远程分支
* @param {string} path
* @param {string} branch
* @param {string} remote
* @returns {$CancellablePromise<void>}
*/
export function DeleteRemoteBranch(path, branch) {
return $Call.ByID(2626609049, path, branch);
export function DeleteRemoteBranch(path, branch, remote) {
return $Call.ByID(2626609049, path, branch, remote);
}
/**
* DeleteTag 删除标签(本地+远程)
* @param {string} path
* @param {string} name
* @param {string} remote
* @returns {$CancellablePromise<void>}
*/
export function DeleteTag(path, name) {
return $Call.ByID(873653241, path, name);
export function DeleteTag(path, name, remote) {
return $Call.ByID(873653241, path, name, remote);
}
/**
@@ -192,12 +207,13 @@ export function DiscardFiles(path, files) {
}
/**
* FetchProject 拉取远程信息
* FetchProject 拉取远程信息(指定 remote空则 fetch --all
* @param {string} path
* @param {string} remote
* @returns {$CancellablePromise<string>}
*/
export function FetchProject(path) {
return $Call.ByID(3541106829, path);
export function FetchProject(path, remote) {
return $Call.ByID(3541106829, path, remote);
}
/**
@@ -383,14 +399,26 @@ export function GetProjectTree() {
/**
* GetRemoteBranches 获取远程分支列表
* @param {string} path
* @param {string} remote
* @returns {$CancellablePromise<$models.BranchInfo[]>}
*/
export function GetRemoteBranches(path) {
return $Call.ByID(994796370, path).then(/** @type {($result: any) => any} */(($result) => {
export function GetRemoteBranches(path, remote) {
return $Call.ByID(994796370, path, remote).then(/** @type {($result: any) => any} */(($result) => {
return $$createType8($result);
}));
}
/**
* GetRemotes 获取项目所有远程仓库列表
* @param {string} path
* @returns {$CancellablePromise<$models.RemoteItem[]>}
*/
export function GetRemotes(path) {
return $Call.ByID(975520027, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType26($result);
}));
}
/**
* GetStashList 获取贮藏列表
* @param {string} path
@@ -398,7 +426,7 @@ export function GetRemoteBranches(path) {
*/
export function GetStashList(path) {
return $Call.ByID(1948257945, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType26($result);
return $$createType28($result);
}));
}
@@ -409,7 +437,7 @@ export function GetStashList(path) {
*/
export function GetTags(path) {
return $Call.ByID(3979169621, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType28($result);
return $$createType30($result);
}));
}
@@ -421,7 +449,7 @@ export function GetTags(path) {
*/
export function GetUserInfo(platform, username) {
return $Call.ByID(2674655707, platform, username).then(/** @type {($result: any) => any} */(($result) => {
return $$createType30($result);
return $$createType32($result);
}));
}
@@ -455,31 +483,34 @@ export function MergeBranch(path, branch) {
}
/**
* PullProject 拉取项目(当前分支)
* PullProject 拉取项目(当前分支,指定 remote
* @param {string} path
* @param {string} remote
* @returns {$CancellablePromise<string>}
*/
export function PullProject(path) {
return $Call.ByID(4145813996, path);
export function PullProject(path, remote) {
return $Call.ByID(4145813996, path, remote);
}
/**
* PushProject 推送项目(当前分支)
* PushProject 推送项目(当前分支,指定 remote
* @param {string} path
* @param {string} remote
* @returns {$CancellablePromise<string>}
*/
export function PushProject(path) {
return $Call.ByID(3887901113, path);
export function PushProject(path, remote) {
return $Call.ByID(3887901113, path, remote);
}
/**
* PushTag 推送标签到远程
* @param {string} path
* @param {string} name
* @param {string} remote
* @returns {$CancellablePromise<void>}
*/
export function PushTag(path, name) {
return $Call.ByID(1424810524, path, name);
export function PushTag(path, name, remote) {
return $Call.ByID(1424810524, path, name, remote);
}
/**
@@ -502,6 +533,16 @@ export function RemoveProject(platform, username, name) {
return $Call.ByID(2748109581, platform, username, name);
}
/**
* RemoveRemote 删除远程仓库
* @param {string} path
* @param {string} name
* @returns {$CancellablePromise<void>}
*/
export function RemoveRemote(path, name) {
return $Call.ByID(2915623022, path, name);
}
/**
* RemoveUser 从平台删除用户
* @param {string} platform
@@ -748,9 +789,11 @@ const $$createType21 = $models.ProjectStatus.createFrom;
const $$createType22 = $Create.Nullable($$createType21);
const $$createType23 = $models.TreeNode.createFrom;
const $$createType24 = $Create.Array($$createType23);
const $$createType25 = $models.StashInfo.createFrom;
const $$createType25 = $models.RemoteItem.createFrom;
const $$createType26 = $Create.Array($$createType25);
const $$createType27 = $models.TagInfo.createFrom;
const $$createType27 = $models.StashInfo.createFrom;
const $$createType28 = $Create.Array($$createType27);
const $$createType29 = $models.UserInfo.createFrom;
const $$createType30 = $Create.Nullable($$createType29);
const $$createType29 = $models.TagInfo.createFrom;
const $$createType30 = $Create.Array($$createType29);
const $$createType31 = $models.UserInfo.createFrom;
const $$createType32 = $Create.Nullable($$createType31);

View File

@@ -19,6 +19,7 @@ export {
PlatformInfo,
ProjectOverview,
ProjectStatus,
RemoteItem,
StashInfo,
TagInfo,
TreeNode,

View File

@@ -507,6 +507,20 @@ export class ProjectStatus {
*/
this["remoteUrl"] = "";
}
if (!("remotes" in $$source)) {
/**
* @member
* @type {RemoteItem[]}
*/
this["remotes"] = [];
}
if (!("currentRemote" in $$source)) {
/**
* @member
* @type {string}
*/
this["currentRemote"] = "";
}
if (!("changedFiles" in $$source)) {
/**
* @member
@@ -525,14 +539,56 @@ export class ProjectStatus {
*/
static createFrom($$source = {}) {
const $$createField2_0 = $$createType1;
const $$createField4_0 = $$createType3;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("remotes" in $$parsedSource) {
$$parsedSource["remotes"] = $$createField2_0($$parsedSource["remotes"]);
}
if ("changedFiles" in $$parsedSource) {
$$parsedSource["changedFiles"] = $$createField2_0($$parsedSource["changedFiles"]);
$$parsedSource["changedFiles"] = $$createField4_0($$parsedSource["changedFiles"]);
}
return new ProjectStatus(/** @type {Partial<ProjectStatus>} */($$parsedSource));
}
}
/**
* RemoteItem 远程仓库信息
*/
export class RemoteItem {
/**
* Creates a new RemoteItem instance.
* @param {Partial<RemoteItem>} [$$source = {}] - The source object to create the RemoteItem.
*/
constructor($$source = {}) {
if (!("name" in $$source)) {
/**
* @member
* @type {string}
*/
this["name"] = "";
}
if (!("url" in $$source)) {
/**
* @member
* @type {string}
*/
this["url"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new RemoteItem instance from a string or object.
* @param {any} [$$source = {}]
* @returns {RemoteItem}
*/
static createFrom($$source = {}) {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new RemoteItem(/** @type {Partial<RemoteItem>} */($$parsedSource));
}
}
/**
* StashInfo 贮藏信息
*/
@@ -692,7 +748,7 @@ export class TreeNode {
* @returns {TreeNode}
*/
static createFrom($$source = {}) {
const $$createField4_0 = $$createType3;
const $$createField4_0 = $$createType5;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("children" in $$parsedSource) {
$$parsedSource["children"] = $$createField4_0($$parsedSource["children"]);
@@ -740,7 +796,9 @@ export class UserInfo {
}
// Private type creation functions
const $$createType0 = FileInfo.createFrom;
const $$createType0 = RemoteItem.createFrom;
const $$createType1 = $Create.Array($$createType0);
const $$createType2 = TreeNode.createFrom;
const $$createType2 = FileInfo.createFrom;
const $$createType3 = $Create.Array($$createType2);
const $$createType4 = TreeNode.createFrom;
const $$createType5 = $Create.Array($$createType4);