You've already forked agentic-coding-workflow
feat(web): WorkspaceListView and create modal
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<AppShell>
|
||||
<NSpin :show="store.loading">
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl font-semibold">
|
||||
工作区
|
||||
</h1>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="showCreate = true"
|
||||
>
|
||||
新建工作区
|
||||
</NButton>
|
||||
</div>
|
||||
|
||||
<!-- Workspace list -->
|
||||
<NDataTable
|
||||
:columns="columns"
|
||||
:data="store.list"
|
||||
:loading="store.loading"
|
||||
:row-props="rowProps"
|
||||
:bordered="false"
|
||||
striped
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="!store.loading && store.list.length === 0"
|
||||
class="text-gray-400 text-sm py-8 text-center"
|
||||
>
|
||||
暂无工作区,点击「新建工作区」创建第一个。
|
||||
</div>
|
||||
</div>
|
||||
</NSpin>
|
||||
|
||||
<!-- Create workspace modal -->
|
||||
<WorkspaceCreateModal
|
||||
v-if="showCreate"
|
||||
:project-slug="projectSlug"
|
||||
@close="showCreate = false"
|
||||
@created="onCreated"
|
||||
/>
|
||||
</AppShell>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, ref, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { NButton, NDataTable, NTag, NSpin, useMessage } from 'naive-ui'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import AppShell from '@/layouts/AppShell.vue'
|
||||
import { useWorkspacesStore } from '@/stores/workspaces'
|
||||
import type { SyncStatus, Workspace } from '@/api/types'
|
||||
import WorkspaceCreateModal from './components/WorkspaceCreateModal.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const store = useWorkspacesStore()
|
||||
const msg = useMessage()
|
||||
|
||||
const projectSlug = computed(() => route.params.slug as string)
|
||||
const showCreate = ref(false)
|
||||
|
||||
const SYNC_TAG_TYPE: Record<SyncStatus, 'success' | 'info' | 'warning' | 'error'> = {
|
||||
idle: 'success',
|
||||
cloning: 'info',
|
||||
syncing: 'info',
|
||||
error: 'error',
|
||||
}
|
||||
|
||||
const columns: DataTableColumns<Workspace> = [
|
||||
{ title: '名称', key: 'name' },
|
||||
{
|
||||
title: 'Slug',
|
||||
key: 'slug',
|
||||
render: (row) => h('span', { class: 'font-mono text-xs text-gray-500' }, row.slug),
|
||||
},
|
||||
{
|
||||
title: '同步状态',
|
||||
key: 'sync_status',
|
||||
width: 120,
|
||||
render: (row) =>
|
||||
h(
|
||||
NTag,
|
||||
{ type: SYNC_TAG_TYPE[row.sync_status] ?? 'default', size: 'small' },
|
||||
{ default: () => row.sync_status },
|
||||
),
|
||||
},
|
||||
{ title: '默认分支', key: 'default_branch' },
|
||||
{
|
||||
title: '上次同步',
|
||||
key: 'last_synced_at',
|
||||
render: (row) => row.last_synced_at ?? '—',
|
||||
},
|
||||
]
|
||||
|
||||
function rowProps(row: Workspace) {
|
||||
return {
|
||||
style: 'cursor: pointer',
|
||||
onClick: () => router.push(`/p/${projectSlug.value}/w/${row.slug}`),
|
||||
}
|
||||
}
|
||||
|
||||
function onCreated() {
|
||||
showCreate.value = false
|
||||
store.fetchByProject(projectSlug.value)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await store.fetchByProject(projectSlug.value)
|
||||
} catch (e: unknown) {
|
||||
msg.error('加载工作区失败:' + (e instanceof Error ? e.message : String(e)))
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<NModal
|
||||
:show="true"
|
||||
preset="card"
|
||||
title="新建工作区"
|
||||
:style="{ width: '560px' }"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<NForm
|
||||
label-placement="left"
|
||||
:label-width="100"
|
||||
>
|
||||
<NFormItem
|
||||
label="Slug"
|
||||
required
|
||||
>
|
||||
<NInput
|
||||
v-model:value="form.slug"
|
||||
placeholder="小写字母/数字/连字符,1–32 位"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="名称">
|
||||
<NInput v-model:value="form.name" />
|
||||
</NFormItem>
|
||||
<NFormItem label="描述">
|
||||
<NInput
|
||||
v-model:value="form.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="(可选)"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem
|
||||
label="Git 远端"
|
||||
required
|
||||
>
|
||||
<NInput
|
||||
v-model:value="form.git_remote_url"
|
||||
placeholder="https://... / git@... / ssh://..."
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="默认分支">
|
||||
<NInput v-model:value="form.default_branch" />
|
||||
</NFormItem>
|
||||
<NFormItem label="凭据类型">
|
||||
<NSelect
|
||||
v-model:value="form.credential.kind"
|
||||
:options="kindOptions"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem
|
||||
v-if="form.credential.kind === 'pat'"
|
||||
label="用户名"
|
||||
>
|
||||
<NInput
|
||||
v-model:value="form.credential.username"
|
||||
placeholder="GitHub 通常填 git 或用户名"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem
|
||||
v-if="form.credential.kind === 'pat'"
|
||||
label="Token"
|
||||
required
|
||||
>
|
||||
<NInput
|
||||
v-model:value="form.credential.secret"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem
|
||||
v-if="form.credential.kind === 'ssh_key'"
|
||||
label="私钥(PEM)"
|
||||
required
|
||||
>
|
||||
<NInput
|
||||
v-model:value="form.credential.secret"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 4, maxRows: 8 }"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
<template #footer>
|
||||
<NSpace justify="end">
|
||||
<NButton @click="emit('close')">
|
||||
取消
|
||||
</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
@click="submit"
|
||||
>
|
||||
创建
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import {
|
||||
NModal,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInput,
|
||||
NSelect,
|
||||
NButton,
|
||||
NSpace,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import { useWorkspacesStore } from '@/stores/workspaces'
|
||||
import type { CredentialKind, CredentialUpsertBody } from '@/api/types'
|
||||
|
||||
interface CredentialFormState {
|
||||
kind: CredentialKind
|
||||
username: string
|
||||
secret: string
|
||||
}
|
||||
|
||||
interface CreateFormState {
|
||||
slug: string
|
||||
name: string
|
||||
description: string
|
||||
git_remote_url: string
|
||||
default_branch: string
|
||||
credential: CredentialFormState
|
||||
}
|
||||
|
||||
const props = defineProps<{ projectSlug: string }>()
|
||||
const emit = defineEmits<{ close: []; created: [] }>()
|
||||
|
||||
const store = useWorkspacesStore()
|
||||
const msg = useMessage()
|
||||
|
||||
const form = ref<CreateFormState>({
|
||||
slug: '',
|
||||
name: '',
|
||||
description: '',
|
||||
git_remote_url: '',
|
||||
default_branch: 'main',
|
||||
credential: {
|
||||
kind: 'pat',
|
||||
username: 'git',
|
||||
secret: '',
|
||||
},
|
||||
})
|
||||
|
||||
const submitting = ref(false)
|
||||
|
||||
const kindOptions: { label: string; value: CredentialKind }[] = [
|
||||
{ label: 'HTTPS PAT', value: 'pat' },
|
||||
{ label: 'SSH Key', value: 'ssh_key' },
|
||||
{ label: '公开仓库(无凭据)', value: 'none' },
|
||||
]
|
||||
|
||||
function buildCredential(): CredentialUpsertBody {
|
||||
if (form.value.credential.kind === 'none') {
|
||||
return { kind: 'none' }
|
||||
}
|
||||
return {
|
||||
kind: form.value.credential.kind,
|
||||
username: form.value.credential.username,
|
||||
secret: form.value.credential.secret,
|
||||
}
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
// 必填字段前置校验:与 ProjectListView/RequirementListView 保持一致,避免空请求穿透到后端。
|
||||
if (!form.value.slug.trim() || !form.value.git_remote_url.trim()) {
|
||||
msg.error('Slug 与 Git 远端为必填')
|
||||
return
|
||||
}
|
||||
if (form.value.credential.kind !== 'none' && !form.value.credential.secret) {
|
||||
msg.error('请填写凭据 secret')
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await store.create(props.projectSlug, {
|
||||
slug: form.value.slug,
|
||||
name: form.value.name || form.value.slug,
|
||||
description: form.value.description,
|
||||
git_remote_url: form.value.git_remote_url,
|
||||
default_branch: form.value.default_branch || 'main',
|
||||
credential: buildCredential(),
|
||||
})
|
||||
msg.success('工作区创建成功,正在异步克隆')
|
||||
emit('created')
|
||||
} catch (e: unknown) {
|
||||
msg.error('创建失败:' + (e instanceof Error ? e.message : String(e)))
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user