feat: Tag管理功能 + 打包脚本优化 + CI/CD发布流水线
- 新增标签管理: 列表/创建/删除/推送标签 (后端API + 前端UI) - 前端新增标签Tab页,支持创建、删除、推送操作 - config.go 改为从可执行文件同目录加载 config.yaml - 各平台构建脚本自动复制 config.yaml 到输出目录 - Windows 打包改为 ZIP 格式 (不依赖 NSIS) - 新增 GitHub Actions Release 工作流 (三平台自动构建+发布)
This commit is contained in:
212
.github/workflows/release.yml
vendored
Normal file
212
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,212 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install Wails CLI
|
||||
run: go install github.com/wailsapp/wails/v3/cmd/wails3@latest
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: npm install
|
||||
working-directory: frontend
|
||||
|
||||
- name: Build frontend
|
||||
run: npm run generate
|
||||
working-directory: frontend
|
||||
|
||||
- name: Generate bindings
|
||||
run: wails3 generate bindings
|
||||
|
||||
- name: Generate icons
|
||||
run: wails3 generate icons -input build/appicon.png -windowsDir build/windows -macDir build/darwin
|
||||
|
||||
- name: Generate syso
|
||||
working-directory: build
|
||||
run: wails3 generate syso -arch amd64 -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_amd64.syso
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
CGO_ENABLED: 1
|
||||
GOOS: windows
|
||||
GOARCH: amd64
|
||||
run: go build -tags production -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui" -o bin/gitpilot.exe
|
||||
|
||||
- name: Copy config.yaml
|
||||
run: Copy-Item config.yaml -Destination bin/config.yaml
|
||||
|
||||
- name: Remove syso
|
||||
run: Remove-Item *.syso
|
||||
|
||||
- name: Package
|
||||
run: Compress-Archive -Path "bin/gitpilot.exe","bin/config.yaml" -DestinationPath "bin/gitpilot-windows-amd64.zip" -Force
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gitpilot-windows-amd64
|
||||
path: bin/gitpilot-windows-amd64.zip
|
||||
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
arch: [amd64, arm64]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install Wails CLI
|
||||
run: go install github.com/wailsapp/wails/v3/cmd/wails3@latest
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: npm install
|
||||
working-directory: frontend
|
||||
|
||||
- name: Build frontend
|
||||
run: npm run generate
|
||||
working-directory: frontend
|
||||
|
||||
- name: Generate bindings
|
||||
run: wails3 generate bindings
|
||||
|
||||
- name: Generate icons
|
||||
run: wails3 generate icons -input build/appicon.png -windowsDir build/windows -macDir build/darwin
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
CGO_ENABLED: 1
|
||||
GOOS: darwin
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
CGO_CFLAGS: "-mmacosx-version-min=10.15"
|
||||
CGO_LDFLAGS: "-mmacosx-version-min=10.15"
|
||||
MACOSX_DEPLOYMENT_TARGET: "10.15"
|
||||
run: go build -tags "production,disable_onnx" -trimpath -buildvcs=false -ldflags="-w -s" -o bin/gitpilot
|
||||
|
||||
- name: Create .app bundle
|
||||
run: |
|
||||
mkdir -p bin/gitpilot.app/Contents/{MacOS,Resources}
|
||||
cp build/darwin/icons.icns bin/gitpilot.app/Contents/Resources/
|
||||
cp bin/gitpilot bin/gitpilot.app/Contents/MacOS/
|
||||
cp config.yaml bin/gitpilot.app/Contents/MacOS/config.yaml
|
||||
cp build/darwin/Info.plist bin/gitpilot.app/Contents/
|
||||
codesign --force --deep --sign - bin/gitpilot.app
|
||||
|
||||
- name: Package
|
||||
run: |
|
||||
cd bin
|
||||
zip -r gitpilot-macos-${{ matrix.arch }}.zip gitpilot.app config.yaml
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gitpilot-macos-${{ matrix.arch }}
|
||||
path: bin/gitpilot-macos-${{ matrix.arch }}.zip
|
||||
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev
|
||||
|
||||
- name: Install Wails CLI
|
||||
run: go install github.com/wailsapp/wails/v3/cmd/wails3@latest
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: npm install
|
||||
working-directory: frontend
|
||||
|
||||
- name: Build frontend
|
||||
run: npm run generate
|
||||
working-directory: frontend
|
||||
|
||||
- name: Generate bindings
|
||||
run: wails3 generate bindings
|
||||
|
||||
- name: Generate icons
|
||||
run: wails3 generate icons -input build/appicon.png -windowsDir build/windows -macDir build/darwin
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
CGO_ENABLED: 1
|
||||
GOOS: linux
|
||||
GOARCH: amd64
|
||||
run: go build -tags "production,disable_onnx" -trimpath -buildvcs=false -ldflags="-w -s" -o bin/gitpilot
|
||||
|
||||
- name: Copy config.yaml
|
||||
run: cp config.yaml bin/config.yaml
|
||||
|
||||
- name: Package
|
||||
run: |
|
||||
cd bin
|
||||
tar czf gitpilot-linux-amd64.tar.gz gitpilot config.yaml
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gitpilot-linux-amd64
|
||||
path: bin/gitpilot-linux-amd64.tar.gz
|
||||
|
||||
release:
|
||||
needs: [build-windows, build-macos, build-linux]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
artifacts/gitpilot-windows-amd64/gitpilot-windows-amd64.zip
|
||||
artifacts/gitpilot-macos-amd64/gitpilot-macos-amd64.zip
|
||||
artifacts/gitpilot-macos-arm64/gitpilot-macos-arm64.zip
|
||||
artifacts/gitpilot-linux-amd64/gitpilot-linux-amd64.tar.gz
|
||||
@@ -1 +1 @@
|
||||
b3473eff2f1694108cc3ae68b4ae04f0
|
||||
e4fca8850949396013f268b14abf0d8a
|
||||
|
||||
41
bin/config.yaml
Normal file
41
bin/config.yaml
Normal file
@@ -0,0 +1,41 @@
|
||||
platforms:
|
||||
gitea:
|
||||
base_url: http://192.168.1.10:3000
|
||||
users: []
|
||||
gitee:
|
||||
base_url: ""
|
||||
users:
|
||||
- username: zhuyuj
|
||||
token: ""
|
||||
projects: []
|
||||
github:
|
||||
base_url: ""
|
||||
users:
|
||||
- username: zhuy1228
|
||||
token: ""
|
||||
projects:
|
||||
- name: GitPilot
|
||||
path: F:\Project\GitPilot
|
||||
enabled: false
|
||||
- name: nakama
|
||||
path: F:\Project\nakama
|
||||
enabled: false
|
||||
- name: DevPack
|
||||
path: F:\Project\DevPack
|
||||
enabled: false
|
||||
- name: go-desk-service
|
||||
path: F:\Project\go-desk\go-desk-service
|
||||
enabled: false
|
||||
- name: go-desk-wails
|
||||
path: F:\Project\go-desk\go-desk-wails
|
||||
enabled: false
|
||||
- name: go-game
|
||||
path: F:\Project\go-game
|
||||
enabled: false
|
||||
- name: PeterAgent
|
||||
path: F:\Project\PeterAgent
|
||||
enabled: false
|
||||
settings:
|
||||
concurrency: 6
|
||||
network_check: true
|
||||
log_level: info
|
||||
BIN
bin/gitpilot.exe
BIN
bin/gitpilot.exe
Binary file not shown.
@@ -17,6 +17,7 @@ tasks:
|
||||
- task: common:generate:icons
|
||||
cmds:
|
||||
- go build {{.BUILD_FLAGS}} -o {{.OUTPUT}}
|
||||
- cp config.yaml {{.BIN_DIR}}/config.yaml
|
||||
vars:
|
||||
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags "production,disable_onnx" -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-tags "disable_onnx" -buildvcs=false -gcflags=all="-l"{{end}}'
|
||||
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||
@@ -68,6 +69,7 @@ tasks:
|
||||
- mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/{MacOS,Resources}
|
||||
- cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources
|
||||
- cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS
|
||||
- cp {{.BIN_DIR}}/config.yaml {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS/config.yaml
|
||||
- cp build/darwin/Info.plist {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents
|
||||
- codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.app
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ tasks:
|
||||
- task: common:generate:icons
|
||||
cmds:
|
||||
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}
|
||||
- cp config.yaml {{.BIN_DIR}}/config.yaml
|
||||
vars:
|
||||
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags "production,disable_onnx" -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-tags "disable_onnx" -buildvcs=false -gcflags=all="-l"{{end}}'
|
||||
env:
|
||||
|
||||
@@ -18,6 +18,10 @@ tasks:
|
||||
cmds:
|
||||
- task: generate:syso
|
||||
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}.exe
|
||||
- cmd: powershell Copy-Item config.yaml -Destination {{.BIN_DIR}}/config.yaml -Force
|
||||
platforms: [windows]
|
||||
- cmd: cp config.yaml {{.BIN_DIR}}/config.yaml
|
||||
platforms: [linux, darwin]
|
||||
- cmd: powershell Remove-item *.syso
|
||||
platforms: [windows]
|
||||
- cmd: rm -f *.syso
|
||||
@@ -31,9 +35,15 @@ tasks:
|
||||
PRODUCTION: '{{.PRODUCTION | default "false"}}'
|
||||
|
||||
package:
|
||||
summary: Packages a production build of the application
|
||||
summary: Packages a production build of the application into a zip file
|
||||
deps:
|
||||
- task: build
|
||||
vars:
|
||||
PRODUCTION: "true"
|
||||
cmds:
|
||||
- task: create:nsis:installer
|
||||
- cmd: powershell Compress-Archive -Path "{{.BIN_DIR}}/{{.APP_NAME}}.exe","{{.BIN_DIR}}/config.yaml" -DestinationPath "{{.BIN_DIR}}/{{.APP_NAME}}-windows-{{.ARCH}}.zip" -Force
|
||||
vars:
|
||||
ARCH: '{{.ARCH | default ARCH}}'
|
||||
|
||||
generate:syso:
|
||||
summary: Generates Windows `.syso` file
|
||||
|
||||
@@ -3,6 +3,7 @@ package config
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
@@ -35,9 +36,18 @@ type AppConfig struct {
|
||||
Settings Settings `yaml:"settings"`
|
||||
}
|
||||
|
||||
// configPath 返回 config.yaml 的绝对路径(与可执行文件同目录)
|
||||
func configPath() string {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return "config.yaml"
|
||||
}
|
||||
return filepath.Join(filepath.Dir(exe), "config.yaml")
|
||||
}
|
||||
|
||||
// LoadConfig 从 config.yaml 文件加载配置
|
||||
func LoadConfig() (*AppConfig, error) {
|
||||
file, err := os.Open("config.yaml")
|
||||
file, err := os.Open(configPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -55,7 +65,7 @@ func LoadConfig() (*AppConfig, error) {
|
||||
|
||||
// SaveConfig 将配置保存到 config.yaml 文件
|
||||
func SaveConfig(config *AppConfig) error {
|
||||
file, err := os.OpenFile("config.yaml", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
|
||||
file, err := os.OpenFile(configPath(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -20,6 +20,11 @@ import {
|
||||
RollbackOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
TagOutlined,
|
||||
TagsOutlined,
|
||||
CloudUploadOutlined as PushTagIcon,
|
||||
DeleteOutlined,
|
||||
SendOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { Modal } from 'ant-design-vue'
|
||||
import { AppService } from '../../bindings/github.com/zhuy1228/GitPilot/internal/app'
|
||||
@@ -62,6 +67,14 @@ const selectedCommitFile = ref(null)
|
||||
const commitFileDiff = ref('')
|
||||
const commitCollapsedDirs = ref(new Set())
|
||||
|
||||
// ---- 标签管理 ----
|
||||
const tags = ref([])
|
||||
const tagsLoading = ref(false)
|
||||
const showCreateTag = ref(false)
|
||||
const newTagName = ref('')
|
||||
const newTagMessage = ref('')
|
||||
const createTagLoading = ref(false)
|
||||
|
||||
// ---- 文件列表拖拽调整宽度 ----
|
||||
const fileListWidth = ref(300)
|
||||
const MIN_FILELIST = 180
|
||||
@@ -653,6 +666,81 @@ function formatTime(ts) {
|
||||
if (diff < 604800) return Math.floor(diff / 86400) + ' 天前'
|
||||
return d.toLocaleDateString('zh-CN')
|
||||
}
|
||||
|
||||
// ---- 标签管理 ----
|
||||
async function loadTags() {
|
||||
if (!props.project?.path) return
|
||||
tagsLoading.value = true
|
||||
try {
|
||||
const list = await AppService.GetTags(props.project.path)
|
||||
tags.value = Array.isArray(list) ? list : []
|
||||
} catch (e) {
|
||||
console.error('获取标签失败:', e)
|
||||
tags.value = []
|
||||
} finally {
|
||||
tagsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createTag() {
|
||||
if (!props.project?.path || !newTagName.value.trim()) return
|
||||
createTagLoading.value = true
|
||||
try {
|
||||
await AppService.CreateTag(props.project.path, newTagName.value.trim(), newTagMessage.value.trim() || newTagName.value.trim())
|
||||
newTagName.value = ''
|
||||
newTagMessage.value = ''
|
||||
showCreateTag.value = false
|
||||
await loadTags()
|
||||
} catch (e) {
|
||||
console.error('创建标签失败:', e)
|
||||
Modal.error({ title: '创建标签失败', content: String(e) })
|
||||
} finally {
|
||||
createTagLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTag(tag) {
|
||||
if (!props.project?.path) return
|
||||
Modal.confirm({
|
||||
title: '确认删除标签',
|
||||
icon: h(ExclamationCircleOutlined),
|
||||
content: h('div', [
|
||||
h('p', `确定要删除标签吗?`),
|
||||
h('p', { style: 'font-family: monospace; color: #89b4fa; font-size: 15px;' }, tag.name),
|
||||
h('p', { style: 'color: var(--text-muted); font-size: 12px; margin-top: 8px;' }, '将同时删除本地和远程标签。'),
|
||||
]),
|
||||
okText: '确认删除',
|
||||
cancelText: '取消',
|
||||
okButtonProps: { danger: true },
|
||||
async onOk() {
|
||||
try {
|
||||
await AppService.DeleteTag(props.project.path, tag.name)
|
||||
await loadTags()
|
||||
} catch (e) {
|
||||
console.error('删除标签失败:', e)
|
||||
Modal.error({ title: '删除失败', content: String(e) })
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function pushTag(tag) {
|
||||
if (!props.project?.path) return
|
||||
try {
|
||||
await AppService.PushTag(props.project.path, tag.name)
|
||||
Modal.success({ title: '推送成功', content: `标签 ${tag.name} 已推送到远程` })
|
||||
} catch (e) {
|
||||
console.error('推送标签失败:', e)
|
||||
Modal.error({ title: '推送失败', content: String(e) })
|
||||
}
|
||||
}
|
||||
|
||||
// 切换到标签 Tab 时加载标签
|
||||
watch(activeTab, (tab) => {
|
||||
if (tab === 'tags' && !tags.value.length && !tagsLoading.value) {
|
||||
loadTags()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -748,6 +836,9 @@ function formatTime(ts) {
|
||||
<div class="file-list-tab" :class="{ active: activeTab === 'history' }" @click="activeTab = 'history'">
|
||||
<HistoryOutlined /> 历史
|
||||
</div>
|
||||
<div class="file-list-tab" :class="{ active: activeTab === 'tags' }" @click="activeTab = 'tags'">
|
||||
<TagsOutlined /> 标签
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== 变更面板 ===== -->
|
||||
@@ -852,7 +943,7 @@ function formatTime(ts) {
|
||||
</template>
|
||||
|
||||
<!-- ===== 提交历史面板 ===== -->
|
||||
<template v-else>
|
||||
<template v-else-if="activeTab === 'history'">
|
||||
<div class="file-list-content history-panel">
|
||||
<!-- 提交列表区域 -->
|
||||
<div class="commit-list-section" :class="{ 'has-selected': selectedCommit }">
|
||||
@@ -994,6 +1085,79 @@ function formatTime(ts) {
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ===== 标签管理面板 ===== -->
|
||||
<template v-else-if="activeTab === 'tags'">
|
||||
<div class="file-list-content tags-panel">
|
||||
<!-- 创建标签区域 -->
|
||||
<div class="tag-create-box">
|
||||
<div v-if="!showCreateTag" style="display: flex; justify-content: flex-end; padding: 6px 0;">
|
||||
<a-button size="small" type="primary" @click="showCreateTag = true">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新建标签
|
||||
</a-button>
|
||||
</div>
|
||||
<template v-else>
|
||||
<a-input v-model:value="newTagName" placeholder="标签名 (例: v1.0.0)" size="small" style="margin-bottom: 6px;" @pressEnter="createTag" />
|
||||
<a-input v-model:value="newTagMessage" placeholder="标签描述 (可选)" size="small" style="margin-bottom: 6px;" />
|
||||
<div style="display: flex; gap: 6px; justify-content: flex-end;">
|
||||
<a-button size="small" @click="showCreateTag = false; newTagName = ''; newTagMessage = ''">取消</a-button>
|
||||
<a-button size="small" type="primary" :loading="createTagLoading" :disabled="!newTagName.trim()" @click="createTag">创建</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 标签列表 -->
|
||||
<div class="tag-list-section">
|
||||
<template v-if="tagsLoading">
|
||||
<div v-for="i in 5" :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="!tags.length" style="padding: 24px; text-align: center; color: var(--text-muted);">
|
||||
<TagOutlined :style="{ fontSize: '28px', marginBottom: '8px' }" />
|
||||
<div>暂无标签</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="tag in tags"
|
||||
:key="tag.name"
|
||||
class="tag-item"
|
||||
>
|
||||
<div class="tag-main-row">
|
||||
<TagOutlined class="tag-icon" />
|
||||
<span class="tag-name">{{ tag.name }}</span>
|
||||
<div class="tag-action-btns" @click.stop>
|
||||
<a-tooltip title="推送到远程">
|
||||
<a-button type="text" size="small" class="tag-action-btn push" @click="pushTag(tag)">
|
||||
<template #icon><SendOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="删除标签">
|
||||
<a-button type="text" size="small" class="tag-action-btn delete" @click="deleteTag(tag)">
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tag-meta">
|
||||
<span class="tag-hash">{{ tag.hash }}</span>
|
||||
<span v-if="tag.message" class="tag-message">{{ tag.message }}</span>
|
||||
<span class="tag-time">{{ formatTime(tag.timestamp) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 刷新按钮 -->
|
||||
<div style="padding: 8px 10px; border-top: 1px solid var(--border-color); text-align: center;">
|
||||
<a-button size="small" :loading="tagsLoading" @click="loadTags" block>
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新标签
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 拖拽分隔条 -->
|
||||
@@ -1023,7 +1187,7 @@ function formatTime(ts) {
|
||||
</template>
|
||||
</template>
|
||||
<!-- 历史模式的文件 Diff -->
|
||||
<template v-else>
|
||||
<template v-else-if="activeTab === 'history'">
|
||||
<div v-if="!selectedCommit" class="empty-state small">
|
||||
<span style="color: var(--text-muted)">点击左侧提交查看详情</span>
|
||||
</div>
|
||||
@@ -1047,6 +1211,14 @@ function formatTime(ts) {
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<!-- 标签模式 -->
|
||||
<template v-else-if="activeTab === 'tags'">
|
||||
<div class="empty-state small">
|
||||
<TagsOutlined :style="{ fontSize: '48px', color: 'var(--text-muted)', marginBottom: '8px' }" />
|
||||
<span style="color: var(--text-muted)">在左侧管理项目标签</span>
|
||||
<span style="color: var(--text-muted); font-size: 12px; margin-top: 4px;">共 {{ tags.length }} 个标签</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1583,4 +1755,107 @@ function formatTime(ts) {
|
||||
margin-left: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 标签管理面板 */
|
||||
.tags-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tag-create-box {
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.tag-list-section {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border-color, rgba(255,255,255,0.04));
|
||||
transition: background 0.12s;
|
||||
}
|
||||
|
||||
.tag-item:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.tag-main-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tag-icon {
|
||||
color: var(--accent, #89b4fa);
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tag-name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tag-action-btns {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
flex-shrink: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.tag-item:hover .tag-action-btns {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.tag-action-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
.tag-action-btn.push:hover {
|
||||
color: var(--accent, #89b4fa) !important;
|
||||
}
|
||||
|
||||
.tag-action-btn.delete:hover {
|
||||
color: var(--danger, #f38ba8) !important;
|
||||
}
|
||||
|
||||
.tag-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
margin-top: 2px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.tag-hash {
|
||||
font-family: 'Consolas', 'Courier New', monospace;
|
||||
color: var(--accent, #89b4fa);
|
||||
}
|
||||
|
||||
.tag-message {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tag-time {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -62,6 +62,27 @@ export function CommitChanges(path, message) {
|
||||
return $Call.ByID(921984546, path, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* CreateTag 创建标签
|
||||
* @param {string} path
|
||||
* @param {string} name
|
||||
* @param {string} message
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function CreateTag(path, name, message) {
|
||||
return $Call.ByID(4209616956, path, name, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* DeleteTag 删除标签(本地+远程)
|
||||
* @param {string} path
|
||||
* @param {string} name
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function DeleteTag(path, name) {
|
||||
return $Call.ByID(873653241, path, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* DiscardFiles 丢弃工作区指定文件的更改
|
||||
* @param {string} path
|
||||
@@ -210,6 +231,17 @@ export function GetProjectTree() {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* GetTags 获取所有标签
|
||||
* @param {string} path
|
||||
* @returns {$CancellablePromise<$models.TagInfo[]>}
|
||||
*/
|
||||
export function GetTags(path) {
|
||||
return $Call.ByID(3979169621, path).then(/** @type {($result: any) => any} */(($result) => {
|
||||
return $$createType15($result);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* GetUserInfo 获取用户信息
|
||||
* @param {string} platform
|
||||
@@ -218,7 +250,7 @@ export function GetProjectTree() {
|
||||
*/
|
||||
export function GetUserInfo(platform, username) {
|
||||
return $Call.ByID(2674655707, platform, username).then(/** @type {($result: any) => any} */(($result) => {
|
||||
return $$createType15($result);
|
||||
return $$createType17($result);
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -240,6 +272,16 @@ export function PushProject(path) {
|
||||
return $Call.ByID(3887901113, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* PushTag 推送标签到远程
|
||||
* @param {string} path
|
||||
* @param {string} name
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function PushTag(path, name) {
|
||||
return $Call.ByID(1424810524, path, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* RemovePlatform 删除平台
|
||||
* @param {string} name
|
||||
@@ -393,5 +435,7 @@ const $$createType10 = $models.ProjectStatus.createFrom;
|
||||
const $$createType11 = $Create.Nullable($$createType10);
|
||||
const $$createType12 = $models.TreeNode.createFrom;
|
||||
const $$createType13 = $Create.Array($$createType12);
|
||||
const $$createType14 = $models.UserInfo.createFrom;
|
||||
const $$createType15 = $Create.Nullable($$createType14);
|
||||
const $$createType14 = $models.TagInfo.createFrom;
|
||||
const $$createType15 = $Create.Array($$createType14);
|
||||
const $$createType16 = $models.UserInfo.createFrom;
|
||||
const $$createType17 = $Create.Nullable($$createType16);
|
||||
|
||||
@@ -14,6 +14,7 @@ export {
|
||||
FileInfo,
|
||||
PlatformInfo,
|
||||
ProjectStatus,
|
||||
TagInfo,
|
||||
TreeNode,
|
||||
UserInfo
|
||||
} from "./models.js";
|
||||
|
||||
@@ -294,6 +294,58 @@ export class ProjectStatus {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TagInfo 标签信息
|
||||
*/
|
||||
export class TagInfo {
|
||||
/**
|
||||
* Creates a new TagInfo instance.
|
||||
* @param {Partial<TagInfo>} [$$source = {}] - The source object to create the TagInfo.
|
||||
*/
|
||||
constructor($$source = {}) {
|
||||
if (!("name" in $$source)) {
|
||||
/**
|
||||
* @member
|
||||
* @type {string}
|
||||
*/
|
||||
this["name"] = "";
|
||||
}
|
||||
if (!("hash" in $$source)) {
|
||||
/**
|
||||
* @member
|
||||
* @type {string}
|
||||
*/
|
||||
this["hash"] = "";
|
||||
}
|
||||
if (!("timestamp" in $$source)) {
|
||||
/**
|
||||
* @member
|
||||
* @type {number}
|
||||
*/
|
||||
this["timestamp"] = 0;
|
||||
}
|
||||
if (!("message" in $$source)) {
|
||||
/**
|
||||
* @member
|
||||
* @type {string}
|
||||
*/
|
||||
this["message"] = "";
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new TagInfo instance from a string or object.
|
||||
* @param {any} [$$source = {}]
|
||||
* @returns {TagInfo}
|
||||
*/
|
||||
static createFrom($$source = {}) {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new TagInfo(/** @type {Partial<TagInfo>} */($$parsedSource));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TreeNode 前端侧边栏树节点
|
||||
*/
|
||||
|
||||
@@ -701,3 +701,97 @@ func (s *AppService) SwitchBranch(path, branch string) error {
|
||||
_, err := s.gitClient.Checkout(path, strings.TrimSpace(branch))
|
||||
return err
|
||||
}
|
||||
|
||||
// TagInfo 标签信息
|
||||
type TagInfo struct {
|
||||
Name string `json:"name"`
|
||||
Hash string `json:"hash"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// GetTags 获取所有标签
|
||||
func (s *AppService) GetTags(path string) ([]TagInfo, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("项目路径不存在: %s", path)
|
||||
}
|
||||
out, err := s.gitClient.TagList(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取标签列表失败: %w", err)
|
||||
}
|
||||
var tags []TagInfo
|
||||
for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, "\t", 5)
|
||||
if len(parts) < 3 {
|
||||
continue
|
||||
}
|
||||
var ts int64
|
||||
fmt.Sscanf(parts[2], "%d", &ts)
|
||||
hash := parts[1]
|
||||
// 注释标签的实际提交哈希在 *objectname
|
||||
if len(parts) > 3 && parts[3] != "" {
|
||||
hash = parts[3]
|
||||
}
|
||||
message := ""
|
||||
if len(parts) > 4 {
|
||||
message = parts[4]
|
||||
}
|
||||
tags = append(tags, TagInfo{
|
||||
Name: parts[0],
|
||||
Hash: hash,
|
||||
Timestamp: ts,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// CreateTag 创建标签
|
||||
func (s *AppService) CreateTag(path, name, message string) error {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return fmt.Errorf("项目路径不存在: %s", path)
|
||||
}
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return fmt.Errorf("标签名不能为空")
|
||||
}
|
||||
if strings.TrimSpace(message) == "" {
|
||||
message = name
|
||||
}
|
||||
_, err := s.gitClient.CreateTag(path, name, message)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteTag 删除标签(本地+远程)
|
||||
func (s *AppService) DeleteTag(path, name string) error {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return fmt.Errorf("项目路径不存在: %s", path)
|
||||
}
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return fmt.Errorf("标签名不能为空")
|
||||
}
|
||||
// 删除本地标签
|
||||
if _, err := s.gitClient.DeleteTag(path, name); err != nil {
|
||||
return fmt.Errorf("删除本地标签失败: %w", err)
|
||||
}
|
||||
// 尝试删除远程标签(忽略错误,可能未推送过)
|
||||
s.gitClient.DeleteRemoteTag(path, name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// PushTag 推送标签到远程
|
||||
func (s *AppService) PushTag(path, name string) error {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return fmt.Errorf("项目路径不存在: %s", path)
|
||||
}
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return fmt.Errorf("标签名不能为空")
|
||||
}
|
||||
_, err := s.gitClient.PushTag(path, name)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -129,6 +129,32 @@ func (g *GitClient) RevertCommit(path, hash string) (string, error) {
|
||||
return g.Run(path, "revert", "--no-edit", hash)
|
||||
}
|
||||
|
||||
// TagList 获取所有标签(按版本号降序,带创建时间和提交哈希)
|
||||
func (g *GitClient) TagList(path string) (string, error) {
|
||||
return g.Run(path, "tag", "-l", "--sort=-version:refname",
|
||||
"--format=%(refname:short)\t%(objectname:short)\t%(creatordate:unix)\t%(*objectname:short)\t%(contents:subject)")
|
||||
}
|
||||
|
||||
// CreateTag 创建注释标签
|
||||
func (g *GitClient) CreateTag(path, name, message string) (string, error) {
|
||||
return g.Run(path, "tag", "-a", name, "-m", message)
|
||||
}
|
||||
|
||||
// DeleteTag 删除本地标签
|
||||
func (g *GitClient) DeleteTag(path, name string) (string, error) {
|
||||
return g.Run(path, "tag", "-d", name)
|
||||
}
|
||||
|
||||
// PushTag 推送标签到远程
|
||||
func (g *GitClient) PushTag(path, name string) (string, error) {
|
||||
return g.Run(path, "push", "origin", name)
|
||||
}
|
||||
|
||||
// DeleteRemoteTag 删除远程标签
|
||||
func (g *GitClient) DeleteRemoteTag(path, name string) (string, error) {
|
||||
return g.Run(path, "push", "origin", "--delete", name)
|
||||
}
|
||||
|
||||
// BranchList 获取所有本地分支
|
||||
func (g *GitClient) BranchList(path string) (string, error) {
|
||||
return g.Run(path, "branch", "--format=%(refname:short)\t%(HEAD)")
|
||||
|
||||
Reference in New Issue
Block a user