Files
bid-doctor/apps/web/src/lib/queries.ts
YANG JIANKUAN 6aaf8e768b feat(web): 用户端骨架——登录、工作台、新建项目、解析进度、读标清单
- packages/api-client:从 openapi.yaml 生成类型;cookie + CSRF / Bearer 两种鉴权、
  分片上传、fetch 读事件流(API2)
- apps/web:Vue 3 + Vite + TanStack Query,样式只来自 tokens.css / components.css(AR10);
  扣点说明放在「开始解析」旁边(红线 9);接口地址、路由方式运行时可配(架构 §5.2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 18:48:10 +08:00

93 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** 服务端数据:全部由 TanStack Query 管理,不放进 Pinia(架构 §4.2)。 */
import { unwrap, type Schemas } from "@bid-doctor/api-client";
import { useQuery } from "@tanstack/vue-query";
import { computed, toValue, type MaybeRefOrGetter } from "vue";
import { api } from "./api";
import { qk } from "./keys";
export type Me = Schemas["Me"];
export type Quota = Schemas["QuotaOverview"];
export type Plan = Schemas["Plan"];
export type ProjectListItem = Schemas["ProjectListItem"];
export type ProjectDetail = Schemas["ProjectDetail"];
export type DocumentInfo = Schemas["Document"];
export type ParseJob = Schemas["ParseJob"];
export type Checklist = Schemas["Checklist"];
/**
* 契约里 ChecklistItem 把 categories、title、importance 列为必填,但它们定义在 allOf 的 ChecklistItemFields 里,
* openapi-typescript 生成的类型仍是可选。这里按契约的 required 收窄,不改字段本身。
*/
export type ChecklistItem = Schemas["ChecklistItem"] & Required<Pick<Schemas["ChecklistItemFields"], "categories" | "title" | "importance">>;
export type Citation = Schemas["Citation"];
export type MarkingRule = Schemas["MarkingRule"];
export type SessionInfo = Schemas["SessionInfo"];
export const fetchMe = () => unwrap(api.GET("/me"));
export function useMe() {
return useQuery({ queryKey: qk.me, queryFn: fetchMe, staleTime: 60_000 });
}
export function useQuota() {
return useQuery({ queryKey: qk.quota, queryFn: () => unwrap(api.GET("/quota")) });
}
/** 某个额度单位(如 parse)的本期账户;没有这个单位时为 undefined */
export function useQuotaAccount(unit: Schemas["QuotaUnit"]) {
const q = useQuota();
return computed(() => q.data.value?.accounts.find((a) => a.unit === unit));
}
export function usePlans() {
return useQuery({ queryKey: qk.plans, queryFn: () => unwrap(api.GET("/plans")), staleTime: 10 * 60_000 });
}
export function useProjects() {
// 骨架阶段只取第一页(默认按截止时间排序);项目多了再接游标分页
return useQuery({ queryKey: qk.projects, queryFn: () => unwrap(api.GET("/projects", { params: { query: { limit: 100 } } })) });
}
export function useProject(id: MaybeRefOrGetter<string | undefined>) {
return useQuery({
queryKey: computed(() => qk.project(toValue(id) ?? "")),
queryFn: () => unwrap(api.GET("/projects/{project_id}", { params: { path: { project_id: toValue(id)! } } })),
enabled: computed(() => !!toValue(id)),
});
}
export function useDocument(id: MaybeRefOrGetter<string | undefined>) {
return useQuery({
queryKey: computed(() => qk.document(toValue(id) ?? "")),
queryFn: () => unwrap(api.GET("/documents/{document_id}", { params: { path: { document_id: toValue(id)! } } })),
enabled: computed(() => !!toValue(id)),
});
}
export const TERMINAL_JOB: ReadonlySet<Schemas["JobStatus"]> = new Set(["succeeded", "failed", "cancelled"]);
/** 解析进度:事件流推送为主(lib/events.ts 直接改这条缓存),每 5 秒轮询兜底,结束后停止轮询。 */
export function useParseJob(id: MaybeRefOrGetter<string>) {
return useQuery({
queryKey: computed(() => qk.parseJob(toValue(id))),
queryFn: () => unwrap(api.GET("/parse-jobs/{job_id}", { params: { path: { job_id: toValue(id) } } })),
refetchInterval: (query) => (query.state.data && TERMINAL_JOB.has(query.state.data.status) ? false : 5000),
});
}
export function useChecklist(projectId: MaybeRefOrGetter<string>) {
return useQuery({
queryKey: computed(() => qk.checklist(toValue(projectId))),
queryFn: () => unwrap(api.GET("/projects/{project_id}/checklist", { params: { path: { project_id: toValue(projectId) } } })),
});
}
export function useChecklistItems(projectId: MaybeRefOrGetter<string>) {
return useQuery({
queryKey: computed(() => qk.checklistItems(toValue(projectId))),
queryFn: () =>
unwrap(api.GET("/projects/{project_id}/checklist/items", { params: { path: { project_id: toValue(projectId) } } })) as Promise<{
items: ChecklistItem[];
}>,
});
}