This commit is contained in:
2025-03-22 17:15:35 +00:00
parent 0e11c4153d
commit 8a54bb29fb
11 changed files with 204 additions and 32 deletions

View File

@@ -94,7 +94,7 @@ export default function Home() {
y={item.y}
width={item.width}
height={item.height}
componentsId={item.componentsId}
widgetsId={item.widgetsId}
/>
))}
</DraggablePanel>

View File

@@ -1,21 +1,32 @@
"use client";
import "@/app/globals.css";
import { useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import ComponentsStore from "@/stores/componentStore";
import { componentsLibrary } from "./Draggable/Draggable";
import { useDynamicWidgets } from "@/hooks/useDynamicWidgets";
import { useWidgets } from "@/hooks/useWidgets";
export default function ComponentPaletteDrawer() {
const checkboxRef = useRef<HTMLInputElement>(null);
const [componentsId, setComponentsId] = useState("logo");
const [widgetsId, setwidgetsId] = useState("logo");
const [data, setData] = useState("");
const {widgets} = useDynamicWidgets();
const {widgets, widgetsLibrary} = useWidgets();
useEffect(() => {
try {
const widget = widgets.find(item => item.id === widgetsId);
const defaultConfig = widget?.defaultConfig({})
const stringData = JSON.stringify(defaultConfig, undefined, 4);
setData(stringData);
} catch (error) {
}
}, [widgetsId]);
const onSubmit = (e: SubmitEvent) => {
e.preventDefault();
ComponentsStore.addComponent(componentsId, data && JSON.parse(data));
ComponentsStore.addComponent(widgetsId, data && JSON.parse(data));
checkboxRef.current?.click();
};
return (
<div className="drawer drawer-end">
<input
@@ -64,20 +75,15 @@ export default function ComponentPaletteDrawer() {
<div className="resize-y border overflow-hidden max-w-full h-64 ">
{
componentsId && componentsLibrary[componentsId](data)
}
9 }
</div>
{
JSON.stringify(widgets)
}
<form onSubmit={onSubmit} className="w-full">
<fieldset className="fieldset w-full">
<legend className="fieldset-legend"></legend>
<select
value={componentsId}
onChange={(e) => setComponentsId(e.target.value)}
value={widgetsId}
onChange={(e) => setwidgetsId(e.target.value)}
className="select w-full"
>
<option disabled={true}></option>
@@ -93,6 +99,7 @@ export default function ComponentPaletteDrawer() {
<fieldset className="fieldset w-full">
<legend className="fieldset-legend"></legend>
<textarea
value={data}
onChange={(e) => setData(e.target.value)}
className="textarea h-24 w-full"
placeholder="请输入 JSON 格式的配置信息"

View File

@@ -6,16 +6,17 @@ import { useDraggable } from "@dnd-kit/core";
import { useEffect, useRef, useState } from "react";
import { nearestMultiple } from "./utils";
import PreviewStore from "@/stores/previewStore";
import Logo from "../../widgets/Logo";
import Text from "../../widgets/Text";
import { useWidgets } from "@/hooks/useWidgets";
export default function Draggable(props: DraggablePropsType) {
const targetRef = useRef<HTMLDivElement>(null);
const timerRef = useRef<NodeJS.Timeout>(null);
const [size, setSize] = useState({ width: 16, height: 16 });
const { id, componentsId, data, x, y, width:_width, height:_height } = props;
const { id, widgetsId, data, x, y, width:_width, height:_height } = props;
const [width, setWidth] = useState(_width);
const [height, setHeight] = useState(_height);
const {widgets, widgetsLibrary} = useWidgets();
const { attributes, listeners, setNodeRef, transform } = useDraggable({
id,
@@ -97,22 +98,17 @@ export default function Draggable(props: DraggablePropsType) {
/>
</svg>
</button>
{componentsLibrary[componentsId] && componentsLibrary[componentsId](data ?? {})}
{widgetsLibrary[widgetsId] && widgetsLibrary[widgetsId](data ?? {})}
</div>
);
}
export type DraggablePropsType = {
id: string;
componentsId: string;
widgetsId: string;
data?: Record<string, unknown>;
x: number;
y: number;
width: number;
height: number;
};
export const componentsLibrary = {
'logo': () => <Logo />,
'text': (data: DraggablePropsType["data"]) => <Text data={{ content: 'default',...data }} />,
}

View File

@@ -43,7 +43,7 @@ export default function DraggablePanel(props: DraggablePanelType) {
PreviewStore.clearPreview();
ComponentStore.changeComponent({
id: event?.active?.data?.current?.id,
componentsId: event?.active?.data?.current?.componentsId,
widgetsId: event?.active?.data?.current?.widgetsId,
x,
y,
width: nearestMultiple(width),

110
hooks/useWidgets.tsx Normal file
View File

@@ -0,0 +1,110 @@
"use client";
import { useState, useCallback, useEffect, ReactNode } from 'react';
import dynamic from 'next/dynamic';
import { DraggablePropsType } from '@/components/Draggable/Draggable';
import * as Logo from '@/widgets/Logo/index';
import * as Text from '@/widgets/Text/index';
import * as Image from '@/widgets/Image/index';
export interface WidgetModule {
id: string;
name: string;
version: string;
defaultConfig: (data: Record<string, unknown>) => Record<string, unknown>
}
export type widgetsLibraryType = Record<string, (data?:DraggablePropsType['data']) => ReactNode>;
const widgetPaths = [
'@/widgets/Logo/index',
'@/widgets/Text/index',
'@/widgets/Image/index',
];
export const useWidgets = (): {
widgets: WidgetModule[];
widgetsLibrary: widgetsLibraryType;
refreshWidgets: () => Promise<void>;
} => {
const [widgets, setWidgets] = useState<WidgetModule[]>([
{
id: 'logo',
name: 'Logo',
version: '1.0.0',
defaultConfig: Logo.defaultConfig
},
{
id: 'text',
name: '文本',
version: '1.0.0',
defaultConfig: Text.defaultConfig
},
{
id: 'image',
name: '图片',
version: '1.0.0',
defaultConfig: Image.defaultConfig
},
]);
const [widgetsLibrary, setWidgetsLibrary] = useState<widgetsLibraryType>({
'logo': (data) => {
const { default:Component, defaultConfig } = Logo;
return <Component {...defaultConfig(data)} />
},
'text': (data) => {
const { default:Component, defaultConfig } = Text;
return <Component data={defaultConfig(data)} />
},
'image': (data) => {
const { default:Component, defaultConfig } = Image;
return <Component data={defaultConfig(data)} />
}
});
const loadWidgets = useCallback(async () => {
try {
// Webpack 的 require.context 匹配 widgets 目录
console.log(window);
// const modules = await Promise.all(
// widgetPaths.map(async (key) => {
// console.log(key);
// debugger
// const meta = await require(key);
// // 创建动态组件(单独导入确保 Tree Shaking
// // const Component = dynamic(
// // () => import(key),
// // { ssr: false }
// // );
// return {
// id: meta.id,
// name: meta.name,
// version: meta.version,
// // component: Component
// };
// })
// );
// console.log(modules);
// setWidgets(modules);
} catch (error) {
console.error('Failed to load widgets:', error);
setWidgets([]);
}
}, []);
// 初始化加载
// useEffect(() => {
// loadWidgets();
// }, [loadWidgets]);
// 暴露刷新方法
const refreshWidgets = useCallback(async () => {
await loadWidgets();
}, [loadWidgets]);
return { widgets, widgetsLibrary, refreshWidgets };
};

View File

@@ -23,6 +23,7 @@
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4.0.14",
"@types/lodash": "^4.17.16",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",

8
pnpm-lock.yaml generated
View File

@@ -45,6 +45,9 @@ importers:
'@tailwindcss/postcss':
specifier: ^4.0.14
version: 4.0.14
'@types/lodash':
specifier: ^4.17.16
version: 4.17.16
'@types/node':
specifier: ^20
version: 20.17.24
@@ -468,6 +471,9 @@ packages:
'@types/json5@0.0.29':
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
'@types/lodash@4.17.16':
resolution: {integrity: sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==}
'@types/node@20.17.24':
resolution: {integrity: sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA==}
@@ -2181,6 +2187,8 @@ snapshots:
'@types/json5@0.0.29': {}
'@types/lodash@4.17.16': {}
'@types/node@20.17.24':
dependencies:
undici-types: 6.19.8

View File

@@ -53,14 +53,14 @@ class ComponentsStore {
);
}
addComponent(componentsId: string, data:Record<string, unknown>) {
addComponent(widgetsId: string, data:Record<string, unknown>) {
this.components.push({
id: String(this.components.length),
x: 0,
y: 0,
width: 0,
height: 0,
componentsId,
widgetsId,
data,
})
}

37
widgets/Image/index.tsx Normal file
View File

@@ -0,0 +1,37 @@
"use client";
import "@/app/globals.css";
import { DraggablePropsType } from "@/components/Draggable/Draggable";
// import Image from "next/Image";
import { CSSProperties } from "react";
export const id = "image";
export const name = "Image";
export const version = "1.0.0";
export const defaultConfig = (config: ImageData) => {
return {
src: 'https://www.todaybing.com/api/today',
...config,
}
}
type ImageData = {
src: string;
customStyles?: CSSProperties; // 自定义CSS扩展
};
interface ResponsiveImageProps {
data: DraggablePropsType["data"] & ImageData;
className?: string; // 外部容器类名
}
const WidgetsImage: React.FC<ResponsiveImageProps> = ({
data,
className
}) => {
return (
<img className="w-full h-full object-cover" src={data.src} style={data.customStyles} />
);
}
export default WidgetsImage;

View File

@@ -1,10 +1,16 @@
"use client";
import "@/app/globals.css";
import { DraggablePropsType } from "@/components/Draggable/Draggable";
export const id = "logo";
export const name = "Logo";
export const version = "1.0.0";
export default function Logo() {
export const defaultConfig = (config: DraggablePropsType['data']) => {
return {
...config
}
}
export default function WidgetsLogo() {
return (
<div className="flex h-full w-full items-center justify-center text-5xl bg-base-200 font-bold">
<span className="whitespace-nowrap text-primary">

View File

@@ -3,7 +3,14 @@ import { DraggablePropsType } from '../../components/Draggable/Draggable';
export const id = "text";
export const name = "文字";
export const version = "1.0.0";
export const version = "1.0.0";
export const defaultConfig = (config: FontData) => {
return {
textAlign: 'center',
content: '默认文本',
...config,
}
}
type FontData = {
content: string;
fontSize?: number; // 字体大小px
@@ -21,7 +28,7 @@ interface ResponsiveTextProps {
className?: string; // 外部容器类名
}
const Text: React.FC<ResponsiveTextProps> = ({
const WidgetsText: React.FC<ResponsiveTextProps> = ({
data,
className
}) => {
@@ -76,4 +83,4 @@ const Text: React.FC<ResponsiveTextProps> = ({
);
};
export default Text;
export default WidgetsText;