feat(web): WorkspaceListView and create modal

This commit is contained in:
2026-05-02 23:13:35 +08:00
parent deaa738bf9
commit d962ba2f16
2 changed files with 313 additions and 0 deletions
@@ -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>