props and context, fixed css bugs

This commit is contained in:
pqcqaq 2024-07-16 16:42:02 +08:00
parent da63ff64e8
commit 1ccfd44aa0
25 changed files with 994 additions and 385 deletions

View File

@ -9,6 +9,26 @@
<script>
tailwind.config = {
theme: {
colors: {
blue: "#1fb6ff",
purple: "#7e5bef",
pink: "#ff49db",
orange: "#ff7849",
green: "#13ce66",
yellow: "#ffc82c",
"gray-dark": "#273444",
gray: "#8492a6",
"gray-light": "#d3dce6",
transparent: "transparent",
current: "currentColor",
white: "#ffffff",
midnight: "#121063",
metal: "#565584",
tahiti: "#3ab7bf",
silver: "#ecebff",
"bubble-gum": "#ff77e9",
bermuda: "#78dcca",
},
extend: {
spacing: {
"8xl": "96rem",
@ -17,9 +37,6 @@
borderRadius: {
"4xl": "2rem",
},
fontFamily: {
display: "Oswald, ui-serif ", // Adds a new `font-display` class
},
colors: {
primary: {
1: "var(--primary-1)",
@ -104,4 +121,5 @@
<script type="module" src="/src/main.tsx"></script>
</body>
<link rel="stylesheet" type="text/css" href="/default.css">
</html>

View File

@ -12,7 +12,8 @@
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-live": "file:../react-live"
"sucrase": "^3.31.0",
"use-editable": "^2.3.3"
},
"devDependencies": {
"@types/react": "^18.0.31",

105
public/default.css Normal file
View File

@ -0,0 +1,105 @@
/* 下面是统一的颜色定义 */
/* 默认的主题样式 */
:root {
background-color: var(--color-bg-1);
color: var(--color-text-1);
font: 14px/1.5 "Helvetica Neue",Helvetica,Arial,"Microsoft Yahei","Hiragino Sans GB","Heiti SC","WenQuanYi Micro Hei",sans-serif;
}
button {
background-color: var(--primary-6);
color: var(--color-bg-white);
border: var(--border-1) solid var(--color-border-3);
border-radius: var(--border-radius-small);
padding: var(--size-3) var(--size-4);
font-size: var(--font-size-body-3);
box-shadow: var(--shadow1-center);
}
button:hover {
background-color: var(--primary-5);
border-color: var(--color-border-4);
}
button:active {
background-color: var(--primary-7);
border-color: var(--color-border-4);
}
input, textarea {
background-color: var(--color-fill-1);
color: var(--color-text-1);
border: var(--border-1) solid var(--color-border-2);
border-radius: var(--border-radius-small);
padding: var(--size-3);
font-size: var(--font-size-body-3);
box-shadow: var(--shadow1-center);
}
.card {
background-color: var(--color-bg-2);
color: var(--color-text-1);
border: var(--border-1) solid var(--color-border-1);
border-radius: var(--border-radius-large);
padding: var(--size-5);
box-shadow: var(--shadow2-center);
font-size: var(--font-size-body-3);
}
a {
color: var(--link-6);
text-decoration: none;
}
a:hover {
color: var(--link-5);
text-decoration: underline;
}
h1, h2, h3, h4, h5, h6 {
color: var(--color-text-1);
}
p {
color: var(--color-text-1);
font-size: var(--font-size-body-3);
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: var(--border-1) solid var(--color-border-2);
padding: var(--size-3);
text-align: left;
}
thead {
background-color: var(--color-bg-2);
}
.alert {
background-color: var(--danger-1);
color: var(--danger-6);
border: var(--border-1) solid var(--danger-4);
border-radius: var(--border-radius-small);
padding: var(--size-3);
}
.success {
background-color: var(--success-1);
color: var(--success-6);
border: var(--border-1) solid var(--success-4);
border-radius: var(--border-radius-small);
padding: var(--size-3);
}
.warning {
background-color: var(--warning-1);
color: var(--warning-6);
border: var(--border-1) solid var(--warning-4);
border-radius: var(--border-radius-small);
padding: var(--size-3);
}

View File

@ -1,226 +1,11 @@
import { LiveProvider, LivePreview, LiveError, LiveEditor } from "react-live";
import { ThemeProvider } from "./components/ThemePrivider";
import LiveEditor from "./components/Live/LiveEditor";
import LiveError from "./components/Live/LiveError";
import LivePreview from "./components/Live/LivePreview";
import LiveProvider from "./components/Live/LiveProvider";
import { ThemeProvider } from "./components/Theme/ThemePrivider";
import { useState } from "react";
const functionBlockList = [
`
const cpn = () => {
function handleClick() {
console.log("Hi there!");
}
return (
<div className="bg-primary-1 py-16 px-8">
<div className="max-w-7xl mx-auto text-center">
<h1 className="text-primary text-5xl md:text-7xl font-extrabold mb-4">
AI智能建站助手
</h1>
<p className="text-primary text-xl md:text-2xl mb-8">
</p>
<button
onClick={handleClick}
className="font-bold py-3 px-6 rounded-full shadow-lg transition duration-300 ease-in-out"
>
使
</button>
</div>
</div>
);
};
`,
`
const cpn = () => {
return (
<div className="bg-background-2 py-16 px-8">
<div className="max-w-7xl mx-auto">
<h2 className="text-text-1 text-4xl font-bold mb-4 text-center">
</h2>
<p className="text-text-2 text-lg mb-8 text-center">
AI智能建站助手提供多种功能
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="p-6 rounded-lg shadow-lg text-center">
<h3 className="text-primary text-2xl font-semibold mb-4">
</h3>
<p className="text-text-3">
AI技术自动生成最优的网页布局
</p>
</div>
<div className="p-6 rounded-lg shadow-lg text-center">
<h3 className="text-primary text-2xl font-semibold mb-4">
</h3>
<p className="text-text-3">
SEO排名
</p>
</div>
<div className="p-6 rounded-lg shadow-lg text-center">
<h3 className="text-primary text-2xl font-semibold mb-4">
</h3>
<p className="text-text-3">
</p>
</div>
</div>
</div>
</div>
);
};
`,
`
const cpn = () => {
return (
<div className="bg-background-1 py-16 px-8">
<div className="max-w-7xl mx-auto text-center">
<h2 className="text-text-1 text-4xl font-bold mb-4">
使
</h2>
<p className="text-text-2 text-lg mb-8">
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="p-6 rounded-lg shadow-lg">
<h3 className="text-primary text-2xl font-semibold mb-4">
</h3>
<p className="text-text-3">
AI工具创建了一个高效
</p>
</div>
<div className="p-6 rounded-lg shadow-lg">
<h3 className="text-primary text-2xl font-semibold mb-4">
</h3>
<p className="text-text-3">
</p>
</div>
</div>
</div>
</div>
);
};
`,
`
const cpn = () => {
return (
<div className="bg-background-2 py-16 px-8">
<div className="max-w-7xl mx-auto text-center">
<h2 className="text-text-1 text-4xl font-bold mb-4">
</h2>
<p className="text-text-2 text-lg mb-8">
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="p-6 rounded-lg shadow-lg">
<h3 className="text-primary text-2xl font-semibold mb-4">
A
</h3>
<p className="text-text-3">
AI智能建站助手帮助我节省了大量时间
</p>
</div>
<div className="p-6 rounded-lg shadow-lg">
<h3 className="text-primary text-2xl font-semibold mb-4">
B
</h3>
<p className="text-text-3">
使
</p>
</div>
</div>
</div>
</div>
);
};
`,
`
const cpn = () => {
return (
<div className="bg-background-1 py-16 px-8">
<div className="max-w-7xl mx-auto text-center">
<h2 className="text-text-1 text-4xl font-bold mb-4">
</h2>
<p className="text-text-2 text-lg mb-8">
使
</p>
<div className="text-left max-w-3xl mx-auto">
<div className="mb-8">
<h3 className="text-primary text-2xl font-semibold mb-2">
使AI智能建站助手
</h3>
<p className="text-text-3">
</p>
</div>
<div className="mb-8">
<h3 className="text-primary text-2xl font-semibold mb-2">
</h3>
<p className="text-text-3">
</p>
</div>
<div className="mb-8">
<h3 className="text-primary text-2xl font-semibold mb-2">
</h3>
<p className="text-text-3">
</p>
</div>
</div>
</div>
</div>
);
};
`,
`
const cpn = () => {
return (
<div className="bg-primary-1 py-16 px-8">
<div className="max-w-7xl mx-auto text-center">
<h2 className="text-text-1 text-4xl font-bold mb-4">
</h2>
<p className="text-text-2 text-lg mb-8">
</p>
<form className="max-w-3xl mx-auto text-left">
<div className="mb-4">
<label className="block text-text-1 text-sm font-bold mb-2">
</label>
<input className="shadow appearance-none border rounded w-full py-2 px-3 text-text-1 leading-tight focus:outline-none focus:shadow-outline" id="name" type="text" placeholder="您的姓名"/>
</div>
<div className="mb-4">
<label className="block text-text-1 text-sm font-bold mb-2">
</label>
<input className="shadow appearance-none border rounded w-full py-2 px-3 text-text-1 leading-tight focus:outline-none focus:shadow-outline" id="email" type="email" placeholder="您的邮箱"/>
</div>
<div className="mb-4">
<label className="block text-text-1 text-sm font-bold mb-2">
</label>
<textarea className="shadow appearance-none border rounded w-full py-2 px-3 text-text-1 leading-tight focus:outline-none focus:shadow-outline" id="message" placeholder="您的留言"></textarea>
</div>
<button className="bg-primary-6 font-bold py-2 px-4 transition duration-300 ease-in-out" type="button">
</button>
</form>
</div>
</div>
);
};
`,
];
import { functionBlockList } from "./data/functionBlocks";
import ClickContextProvider from "./components/Selected/ClickContext";
const code = (func: string) => `
${func}
@ -229,7 +14,7 @@ render(cpn)
const editor = (setShowEdit: React.Dispatch<React.SetStateAction<boolean>>) => {
return (
<div className="fixed inset-0 flex flex-col items-center bg-gray-900 p-4 overflow-y-auto">
<div className="bg-primary-1 fixed inset-0 flex flex-col items-center bg-gray-900 p-4 overflow-y-auto">
<div className="fixed top-4 right-2">
<button
className="bg-primary-6 text-white py-2 px-4 rounded-lg mr-2"
@ -246,10 +31,10 @@ const editor = (setShowEdit: React.Dispatch<React.SetStateAction<boolean>>) => {
key={index}
>
<LiveEditor
code={code(func)}
code={code(func.code)}
language={"tsx"}
onChange={(code) => {
functionBlockList[index] = code
functionBlockList[index].code = code
.replace("render(cpn)", "")
.trim();
}}
@ -258,11 +43,10 @@ const editor = (setShowEdit: React.Dispatch<React.SetStateAction<boolean>>) => {
<button
className="bg-primary-6 text-white py-2 px-4 rounded-lg mt-2"
onClick={() => {
functionBlockList.splice(
index + 1,
0,
"const cpn = () => {return <>Hello World</>};"
);
functionBlockList.splice(index + 1, 0, {
code: "const cpn = () => {return <>Hello World</>};",
props: {},
});
setShowEdit(false);
}}
>
@ -327,9 +111,32 @@ export const DemoApp = () => {
)}
{functionBlockList.map((func, index) => {
return (
<LiveProvider code={code(func)} noInline key={index}>
<LivePreview />
<LiveError />
<LiveProvider code={code(func.code)} noInline key={index}>
<ClickContextProvider
meun={[
{
key: "copy",
text: "复制",
handler: ({ selected }) => {
if (selected) {
navigator.clipboard.writeText(selected.outerHTML);
}
},
},
{
key: "delete",
text: "删除",
handler: ({ selected }) => {
if (selected) {
selected.remove();
}
},
},
]}
>
<LivePreview {...func.props} />
<LiveError />
</ClickContextProvider>
</LiveProvider>
);
})}

View File

@ -0,0 +1,85 @@
import { Highlight, Prism, themes } from "prism-react-renderer";
import { CSSProperties, useEffect, useRef, useState } from "react";
import { useEditable } from "use-editable";
export type Props = {
className?: string;
code: string;
disabled?: boolean;
language: string;
prism?: typeof Prism;
style?: CSSProperties;
tabMode?: "focus" | "indentation";
theme?: typeof themes.nightOwl;
onChange?(value: string): void;
};
const CodeEditor = (props: Props) => {
const { tabMode = "indentation" } = props;
const editorRef = useRef(null);
const [code, setCode] = useState(props.code || "");
const { theme } = props;
useEffect(() => {
setCode(props.code);
}, [props.code]);
useEditable(editorRef, (text) => setCode(text.slice(0, -1)), {
disabled: props.disabled,
indentation: tabMode === "indentation" ? 2 : undefined,
});
useEffect(() => {
if (props.onChange) {
props.onChange(code);
}
}, [code]);
return (
<div className={props.className} style={props.style}>
<Highlight
code={code}
theme={props.theme || themes.nightOwl}
language={props.language}
>
{({
className: _className,
tokens,
getLineProps,
getTokenProps,
style: _style,
}) => (
<pre
className={_className}
style={{
margin: 0,
outline: "none",
padding: 10,
fontFamily: "inherit",
...(theme && typeof theme.plain === "object" ? theme.plain : {}),
..._style,
}}
ref={editorRef}
spellCheck="false"
>
{tokens.map((line, lineIndex) => (
<span key={`line-${lineIndex}`} {...getLineProps({ line })}>
{line
.filter((token) => !token.empty)
.map((token, tokenIndex) => (
<span
key={`token-${tokenIndex}`}
{...getTokenProps({ token })}
/>
))}
{"\n"}
</span>
))}
</pre>
)}
</Highlight>
</div>
);
};
export default CodeEditor;

View File

@ -0,0 +1,17 @@
import { themes } from "prism-react-renderer";
import { ComponentType, createContext } from "react";
type ContextValue = {
error?: string;
element?: ComponentType | null;
code: string;
disabled: boolean;
language: string;
theme?: typeof themes.nightOwl;
onError(error: Error): void;
onChange(value: string): void;
};
const LiveContext = createContext<ContextValue>({} as ContextValue);
export default LiveContext;

View File

@ -0,0 +1,18 @@
import { useContext } from "react";
import LiveContext from "./LiveContext";
import Editor, { Props as EditorProps } from "../Editor";
export default function LiveEditor(props: Partial<EditorProps>) {
const { code, language, theme, disabled, onChange } = useContext(LiveContext);
return (
<Editor
theme={theme}
code={code}
language={language}
disabled={disabled}
onChange={onChange}
{...props}
/>
);
}

View File

@ -0,0 +1,7 @@
import { useContext } from "react";
import LiveContext from "./LiveContext";
export default function LiveError<T extends Record<string, unknown>>(props: T) {
const { error } = useContext(LiveContext);
return error ? <pre {...props}>{error}</pre> : null;
}

View File

@ -0,0 +1,22 @@
import React, { useContext } from "react";
import LiveContext from "./LiveContext";
type Props<T extends React.ElementType = React.ElementType> = {
Component?: T;
} & React.ComponentPropsWithoutRef<T>;
function LivePreview<T extends keyof JSX.IntrinsicElements>(
props: Props<T>
): JSX.Element;
function LivePreview<T extends React.ElementType>(props: Props<T>): JSX.Element;
function LivePreview({ Component = "div", ...rest }: Props): JSX.Element {
const { element: Element } = useContext(LiveContext);
return (
<Component {...rest}>
{Element ? React.cloneElement(<Element />, rest) : null}
</Component>
);
}
export default LivePreview;

View File

@ -0,0 +1,109 @@
import { useEffect, useState, ComponentType, PropsWithChildren } from "react";
import LiveContext from "./LiveContext";
import { generateElement, renderElementAsync } from "../../utils/transpile";
import { themes } from "prism-react-renderer";
type ProviderState = {
element?: ComponentType | null;
error?: string;
};
type Props = {
code?: string;
disabled?: boolean;
enableTypeScript?: boolean;
language?: string;
noInline?: boolean;
scope?: Record<string, unknown>;
theme?: typeof themes.nightOwl;
transformCode?(code: string): void;
};
function LiveProvider({
children,
code = "",
language = "tsx",
theme,
enableTypeScript = true,
disabled = false,
scope,
transformCode,
noInline = false,
}: PropsWithChildren<Props>) {
const [state, setState] = useState<ProviderState>({
error: undefined,
element: undefined,
});
async function transpileAsync(newCode: string) {
const errorCallback = (error: Error) => {
setState({ error: error.toString(), element: undefined });
};
// - transformCode may be synchronous or asynchronous.
// - transformCode may throw an exception or return a rejected promise, e.g.
// if newCode is invalid and cannot be transformed.
// - Not using async-await to since it requires targeting ES 2017 or
// importing regenerator-runtime... in the next major version of
// react-live, should target ES 2017+
try {
const transformResult = transformCode ? transformCode(newCode) : newCode;
try {
const transformedCode = await Promise.resolve(transformResult);
const renderElement = (element: ComponentType) =>
setState({ error: undefined, element });
if (typeof transformedCode !== "string") {
throw new Error("Code failed to transform");
}
// Transpilation arguments
const input = {
code: transformedCode,
scope,
enableTypeScript,
};
if (noInline) {
setState({ error: undefined, element: null }); // Reset output for async (no inline) evaluation
renderElementAsync(input, renderElement, errorCallback);
} else {
renderElement(generateElement(input, errorCallback));
}
} catch (error) {
return errorCallback(error as Error);
}
} catch (e) {
errorCallback(e as Error);
return Promise.resolve();
}
}
const onError = (error: Error) => setState({ error: error.toString() });
useEffect(() => {
transpileAsync(code).catch(onError);
}, [code, scope, noInline, transformCode]);
const onChange = (newCode: string) => {
transpileAsync(newCode).catch(onError);
};
return (
<LiveContext.Provider
value={{
...state,
code,
language,
theme,
disabled,
onError,
onChange,
}}
>
{children}
</LiveContext.Provider>
);
}
export default LiveProvider;

View File

@ -0,0 +1,14 @@
.selected {
border: 2px solid black;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.context-menu {
position: absolute;
background-color: white;
border: 1px solid #ccc;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
padding: 10px;
z-index: 1000;
}

View File

@ -0,0 +1,122 @@
import React, {
createContext,
useContext,
useEffect,
useState,
ReactNode,
CSSProperties,
} from "react";
import "./ClickContext.css";
interface ClickContextType {
selectedElement: HTMLElement | null;
}
const ClickContext = createContext<ClickContextType | undefined>(undefined);
type MeunClickHandlerProps = {
selected: HTMLElement | null;
event: React.MouseEvent<HTMLDivElement, MouseEvent>;
};
type MenuItem = {
key: string;
text: string;
handler: (props: MeunClickHandlerProps) => void;
};
interface ClickContextProviderProps {
children: ReactNode;
meun: MenuItem[];
}
const ClickContextProvider: React.FC<ClickContextProviderProps> = ({
children,
meun,
}) => {
const [selectedElement, setSelectedElement] = useState<HTMLElement | null>(
null
);
const [menuPosition, setMenuPosition] = useState<CSSProperties>({
top: 0,
left: 0,
});
const [showMenu, setShowMenu] = useState(false);
useEffect(() => {
const handleClick = (event: MouseEvent) => {
const target = event.target as HTMLElement;
if (target.tagName === "DIV") {
if (selectedElement) {
selectedElement.classList.remove("selected");
}
setSelectedElement(target);
target.classList.add("selected");
// 设置菜单位置
const rect = target.getBoundingClientRect();
const top = rect.top + window.scrollY;
let left = rect.left + window.scrollX + rect.width + 10;
// 检查是否超出视口
const menuWidth = 60; // 假设菜单宽度为150像素
if (left + menuWidth > window.innerWidth) {
left = rect.right - window.scrollX - menuWidth; // 调整到视口内
}
setMenuPosition({ top, left });
setShowMenu(true);
} else {
if (selectedElement) {
selectedElement.classList.remove("selected");
setSelectedElement(null);
}
setShowMenu(false);
}
};
document.addEventListener("click", handleClick);
return () => {
document.removeEventListener("click", handleClick);
};
}, [selectedElement]);
return (
<ClickContext.Provider value={{ selectedElement }}>
{children}
{showMenu && (
<div
className="context-menu"
style={{ top: menuPosition.top, left: menuPosition.left }}
>
{meun.map((item) => (
<div
key={item.key}
className="context-menu-item"
onClick={(event) => {
item.handler({ selected: selectedElement, event });
setShowMenu(false);
}}
>
{item.text}
</div>
))}
</div>
)}
</ClickContext.Provider>
);
};
export const useClickContext = () => {
const context = useContext(ClickContext);
if (context === undefined) {
throw new Error(
"useClickContext must be used within a ClickContextProvider"
);
}
return context;
};
export default ClickContextProvider;

293
src/data/functionBlocks.ts Normal file
View File

@ -0,0 +1,293 @@
export const functionBlockList: {
code: string;
props: Record<string, string>;
}[] = [
{
code: `
const cpn = (props: {
title: string
subTitle: string
}) => {
function handleClick() {
console.log("Hi there!");
}
return (
<div className="bg-primary-1 py-16 px-8">
<div className="max-w-7xl mx-auto text-center">
<h1 className="text-primary text-5xl md:text-7xl font-extrabold mb-4">
{props.title}
</h1>
<p className="text-primary text-xl md:text-2xl mb-8">
{props.subTitle}
</p>
<button
onClick={handleClick}
className="font-bold py-3 px-6 rounded-full shadow-lg transition duration-300 ease-in-out"
>
使
</button>
</div>
</div>
);
};
`,
props: {
title: "AI智能建站助手",
subTitle: "让我们帮助您快速构建和优化您的网站",
},
},
{
code: `
const cpn = (props: {
title: string
subTitle: string
featureTitle1: string
featureDesc1: string
featureTitle2: string
featureDesc2: string
featureTitle3: string
featureDesc3: string
}) => {
return (
<div className="bg-background-2 py-16 px-8">
<div className="max-w-7xl mx-auto">
<h2 className="text-text-1 text-4xl font-bold mb-4 text-center">
{props.title}
</h2>
<p className="text-text-2 text-lg mb-8 text-center">
{props.subTitle}
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="p-6 rounded-lg shadow-lg text-center">
<h3 className="text-primary text-2xl font-semibold mb-4">
{props.featureTitle1}
</h3>
<p className="text-text-3">
{props.featureDesc1}
</p>
</div>
<div className="p-6 rounded-lg shadow-lg text-center">
<h3 className="text-primary text-2xl font-semibold mb-4">
{props.featureTitle2}
</h3>
<p className="text-text-3">
{props.featureDesc2}
</p>
</div>
<div className="p-6 rounded-lg shadow-lg text-center">
<h3 className="text-primary text-2xl font-semibold mb-4">
{props.featureTitle3}
</h3>
<p className="text-text-3">
{props.featureDesc3}
</p>
</div>
</div>
</div>
</div>
);
};
`,
props: {
title: "功能介绍",
subTitle: "AI智能建站助手提供多种功能帮助您轻松创建和管理网站",
featureTitle1: "自动布局",
featureDesc1: "通过AI技术自动生成最优的网页布局提高用户体验",
featureTitle2: "智能优化",
featureDesc2: "利用大数据分析和优化工具提升网站性能和SEO排名",
featureTitle3: "个性化定制",
featureDesc3: "提供多种定制选项,让您的网站独一无二",
},
},
{
code: `
const cpn = (props: {
title: string
subTitle: string
caseTitle1: string
caseDesc1: string
caseTitle2: string
caseDesc2: string
}) => {
return (
<div className="bg-background-1 py-16 px-8">
<div className="max-w-7xl mx-auto text-center">
<h2 className="text-text-1 text-4xl font-bold mb-4">
{props.title}
</h2>
<p className="text-text-2 text-lg mb-8">
{props.subTitle}
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="p-6 rounded-lg shadow-lg">
<h3 className="text-primary text-2xl font-semibold mb-4">
{props.caseTitle1}
</h3>
<p className="text-text-3">
{props.caseDesc1}
</p>
</div>
<div className="p-6 rounded-lg shadow-lg">
<h3 className="text-primary text-2xl font-semibold mb-4">
{props.caseTitle2}
</h3>
<p className="text-text-3">
{props.caseDesc2}
</p>
</div>
</div>
</div>
</div>
);
};
`,
props: {
title: "使用案例",
subTitle: "了解其他用户如何利用我们的工具创建令人惊叹的网站",
caseTitle1: "案例一",
caseDesc1: "某公司利用我们的AI工具创建了一个高效、美观的企业官网",
caseTitle2: "案例二",
caseDesc2: "某设计师通过我们的平台轻松构建了一个个人作品展示网站",
},
},
{
code: `
const cpn = (props: {
title: string
subTitle: string
reviewUser1: string
reviewDesc1: string
reviewUser2: string
reviewDesc2: string
}) => {
return (
<div className="bg-background-2 py-16 px-8">
<div className="max-w-7xl mx-auto text-center">
<h2 className="text-text-1 text-4xl font-bold mb-4">
{props.title}
</h2>
<p className="text-text-2 text-lg mb-8">
{props.subTitle}
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="p-6 rounded-lg shadow-lg">
<h3 className="text-primary text-2xl font-semibold mb-4">
{props.reviewUser1}
</h3>
<p className="text-text-3">
{props.reviewDesc1}
</p>
</div>
<div className="p-6 rounded-lg shadow-lg">
<h3 className="text-primary text-2xl font-semibold mb-4">
{props.reviewUser2}
</h3>
<p className="text-text-3">
{props.reviewDesc2}
</p>
</div>
</div>
</div>
</div>
);
};
`,
props: {
title: "用户评价",
subTitle: "我们的用户对我们的工具评价很高",
reviewUser1: "用户A",
reviewDesc1: "“AI智能建站助手帮助我节省了大量时间让我的网站更专业。”",
reviewUser2: "用户B",
reviewDesc2: "“非常容易使用,功能强大,效果出色!”",
},
},
{
code: `
const cpn = (props: {
title: string
subTitle: string
faq1: string
faqAnswer1: string
faq2: string
faqAnswer2: string
faq3: string
faqAnswer3: string
}) => {
return (
<div className="bg-background-1 py-16 px-8">
<div className="max-w-7xl mx-auto text-center">
<h2 className="text-text-1 text-4xl font-bold mb-4">
{props.title}
</h2>
<p className="text-text-2 text-lg mb-8">
{props.subTitle}
</p>
<div className="text-left max-w-3xl mx-auto">
<div className="mb-8">
<h3 className="text-primary text-2xl font-semibold mb-2">
{props.faq1}
</h3>
<p className="text-text-3">
{props.faqAnswer1}
</p>
</div>
<div className="mb-8">
<h3 className="text-primary text-2xl font-semibold mb-2">
{props.faq2}
</h3>
<p className="text-text-3">
{props.faqAnswer2}
</p>
</div>
<div className="mb-8">
<h3 className="text-primary text-2xl font-semibold mb-2">
{props.faq3}
</h3>
<p className="text-text-3">
{props.faqAnswer3}
</p>
</div>
</div>
</div>
</div>
);
};
`,
props: {
title: "常见问题",
subTitle: "解答您在使用过程中可能遇到的问题",
faq1: "问题一如何开始使用AI智能建站助手",
faqAnswer1: "您只需注册一个账号,选择一个模板即可开始构建您的网站。",
faq2: "问题二:是否支持移动端?",
faqAnswer2: "是的,我们的所有模板都是响应式设计,兼容各类设备。",
faq3: "问题三:如何获取技术支持?",
faqAnswer3: "您可以通过我们的客服系统或者发送邮件获取技术支持。",
},
},
{
code: `
const cpn = (props: {
title: string
subTitle: string
}) => {
return (
<div className="bg-primary-1 py-16 px-8">
<div className="max-w-7xl mx-auto text-center">
<h2 className="text-text-1 text-4xl font-bold mb-4">
{props.title}
</h2>
<p className="text-text-2 text-lg mb-8">
{props.subTitle}
</p>
</div>
</div>
);
};
`,
props: {
title: "联系我们",
subTitle: "如果您有任何疑问或需要帮助,请随时联系我们",
},
},
];

View File

@ -3,112 +3,7 @@
@tailwind utilities;
/* 导入主题 */
@import "./theme/index.scss";
/* 下面是统一的颜色定义 */
/* 默认的主题样式 */
:root {
background-color: var(--color-bg-1);
color: var(--color-text-1);
}
button {
background-color: var(--primary-6);
color: var(--color-bg-white);
border: var(--border-1) solid var(--color-border-3);
border-radius: var(--border-radius-small);
padding: var(--size-3) var(--size-4);
font-size: var(--font-size-body-3);
box-shadow: var(--shadow1-center);
}
button:hover {
background-color: var(--primary-5);
border-color: var(--color-border-4);
}
button:active {
background-color: var(--primary-7);
border-color: var(--color-border-4);
}
input, textarea {
background-color: var(--color-fill-1);
color: var(--color-text-1);
border: var(--border-1) solid var(--color-border-2);
border-radius: var(--border-radius-small);
padding: var(--size-3);
font-size: var(--font-size-body-3);
box-shadow: var(--shadow1-center);
}
.card {
background-color: var(--color-bg-2);
color: var(--color-text-1);
border: var(--border-1) solid var(--color-border-1);
border-radius: var(--border-radius-large);
padding: var(--size-5);
box-shadow: var(--shadow2-center);
font-size: var(--font-size-body-3);
}
a {
color: var(--link-6);
text-decoration: none;
}
a:hover {
color: var(--link-5);
text-decoration: underline;
}
h1, h2, h3, h4, h5, h6 {
color: var(--color-text-1);
}
p {
color: var(--color-text-1);
font-size: var(--font-size-body-3);
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: var(--border-1) solid var(--color-border-2);
padding: var(--size-3);
text-align: left;
}
thead {
background-color: var(--color-bg-2);
}
.alert {
background-color: var(--danger-1);
color: var(--danger-6);
border: var(--border-1) solid var(--danger-4);
border-radius: var(--border-radius-small);
padding: var(--size-3);
}
.success {
background-color: var(--success-1);
color: var(--success-6);
border: var(--border-1) solid var(--success-4);
border-radius: var(--border-radius-small);
padding: var(--size-3);
}
.warning {
background-color: var(--warning-1);
color: var(--warning-6);
border: var(--border-1) solid var(--warning-4);
border-radius: var(--border-radius-small);
padding: var(--size-3);
}
@import "./theme/index.css";
:root {
/* .font-size-global { */

View File

@ -1,6 +1,3 @@
// 定义主体名称变量
$theme-name: dark;
.theme-color-dark {
--primary-7:rgb( 104,159,255);
--primary-6:rgb( 60,126,255);

2
src/theme/index.css Normal file
View File

@ -0,0 +1,2 @@
@import "./dark/index.css";
@import "./light/index.css"

View File

@ -1,2 +0,0 @@
@import "./dark/index.scss";
@import "./light/index.scss"

View File

@ -1,6 +1,3 @@
// 定义主体名称变量
$theme-name: light;
.theme-color-light {
--primary-7:rgb( 14, 66,210);
--primary-6:rgb( 22, 93,255);

View File

@ -0,0 +1,11 @@
/**
* Creates a new composite function that invokes the functions from right to left
*/
export default function compose<T>(...functions: ((...args: T[]) => T)[]) {
return functions.reduce(
(acc, currentFn) =>
(...args: T[]) =>
acc(currentFn(...args))
);
}

View File

@ -0,0 +1,23 @@
import React, { ComponentType, Component, PropsWithChildren } from "react";
const errorBoundary = (
Element: ComponentType,
errorCallback: (error: Error) => void
) => {
return class ErrorBoundary extends Component<PropsWithChildren> {
componentDidCatch(error: Error) {
errorCallback(error);
}
render() {
const { children, ...props } = this.props;
return typeof Element === "function" ? (
<Element {...props} />
) : React.isValidElement(Element) ? (
React.cloneElement(Element, props, children)
) : null;
}
};
};
export default errorBoundary;

View File

@ -0,0 +1,12 @@
import type { ComponentType } from "react";
const evalCode = (
code: string,
scope: Record<string, unknown>
): ComponentType => {
const scopeKeys = Object.keys(scope);
const scopeValues = scopeKeys.map((key) => scope[key]);
return new Function(...scopeKeys, code)(...scopeValues);
};
export default evalCode;

View File

@ -0,0 +1,75 @@
import React, { ComponentType } from "react";
import transform from "./transform";
import errorBoundary from "./errorBoundary";
import evalCode from "./evalCode";
import compose from "./compose";
import { Transform } from "sucrase";
const jsxConst = 'const _jsxFileName = "";';
const trimCode = (code: string) => code.trim().replace(/;$/, "");
const spliceJsxConst = (code: string) => code.replace(jsxConst, "").trim();
const addJsxConst = (code: string) => jsxConst + code;
const wrapReturn = (code: string) => `return (${code})`;
type GenerateOptions = {
code: string;
scope?: Record<string, unknown>;
enableTypeScript: boolean;
};
export const generateElement = (
{ code = "", scope = {}, enableTypeScript = true }: GenerateOptions,
errorCallback: (error: Error) => void
) => {
/**
* To enable TypeScript we need to transform the TS to JS code first,
* splice off the JSX const, wrap the eval in a return statement, then
* transform any imports. The two-phase approach is required to do
* the implicit evaluation and not wrap leading Interface or Type
* statements in the return.
*/
const firstPassTransforms: Transform[] = ["jsx"];
enableTypeScript && firstPassTransforms.push("typescript");
const transformed = compose<string>(
addJsxConst,
transform({ transforms: ["imports"] }),
spliceJsxConst,
trimCode,
transform({ transforms: firstPassTransforms }),
wrapReturn,
trimCode
)(code);
return errorBoundary(
evalCode(transformed, { React, ...scope }),
errorCallback
);
};
export const renderElementAsync = (
{ code = "", scope = {}, enableTypeScript = true }: GenerateOptions,
resultCallback: (sender: ComponentType) => void,
errorCallback: (error: Error) => void
// eslint-disable-next-line consistent-return
) => {
const render = (element: ComponentType) => {
if (typeof element === "undefined") {
errorCallback(new SyntaxError("`render` must be called with valid JSX."));
} else {
resultCallback(errorBoundary(element, errorCallback));
}
};
if (!/render\s*\(/.test(code)) {
return errorCallback(
new SyntaxError("No-Inline evaluations must call `render`.")
);
}
const transforms: Transform[] = ["jsx", "imports"];
enableTypeScript && transforms.splice(1, 0, "typescript");
evalCode(transform({ transforms })(code), { React, ...scope, render });
};

View File

@ -0,0 +1,15 @@
import { transform as _transform, Transform } from "sucrase";
const defaultTransforms: Transform[] = ["jsx", "imports"];
type Options = {
transforms?: Transform[];
};
export default function transform(opts: Options = {}) {
const transforms = Array.isArray(opts.transforms)
? opts.transforms.filter(Boolean)
: defaultTransforms;
return (code: string) => _transform(code, { transforms }).code;
}

View File

@ -2,41 +2,7 @@
module.exports = {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
colors: {
blue: "#1fb6ff",
purple: "#7e5bef",
pink: "#ff49db",
orange: "#ff7849",
green: "#13ce66",
yellow: "#ffc82c",
"gray-dark": "#273444",
gray: "#8492a6",
"gray-light": "#d3dce6",
transparent: "transparent",
current: "currentColor",
white: "#ffffff",
midnight: "#121063",
metal: "#565584",
tahiti: "#3ab7bf",
silver: "#ecebff",
"bubble-gum": "#ff77e9",
bermuda: "#78dcca",
},
fontFamily: {
sans: ["Graphik", "sans-serif"],
serif: ["Merriweather", "serif"],
},
extend: {
spacing: {
"8xl": "96rem",
"9xl": "128rem",
},
borderRadius: {
"4xl": "2rem",
},
fontFamily: {
display: "Oswald, ui-serif ", // Adds a new `font-display` class
},
colors: {
primary: {
1: "var(--primary-1)",