chore: init monorepo snapshot
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type Status = "idle" | "ok" | "error";
|
||||
|
||||
export function useBackendHealth() {
|
||||
const [status, setStatus] = useState<Status>(() => {
|
||||
if (!process.env.NEXT_PUBLIC_BACKEND_URL) {
|
||||
return "error";
|
||||
}
|
||||
return "idle";
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let destroyed = false;
|
||||
const url = process.env.NEXT_PUBLIC_BACKEND_URL;
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
const controller = new AbortController();
|
||||
const check = async () => {
|
||||
try {
|
||||
const response = await fetch(`${url}/health`, { signal: controller.signal });
|
||||
if (!destroyed) {
|
||||
setStatus(response.ok ? "ok" : "error");
|
||||
}
|
||||
} catch {
|
||||
if (!destroyed) {
|
||||
setStatus("error");
|
||||
}
|
||||
}
|
||||
};
|
||||
check();
|
||||
return () => {
|
||||
destroyed = true;
|
||||
controller.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return status;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { BacklinkRecord } from "@/types/references";
|
||||
|
||||
interface UseBacklinksOptions {
|
||||
workspaceId: string | null;
|
||||
documentId: string | null;
|
||||
}
|
||||
|
||||
interface RawBacklink {
|
||||
id: string;
|
||||
source_page_id: string;
|
||||
source_block_id: string | null;
|
||||
alias: string | null;
|
||||
display_mode: string;
|
||||
is_previewable: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
source_title: string | null;
|
||||
}
|
||||
|
||||
const mapBacklink = (raw: RawBacklink): BacklinkRecord => ({
|
||||
id: raw.id,
|
||||
sourcePageId: raw.source_page_id,
|
||||
sourceBlockId: raw.source_block_id,
|
||||
alias: raw.alias,
|
||||
displayMode: (raw.display_mode as BacklinkRecord["displayMode"]) ?? "inline",
|
||||
isPreviewable: raw.is_previewable,
|
||||
createdAt: raw.created_at,
|
||||
updatedAt: raw.updated_at,
|
||||
sourceTitle: raw.source_title,
|
||||
});
|
||||
|
||||
const fetchBacklinks = async (workspaceId: string, documentId: string): Promise<BacklinkRecord[]> => {
|
||||
const params = new URLSearchParams({
|
||||
workspaceId,
|
||||
pageId: documentId,
|
||||
});
|
||||
const response = await fetch(`/api/references/backlinks?${params.toString()}`);
|
||||
if (!response.ok) {
|
||||
const message = (await response.json().catch(() => null))?.error ?? "加载引用失败";
|
||||
throw new Error(message);
|
||||
}
|
||||
const payload = (await response.json()) as { backlinks: RawBacklink[] };
|
||||
return (payload.backlinks ?? []).map(mapBacklink);
|
||||
};
|
||||
|
||||
export function useBacklinks({ workspaceId, documentId }: UseBacklinksOptions) {
|
||||
return useQuery({
|
||||
queryKey: ["page-backlinks", workspaceId, documentId],
|
||||
queryFn: () => {
|
||||
if (!workspaceId || !documentId) {
|
||||
throw new Error("缺少引用参数");
|
||||
}
|
||||
return fetchBacklinks(workspaceId, documentId);
|
||||
},
|
||||
enabled: Boolean(workspaceId && documentId),
|
||||
staleTime: 15_000,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
|
||||
type Procedure<T extends unknown[]> = (...args: T) => void;
|
||||
type DebouncedProcedure<T extends unknown[]> = Procedure<T> & { cancel: () => void };
|
||||
|
||||
/**
|
||||
* 用于本地输入的防抖工具,默认等待 800ms 再触发回调。
|
||||
*/
|
||||
export const useDebouncedCallback = <T extends unknown[]>(
|
||||
callback: Procedure<T>,
|
||||
delay: number,
|
||||
): DebouncedProcedure<T> => {
|
||||
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const callbackRef = useRef(callback);
|
||||
|
||||
useEffect(() => {
|
||||
callbackRef.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
return useMemo(() => {
|
||||
const debounced = ((...args: T) => {
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
}
|
||||
timerRef.current = setTimeout(() => {
|
||||
callbackRef.current(...args);
|
||||
}, delay);
|
||||
}) as DebouncedProcedure<T>;
|
||||
|
||||
debounced.cancel = () => {
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
return debounced;
|
||||
}, [delay]);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { DocumentSearchRequest, DocumentSearchResponse } from "@/types/search";
|
||||
|
||||
const fetchDocumentSearch = async (payload: DocumentSearchRequest): Promise<DocumentSearchResponse> => {
|
||||
const response = await fetch("/api/search/documents", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const message = (await response.json().catch(() => null))?.error ?? "搜索失败,请稍后再试";
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return response.json() as Promise<DocumentSearchResponse>;
|
||||
};
|
||||
|
||||
export function useDocumentSearch(payload: DocumentSearchRequest | null, enabled: boolean) {
|
||||
return useQuery({
|
||||
queryKey: ["document-search", payload],
|
||||
queryFn: () => {
|
||||
if (!payload) {
|
||||
throw new Error("缺少搜索参数");
|
||||
}
|
||||
return fetchDocumentSearch(payload);
|
||||
},
|
||||
enabled: enabled && Boolean(payload?.workspaceId),
|
||||
staleTime: 30_000,
|
||||
gcTime: 60_000,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import type { DocumentSearchResult } from "@/types/search";
|
||||
import type { ReferenceInsertMode } from "@/store/search-palette";
|
||||
import { useEditorBridgeStore } from "@/store/editor-bridge";
|
||||
import { recordRecentPage } from "@/lib/search/record-recent";
|
||||
|
||||
interface ReferenceComposerOptions {
|
||||
workspaceId: string | null;
|
||||
sourcePageId: string | null;
|
||||
}
|
||||
|
||||
interface ComposeParams {
|
||||
mode: ReferenceInsertMode;
|
||||
alias?: string;
|
||||
}
|
||||
|
||||
export function useReferenceComposer({ workspaceId, sourcePageId }: ReferenceComposerOptions) {
|
||||
const bridge = useEditorBridgeStore((state) => state.bridge);
|
||||
const [pending, setPending] = useState(false);
|
||||
|
||||
const insertReference = useCallback(
|
||||
async (target: DocumentSearchResult, params: ComposeParams) => {
|
||||
if (!workspaceId || !sourcePageId) {
|
||||
window.alert("当前页面或工作空间信息缺失,无法插入引用");
|
||||
return;
|
||||
}
|
||||
if (!bridge) {
|
||||
window.alert("编辑器尚未准备好,请稍后再试");
|
||||
return;
|
||||
}
|
||||
|
||||
const aliasText = params.alias?.trim();
|
||||
const label = aliasText || target.title || "无标题";
|
||||
const mode = params.mode;
|
||||
const result =
|
||||
mode === "inline"
|
||||
? bridge.insertInlineReference(target, label)
|
||||
: bridge.insertEmbedReference(target);
|
||||
setPending(true);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/references/record", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
workspaceId,
|
||||
sourcePageId,
|
||||
targetPageId: target.id,
|
||||
sourceBlockId: result?.blockId ?? null,
|
||||
alias: label,
|
||||
displayMode: mode,
|
||||
isPreviewable: true,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const message = (await response.json().catch(() => null))?.error ?? "引用记录失败";
|
||||
window.alert(message);
|
||||
} else {
|
||||
void recordRecentPage(workspaceId, target.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
window.alert("引用记录失败,请稍后再试");
|
||||
} finally {
|
||||
setPending(false);
|
||||
}
|
||||
},
|
||||
[bridge, sourcePageId, workspaceId],
|
||||
);
|
||||
|
||||
return {
|
||||
insertReference,
|
||||
referencing: pending,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useQuery, type UseQueryResult } from "@tanstack/react-query";
|
||||
import type { SidebarInitialData } from "@/components/sidebar/types";
|
||||
|
||||
async function requestSidebarData(workspaceId: string): Promise<SidebarInitialData> {
|
||||
const response = await fetch(`/api/sidebar?workspaceId=${workspaceId}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
const message = payload?.error ?? "获取侧边栏数据失败";
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export function useSidebarData(initialData: SidebarInitialData): UseQueryResult<SidebarInitialData> {
|
||||
const workspaceId = initialData.activeWorkspaceId;
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["sidebar", workspaceId],
|
||||
queryFn: () => requestSidebarData(workspaceId),
|
||||
initialData,
|
||||
staleTime: 1000 * 60,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user