diff --git a/frontend/app/components/ContentArea.vue b/frontend/app/components/ContentArea.vue
index e10172a..e2d659b 100644
--- a/frontend/app/components/ContentArea.vue
+++ b/frontend/app/components/ContentArea.vue
@@ -31,6 +31,9 @@ import {
GlobalOutlined,
SaveOutlined,
DownloadOutlined,
+ SearchOutlined,
+ WarningOutlined,
+ StopOutlined,
} from '@ant-design/icons-vue'
import { Modal, message } from 'ant-design-vue'
import { AppService } from '../../bindings/github.com/zhuy1228/GitPilot/internal/app'
@@ -100,6 +103,21 @@ const showSettings = ref(false)
const settingsLoading = ref(false)
const gitConfig = ref({ userName: '', userEmail: '' })
+// ---- 冲突处理 ----
+const isMerging = ref(false)
+const conflictFiles = ref([])
+const selectedConflictFile = ref(null)
+const conflictContent = ref('')
+const conflictSaving = ref(false)
+
+// ---- 提交搜索 ----
+const searchKeyword = ref('')
+const searchAuthor = ref('')
+const searchLoading = ref(false)
+const isSearchMode = ref(false)
+const searchResults = ref([])
+const displayLogs = computed(() => isSearchMode.value ? searchResults.value : commitLogs.value)
+
// ---- 文件列表拖拽调整宽度 ----
const fileListWidth = ref(300)
const MIN_FILELIST = 180
@@ -256,6 +274,20 @@ async function loadStatus() {
loadingFiles.value = false
}
+ // 检查合并冲突状态
+ try {
+ isMerging.value = await AppService.IsMerging(props.project.path)
+ if (isMerging.value) {
+ const cFiles = await AppService.GetConflictFiles(props.project.path)
+ conflictFiles.value = Array.isArray(cFiles) ? cFiles : []
+ } else {
+ conflictFiles.value = []
+ selectedConflictFile.value = null
+ }
+ } catch (e) {
+ console.error('检查合并状态失败:', e)
+ }
+
// 第三步:并行加载分支列表 + 提交历史
loadBranches()
if (activeTab.value === 'history') {
@@ -794,6 +826,122 @@ async function saveSettings() {
}
}
+// ---- 冲突处理 ----
+async function selectConflictFile(file) {
+ selectedConflictFile.value = file
+ conflictContent.value = ''
+ try {
+ const content = await AppService.GetConflictFileContent(props.project.path, file.filePath)
+ conflictContent.value = content
+ } catch (e) {
+ conflictContent.value = '读取冲突文件失败: ' + e
+ }
+}
+
+async function saveConflictContent() {
+ if (!selectedConflictFile.value) return
+ conflictSaving.value = true
+ try {
+ await AppService.SaveConflictFile(props.project.path, selectedConflictFile.value.filePath, conflictContent.value)
+ message.success('冲突文件已保存')
+ } catch (e) {
+ Modal.error({ title: '保存失败', content: String(e) })
+ } finally {
+ conflictSaving.value = false
+ }
+}
+
+async function resolveConflictFiles(files) {
+ if (!props.project?.path) return
+ try {
+ const paths = files.map(f => f.filePath)
+ await AppService.ResolveConflictFile(props.project.path, paths)
+ message.success('已标记为已解决')
+ isMerging.value = await AppService.IsMerging(props.project.path)
+ if (isMerging.value) {
+ const cFiles = await AppService.GetConflictFiles(props.project.path)
+ conflictFiles.value = Array.isArray(cFiles) ? cFiles : []
+ } else {
+ conflictFiles.value = []
+ }
+ selectedConflictFile.value = null
+ await refreshFiles()
+ } catch (e) {
+ Modal.error({ title: '解决冲突失败', content: String(e) })
+ }
+}
+
+async function completeMerge() {
+ if (!props.project?.path) return
+ const msg = commitMessage.value.trim() || 'Merge completed'
+ commitLoading.value = true
+ try {
+ await AppService.CommitChanges(props.project.path, msg)
+ commitMessage.value = ''
+ isMerging.value = false
+ conflictFiles.value = []
+ selectedConflictFile.value = null
+ await loadStatus()
+ } catch (e) {
+ Modal.error({ title: '完成合并失败', content: String(e) })
+ } finally {
+ commitLoading.value = false
+ }
+}
+
+async function handleAbortMerge() {
+ if (!props.project?.path) return
+ Modal.confirm({
+ title: '确认中止合并',
+ icon: h(ExclamationCircleOutlined),
+ content: '中止合并将丢弃所有合并更改,回到合并之前的状态。确定继续?',
+ okText: '中止合并',
+ cancelText: '取消',
+ okButtonProps: { danger: true },
+ async onOk() {
+ try {
+ await AppService.AbortMerge(props.project.path)
+ message.success('已中止合并')
+ isMerging.value = false
+ conflictFiles.value = []
+ selectedConflictFile.value = null
+ await loadStatus()
+ } catch (e) {
+ Modal.error({ title: '中止合并失败', content: String(e) })
+ }
+ },
+ })
+}
+
+// ---- 提交搜索 ----
+async function searchCommits() {
+ if (!props.project?.path) return
+ if (!searchKeyword.value.trim() && !searchAuthor.value.trim()) return
+ searchLoading.value = true
+ isSearchMode.value = true
+ try {
+ const results = await AppService.SearchCommitLog(
+ props.project.path,
+ searchKeyword.value.trim(),
+ searchAuthor.value.trim(),
+ 100
+ )
+ searchResults.value = Array.isArray(results) ? results : []
+ } catch (e) {
+ console.error('搜索提交失败:', e)
+ searchResults.value = []
+ } finally {
+ searchLoading.value = false
+ }
+}
+
+function clearSearch() {
+ isSearchMode.value = false
+ searchKeyword.value = ''
+ searchAuthor.value = ''
+ searchResults.value = []
+}
+
// ---- 提交历史 ----
async function loadCommitLog() {
if (!props.project?.path) return
@@ -1237,6 +1385,57 @@ watch(activeTab, (tab) => {
+
+
+
+
+
+
+
{{ cf.filePath }}
+
+
+
+
+
+
@@ -1316,20 +1515,46 @@ watch(activeTab, (tab) => {
+
+
-
+
-
- 暂无提交历史
+
+ {{ isSearchMode ? '未找到匹配的提交' : '暂无提交历史' }}
{
-
+
+
+
+
+
+
+
+
+
点击左侧文件查看详情
@@ -2375,4 +2627,108 @@ watch(activeTab, (tab) => {
margin-left: auto;
flex-shrink: 0;
}
+
+/* 冲突处理 */
+.conflict-banner {
+ border-bottom: 1px solid var(--border-color);
+ background: rgba(250, 179, 135, 0.08);
+}
+
+.conflict-banner-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 10px;
+ font-size: 13px;
+}
+
+.conflict-file-list {
+ max-height: 150px;
+ overflow-y: auto;
+ border-top: 1px solid var(--border-color);
+}
+
+.conflict-file-item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 12px;
+ font-size: 12px;
+ cursor: pointer;
+ transition: background 0.12s;
+}
+
+.conflict-file-item:hover {
+ background: var(--bg-hover);
+}
+
+.conflict-file-item.active {
+ background: var(--bg-active);
+}
+
+.conflict-file-name {
+ flex: 1;
+ min-width: 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-family: 'Consolas', 'Courier New', monospace;
+ color: var(--text-secondary);
+}
+
+.conflict-file-actions {
+ visibility: hidden;
+ flex-shrink: 0;
+}
+
+.conflict-file-item:hover .conflict-file-actions {
+ visibility: visible;
+}
+
+.conflict-resolve-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 22px;
+ height: 22px;
+ border-radius: 4px;
+ font-size: 13px;
+ color: var(--text-muted);
+ cursor: pointer;
+ transition: all 0.12s;
+}
+
+.conflict-resolve-btn:hover {
+ background: rgba(166, 227, 161, 0.2);
+ color: var(--success, #a6e3a1);
+}
+
+.conflict-editor {
+ display: flex;
+ flex-direction: column;
+}
+
+.conflict-textarea {
+ flex: 1;
+ width: 100%;
+ background: var(--bg-primary, #1e1e2e);
+ color: var(--text-primary);
+ border: none;
+ outline: none;
+ resize: none;
+ padding: 12px;
+ font-family: 'Consolas', 'Courier New', monospace;
+ font-size: 13px;
+ line-height: 1.6;
+ tab-size: 4;
+}
+
+/* 提交搜索 */
+.search-bar {
+ display: flex;
+ gap: 6px;
+ padding: 8px 10px;
+ border-bottom: 1px solid var(--border-color);
+ flex-shrink: 0;
+}
diff --git a/frontend/app/components/Sidebar.vue b/frontend/app/components/Sidebar.vue
index 561c922..ff1ceb6 100644
--- a/frontend/app/components/Sidebar.vue
+++ b/frontend/app/components/Sidebar.vue
@@ -9,8 +9,14 @@ import {
DeleteOutlined,
CloudDownloadOutlined,
LoadingOutlined,
+ AppstoreOutlined,
+ CloudUploadOutlined,
+ SyncOutlined,
+ CheckCircleOutlined,
+ CloseCircleOutlined,
+ BranchesOutlined,
} from '@ant-design/icons-vue'
-import { message } from 'ant-design-vue'
+import { message, Modal } from 'ant-design-vue'
import { AppService } from '../../bindings/github.com/zhuy1228/GitPilot/internal/app'
const props = defineProps({
@@ -32,6 +38,19 @@ const showCloneDialog = ref(false)
const cloneForm = ref({ platform: '', username: '', repoURL: '', parentDir: '', name: '' })
const cloneLoading = ref(false)
+// ---- 批量操作 ----
+const showBatchModal = ref(false)
+const batchOverviews = ref([])
+const batchLoading = ref(false)
+const selectedBatchPaths = ref([])
+const batchResults = ref([])
+const batchActionLoading = ref('')
+const batchResultMap = computed(() => {
+ const map = {}
+ batchResults.value.forEach(r => { map[r.path] = r })
+ return map
+})
+
// 右键菜单
const contextMenu = ref({ visible: false, x: 0, y: 0, type: '', data: {} })
@@ -393,6 +412,79 @@ async function removeProject(platform, username, name) {
}
}
+// ---- 批量操作 ----
+async function openBatchModal() {
+ showBatchModal.value = true
+ batchResults.value = []
+ await loadBatchOverview()
+}
+
+async function loadBatchOverview() {
+ batchLoading.value = true
+ try {
+ const overviews = await AppService.GetAllProjectOverview()
+ batchOverviews.value = Array.isArray(overviews) ? overviews : []
+ selectedBatchPaths.value = batchOverviews.value
+ .filter(p => !p.error)
+ .map(p => p.path)
+ } catch (e) {
+ console.error('获取项目概览失败:', e)
+ } finally {
+ batchLoading.value = false
+ }
+}
+
+function toggleBatchPath(path) {
+ const idx = selectedBatchPaths.value.indexOf(path)
+ if (idx >= 0) {
+ selectedBatchPaths.value = selectedBatchPaths.value.filter(p => p !== path)
+ } else {
+ selectedBatchPaths.value = [...selectedBatchPaths.value, path]
+ }
+}
+
+function toggleAllBatchPaths(e) {
+ if (e.target.checked) {
+ selectedBatchPaths.value = batchOverviews.value
+ .filter(p => !p.error)
+ .map(p => p.path)
+ } else {
+ selectedBatchPaths.value = []
+ }
+}
+
+async function doBatchPull() {
+ if (!selectedBatchPaths.value.length) return
+ batchActionLoading.value = 'pull'
+ try {
+ const results = await AppService.BatchPull(selectedBatchPaths.value)
+ batchResults.value = Array.isArray(results) ? results : []
+ const successCount = batchResults.value.filter(r => r.success).length
+ message.info(`批量 Pull 完成:${successCount}/${batchResults.value.length} 成功`)
+ await loadBatchOverview()
+ } catch (e) {
+ Modal.error({ title: '批量 Pull 失败', content: String(e) })
+ } finally {
+ batchActionLoading.value = ''
+ }
+}
+
+async function doBatchPush() {
+ if (!selectedBatchPaths.value.length) return
+ batchActionLoading.value = 'push'
+ try {
+ const results = await AppService.BatchPush(selectedBatchPaths.value)
+ batchResults.value = Array.isArray(results) ? results : []
+ const successCount = batchResults.value.filter(r => r.success).length
+ message.info(`批量 Push 完成:${successCount}/${batchResults.value.length} 成功`)
+ await loadBatchOverview()
+ } catch (e) {
+ Modal.error({ title: '批量 Push 失败', content: String(e) })
+ } finally {
+ batchActionLoading.value = ''
+ }
+}
+
// 平台 SVG Logo
const platformLogos = {
github: ``,
@@ -415,9 +507,14 @@ onMounted(() => {