Update index.tsx

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

View File

@ -1,194 +1,352 @@
import React, { useMemo, useState, useCallback } from "react";
import { Controlled } from "react-codemirror2";
import { Button, message } from "antd";
import { saveAs } from "file-saver";
import Logo from "@/assets/logo.png";
import styles from "./index.less";
import { isDev, useGetRect } from "utils/tool";
import { SaveOutlined } from "@ant-design/icons";
import { useHotkeys } from "react-hotkeys-hook";
require("codemirror/mode/xml/xml");
require("codemirror/mode/javascript/javascript");
import React, { useMemo, useState, useCallback, useEffect } from "react";
import { Button, message, Table, Space, Tag, Input, Modal, Form, Select } from "antd";
import { SearchOutlined, PlusOutlined, EditOutlined, DeleteOutlined, SaveOutlined } from "@ant-design/icons";
import type { ColumnsType } from "antd/es/table";
import styles from "./index.module.less";
const serverUrl = isDev ? "http://localhost:3000" : "http://localhost:3000";
const { Search } = Input;
const { Option } = Select;
const defaultHtml = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<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>
`;
interface UserData {
id: string;
username: string;
email: string;
phone: string;
status: "active" | "inactive" | "suspended";
createTime: string;
lastLoginTime: string;
}
export default function CodeEditor() {
const [htmlContent, setHtmlContent] = useState(defaultHtml);
const [cursorPosition, setCursorPosition] = useState({ line: 1, ch: 1 });
const [isUpdated, setIsUpdated] = useState(false);
const defaultUsers: UserData[] = [
{
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",
},
];
const rect = useGetRect();
export default function UserPage() {
const [users, setUsers] = useState<UserData[]>(defaultUsers);
const [loading, setLoading] = useState(false);
const [searchText, setSearchText] = useState("");
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [isModalVisible, setIsModalVisible] = useState(false);
const [editingUser, setEditingUser] = useState<UserData | null>(null);
const [form] = Form.useForm();
// 计算高度
const contentHeight = useMemo(() => {
const baseHeight = rect.height - 42 - 1; // 减去顶部高度和滚动条防溢出
return Math.max(baseHeight, 694); // 最小高度为 694
}, [rect.height]);
// 过滤用户列表
const filteredUsers = useMemo(() => {
if (!searchText) return users;
return users.filter(
(user) =>
user.username.toLowerCase().includes(searchText.toLowerCase()) ||
user.email.toLowerCase().includes(searchText.toLowerCase()) ||
user.phone.includes(searchText)
);
}, [users, searchText]);
const phoneHeight = 694; // 固定高度,避免大屏幕问题
const iframeHeight = phoneHeight - 30 - 24; // 上边距 30上下 padding 各 12
// 保存页面
const savePage = useCallback(
(content?: string) => {
const contentToSave = content ?? htmlContent;
fetch(`${serverUrl}/dooring/render`, {
method: "POST",
body: contentToSave,
}).then(() => {
message.success("已保存");
setIsUpdated((prev) => !prev); // 触发重新渲染
});
},
[htmlContent]
// 表格列定义
const columns: ColumnsType<UserData> = useMemo(
() => [
{
title: "用户名",
dataIndex: "username",
key: "username",
width: 150,
},
{
title: "邮箱",
dataIndex: "email",
key: "email",
width: 200,
},
{
title: "手机号",
dataIndex: "phone",
key: "phone",
width: 150,
},
{
title: "状态",
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>;
},
},
{
title: "创建时间",
dataIndex: "createTime",
key: "createTime",
width: 180,
},
{
title: "最后登录",
dataIndex: "lastLoginTime",
key: "lastLoginTime",
width: 180,
},
{
title: "操作",
key: "action",
width: 200,
fixed: "right",
render: (_, record) => (
<Space size="middle">
<Button
type="link"
icon={<EditOutlined />}
onClick={() => handleEdit(record)}
>
</Button>
<Button
type="link"
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(record.id)}
>
</Button>
</Space>
),
},
],
[]
);
// 下载 HTML 文件
const downloadHtml = useCallback(() => {
const file = new File([htmlContent], `${Date.now()}.html`, {
type: "text/html;charset=utf-8",
// 编辑用户
const handleEdit = useCallback((user: UserData) => {
setEditingUser(user);
form.setFieldsValue(user);
setIsModalVisible(true);
}, [form]);
// 删除用户
const handleDelete = useCallback((id: string) => {
Modal.confirm({
title: "确认删除",
content: "确定要删除这个用户吗?",
onOk: () => {
setUsers((prev) => prev.filter((user) => user.id !== id));
message.success("删除成功");
},
});
saveAs(file);
}, [htmlContent]);
}, []);
const downloadcss = useCallback(() => {
const file = new File([htmlContent], `${Date.now()}.html`, {
type: "text/css;charset=utf-8",
});
saveAs(file);
}, [htmlContent]);
// 快捷键保存
useHotkeys(
"ctrl+s",
(event) => {
savePage();
event.preventDefault();
},
[savePage]
);
// CodeMirror 编辑器事件处理
const handleCodeChange = (
_editor: CodeMirror.Editor,
_data: CodeMirror.EditorChange,
value: string
) => {
setHtmlContent(value);
};
const handleCursorChange = (
_editor: CodeMirror.Editor,
position: CodeMirror.Position
) => {
setCursorPosition(position);
};
const handleEditorKeyDown = (editor: CodeMirror.Editor, event: KeyboardEvent) => {
if (event.ctrlKey && event.key === "s") {
savePage(editor.getValue());
event.preventDefault();
// 保存用户
const handleSave = useCallback(async () => {
try {
const values = await form.validateFields();
if (editingUser) {
// 更新用户
setUsers((prev) =>
prev.map((user) =>
user.id === editingUser.id ? { ...user, ...values } : user
)
);
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]);
// CodeMirror 渲染
const codeMirrorRender = useMemo(() => (
<Controlled
className={styles.codeWrap}
value={htmlContent}
options={{
mode: "xml",
theme: "material",
lineNumbers: true,
}}
onBeforeChange={handleCodeChange}
onCursor={handleCursorChange}
onKeyDown={handleEditorKeyDown}
/>
), [htmlContent]);
// 取消编辑
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 (
<div className={styles.wrap}>
{/* Header */}
<div className={styles.header}>
<div className={styles.logoArea}>
<div className={styles.logo} title="Dooring">
<a href="http://h5.dooring.cn">
<img src={Logo} alt="Dooring-强大的h5编辑器" />
</a>
</div>
<div className={styles.logoText}>| 线</div>
<div className={styles.titleArea}>
<h2 className={styles.title}></h2>
<p className={styles.description}></p>
</div>
<div className={styles.operationBar}>
<Button type="primary" title="保存ctrl+s" onClick={savePage} style={{ marginRight: "10px" }}>
<SaveOutlined />
</Button>
<Button type="primary" onClick={downloadHtml} style={{ marginRight: "10px" }}>
</Button>
<Button danger onClick={downloadHtml}>
</Button>
</div>
</div>
{/* 内容区域 */}
<div className={styles.contentWrap} style={{ height: `${contentHeight}px`, position: "relative" }}>
{/* 代码编辑器 */}
<div className={styles.codeWrap} style={{ height: `${contentHeight}px`, position: "relative" }}>
{codeMirrorRender}
</div>
{/* 预览区域 */}
<div className={styles.previewWrap} style={{ height: `${phoneHeight}px` }}>
<iframe
title="preview"
src={`${serverUrl}/html?flag=${isUpdated}`}
style={{
width: "100%",
height: `${iframeHeight}px`,
margin: 0,
padding: 0,
border: "none",
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
setEditingUser(null);
form.resetFields();
setIsModalVisible(true);
}}
></iframe>
style={{ marginRight: "10px" }}
>
</Button>
<Button
danger
onClick={handleBatchDelete}
disabled={selectedRowKeys.length === 0}
>
</Button>
</div>
</div>
{/* 搜索栏 */}
<div className={styles.searchBar}>
<Search
placeholder="搜索用户名、邮箱或手机号"
allowClear
enterButton={<SearchOutlined />}
size="large"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
style={{ width: 400 }}
/>
</div>
{/* 表格区域 */}
<div className={styles.tableWrap}>
<Table
columns={columns}
dataSource={filteredUsers}
rowKey="id"
loading={loading}
rowSelection={rowSelection}
scroll={{ x: 1200 }}
pagination={{
total: filteredUsers.length,
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total}`,
}}
/>
</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>
);
}
}