This commit is contained in:
fengxiangdebaozi committed 2026-06-05 18:58:37 +08:00
commit a642ba91fa
21 files changed
+1587 -47

No files matched your search

@@ -0,0 +1,131 @@
import { apiFetch } from "@/core/api";
import { getBackendBaseURL } from "@/core/config";
// Share codes share the same alphabet/shape as conversation shares, so reuse the
// extractor instead of duplicating the regex.
export { extractShareCode } from "@/core/thread-shares/api";
/**
* Roundtable-draft share modes:
* - `import` — recipient (a logged-in user) copies the 会商 draft into their own
* account and can keep working on the copy.
* - `view` — a public, read-only page anyone can open without logging in; it
* renders the draft's Step 3 result report (方案总览 HTML).
*/
export type DraftShareMode = "import" | "view";
export interface DraftShare {
share_code: string;
owner_user_id: string;
draft_id: string;
revoked: boolean;
mode: DraftShareMode;
created_at: string | null;
}
/** Read-only roundtable-result snapshot returned by the public view-share endpoint. */
export interface PublicDraftShareSnapshot {
share_code: string;
title: string;
mode: string;
/** Self-contained Step 3 result report HTML (empty when no report exists). */
html: string;
summary: string | null;
generated_at: string | null;
has_report: boolean;
created_at: string | null;
}
export interface DraftSharePreview {
share_code: string;
shared_by: string;
title: string;
}
export interface DraftImportResult {
imported: number;
skipped: number;
draft_id: string | null;
}
const baseUrl = () => `${getBackendBaseURL()}/api/roundtable-draft-shares`;
async function parseError(response: Response, fallback: string): Promise<Error> {
const data = (await response.json().catch(() => ({}))) as { detail?: unknown };
const detail = data.detail;
const message =
typeof detail === "string" && detail
? detail
: `${fallback} (HTTP ${response.status})`;
return new Error(message);
}
/** Create (or reuse) a share code for one roundtable draft in the given mode. */
export async function createDraftShare(
draftId: string,
mode: DraftShareMode = "import",
): Promise<DraftShare> {
const response = await apiFetch(baseUrl(), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ draft_id: draftId, mode }),
});
if (!response.ok) throw await parseError(response, "创建分享失败");
return response.json() as Promise<DraftShare>;
}
/** List the current user's own roundtable-draft share links. */
export async function listDraftShares(): Promise<DraftShare[]> {
const response = await apiFetch(baseUrl());
if (!response.ok) throw await parseError(response, "加载分享列表失败");
const json = (await response.json()) as { shares: DraftShare[] };
return json.shares;
}
/** Revoke a share link so it can no longer be previewed, imported, or viewed. */
export async function revokeDraftShare(shareCode: string): Promise<void> {
const response = await apiFetch(`${baseUrl()}/${encodeURIComponent(shareCode)}`, {
method: "DELETE",
});
if (!response.ok) throw await parseError(response, "撤销分享失败");
}
/** Preview a share link before importing it. */
export async function previewDraftShare(shareCode: string): Promise<DraftSharePreview> {
const response = await apiFetch(
`${baseUrl()}/${encodeURIComponent(shareCode)}/preview`,
);
if (!response.ok) throw await parseError(response, "无法读取该分享");
return response.json() as Promise<DraftSharePreview>;
}
/** Import the roundtable draft behind a share link into the current account. */
export async function importDraftShare(shareCode: string): Promise<DraftImportResult> {
const response = await apiFetch(
`${baseUrl()}/${encodeURIComponent(shareCode)}/import`,
{ method: "POST" },
);
if (!response.ok) throw await parseError(response, "导入失败");
return response.json() as Promise<DraftImportResult>;
}
/**
* Fetch the public read-only snapshot of a `view`-mode roundtable share.
*
* Uses a plain `fetch` (no auth header) — the `/api/public/` endpoint is
* intentionally world-readable so anyone with the link can open it.
*/
export async function getPublicDraftShareSnapshot(
shareCode: string,
): Promise<PublicDraftShareSnapshot> {
const response = await fetch(
`${getBackendBaseURL()}/api/public/roundtable-shares/${encodeURIComponent(shareCode)}`,
);
if (!response.ok) throw await parseError(response, "无法打开该分享");
return response.json() as Promise<PublicDraftShareSnapshot>;
}
/** Build the shareable public URL for a `view`-mode roundtable share code. */
export function buildPublicDraftShareUrl(shareCode: string): string {
// The app uses HashRouter, so the public viewer lives at `#/roundtable-share/<code>`.
return `${window.location.origin}${window.location.pathname}#/roundtable-share/${shareCode}`;
}
@@ -0,0 +1,61 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
createDraftShare,
importDraftShare,
listDraftShares,
previewDraftShare,
revokeDraftShare,
type DraftShareMode,
} from "./api";
export function useDraftShares(enabled = true) {
return useQuery({
queryKey: ["roundtable-draft-shares"],
queryFn: listDraftShares,
enabled,
});
}
export function useCreateDraftShare() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
draftId,
mode = "import",
}: {
draftId: string;
mode?: DraftShareMode;
}) => createDraftShare(draftId, mode),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["roundtable-draft-shares"] });
},
});
}
export function useRevokeDraftShare() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (shareCode: string) => revokeDraftShare(shareCode),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["roundtable-draft-shares"] });
},
});
}
export function usePreviewDraftShare() {
return useMutation({
mutationFn: (shareCode: string) => previewDraftShare(shareCode),
});
}
export function useImportDraftShare() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (shareCode: string) => importDraftShare(shareCode),
onSuccess: () => {
// Imported drafts show up in the user's roundtable history sidebar.
void queryClient.invalidateQueries({ queryKey: ["roundtable-drafts"] });
},
});
}