chore: remove luckysheet integration

This commit is contained in:
liaibo
2025-11-23 10:45:45 +08:00
parent 8bf5d06be1
commit 38a8dc93d1
126 changed files with 15824 additions and 94 deletions
View File
+40
View File
@@ -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;
}
+61
View File
@@ -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,
});
}
+41
View File
@@ -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]);
};
+34
View File
@@ -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,
});
}
+77
View File
@@ -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,
};
}
+28
View File
@@ -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,
});
}