摘要
在上一篇中,我们已经把和页面相关的接口完成的差不多了。从创建页面,更新页面等等:
有了接口之后,我们就可以构建前端页面了。那这部分前端内容我们应该写在哪里呢?
有两种方式:
- 直接写在我们的XinBuilder项目里面,然后通过前端路由拆分成两个路由
- 在创建一个项目,然后打包到后端服务中,也就是通过后端路由去控制
因为我不确定这个项目后面会有多少代码,虽然我们目前只是想实现页面的管理功能,但是后面我也不知道会增加到多少。
所以我准备使用两个React项目,和页面相关的这些功能我都会写在新的项目里,
1.创建项目
首先就是创建项目了,我们使用create-react-app创建一个项目:
> npx create-react-app app-builder --template typescript
然后再安装antD
npm install antd --save
然后把项目里没有用的文件删一删:
最后,因为我们要请求我们写好的接口,在安装一下axios。
npm install axios --save
2.路由的配置
对于这个项目,我们现在只准备完成和pageJson相关的。但是后面可能会有其他的页面,所以我们是需要路由的。
我们就先安装一下react-router-dom,然后使用路由来管理前端的页面。
npm install react-router-dom --save
对于路由,我们在src下新建一个routes用来管理所有的路由页面。
page文件夹就是代表和pageJson相关的路由。
现在我们回到index.tsx中,对page路由进行引入。
import React, { Suspense } from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import Page from './routes/page';
import { HashRouter as Router, Routes , Route} from "react-router-dom";
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<Router>
<Suspense>
<Routes>
<Route path={'/'} element={<Page />}></Route>
</Routes>
</Suspense>
</Router>
);
3.服务端的CORS配置
这时候,如果我们在项目里调用服务端的接口,会有跨域的问题。所以在XinBuilderServer中,我们修改一下main.ts文件:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'
import { NestExpressApplication } from '@nestjs/platform-express';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule,{ cors: true });
const options = new DocumentBuilder()
.setTitle('API example')
.addBearerAuth()
.setVersion('1.0')
.build()
const document = SwaggerModule.createDocument(app, options)
SwaggerModule.setup('api-docs', app, document)
await app.listen(4000);
}
bootstrap();
通过修改CORS配置,来解决跨域的问题
的代码提交在github上:
https://github.com/TeacherXin/XinBuilderServer2commit: 第二节:修改CORS配置解决跨域问题
4.构建前端页面
那我们需要的效果就是:
至于这部分比较简单,我把代码的注释写一下,读者自己看就行。
不过编辑页面和预览页面,一会再细说。
import React, { useEffect, useState } from 'react'
import { Card, Col, Row, Button,Input, message, Modal,Divider, Select } from 'antd';
import {DeleteOutlined,DatabaseOutlined,FormOutlined,InsertRowBelowOutlined,UsergroupDeleteOutlined} from '@ant-design/icons';
import axios from 'axios'
import './index.css'
const { Search } = Input
interface PageJson {
pageName: string,
pageId: string,
pageJson: {
[key: string]: any
},
_id: string
}
export default function Page() {
const [messageApi, contextHolder] = message.useMessage();
const [pageList, setPageList] = useState<PageJson []>()
const [isModalOpen,setIsModalOpen] = useState<boolean>(false)
const [pageName,setPageName] = useState<string>('')
const [searchValue,setSearchValue] = useState<string>('')
useEffect(() => {
getPageList()
}, [])
/**
* 获取全部List的接口
*/
const getPageList = () => {
axios.post(`http://localhost:4000/page-json/findAllPage`)
.then(res => {
setPageList(res.data.data)
})
.catch(err => {
messageApi.open({
type: 'error',
content: '获取页面列表失败',
});
})
}
/**
* 更改搜索框的内容
* @param value 搜索框的内容
*/
const onSearch = (value: string) => {
setSearchValue(value)
}
/**
* 新建页面的弹窗
*/
const addNewPage = () => {
setIsModalOpen(true);
setPageName('')
}
/**
* 搜索内容的过滤
* @param list 页面列表
* @returns 过滤后的页面列表
*/
const getSearchList = (list: PageJson [] | undefined) => {
return (list || []).filter(item => {
return item.pageName.indexOf(searchValue) > -1
})
}
/**
* 根据页面ID进行删除
* @param pageId 页面的ID
* @returns
*/
const deletePage = (pageId: string) => {
return () => {
axios.post(`http://localhost:4000/page-json/deletePage`,{
pageId
})
.then(res => {
messageApi.open({
type: 'success',
content: '删除成功',
});
getPageList()
})
.catch(err => {
messageApi.open({
type: 'error',
content: '删除失败',
});
})
}
}
/**
* 新增页面掉的接口
*/
const handleOk = () => {
const user = JSON.parse(localStorage.getItem('user') || '{}');
axios.post(`http://localhost:4000/page-json/addPage`,{
pageName: pageName,
pageId:'pageInfo_' + new Date().getTime(),
pageJson: {},
})
.then(res => {
messageApi.open({
type: 'success',
content: '新建页面成功',
});
getPageList()
setIsModalOpen(false)
})
.catch(err => {
messageApi.open({
type: 'error',
content: '新建页面失败',
});
})
}
/**
* 新建页面弹窗的取消回调
*/
const handleCancel = () => {
setIsModalOpen(false)
}
/**
* 更改输入的页面名称
* @param e 页面名称
*/
const changePageName = (e: any) => {
setPageName(e.target.value)
}
const toBuilderPage = (pageId: string) => {
return () => {
}
}
return (
<div className='PageList'>
{contextHolder}
<div className='pageLeft'>
<div className='leftHeader'>XinBuilder</div>
<div className='leftDiscribe'>轻量级的低代码平台</div>
<Divider />
</div>
<div className='pageRight'>
<div className='PageHeader'>
<Search
style={{ width: 304 }}
onSearch={onSearch}
/>
<Button className='pageButton' onClick={addNewPage}>新建页面</Button>
</div>
<Divider />
<div className='PageBody'>
<Row style={{width:'100%'}} gutter={16}>
{
(getSearchList(pageList) || []).map(item => {
return <Col style={{marginTop:'10px'}} key={item._id} span={6}>
<Card
title={<div><span>{item.pageName || '匿名'}</span><DeleteOutlined onClick={deletePage(item.pageId)}style={{float:'right',cursor:'pointer'}} /></div>}
bordered={false}
headStyle={{fontSize:'14px'}}
>
<div style={{height:'50px'}}>
<Button type='text' onClick={toBuilderPage(item.pageId)}>编辑页面</Button>
<Button type='text'>预览页面</Button>
</div>
</Card>
</Col>
})
}
</Row>
</div>
</div>
<Modal title="创建页面" open={isModalOpen} onOk={handleOk} onCancel={handleCancel} okText='创建' cancelText='取消'>
<Input addonBefore="页面名称" value={pageName} onChange={changePageName} />
</Modal>
</div>
)
}
5.跳转页面详情
当我点击编辑页面的时候,应该跳转到对应页面的编辑状态。也就是我们之前实现的项目。
那我在我们的设计器项目怎么知道当前的页面ID呢?
所以我们需要再跳转的时候,将pageId带过去,怎么带呢,只能通过URL上面的参数实现,所以我们现在可以实现一下toBuilderPage方法。
/**
* 根据页面ID跳转到详情页
* @param pageId 页面ID
* @returns
*/
const toBuilderPage = (pageId: string) => {
return () => {
window.open(`http://localhost:3000?pageId=${pageId}`)
}
}
6.修改XinBuilder项目
OK,现在我们现在回到我们的低代码项目里,在builder目录下的index.tsx中,我们要根据URL上的pageId,调取接口来获取到页面详情
获取到之后,我们再通过Store去更新redux。
import { useEffect } from 'react'
import DesignTop from './designTop'
import LeftCom from './leftPart'
import MainCom from './mainPart'
import RightCom from './rightPart'
import axios from 'axios'
import Store from '../../store'
import { message } from 'antd'
export default function Builder() {
useEffect(() => {
const search = window.location.search || '';
const pageId = search.replace('?pageId=', '');
axios.post('http://localhost:4000/page-json/findPageByID', {
pageId
})
.then(res => {
if(res.data.data) {
Store.dispatch({type: 'changeComList', value: res.data.data.pageJson || []})
}else{
message.error('获取页面详情失败')
}
})
}, [])
return (
<div>
<DesignTop />
<LeftCom />
<MainCom />
<RightCom />
</div>
)
}
OK,现在我们还需要就是给设计器增加保存的功能,我们来到designTop中,给它添加一个保存的按钮。
import { Button, message } from 'antd'
import './index.css'
import Store from '../../../store'
import axios from 'axios'
export default function DesignTop() {
const savePage = () => {
const search = window.location.search || '';
const pageId = search.replace('?pageId=', '');
const comList = Store.getState().comList;
axios.post('http://localhost:4000/page-json/updatePage', {
pageId,
pageJson: comList
})
.then(res => {
if(res.data.code == 200) {
message.success('保存成功')
}
})
}
return (
<div className='designTop'>
<span className='title'>XinBuilder</span>
<Button onClick={savePage} type='primary' ghost>保存</Button>
</div>
)
}
到此为止,在上一篇中实现的所有接口,我们就实现完对它的调用了。
和XinBuilder相关的代码提交在github上:
https://github.com/TeacherXin/XinBuilder2commit: 第十七节:实现页面的保存以及加载
博主补充
本篇相关的代码提交在github上:
https://github.com/TeacherXin/AppBuildercommit: 第一节:初始化项目,实现页面的创建等操作
目前我们已经有三个项目了:
- AppBuilder 最外层的壳子,提供创建页面等操作
- XinBuilder 设计器项目,负责对页面进行配置
- XinBuilderServer 后端服务,负责数据的存储
后面还会有一个运行时的项目。。。。。