You've already forked agentic-coding-workflow
原型阶段
This commit is contained in:
+26
-1
@@ -1,6 +1,6 @@
|
||||
import { request } from './client'
|
||||
|
||||
export type Phase = 'planning' | 'auditing' | 'implementing' | 'reviewing' | 'done'
|
||||
export type Phase = 'planning' | 'prototyping' | 'auditing' | 'implementing' | 'reviewing' | 'done'
|
||||
export type Status = 'open' | 'closed'
|
||||
export type Visibility = 'private' | 'internal'
|
||||
|
||||
@@ -47,6 +47,16 @@ export interface Issue {
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface Prototype {
|
||||
id: string
|
||||
requirement_id: string
|
||||
version: number
|
||||
content: string
|
||||
note: string
|
||||
created_by: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface ListWrapper<T> {
|
||||
items: T[]
|
||||
}
|
||||
@@ -116,6 +126,21 @@ export const projectsApi = {
|
||||
reopenRequirement: (slug: string, number: number) =>
|
||||
request<void>(`/api/v1/projects/${enc(slug)}/requirements/${number}/reopen`, { method: 'POST' }),
|
||||
|
||||
// ===== Prototype =====
|
||||
listPrototypes: (slug: string, number: number) =>
|
||||
request<{ prototypes: Prototype[] }>(
|
||||
`/api/v1/projects/${enc(slug)}/requirements/${number}/prototypes`,
|
||||
),
|
||||
createPrototype: (slug: string, number: number, body: { content: string; note?: string }) =>
|
||||
request<Prototype>(`/api/v1/projects/${enc(slug)}/requirements/${number}/prototypes`, {
|
||||
method: 'POST',
|
||||
body,
|
||||
}),
|
||||
getPrototype: (slug: string, number: number, version: number) =>
|
||||
request<{ prototype: Prototype }>(
|
||||
`/api/v1/projects/${enc(slug)}/requirements/${number}/prototypes/${version}`,
|
||||
),
|
||||
|
||||
// ===== Issue =====
|
||||
listIssues: (
|
||||
slug: string,
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { projectsApi, type Phase, type Requirement, type Status } from '@/api/projects'
|
||||
import {
|
||||
projectsApi,
|
||||
type Phase,
|
||||
type Prototype,
|
||||
type Requirement,
|
||||
type Status,
|
||||
} from '@/api/projects'
|
||||
|
||||
export const useRequirementsStore = defineStore('requirements', () => {
|
||||
const items = ref<Requirement[]>([])
|
||||
@@ -45,5 +51,41 @@ export const useRequirementsStore = defineStore('requirements', () => {
|
||||
await refresh(forSlug)
|
||||
}
|
||||
|
||||
return { items, slug, loading, filterPhase, filterStatus, refresh, create, changePhase, close, reopen }
|
||||
async function listPrototypes(forSlug: string, number: number): Promise<Prototype[]> {
|
||||
const out = await projectsApi.listPrototypes(forSlug, number)
|
||||
return out.prototypes
|
||||
}
|
||||
|
||||
async function createPrototype(
|
||||
forSlug: string,
|
||||
number: number,
|
||||
body: { content: string; note?: string },
|
||||
): Promise<Prototype> {
|
||||
return await projectsApi.createPrototype(forSlug, number, body)
|
||||
}
|
||||
|
||||
async function getPrototype(
|
||||
forSlug: string,
|
||||
number: number,
|
||||
version: number,
|
||||
): Promise<Prototype> {
|
||||
const out = await projectsApi.getPrototype(forSlug, number, version)
|
||||
return out.prototype
|
||||
}
|
||||
|
||||
return {
|
||||
items,
|
||||
slug,
|
||||
loading,
|
||||
filterPhase,
|
||||
filterStatus,
|
||||
refresh,
|
||||
create,
|
||||
changePhase,
|
||||
close,
|
||||
reopen,
|
||||
listPrototypes,
|
||||
createPrototype,
|
||||
getPrototype,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -85,6 +85,99 @@
|
||||
</p>
|
||||
</NCard>
|
||||
|
||||
<!-- Prototypes -->
|
||||
<NCard title="原型">
|
||||
<div class="flex gap-4">
|
||||
<!-- Version list -->
|
||||
<div class="w-56 shrink-0">
|
||||
<NList
|
||||
v-if="prototypes.length > 0"
|
||||
:show-divider="false"
|
||||
>
|
||||
<NListItem
|
||||
v-for="p in prototypes"
|
||||
:key="p.id"
|
||||
>
|
||||
<NThing
|
||||
class="cursor-pointer"
|
||||
@click="onSelectPrototype(p.version)"
|
||||
>
|
||||
<template #header>
|
||||
<span
|
||||
class="text-sm font-medium"
|
||||
:class="selectedVersion === p.version ? 'text-blue-600' : ''"
|
||||
>v{{ p.version }}</span>
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<span class="text-xs text-gray-400">
|
||||
{{ new Date(p.created_at).toLocaleString() }}
|
||||
</span>
|
||||
</template>
|
||||
<span
|
||||
v-if="p.note"
|
||||
class="text-xs text-gray-500"
|
||||
>{{ p.note }}</span>
|
||||
</NThing>
|
||||
</NListItem>
|
||||
</NList>
|
||||
<NEmpty
|
||||
v-else
|
||||
description="暂无原型版本"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Selected content -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<p
|
||||
v-if="selectedPrototype"
|
||||
class="text-gray-700 whitespace-pre-wrap text-sm"
|
||||
>
|
||||
{{ selectedPrototype.content }}
|
||||
</p>
|
||||
<p
|
||||
v-else
|
||||
class="text-gray-400 text-sm"
|
||||
>
|
||||
选择左侧版本以查看内容
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New version form -->
|
||||
<NDivider />
|
||||
<div
|
||||
v-if="data.requirement.status === 'open'"
|
||||
class="space-y-2"
|
||||
>
|
||||
<NInput
|
||||
v-model:value="newContent"
|
||||
type="textarea"
|
||||
placeholder="原型内容"
|
||||
:autosize="{ minRows: 4, maxRows: 12 }"
|
||||
/>
|
||||
<NInput
|
||||
v-model:value="newNote"
|
||||
placeholder="备注(可选)"
|
||||
/>
|
||||
<NButton
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="submitting"
|
||||
:disabled="!newContent.trim()"
|
||||
@click="onSubmitPrototype"
|
||||
>
|
||||
提交新版本
|
||||
</NButton>
|
||||
</div>
|
||||
<p
|
||||
v-else
|
||||
class="text-gray-400 text-sm"
|
||||
>
|
||||
需求已关闭,无法提交新版本
|
||||
</p>
|
||||
</NCard>
|
||||
|
||||
<!-- Workspace card -->
|
||||
<NCard title="工作区">
|
||||
<NSelect
|
||||
@@ -153,17 +246,20 @@ import {
|
||||
NListItem,
|
||||
NThing,
|
||||
NEmpty,
|
||||
NInput,
|
||||
NDivider,
|
||||
} from 'naive-ui'
|
||||
import AppShell from '@/layouts/AppShell.vue'
|
||||
import { projectsApi } from '@/api/projects'
|
||||
import { useRequirementsStore } from '@/stores/requirements'
|
||||
import { useWorkspacesStore } from '@/stores/workspaces'
|
||||
import type { Phase, RequirementDetail } from '@/api/projects'
|
||||
import type { Phase, Prototype, RequirementDetail } from '@/api/projects'
|
||||
|
||||
const PHASES: Phase[] = ['planning', 'auditing', 'implementing', 'reviewing', 'done']
|
||||
const PHASES: Phase[] = ['planning', 'prototyping', 'auditing', 'implementing', 'reviewing', 'done']
|
||||
|
||||
const PHASE_LABELS: Record<Phase, string> = {
|
||||
planning: '规划中',
|
||||
prototyping: '原型设计',
|
||||
auditing: '审核中',
|
||||
implementing: '实施中',
|
||||
reviewing: '评审中',
|
||||
@@ -183,10 +279,20 @@ const loading = ref(false)
|
||||
const toggling = ref(false)
|
||||
const data = ref<RequirementDetail | null>(null)
|
||||
|
||||
const prototypes = ref<Prototype[]>([])
|
||||
const selectedVersion = ref<number | null>(null)
|
||||
const newContent = ref('')
|
||||
const newNote = ref('')
|
||||
const submitting = ref(false)
|
||||
|
||||
const workspaceOptions = computed(() =>
|
||||
wsStore.list.map((w) => ({ label: `${w.name} (${w.slug})`, value: w.id })),
|
||||
)
|
||||
|
||||
const selectedPrototype = computed(() =>
|
||||
prototypes.value.find((p) => p.version === selectedVersion.value) ?? null,
|
||||
)
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -195,11 +301,46 @@ async function fetchData() {
|
||||
wsStore.fetchByProject(slug),
|
||||
])
|
||||
data.value = detail
|
||||
await refreshPrototypes()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshPrototypes() {
|
||||
prototypes.value = await store.listPrototypes(slug, number)
|
||||
// 默认选中最新版本
|
||||
if (prototypes.value.length > 0) {
|
||||
const latest = prototypes.value[prototypes.value.length - 1]
|
||||
if (selectedVersion.value === null || !selectedPrototype.value) {
|
||||
selectedVersion.value = latest.version
|
||||
}
|
||||
} else {
|
||||
selectedVersion.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function onSelectPrototype(version: number) {
|
||||
selectedVersion.value = version
|
||||
}
|
||||
|
||||
async function onSubmitPrototype() {
|
||||
if (!newContent.value.trim()) return
|
||||
submitting.value = true
|
||||
try {
|
||||
const created = await store.createPrototype(slug, number, {
|
||||
content: newContent.value,
|
||||
note: newNote.value || undefined,
|
||||
})
|
||||
newContent.value = ''
|
||||
newNote.value = ''
|
||||
selectedVersion.value = created.version
|
||||
await refreshPrototypes()
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onWorkspaceChange(v: string | null) {
|
||||
if (!data.value) return
|
||||
await projectsApi.updateRequirement(slug, number, { workspace_id: v })
|
||||
|
||||
@@ -162,10 +162,11 @@ import AppShell from '@/layouts/AppShell.vue'
|
||||
import { useRequirementsStore } from '@/stores/requirements'
|
||||
import type { Phase, Requirement } from '@/api/projects'
|
||||
|
||||
const PHASES: Phase[] = ['planning', 'auditing', 'implementing', 'reviewing', 'done']
|
||||
const PHASES: Phase[] = ['planning', 'prototyping', 'auditing', 'implementing', 'reviewing', 'done']
|
||||
|
||||
const PHASE_LABELS: Record<Phase, string> = {
|
||||
planning: '规划中',
|
||||
prototyping: '原型设计',
|
||||
auditing: '审核中',
|
||||
implementing: '实施中',
|
||||
reviewing: '评审中',
|
||||
|
||||
Reference in New Issue
Block a user