feat(web): api client for project/requirement/issue

This commit is contained in:
2026-05-01 10:40:21 +08:00
parent a0087d2c25
commit d65155061a
+159
View File
@@ -0,0 +1,159 @@
import { request } from './client'
export type Phase = 'planning' | 'auditing' | 'implementing' | 'reviewing' | 'done'
export type Status = 'open' | 'closed'
export type Visibility = 'private' | 'internal'
export interface Project {
id: string
slug: string
name: string
description: string
visibility: Visibility
owner_id: string
archived_at?: string | null
created_at: string
updated_at: string
}
export interface Requirement {
id: string
project_id: string
number: number
title: string
description: string
phase: Phase
status: Status
owner_id: string
closed_at?: string | null
created_at: string
updated_at: string
}
export interface Issue {
id: string
project_id: string
requirement_id: string | null
number: number
title: string
description: string
status: Status
assignee_id: string | null
created_by: string
closed_at?: string | null
created_at: string
updated_at: string
}
export interface ListWrapper<T> {
items: T[]
}
export interface RequirementDetail {
requirement: Requirement
issues: Issue[]
}
const enc = (v: string) => encodeURIComponent(v)
export const projectsApi = {
// ===== Project =====
list: (includeArchived = false) =>
request<ListWrapper<Project>>(`/api/v1/projects/?archived=${includeArchived}`),
create: (body: { slug: string; name: string; description?: string; visibility?: Visibility }) =>
request<Project>('/api/v1/projects/', { method: 'POST', body }),
get: (slug: string) => request<Project>(`/api/v1/projects/${enc(slug)}`),
update: (
slug: string,
body: Partial<{ name: string; description: string; visibility: Visibility }>,
) => request<Project>(`/api/v1/projects/${enc(slug)}`, { method: 'PATCH', body }),
archive: (slug: string) =>
request<void>(`/api/v1/projects/${enc(slug)}/archive`, { method: 'POST' }),
unarchive: (slug: string) =>
request<void>(`/api/v1/projects/${enc(slug)}/unarchive`, { method: 'POST' }),
// ===== Requirement =====
listRequirements: (slug: string, params: { phase?: Phase; status?: Status } = {}) => {
const q = new URLSearchParams()
if (params.phase) q.set('phase', params.phase)
if (params.status) q.set('status', params.status)
const qs = q.toString()
return request<ListWrapper<Requirement>>(
`/api/v1/projects/${enc(slug)}/requirements${qs ? '?' + qs : ''}`,
)
},
createRequirement: (slug: string, body: { title: string; description?: string; owner_id?: string }) =>
request<Requirement>(`/api/v1/projects/${enc(slug)}/requirements`, {
method: 'POST',
body,
}),
getRequirement: (slug: string, number: number) =>
request<RequirementDetail>(`/api/v1/projects/${enc(slug)}/requirements/${number}`),
updateRequirement: (
slug: string,
number: number,
body: Partial<{ title: string; description: string; owner_id: string }>,
) =>
request<Requirement>(`/api/v1/projects/${enc(slug)}/requirements/${number}`, {
method: 'PATCH',
body,
}),
changeRequirementPhase: (slug: string, number: number, phase: Phase) =>
request<Requirement>(`/api/v1/projects/${enc(slug)}/requirements/${number}/phase`, {
method: 'POST',
body: { phase },
}),
closeRequirement: (slug: string, number: number) =>
request<void>(`/api/v1/projects/${enc(slug)}/requirements/${number}/close`, { method: 'POST' }),
reopenRequirement: (slug: string, number: number) =>
request<void>(`/api/v1/projects/${enc(slug)}/requirements/${number}/reopen`, { method: 'POST' }),
// ===== Issue =====
listIssues: (
slug: string,
params: { requirement?: number | 'none'; status?: Status; assignee?: string } = {},
) => {
const q = new URLSearchParams()
if (params.requirement !== undefined) q.set('requirement', String(params.requirement))
if (params.status) q.set('status', params.status)
if (params.assignee) q.set('assignee', params.assignee)
const qs = q.toString()
return request<ListWrapper<Issue>>(
`/api/v1/projects/${enc(slug)}/issues${qs ? '?' + qs : ''}`,
)
},
createIssue: (
slug: string,
body: {
title: string
description?: string
requirement_number?: number
assignee_id?: string
},
) => request<Issue>(`/api/v1/projects/${enc(slug)}/issues`, { method: 'POST', body }),
getIssue: (slug: string, number: number) =>
request<Issue>(`/api/v1/projects/${enc(slug)}/issues/${number}`),
updateIssue: (
slug: string,
number: number,
body: Partial<{
title: string
description: string
// 三态:未传字段 / 显式 null(detach) / number(attach/move)
requirement_number: number | null
}>,
) =>
request<Issue>(`/api/v1/projects/${enc(slug)}/issues/${number}`, {
method: 'PATCH',
body,
}),
assignIssue: (slug: string, number: number, assignee_id: string | null) =>
request<Issue>(`/api/v1/projects/${enc(slug)}/issues/${number}/assign`, {
method: 'POST',
body: { assignee_id },
}),
closeIssue: (slug: string, number: number) =>
request<void>(`/api/v1/projects/${enc(slug)}/issues/${number}/close`, { method: 'POST' }),
reopenIssue: (slug: string, number: number) =>
request<void>(`/api/v1/projects/${enc(slug)}/issues/${number}/reopen`, { method: 'POST' }),
}