training_data/apps/web/src/components/common/editor/NodeMenu.tsx

250 lines
7.1 KiB
TypeScript
Raw Normal View History

2025-03-02 11:49:46 +08:00
import React, { useState, useEffect, useRef, useMemo } from "react";
import { Input, Button, ColorPicker, Select } from "antd";
2025-02-26 23:18:14 +08:00
import {
2025-03-02 11:49:46 +08:00
FontSizeOutlined,
BoldOutlined,
LinkOutlined,
GlobalOutlined,
SwapOutlined,
} from "@ant-design/icons";
import type { MindElixirInstance, NodeObj } from "mind-elixir";
import PostSelect from "../../models/post/PostSelect/PostSelect";
import { Lecture, PostType } from "@nice/common";
import { xmindColorPresets } from "./constant";
import { api } from "@nice/client";
import { env } from "@web/src/env";
2025-02-26 23:18:14 +08:00
interface NodeMenuProps {
2025-03-02 11:49:46 +08:00
mind: MindElixirInstance;
2025-02-26 23:18:14 +08:00
}
const NodeMenu: React.FC<NodeMenuProps> = ({ mind }) => {
2025-03-02 11:49:46 +08:00
const [isOpen, setIsOpen] = useState(false);
const [selectedFontColor, setSelectedFontColor] = useState<string>("");
const [selectedBgColor, setSelectedBgColor] = useState<string>("");
const [selectedSize, setSelectedSize] = useState<string>("");
const [isBold, setIsBold] = useState(false);
const [urlMode, setUrlMode] = useState<"URL" | "POSTURL">("POSTURL");
const [url, setUrl] = useState<string>("");
const [postId, setPostId] = useState<string>("");
const containerRef = useRef<HTMLDivElement | null>(null);
const { data: lecture, isLoading }: { data: Lecture; isLoading: boolean } =
api.post.findFirst.useQuery(
{
where: { id: postId },
},
{ enabled: !!postId }
);
useEffect(() => {
if (urlMode === "POSTURL")
setUrl(`/course/${lecture?.courseId}/detail/${lecture?.id}`);
mind.reshapeNode(mind.currentNode, {
hyperLink: `/course/${lecture?.courseId}/detail/${lecture?.id}`,
});
}, [postId, lecture, isLoading, urlMode]);
useEffect(() => {
const handleSelectNode = (nodeObj: NodeObj) => {
setIsOpen(true);
const style = nodeObj.style || {};
setSelectedFontColor(style.color || "");
setSelectedBgColor(style.background || "");
setSelectedSize(style.fontSize || "24");
setIsBold(style.fontWeight === "bold");
setUrl(nodeObj.hyperLink || "");
};
const handleUnselectNode = () => {
setIsOpen(false);
};
mind.bus.addListener("selectNode", handleSelectNode);
mind.bus.addListener("unselectNode", handleUnselectNode);
}, [mind]);
useEffect(() => {
if (containerRef.current && mind.container) {
mind.container.appendChild(containerRef.current);
}
}, [mind.container]);
const handleColorChange = (type: "font" | "background", color: string) => {
if (type === "font") {
setSelectedFontColor(color);
} else {
setSelectedBgColor(color);
}
const patch = { style: {} as any };
if (type === "font") {
patch.style.color = color;
} else {
patch.style.background = color;
}
mind.reshapeNode(mind.currentNode, patch);
};
const handleSizeChange = (size: string) => {
setSelectedSize(size);
mind.reshapeNode(mind.currentNode, { style: { fontSize: size } });
};
const handleBoldToggle = () => {
const fontWeight = isBold ? "" : "bold";
setIsBold(!isBold);
mind.reshapeNode(mind.currentNode, { style: { fontWeight } });
};
const handleUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setUrl(value);
mind.reshapeNode(mind.currentNode, {
hyperLink: value,
});
};
return (
<div
className={`node-menu-container absolute right-2 top-2 rounded-lg bg-slate-200 shadow-xl ring-2 ring-white transition-all duration-300 ${
isOpen
? "opacity-100 translate-y-0"
: "opacity-0 translate-y-4 pointer-events-none"
}`}
ref={containerRef}>
<div className="p-5 space-y-6">
{/* Font Size Selector */}
<div className="space-y-2">
<h3 className="text-sm font-medium text-gray-600">
</h3>
<div className="flex gap-3 items-center justify-between">
<Select
value={selectedSize}
onChange={handleSizeChange}
prefix={<FontSizeOutlined className="mr-2" />}
className="w-1/2"
options={[
{ value: "12", label: "12" },
{ value: "14", label: "14" },
{ value: "16", label: "16" },
{ value: "18", label: "18" },
{ value: "20", label: "20" },
{ value: "24", label: "24" },
{ value: "28", label: "28" },
{ value: "32", label: "32" },
]}
/>
<Button
type={isBold ? "primary" : "default"}
onClick={handleBoldToggle}
className="w-1/2"
icon={<BoldOutlined />}>
</Button>
</div>
</div>
{/* Color Picker */}
<div className="space-y-3">
<h3 className="text-sm font-medium text-gray-600">
</h3>
{/* Font Color Picker */}
<div className="space-y-2">
<h4 className="text-xs font-medium text-gray-500">
</h4>
<div className="grid grid-cols-8 gap-2 p-2 bg-gray-50 rounded-lg">
{xmindColorPresets.map((color) => (
<div
key={`font-${color}`}
className={`w-6 h-6 rounded-full cursor-pointer hover:scale-105 transition-transform outline outline-2 outline-offset-1 ${
selectedFontColor === color
? "outline-blue-500"
: "outline-transparent"
}`}
style={{ backgroundColor: color }}
onClick={() => {
handleColorChange("font", color);
}}
/>
))}
</div>
</div>
{/* Background Color Picker */}
<div className="space-y-2">
<h4 className="text-xs font-medium text-gray-500">
</h4>
<div className="grid grid-cols-8 gap-2 p-2 bg-gray-50 rounded-lg">
{xmindColorPresets.map((color) => (
<div
key={`bg-${color}`}
className={`w-6 h-6 rounded-full cursor-pointer hover:scale-105 transition-transform outline outline-2 outline-offset-1 ${
selectedBgColor === color
? "outline-blue-500"
: "outline-transparent"
}`}
style={{ backgroundColor: color }}
onClick={() => {
handleColorChange("background", color);
}}
/>
))}
</div>
</div>
</div>
<div className="text-sm font-medium text-gray-600 flex items-center gap-2">
{urlMode === "URL" ? "关联链接" : "关联课时"}
<Button
type="text"
className=" hover:bg-gray-400 active:bg-gray-300 rounded-md text-gray-600 border transition-colors"
size="small"
icon={<SwapOutlined />}
onClick={() =>
setUrlMode((prev) =>
prev === "POSTURL" ? "URL" : "POSTURL"
)
}
/>
</div>
<div className="space-y-1">
{urlMode === "POSTURL" ? (
<PostSelect
onChange={(value) => {
if (typeof value === "string") {
setPostId(value);
}
}}
params={{
where: {
type: PostType.LECTURE,
},
}}
/>
) : (
<Input
placeholder="例如https://example.com"
value={url}
onChange={handleUrlChange}
addonBefore={<LinkOutlined />}
/>
)}
{urlMode === "URL" &&
url &&
!/^(https?:\/\/\S+|\/|\.\/|\.\.\/)?\S+$/.test(url) && (
<p className="text-xs text-red-500">
URL地址
</p>
)}
</div>
</div>
</div>
);
2025-02-26 23:18:14 +08:00
};
export default NodeMenu;