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>
This commit is contained in:
92
apps/web/src/lib/queries.ts
Normal file
92
apps/web/src/lib/queries.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
/** 服务端数据:全部由 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[];
|
||||
}>,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user