Update index.tsx

This commit is contained in:
ggchinazhangwei 2025-12-19 15:24:04 +08:00 committed by GitHub
parent ee1c5e73f8
commit 07f753c672
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1,352 +1,326 @@
import React, { useMemo, useState, useCallback, useEffect } from "react"; import React, { useMemo, useState, useCallback, useRef, useEffect } from 'react';
import { Button, message, Table, Space, Tag, Input, Modal, Form, Select } from "antd"; import { Controlled as CodeMirrorControlled } from 'react-codemirror2';
import { SearchOutlined, PlusOutlined, EditOutlined, DeleteOutlined, SaveOutlined } from "@ant-design/icons"; import { Button, message } from 'antd';
import type { ColumnsType } from "antd/es/table"; import { saveAs } from 'file-saver';
import styles from "./index.module.less"; import Logo from '@/assets/logo.png';
import { isDev, useGetRect } from 'utils/tool';
import { SaveOutlined, DownloadOutlined, RocketOutlined } from '@ant-design/icons';
import { useHotkeys } from 'react-hotkeys-hook';
const { Search } = Input; // 导入样式
const { Option } = Select; import {
wrapStyle,
headerStyle,
logoAreaStyle,
logoStyle,
logoImgStyle,
logoTextStyle,
operationBarStyle,
contentWrapStyle,
codeWrapStyle,
previewWrapStyle,
iframeStyle
} from './styles';
interface UserData { // 导入CodeMirror样式和模式
id: string; import 'codemirror/lib/codemirror.css';
username: string; import 'codemirror/theme/material.css';
email: string; import 'codemirror/mode/xml/xml';
phone: string; import 'codemirror/mode/javascript/javascript';
status: "active" | "inactive" | "suspended";
createTime: string;
lastLoginTime: string;
}
const defaultUsers: UserData[] = [ // 定义服务器URL
{ const serverUrl = isDev ? 'http://localhost:3000' : 'http://localhost:3000';
id: "1",
username: "admin",
email: "admin@example.com",
phone: "13800138000",
status: "active",
createTime: "2024-01-01 10:00:00",
lastLoginTime: "2024-01-15 14:30:00",
},
{
id: "2",
username: "user001",
email: "user001@example.com",
phone: "13800138001",
status: "active",
createTime: "2024-01-02 11:00:00",
lastLoginTime: "2024-01-14 09:20:00",
},
{
id: "3",
username: "user002",
email: "user002@example.com",
phone: "13800138002",
status: "inactive",
createTime: "2024-01-03 12:00:00",
lastLoginTime: "2024-01-10 16:45:00",
},
];
export default function UserPage() { // 默认HTML内容
const [users, setUsers] = useState<UserData[]>(defaultUsers); const defaultHtml = `<!DOCTYPE html>
const [loading, setLoading] = useState(false); <html lang="en">
const [searchText, setSearchText] = useState(""); <head>
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); <meta charset="UTF-8">
const [isModalVisible, setIsModalVisible] = useState(false); <meta name="viewport" content="width=device-width, initial-scale=1.0">
const [editingUser, setEditingUser] = useState<UserData | null>(null); <title>Document</title>
const [form] = Form.useForm(); <style>
html,body {
margin: 0;
padding: 0;
}
#root {
padding-top: 200px;
text-align: center;
}
p {
padding: 0 10px;
color: #06c;
line-height: 1.8;
font-size: 12px;
}
</style>
</head>
<body>
<div id="root">
<img src="http://io.nainor.com/uploads/logo_1747374040f.png" />
<p>
(H5编辑器)H5-Dooring是一款功能强大H5可视化页面配置解决方案
便H5落地页最佳实践
</p>
</div>
</body>
</html>`;
// 过滤用户列表 /**
const filteredUsers = useMemo(() => { * H5-Dooring在线代码编辑器组件
if (!searchText) return users; */
return users.filter( export default function CodeEditor() {
(user) => // 状态管理
user.username.toLowerCase().includes(searchText.toLowerCase()) || const [htmlContent, setHtmlContent] = useState<string>(defaultHtml);
user.email.toLowerCase().includes(searchText.toLowerCase()) || const [cursorPosition, setCursorPosition] = useState<{ line: number; ch: number }>({ line: 1, ch: 1 });
user.phone.includes(searchText) const [isUpdated, setIsUpdated] = useState<boolean>(false);
); const editorRef = useRef<CodeMirror.Editor | null>(null);
}, [users, searchText]);
// 表格列定义 // 获取窗口尺寸
const columns: ColumnsType<UserData> = useMemo( const rect = useGetRect();
() => [
{ // 计算高度
title: "用户名", const contentHeight = useMemo(() => {
dataIndex: "username", const baseHeight = rect.height - 42 - 1; // 减去顶部高度和滚动条防溢出
key: "username", return Math.max(baseHeight, 694); // 最小高度为 694
width: 150, }, [rect.height]);
},
{ const phoneHeight = 694; // 固定高度,避免大屏幕问题
title: "邮箱", const iframeHeight = phoneHeight - 30 - 24; // 上边距 30上下 padding 各 12
dataIndex: "email",
key: "email", /**
width: 200, *
}, * @param content - HTML内容使
{ */
title: "手机号", const savePage = useCallback(async (content?: string) => {
dataIndex: "phone", try {
key: "phone", const contentToSave = content ?? htmlContent;
width: 150, const response = await fetch(`${serverUrl}/dooring/render`, {
}, method: 'POST',
{ headers: {
title: "状态", 'Content-Type': 'text/html;charset=utf-8',
dataIndex: "status",
key: "status",
width: 120,
render: (status: string) => {
const statusMap = {
active: { color: "green", text: "活跃" },
inactive: { color: "default", text: "未激活" },
suspended: { color: "red", text: "已暂停" },
};
const statusInfo = statusMap[status as keyof typeof statusMap] || statusMap.inactive;
return <Tag color={statusInfo.color}>{statusInfo.text}</Tag>;
}, },
}, body: contentToSave,
{ });
title: "创建时间",
dataIndex: "createTime", if (!response.ok) {
key: "createTime", throw new Error(`HTTP error! Status: ${response.status}`);
width: 180, }
},
{ message.success('已保存');
title: "最后登录", setIsUpdated(prev => !prev); // 触发重新渲染
dataIndex: "lastLoginTime", } catch (error) {
key: "lastLoginTime", console.error('保存失败:', error);
width: 180, message.error('保存失败,请稍后重试');
}, }
{ }, [htmlContent]);
title: "操作",
key: "action", /**
width: 200, * HTML文件
fixed: "right", */
render: (_, record) => ( const downloadHtml = useCallback(() => {
<Space size="middle"> try {
<Button const file = new File([htmlContent], `${Date.now()}.html`, {
type="link" type: 'text/html;charset=utf-8',
icon={<EditOutlined />} });
onClick={() => handleEdit(record)} saveAs(file);
> message.success('HTML文件下载成功');
} catch (error) {
</Button> console.error('下载HTML失败:', error);
<Button message.error('下载HTML失败请稍后重试');
type="link" }
danger }, [htmlContent]);
icon={<DeleteOutlined />}
onClick={() => handleDelete(record.id)} /**
> * CSS文件HTML内容中提取CSS
*/
</Button> const downloadCss = useCallback(() => {
</Space> try {
), // 从HTML中提取CSS内容
}, const styleMatch = htmlContent.match(/<style[^>]*>([\s\S]*?)<\/style>/i);
], const cssContent = styleMatch ? styleMatch[1] : '';
const file = new File([cssContent], `${Date.now()}.css`, {
type: 'text/css;charset=utf-8',
});
saveAs(file);
message.success('CSS文件下载成功');
} catch (error) {
console.error('下载CSS失败:', error);
message.error('下载CSS失败请稍后重试');
}
}, [htmlContent]);
/**
*
*/
const deployWebsite = useCallback(() => {
try {
// 这里应该是实际的部署逻辑
message.success('部署功能开发中...');
} catch (error) {
console.error('部署失败:', error);
message.error('部署失败,请稍后重试');
}
}, []);
// 快捷键保存
useHotkeys(
'ctrl+s',
(event) => {
savePage();
event.preventDefault();
},
[savePage]
);
/**
*
*/
const handleCodeChange = useCallback(
(_editor: CodeMirror.Editor, _data: CodeMirror.EditorChange, value: string) => {
setHtmlContent(value);
},
[] []
); );
// 编辑用户 /**
const handleEdit = useCallback((user: UserData) => { *
setEditingUser(user); */
form.setFieldsValue(user); const handleCursorChange = useCallback(
setIsModalVisible(true); (_editor: CodeMirror.Editor, position: CodeMirror.Position) => {
}, [form]); setCursorPosition(position);
},
[]
);
// 删除用户 /**
const handleDelete = useCallback((id: string) => { *
Modal.confirm({ */
title: "确认删除", const handleEditorKeyDown = useCallback(
content: "确定要删除这个用户吗?", (editor: CodeMirror.Editor, event: KeyboardEvent) => {
onOk: () => { if (event.ctrlKey && event.key === 's') {
setUsers((prev) => prev.filter((user) => user.id !== id)); savePage(editor.getValue());
message.success("删除成功"); event.preventDefault();
}, }
}); },
[savePage]
);
/**
*
*/
const handleEditorDidMount = useCallback((editor: CodeMirror.Editor) => {
editorRef.current = editor;
}, []); }, []);
// 保存用户 // CodeMirror 编辑器配置
const handleSave = useCallback(async () => { const codeMirrorOptions = useMemo(() => ({
try { mode: 'xml',
const values = await form.validateFields(); theme: 'material',
if (editingUser) { lineNumbers: true,
// 更新用户 lineWrapping: true,
setUsers((prev) => autoCloseBrackets: true,
prev.map((user) => matchBrackets: true,
user.id === editingUser.id ? { ...user, ...values } : user indentUnit: 2,
) tabSize: 2,
); indentWithTabs: false,
message.success("更新成功"); }), []);
} else {
// 新增用户
const newUser: UserData = {
id: Date.now().toString(),
...values,
createTime: new Date().toLocaleString("zh-CN"),
lastLoginTime: "-",
};
setUsers((prev) => [...prev, newUser]);
message.success("创建成功");
}
setIsModalVisible(false);
setEditingUser(null);
form.resetFields();
} catch (error) {
console.error("表单验证失败:", error);
}
}, [editingUser, form]);
// 取消编辑
const handleCancel = useCallback(() => {
setIsModalVisible(false);
setEditingUser(null);
form.resetFields();
}, [form]);
// 批量删除
const handleBatchDelete = useCallback(() => {
if (selectedRowKeys.length === 0) {
message.warning("请选择要删除的用户");
return;
}
Modal.confirm({
title: "确认删除",
content: `确定要删除选中的 ${selectedRowKeys.length} 个用户吗?`,
onOk: () => {
setUsers((prev) => prev.filter((user) => !selectedRowKeys.includes(user.id)));
setSelectedRowKeys([]);
message.success("删除成功");
},
});
}, [selectedRowKeys]);
// 行选择配置
const rowSelection = {
selectedRowKeys,
onChange: (keys: React.Key[]) => {
setSelectedRowKeys(keys);
},
};
return ( return (
<div className={styles.wrap}> <div style={wrapStyle}>
{/* Header */} {/* Header */}
<div className={styles.header}> <div style={headerStyle}>
<div className={styles.titleArea}> <div style={logoAreaStyle}>
<h2 className={styles.title}></h2> <div style={logoStyle} title="Dooring">
<p className={styles.description}></p> <a href="http://h5.dooring.cn" target="_blank" rel="noopener noreferrer">
<img src={Logo} alt="Dooring-强大的h5编辑器" style={logoImgStyle} />
</a>
</div>
<div style={logoTextStyle}>| 线</div>
</div> </div>
<div className={styles.operationBar}> <div style={operationBarStyle}>
<Button <Button
type="primary" type="primary"
icon={<PlusOutlined />} icon={<SaveOutlined />}
onClick={() => { title="保存ctrl+s"
setEditingUser(null); onClick={savePage}
form.resetFields(); style={{ marginRight: '10px' }}
setIsModalVisible(true);
}}
style={{ marginRight: "10px" }}
> >
</Button> </Button>
<Button <Button
danger type="primary"
onClick={handleBatchDelete} icon={<DownloadOutlined />}
disabled={selectedRowKeys.length === 0} onClick={downloadHtml}
style={{ marginRight: '10px' }}
> >
HTML
</Button>
<Button
type="default"
icon={<DownloadOutlined />}
onClick={downloadCss}
style={{ marginRight: '10px' }}
>
CSS
</Button>
<Button
type="primary"
danger
icon={<RocketOutlined />}
onClick={deployWebsite}
>
</Button> </Button>
</div> </div>
</div> </div>
{/* 搜索栏 */} {/* 内容区域 */}
<div className={styles.searchBar}> <div style={contentWrapStyle(contentHeight)}>
<Search {/* 代码编辑器 */}
placeholder="搜索用户名、邮箱或手机号" <div style={codeWrapStyle(contentHeight)}>
allowClear <CodeMirrorControlled
enterButton={<SearchOutlined />} value={htmlContent}
size="large" options={codeMirrorOptions}
value={searchText} onBeforeChange={handleCodeChange}
onChange={(e) => setSearchText(e.target.value)} onCursor={handleCursorChange}
style={{ width: 400 }} onKeyDown={handleEditorKeyDown}
/> onReady={handleEditorDidMount}
</div> />
</div>
{/* 表格区域 */} {/* 预览区域 */}
<div className={styles.tableWrap}> <div style={previewWrapStyle(phoneHeight)}>
<Table <iframe
columns={columns} title="H5预览"
dataSource={filteredUsers} src={`${serverUrl}/html?flag=${isUpdated}`}
rowKey="id" style={iframeStyle(iframeHeight)}
loading={loading} sandbox="allow-same-origin allow-scripts"
rowSelection={rowSelection} loading="lazy"
scroll={{ x: 1200 }} />
pagination={{ </div>
total: filteredUsers.length,
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total}`,
}}
/>
</div> </div>
{/* 编辑/新增弹窗 */}
<Modal
title={editingUser ? "编辑用户" : "新增用户"}
open={isModalVisible}
onOk={handleSave}
onCancel={handleCancel}
okText="保存"
cancelText="取消"
width={600}
>
<Form
form={form}
layout="vertical"
initialValues={{
status: "active",
}}
>
<Form.Item
name="username"
label="用户名"
rules={[{ required: true, message: "请输入用户名" }]}
>
<Input placeholder="请输入用户名" />
</Form.Item>
<Form.Item
name="email"
label="邮箱"
rules={[
{ required: true, message: "请输入邮箱" },
{ type: "email", message: "请输入有效的邮箱地址" },
]}
>
<Input placeholder="请输入邮箱" />
</Form.Item>
<Form.Item
name="phone"
label="手机号"
rules={[
{ required: true, message: "请输入手机号" },
{ pattern: /^1[3-9]\d{9}$/, message: "请输入有效的手机号" },
]}
>
<Input placeholder="请输入手机号" />
</Form.Item>
<Form.Item
name="status"
label="状态"
rules={[{ required: true, message: "请选择状态" }]}
>
<Select placeholder="请选择状态">
<Option value="active"></Option>
<Option value="inactive"></Option>
<Option value="suspended"></Option>
</Select>
</Form.Item>
</Form>
</Modal>
</div> </div>
); );
} }
// 声明CodeMirror全局类型
declare global {
namespace CodeMirror {
interface Editor {
getValue(): string;
}
interface EditorChange {
from: Position;
to: Position;
text: string[];
removed: string[];
origin: string | undefined;
}
interface Position {
line: number;
ch: number;
}
}
}