You've already forked agentic-coding-workflow
feat(web): WorkspaceHomeView with worktrees/main/settings tabs and CommitDialog
This commit is contained in:
@@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<AppShell>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<NPageHeader :title="store.current?.name ?? wsSlug">
|
||||||
|
<template #subtitle>
|
||||||
|
{{ store.current?.slug }}
|
||||||
|
</template>
|
||||||
|
</NPageHeader>
|
||||||
|
|
||||||
|
<SyncBar
|
||||||
|
v-if="store.current"
|
||||||
|
:ws="store.current"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NTabs
|
||||||
|
v-model:value="tab"
|
||||||
|
type="line"
|
||||||
|
>
|
||||||
|
<NTabPane
|
||||||
|
name="worktrees"
|
||||||
|
tab="Worktrees"
|
||||||
|
>
|
||||||
|
<WorktreesTab
|
||||||
|
v-if="store.current"
|
||||||
|
:ws="store.current"
|
||||||
|
/>
|
||||||
|
</NTabPane>
|
||||||
|
<NTabPane
|
||||||
|
name="main"
|
||||||
|
tab="Main"
|
||||||
|
>
|
||||||
|
<MainTab
|
||||||
|
v-if="store.current"
|
||||||
|
:ws="store.current"
|
||||||
|
/>
|
||||||
|
</NTabPane>
|
||||||
|
<NTabPane
|
||||||
|
name="settings"
|
||||||
|
tab="Settings"
|
||||||
|
>
|
||||||
|
<SettingsTab
|
||||||
|
v-if="store.current"
|
||||||
|
:ws="store.current"
|
||||||
|
/>
|
||||||
|
</NTabPane>
|
||||||
|
</NTabs>
|
||||||
|
</div>
|
||||||
|
</AppShell>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref, computed } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { NTabs, NTabPane, NPageHeader, useMessage } from 'naive-ui'
|
||||||
|
import AppShell from '@/layouts/AppShell.vue'
|
||||||
|
import { useWorkspacesStore } from '@/stores/workspaces'
|
||||||
|
import SyncBar from './components/SyncBar.vue'
|
||||||
|
import WorktreesTab from './components/WorktreesTab.vue'
|
||||||
|
import MainTab from './components/MainTab.vue'
|
||||||
|
import SettingsTab from './components/SettingsTab.vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const store = useWorkspacesStore()
|
||||||
|
const msg = useMessage()
|
||||||
|
const tab = ref<'worktrees' | 'main' | 'settings'>('worktrees')
|
||||||
|
|
||||||
|
const projectSlug = computed(() => route.params.slug as string)
|
||||||
|
const wsSlug = computed(() => route.params.wsSlug as string)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
// 通过 fetchByProject 拿列表后定位 wsID
|
||||||
|
await store.fetchByProject(projectSlug.value)
|
||||||
|
const ws = store.list.find((w) => w.slug === wsSlug.value)
|
||||||
|
if (!ws) {
|
||||||
|
msg.error('找不到工作区')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await store.fetchOne(ws.id)
|
||||||
|
await store.fetchWorktrees(ws.id)
|
||||||
|
await store.fetchCredential(ws.id)
|
||||||
|
} catch (e: unknown) {
|
||||||
|
msg.error('加载失败:' + (e instanceof Error ? e.message : String(e)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<template>
|
||||||
|
<NModal
|
||||||
|
:show="true"
|
||||||
|
preset="card"
|
||||||
|
title="提交修改"
|
||||||
|
:style="{ width: '640px' }"
|
||||||
|
@close="emit('close')"
|
||||||
|
>
|
||||||
|
<p class="text-sm text-gray-600 mb-2">
|
||||||
|
{{ status.length }} 个改动
|
||||||
|
</p>
|
||||||
|
<NScrollbar
|
||||||
|
:style="{ maxHeight: '240px' }"
|
||||||
|
class="mb-4 border rounded p-2 bg-gray-50"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="f in status"
|
||||||
|
:key="f.path"
|
||||||
|
class="font-mono text-sm py-1"
|
||||||
|
>
|
||||||
|
<NTag
|
||||||
|
size="small"
|
||||||
|
type="info"
|
||||||
|
class="mr-2"
|
||||||
|
>
|
||||||
|
{{ f.index_x }}{{ f.worktree_y }}
|
||||||
|
</NTag>{{ f.path }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="status.length === 0"
|
||||||
|
class="text-sm text-gray-400"
|
||||||
|
>
|
||||||
|
无改动
|
||||||
|
</div>
|
||||||
|
</NScrollbar>
|
||||||
|
|
||||||
|
<NForm label-placement="top">
|
||||||
|
<NFormItem
|
||||||
|
label="Commit message"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<NInput
|
||||||
|
v-model:value="message"
|
||||||
|
type="textarea"
|
||||||
|
:autosize="{ minRows: 3, maxRows: 6 }"
|
||||||
|
/>
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem>
|
||||||
|
<NCheckbox v-model:checked="push">
|
||||||
|
提交后推送到远端
|
||||||
|
</NCheckbox>
|
||||||
|
</NFormItem>
|
||||||
|
</NForm>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<NSpace justify="end">
|
||||||
|
<NButton @click="emit('close')">
|
||||||
|
取消
|
||||||
|
</NButton>
|
||||||
|
<NButton
|
||||||
|
type="primary"
|
||||||
|
:loading="submitting"
|
||||||
|
:disabled="status.length === 0"
|
||||||
|
@click="submit"
|
||||||
|
>
|
||||||
|
提交
|
||||||
|
</NButton>
|
||||||
|
</NSpace>
|
||||||
|
</template>
|
||||||
|
</NModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import {
|
||||||
|
NModal,
|
||||||
|
NForm,
|
||||||
|
NFormItem,
|
||||||
|
NInput,
|
||||||
|
NCheckbox,
|
||||||
|
NButton,
|
||||||
|
NSpace,
|
||||||
|
NScrollbar,
|
||||||
|
NTag,
|
||||||
|
useMessage,
|
||||||
|
} from 'naive-ui'
|
||||||
|
import { useWorkspacesStore } from '@/stores/workspaces'
|
||||||
|
import type { FileStatus } from '@/api/types'
|
||||||
|
|
||||||
|
// target: 'main' 时用 wsID 调 commitOnMain;'worktree' 时用 wtID 调 commitOnWorktree。
|
||||||
|
const props = defineProps<{ target: 'main' | 'worktree'; targetId: string }>()
|
||||||
|
const emit = defineEmits<{ close: []; committed: [sha: string] }>()
|
||||||
|
const store = useWorkspacesStore()
|
||||||
|
const msg = useMessage()
|
||||||
|
|
||||||
|
const status = ref<FileStatus[]>([])
|
||||||
|
const message = ref<string>('')
|
||||||
|
const push = ref<boolean>(true)
|
||||||
|
const submitting = ref<boolean>(false)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
status.value =
|
||||||
|
props.target === 'main'
|
||||||
|
? await store.statusOnMain(props.targetId)
|
||||||
|
: await store.statusOnWorktree(props.targetId)
|
||||||
|
} catch (e: unknown) {
|
||||||
|
msg.error('读取 git status 失败:' + (e instanceof Error ? e.message : String(e)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
if (!message.value.trim()) {
|
||||||
|
msg.warning('请填写 commit message')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const body = { message: message.value, push: push.value, add_all: true }
|
||||||
|
const resp =
|
||||||
|
props.target === 'main'
|
||||||
|
? await store.commitOnMain(props.targetId, body)
|
||||||
|
: await store.commitOnWorktree(props.targetId, body)
|
||||||
|
msg.success('已提交:' + resp.sha.slice(0, 8))
|
||||||
|
emit('committed', resp.sha)
|
||||||
|
} catch (e: unknown) {
|
||||||
|
msg.error('提交失败:' + (e instanceof Error ? e.message : String(e)))
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<NSpace>
|
||||||
|
<NButton
|
||||||
|
type="primary"
|
||||||
|
@click="showCommit = true"
|
||||||
|
>
|
||||||
|
提交修改
|
||||||
|
</NButton>
|
||||||
|
<NButton @click="onPush">
|
||||||
|
Push 到远端
|
||||||
|
</NButton>
|
||||||
|
</NSpace>
|
||||||
|
|
||||||
|
<CommitDialog
|
||||||
|
v-if="showCommit"
|
||||||
|
target="main"
|
||||||
|
:target-id="ws.id"
|
||||||
|
@close="showCommit = false"
|
||||||
|
@committed="showCommit = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { NSpace, NButton, useMessage } from 'naive-ui'
|
||||||
|
import { useWorkspacesStore } from '@/stores/workspaces'
|
||||||
|
import CommitDialog from './CommitDialog.vue'
|
||||||
|
import type { Workspace } from '@/api/types'
|
||||||
|
|
||||||
|
const props = defineProps<{ ws: Workspace }>()
|
||||||
|
const store = useWorkspacesStore()
|
||||||
|
const msg = useMessage()
|
||||||
|
const showCommit = ref<boolean>(false)
|
||||||
|
|
||||||
|
async function onPush() {
|
||||||
|
try {
|
||||||
|
await store.pushOnMain(props.ws.id)
|
||||||
|
msg.success('已 push')
|
||||||
|
} catch (e: unknown) {
|
||||||
|
msg.error('push 失败:' + (e instanceof Error ? e.message : String(e)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
<template>
|
||||||
|
<NSpace
|
||||||
|
vertical
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
<NCard title="基本信息">
|
||||||
|
<NForm
|
||||||
|
label-placement="left"
|
||||||
|
:label-width="100"
|
||||||
|
>
|
||||||
|
<NFormItem label="名称">
|
||||||
|
<NInput v-model:value="form.name" />
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="描述">
|
||||||
|
<NInput
|
||||||
|
v-model:value="form.description"
|
||||||
|
type="textarea"
|
||||||
|
/>
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="默认分支">
|
||||||
|
<NInput v-model:value="form.default_branch" />
|
||||||
|
</NFormItem>
|
||||||
|
</NForm>
|
||||||
|
<NButton
|
||||||
|
type="primary"
|
||||||
|
:loading="savingCore"
|
||||||
|
@click="saveCore"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</NButton>
|
||||||
|
</NCard>
|
||||||
|
|
||||||
|
<NCard title="Git 凭据">
|
||||||
|
<p class="text-sm text-gray-500 mb-2">
|
||||||
|
当前指纹:{{ store.credential?.fingerprint || '(未设置)' }}
|
||||||
|
</p>
|
||||||
|
<NForm
|
||||||
|
label-placement="left"
|
||||||
|
:label-width="100"
|
||||||
|
>
|
||||||
|
<NFormItem label="类型">
|
||||||
|
<NSelect
|
||||||
|
v-model:value="credForm.kind"
|
||||||
|
:options="kindOptions"
|
||||||
|
/>
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem
|
||||||
|
v-if="credForm.kind === 'pat'"
|
||||||
|
label="用户名"
|
||||||
|
>
|
||||||
|
<NInput v-model:value="credForm.username" />
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem
|
||||||
|
v-if="credForm.kind === 'pat'"
|
||||||
|
label="Token"
|
||||||
|
>
|
||||||
|
<NInput
|
||||||
|
v-model:value="credForm.secret"
|
||||||
|
type="password"
|
||||||
|
show-password-on="click"
|
||||||
|
/>
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem
|
||||||
|
v-if="credForm.kind === 'ssh_key'"
|
||||||
|
label="私钥(PEM)"
|
||||||
|
>
|
||||||
|
<NInput
|
||||||
|
v-model:value="credForm.secret"
|
||||||
|
type="textarea"
|
||||||
|
:autosize="{ minRows: 4, maxRows: 8 }"
|
||||||
|
/>
|
||||||
|
</NFormItem>
|
||||||
|
</NForm>
|
||||||
|
<NButton
|
||||||
|
type="primary"
|
||||||
|
:loading="savingCred"
|
||||||
|
@click="saveCred"
|
||||||
|
>
|
||||||
|
替换凭据
|
||||||
|
</NButton>
|
||||||
|
</NCard>
|
||||||
|
|
||||||
|
<NCard title="危险区">
|
||||||
|
<NButton
|
||||||
|
type="error"
|
||||||
|
@click="onDelete"
|
||||||
|
>
|
||||||
|
删除工作区
|
||||||
|
</NButton>
|
||||||
|
</NCard>
|
||||||
|
</NSpace>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import {
|
||||||
|
NForm,
|
||||||
|
NFormItem,
|
||||||
|
NInput,
|
||||||
|
NSelect,
|
||||||
|
NButton,
|
||||||
|
NSpace,
|
||||||
|
NCard,
|
||||||
|
useMessage,
|
||||||
|
useDialog,
|
||||||
|
} from 'naive-ui'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useWorkspacesStore } from '@/stores/workspaces'
|
||||||
|
import type { CredentialKind, CredentialUpsertBody, Workspace } from '@/api/types'
|
||||||
|
|
||||||
|
interface CoreFormState {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
default_branch: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CredFormState {
|
||||||
|
kind: CredentialKind
|
||||||
|
username: string
|
||||||
|
secret: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{ ws: Workspace }>()
|
||||||
|
const store = useWorkspacesStore()
|
||||||
|
const msg = useMessage()
|
||||||
|
const dialog = useDialog()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const form = ref<CoreFormState>({
|
||||||
|
name: props.ws.name,
|
||||||
|
description: props.ws.description,
|
||||||
|
default_branch: props.ws.default_branch,
|
||||||
|
})
|
||||||
|
|
||||||
|
const credForm = ref<CredFormState>({
|
||||||
|
kind: store.credential?.kind ?? 'none',
|
||||||
|
username: store.credential?.username ?? '',
|
||||||
|
secret: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const savingCore = ref(false)
|
||||||
|
const savingCred = ref(false)
|
||||||
|
|
||||||
|
const kindOptions: { label: string; value: CredentialKind }[] = [
|
||||||
|
{ label: 'HTTPS PAT', value: 'pat' },
|
||||||
|
{ label: 'SSH Key', value: 'ssh_key' },
|
||||||
|
{ label: '公开仓库(无凭据)', value: 'none' },
|
||||||
|
]
|
||||||
|
|
||||||
|
async function saveCore() {
|
||||||
|
savingCore.value = true
|
||||||
|
try {
|
||||||
|
await store.update(props.ws.id, form.value)
|
||||||
|
msg.success('已保存')
|
||||||
|
} catch (e: unknown) {
|
||||||
|
msg.error('保存失败:' + (e instanceof Error ? e.message : String(e)))
|
||||||
|
} finally {
|
||||||
|
savingCore.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveCred() {
|
||||||
|
// kind!=='none' 时 secret 必填,避免向后端推空 token;与 WorkspaceCreateModal 校验一致。
|
||||||
|
if (credForm.value.kind !== 'none' && !credForm.value.secret) {
|
||||||
|
msg.error('请填写凭据 secret')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// kind==='none' 时不带 username/secret,避免之前残留的 PAT 用户名混入 'none' 请求体。
|
||||||
|
const body: CredentialUpsertBody =
|
||||||
|
credForm.value.kind === 'none'
|
||||||
|
? { kind: 'none' }
|
||||||
|
: {
|
||||||
|
kind: credForm.value.kind,
|
||||||
|
username: credForm.value.username,
|
||||||
|
secret: credForm.value.secret,
|
||||||
|
}
|
||||||
|
savingCred.value = true
|
||||||
|
try {
|
||||||
|
await store.upsertCredential(props.ws.id, body)
|
||||||
|
credForm.value.secret = ''
|
||||||
|
msg.success('凭据已替换')
|
||||||
|
} catch (e: unknown) {
|
||||||
|
msg.error('替换失败:' + (e instanceof Error ? e.message : String(e)))
|
||||||
|
} finally {
|
||||||
|
savingCred.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDelete() {
|
||||||
|
dialog.error({
|
||||||
|
title: '确认删除工作区',
|
||||||
|
content: '将级联删除所有 worktree 并 rm -rf 磁盘文件,不可恢复。',
|
||||||
|
positiveText: '删除',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
await store.remove(props.ws.id)
|
||||||
|
msg.success('已删除')
|
||||||
|
router.push(`/p/${router.currentRoute.value.params.slug}/workspaces`)
|
||||||
|
} catch (e: unknown) {
|
||||||
|
msg.error('删除失败:' + (e instanceof Error ? e.message : String(e)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<template>
|
||||||
|
<NCard size="small">
|
||||||
|
<NSpace
|
||||||
|
align="center"
|
||||||
|
justify="space-between"
|
||||||
|
>
|
||||||
|
<NSpace align="center">
|
||||||
|
<NTag :type="ws.sync_status === 'idle' ? 'success' : ws.sync_status === 'error' ? 'error' : 'info'">
|
||||||
|
{{ ws.sync_status }}
|
||||||
|
</NTag>
|
||||||
|
<span
|
||||||
|
v-if="ws.last_synced_at"
|
||||||
|
class="text-sm text-gray-500"
|
||||||
|
>
|
||||||
|
上次同步 {{ ws.last_synced_at }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="text-sm text-gray-500"
|
||||||
|
>尚未同步</span>
|
||||||
|
<span
|
||||||
|
v-if="ws.last_sync_error"
|
||||||
|
class="text-sm text-red-600"
|
||||||
|
>{{ ws.last_sync_error }}</span>
|
||||||
|
</NSpace>
|
||||||
|
<NButton
|
||||||
|
:disabled="busy"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="onSync"
|
||||||
|
>
|
||||||
|
立即同步
|
||||||
|
</NButton>
|
||||||
|
</NSpace>
|
||||||
|
</NCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { NCard, NTag, NButton, NSpace, useMessage } from 'naive-ui'
|
||||||
|
import { useWorkspacesStore } from '@/stores/workspaces'
|
||||||
|
import type { Workspace } from '@/api/types'
|
||||||
|
|
||||||
|
const props = defineProps<{ ws: Workspace }>()
|
||||||
|
const store = useWorkspacesStore()
|
||||||
|
const msg = useMessage()
|
||||||
|
|
||||||
|
const busy = computed(() => props.ws.sync_status === 'cloning' || props.ws.sync_status === 'syncing')
|
||||||
|
|
||||||
|
async function onSync() {
|
||||||
|
// store.sync() 仅触发异步 fetch;要等真正落到 idle/error,必须 pollUntilSettled。
|
||||||
|
try {
|
||||||
|
await store.sync(props.ws.id)
|
||||||
|
msg.info('已开始同步')
|
||||||
|
await store.pollUntilSettled(props.ws.id)
|
||||||
|
msg.success('已同步')
|
||||||
|
} catch (e: unknown) {
|
||||||
|
msg.error('同步失败:' + (e instanceof Error ? e.message : String(e)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<NSpace
|
||||||
|
justify="end"
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
|
<NButton
|
||||||
|
type="primary"
|
||||||
|
@click="showCreate = true"
|
||||||
|
>
|
||||||
|
新建 worktree
|
||||||
|
</NButton>
|
||||||
|
</NSpace>
|
||||||
|
<NDataTable
|
||||||
|
:columns="columns"
|
||||||
|
:data="store.worktrees"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NModal
|
||||||
|
:show="showCreate"
|
||||||
|
preset="card"
|
||||||
|
title="新建 worktree"
|
||||||
|
:style="{ width: '420px' }"
|
||||||
|
@close="showCreate = false"
|
||||||
|
>
|
||||||
|
<NForm>
|
||||||
|
<NFormItem
|
||||||
|
label="分支"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<NInput v-model:value="newBranch" />
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="基线分支(默认 default_branch)">
|
||||||
|
<NInput v-model:value="newBase" />
|
||||||
|
</NFormItem>
|
||||||
|
</NForm>
|
||||||
|
<template #footer>
|
||||||
|
<NSpace justify="end">
|
||||||
|
<NButton @click="showCreate = false">
|
||||||
|
取消
|
||||||
|
</NButton>
|
||||||
|
<NButton
|
||||||
|
type="primary"
|
||||||
|
:loading="creating"
|
||||||
|
@click="onCreate"
|
||||||
|
>
|
||||||
|
创建
|
||||||
|
</NButton>
|
||||||
|
</NSpace>
|
||||||
|
</template>
|
||||||
|
</NModal>
|
||||||
|
|
||||||
|
<CommitDialog
|
||||||
|
v-if="commitFor"
|
||||||
|
target="worktree"
|
||||||
|
:target-id="commitFor.id"
|
||||||
|
@close="commitFor = null"
|
||||||
|
@committed="commitFor = null"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, h } from 'vue'
|
||||||
|
import {
|
||||||
|
NDataTable,
|
||||||
|
NButton,
|
||||||
|
NSpace,
|
||||||
|
NTag,
|
||||||
|
NModal,
|
||||||
|
NForm,
|
||||||
|
NFormItem,
|
||||||
|
NInput,
|
||||||
|
useMessage,
|
||||||
|
useDialog,
|
||||||
|
} from 'naive-ui'
|
||||||
|
import type { DataTableColumns } from 'naive-ui'
|
||||||
|
import { useWorkspacesStore } from '@/stores/workspaces'
|
||||||
|
import CommitDialog from './CommitDialog.vue'
|
||||||
|
import type { Workspace, Worktree, WorktreeStatus } from '@/api/types'
|
||||||
|
|
||||||
|
const props = defineProps<{ ws: Workspace }>()
|
||||||
|
const store = useWorkspacesStore()
|
||||||
|
const msg = useMessage()
|
||||||
|
const dialog = useDialog()
|
||||||
|
|
||||||
|
const showCreate = ref<boolean>(false)
|
||||||
|
const creating = ref<boolean>(false)
|
||||||
|
const newBranch = ref<string>('')
|
||||||
|
const newBase = ref<string>('')
|
||||||
|
|
||||||
|
const commitFor = ref<Worktree | null>(null)
|
||||||
|
|
||||||
|
const STATUS_TAG_TYPE: Record<WorktreeStatus, 'warning' | 'error' | 'default'> = {
|
||||||
|
active: 'warning',
|
||||||
|
pruning: 'error',
|
||||||
|
idle: 'default',
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onCreate() {
|
||||||
|
if (!newBranch.value.trim()) {
|
||||||
|
msg.error('分支名为必填')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
creating.value = true
|
||||||
|
try {
|
||||||
|
await store.createWorktree(props.ws.id, {
|
||||||
|
branch: newBranch.value,
|
||||||
|
base_branch: newBase.value || undefined,
|
||||||
|
})
|
||||||
|
showCreate.value = false
|
||||||
|
newBranch.value = ''
|
||||||
|
newBase.value = ''
|
||||||
|
} catch (e: unknown) {
|
||||||
|
msg.error('创建失败:' + (e instanceof Error ? e.message : String(e)))
|
||||||
|
} finally {
|
||||||
|
creating.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onAcquire(wt: Worktree) {
|
||||||
|
try {
|
||||||
|
await store.acquire(wt.id)
|
||||||
|
} catch (e: unknown) {
|
||||||
|
msg.error('占用失败:' + (e instanceof Error ? e.message : String(e)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onRelease(wt: Worktree) {
|
||||||
|
try {
|
||||||
|
await store.release(wt.id)
|
||||||
|
} catch (e: unknown) {
|
||||||
|
msg.error('释放失败:' + (e instanceof Error ? e.message : String(e)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDelete(wt: Worktree) {
|
||||||
|
dialog.warning({
|
||||||
|
title: '确认删除',
|
||||||
|
content: `删除 worktree "${wt.branch}" 将移除磁盘文件夹,不可恢复。`,
|
||||||
|
positiveText: '删除',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
await store.deleteWorktree(wt.id)
|
||||||
|
} catch (e: unknown) {
|
||||||
|
msg.error('删除失败:' + (e instanceof Error ? e.message : String(e)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onPush(wt: Worktree) {
|
||||||
|
try {
|
||||||
|
await store.pushOnWorktree(wt.id)
|
||||||
|
msg.success('已 push')
|
||||||
|
} catch (e: unknown) {
|
||||||
|
msg.error('push 失败:' + (e instanceof Error ? e.message : String(e)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = computed<DataTableColumns<Worktree>>(() => [
|
||||||
|
{ title: '分支', key: 'branch' },
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
key: 'status',
|
||||||
|
render(row: Worktree) {
|
||||||
|
return h(
|
||||||
|
NTag,
|
||||||
|
{ type: STATUS_TAG_TYPE[row.status] ?? 'default', size: 'small' },
|
||||||
|
{ default: () => row.status },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ title: '占用者', key: 'active_holder' },
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'ops',
|
||||||
|
render(row: Worktree) {
|
||||||
|
return h(
|
||||||
|
NSpace,
|
||||||
|
{ size: 'small' },
|
||||||
|
{
|
||||||
|
default: () => [
|
||||||
|
h(
|
||||||
|
NButton,
|
||||||
|
{ size: 'tiny', onClick: () => (commitFor.value = row) },
|
||||||
|
{ default: () => '提交' },
|
||||||
|
),
|
||||||
|
h(
|
||||||
|
NButton,
|
||||||
|
{ size: 'tiny', onClick: () => onPush(row) },
|
||||||
|
{ default: () => 'Push' },
|
||||||
|
),
|
||||||
|
row.status === 'idle'
|
||||||
|
? h(
|
||||||
|
NButton,
|
||||||
|
{ size: 'tiny', type: 'primary', onClick: () => onAcquire(row) },
|
||||||
|
{ default: () => '占用' },
|
||||||
|
)
|
||||||
|
: h(
|
||||||
|
NButton,
|
||||||
|
{ size: 'tiny', onClick: () => onRelease(row) },
|
||||||
|
{ default: () => '释放' },
|
||||||
|
),
|
||||||
|
h(
|
||||||
|
NButton,
|
||||||
|
{ size: 'tiny', type: 'error', onClick: () => onDelete(row) },
|
||||||
|
{ default: () => '删除' },
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user