105 lines
4.0 KiB
TypeScript
105 lines
4.0 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { useBacklinks } from "@/hooks/use-backlinks";
|
|
import type { BacklinkRecord } from "@/types/references";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface PageBacklinksPanelProps {
|
|
workspaceId: string;
|
|
documentId: string;
|
|
className?: string;
|
|
defaultCollapsed?: boolean;
|
|
}
|
|
|
|
const formatRelative = (value: string) => {
|
|
if (!value) return "";
|
|
const date = new Date(value);
|
|
return date.toLocaleString();
|
|
};
|
|
|
|
const EmptyState = () => (
|
|
<div className="rounded-xl border border-dashed border-[#e2e8f0] bg-white/60 px-4 py-6 text-center text-sm text-gray-500">
|
|
暂无反向引用。试试输入 <code className="rounded bg-gray-100 px-1">[[</code> 或 <code className="rounded bg-gray-100 px-1">#</code>{" "}
|
|
来引用其他页面。
|
|
</div>
|
|
);
|
|
|
|
const BacklinkItem = ({ record }: { record: BacklinkRecord }) => (
|
|
<div className="rounded-xl border border-[#f1f5f9] bg-white p-4 shadow-sm">
|
|
<div className="flex items-center justify-between text-sm font-medium text-gray-900">
|
|
<span>{record.alias || record.sourceTitle || "无标题"}</span>
|
|
<span className="text-xs text-gray-400">{record.displayMode === "embed" ? "嵌入块" : "行内引用"}</span>
|
|
</div>
|
|
<div className="mt-1 text-xs text-gray-500">
|
|
来自页面:{record.sourceTitle || "无标题"} · 更新:{formatRelative(record.updatedAt)}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
export function PageBacklinksPanel({ workspaceId, documentId, className, defaultCollapsed }: PageBacklinksPanelProps) {
|
|
const { data, isLoading, error, refetch, isFetching } = useBacklinks({
|
|
workspaceId,
|
|
documentId,
|
|
});
|
|
|
|
const records = useMemo(() => data ?? [], [data]);
|
|
// 说明:collapsed 需要可交互;这里用一个轻量的局部状态,但默认值来自 props(用于“自定义页面:折叠引用列表”)。
|
|
const [isCollapsed, setIsCollapsed] = useState(Boolean(defaultCollapsed));
|
|
useEffect(() => setIsCollapsed(Boolean(defaultCollapsed)), [defaultCollapsed]);
|
|
// 避免页面切换时“先出现加载态、随后又消失”的闪烁:
|
|
// 当尚未拿到任何记录且正在加载时,直接不渲染面板。
|
|
if (!error && records.length === 0 && (isLoading || isFetching)) {
|
|
return null;
|
|
}
|
|
|
|
if (!isLoading && !error && records.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<section className={cn("rounded-2xl border border-[#eef2ff] bg-[#fdfdff] p-5 shadow-sm", className)}>
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<div>
|
|
<p className="text-base font-semibold text-[#1f2933]">反向引用</p>
|
|
<p className="text-xs text-gray-500">展示指向当前页面的所有页面或块引用</p>
|
|
</div>
|
|
<Button size="sm" variant="ghost" onClick={() => refetch()} disabled={isFetching}>
|
|
{isFetching ? "刷新中..." : "刷新"}
|
|
</Button>
|
|
</div>
|
|
{records.length > 0 && (
|
|
<div className="mb-4 flex items-center justify-between text-xs text-gray-500">
|
|
<span>共 {records.length} 条</span>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => setIsCollapsed((prev) => !prev)}
|
|
className="h-7 px-2"
|
|
>
|
|
{isCollapsed ? "展开" : "折叠"}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
{isLoading ? (
|
|
<div className="py-6 text-center text-sm text-gray-500">加载引用中...</div>
|
|
) : error ? (
|
|
<div className="py-6 text-center text-sm text-red-500">{(error as Error).message}</div>
|
|
) : records.length === 0 ? (
|
|
<EmptyState />
|
|
) : isCollapsed ? (
|
|
<div className="rounded-xl border border-dashed border-[#e2e8f0] bg-white/60 px-4 py-4 text-center text-xs text-gray-500">
|
|
已折叠。点击“展开”查看。
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{records.map((record) => (
|
|
<BacklinkItem key={record.id} record={record} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|