修复移动端预览bug,添加登录页面,优化组件结构

This commit is contained in:
xujiang 2020-08-24 00:19:42 +08:00
parent 5be6dac382
commit 47ef72dd82
28 changed files with 336 additions and 93 deletions

View File

@ -1,9 +1,27 @@
## H5-Visible-Tool
## H5-dooring
H5-Visible-Tool是一款功能强大开源免费的H5可视化页面配置解决方案致力于提供一套简单方便、专业可靠、无限可能的H5落地页最佳实践。
H5-Dooring是一款功能强大开源免费的H5可视化页面配置解决方案致力于提供一套简单方便、专业可靠、无限可能的H5落地页最佳实践。技术栈以react为主 后台采用nodejs开发。
## 已完成功能
* 1. 组件库拖拽和显示
* 2. 组件库动态编辑
* 3. H5页面预览功能
* 4. 保存H5页面配置文件
* 5. 保存为模版
* 6. 移动端跨端适配
* 7. 媒体组件
## 正在完成功能
* 添加模版库模块
* 添加在线下载网站代码功能
* 丰富组件库组件,添加可视化组件
* 添加配置交互功能
* 组件细分和代码优化
* 添加typescript支持和单元测试
## 持续升级
正在升级1.1版本,敬请期待...
## 技术反馈和交流
微信beautifulFront
<img src="./code.png" />

View File

@ -2,7 +2,7 @@
import { createBrowserHistory } from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/@umijs/runtime';
let options = {
"basename": "/"
"basename": "h5_plus"
};
if ((<any>window).routerBase) {
options.basename = (<any>window).routerBase;

View File

@ -14,13 +14,13 @@ export function getRoutes() {
"exact": true
},
{
"path": "/preview",
"component": dynamic({ loader: () => import(/* webpackChunkName: 'p__editor__preview' */'/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/src/pages/editor/preview'), loading: LoadingComponent}),
"path": "/login",
"component": dynamic({ loader: () => import(/* webpackChunkName: 'p__login' */'/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/src/pages/login'), loading: LoadingComponent}),
"exact": true
},
{
"path": "/prevH5",
"component": dynamic({ loader: () => import(/* webpackChunkName: 'p__editor__preH5' */'/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/src/pages/editor/preH5'), loading: LoadingComponent}),
"path": "/preview",
"component": dynamic({ loader: () => import(/* webpackChunkName: 'p__editor__preview' */'/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/src/pages/editor/preview'), loading: LoadingComponent}),
"exact": true
}
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 KiB

BIN
src/assets/code.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

BIN
src/assets/login_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

View File

@ -91,7 +91,7 @@ const List = memo((props) => {
sourceData.map((item, i) => {
return <div className={styles.sourceItem} key={i}>
<div className={styles.imgWrap}>
<img src={item.imgUrl[0] ? item.imgUrl[0].url : 'http://io.nainor.com/uploads/01_173e15d3493.png'} alt={item.desc} style={{width: imgSize, height: imgSize, objectFit: 'cover', borderRadius: round}} />
<img src={item.imgUrl[0] ? item.imgUrl[0].url : ''} alt={item.desc} style={{width: imgSize, height: imgSize, objectFit: 'cover', borderRadius: round}} />
</div>
<div className={styles.content}>
<a className={styles.tit} style={{fontSize, color}} href={item.link ? item.link : '#'}>

View File

@ -6,11 +6,33 @@ const { Panel } = Tabs;
const XTab = (props) => {
const {
tabs,
tabs = ['分类一', '分类二'],
activeColor,
color,
fontSize,
sourceData
sourceData = [
{
"title": "趣谈小课",
"desc": "致力于打造优质小课程",
"link": "xxxxx",
"type": 0,
"imgUrl": "http://io.nainor.com/uploads/01_173e15d3493.png"
},
{
"title": "趣谈小课",
"desc": "致力于打造优质小课程",
"link": "xxxxx",
"type": 1,
"imgUrl": "http://io.nainor.com/uploads/01_173e15d3493.png"
},
{
"title": "趣谈小课",
"desc": "致力于打造优质小课程",
"link": "xxxxx",
"type": 0,
"imgUrl": "http://io.nainor.com/uploads/01_173e15d3493.png"
}
]
} = props
const tabWrapRef = useRef(null)

View File

@ -4,6 +4,8 @@ import { PlusOutlined } from '@ant-design/icons';
import ImgCrop from 'antd-img-crop';
import styles from './index.less';
const isDev = process.env.NODE_ENV === 'development'
function getBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
@ -62,8 +64,8 @@ class PicturesWall extends React.Component {
render() {
const { previewVisible, previewImage, fileList, previewTitle } = this.state;
const {
// action换上你的服务器接口地址
action = '',
// 配置自己的服务器地址
action = isDev ? 'http://192.168.1.6:3000/api/xxx' : 'http://xxxx',
headers,
withCredentials = true,
maxLen = 1

View File

@ -1,16 +1,11 @@
import React, { useState, useEffect, useRef, memo } from 'react'
import { Button, Input, Collapse, Slider, Empty, Popover, Modal, message } from 'antd'
import React, { useState, useEffect, memo } from 'react'
import { Input, Collapse, Slider, Empty } from 'antd'
import {
ArrowLeftOutlined,
PieChartOutlined,
ExpandOutlined,
MobileOutlined,
DownloadOutlined,
CopyOutlined
ExpandOutlined
} from '@ant-design/icons'
import { connect } from 'dva'
import QRCode from 'qrcode.react'
import { saveAs } from 'file-saver'
import HeaderComponent from './components/Header'
import SourceBox from './SourceBox'
import TargetBox from './TargetBox'
import Calibration from 'components/Calibration'
@ -25,9 +20,6 @@ import styles from './index.less'
const { Search } = Input;
const { Panel } = Collapse;
const { confirm } = Modal;
const isDev = process.env.NODE_ENV === 'development';
const Container = memo((props) => {
const [ scaleNum , setScale ] = useState(1)
@ -37,8 +29,6 @@ const Container = memo((props) => {
// 指定画布的id
let canvasId = 'js_canvas'
const iptRef = useRef(null)
const backSize = () => {
setScale(1)
}
@ -47,14 +37,6 @@ const Container = memo((props) => {
return <div><PieChartOutlined /> { text }</div>
}
const toPreview = () => {
localStorage.setItem('pointData', JSON.stringify(pointData))
savePreview()
setTimeout(() => {
window.open(`/preview?tid=${props.location.query.tid}`)
}, 1000)
}
const handleSliderChange = (v) => {
setScale(prev => v >= 150 ? 1.5 : (v / 100))
}
@ -81,42 +63,6 @@ const Container = memo((props) => {
})
}
const content = () => {
const { tid } = props.location.query || ''
return <QRCode value={`${location.protocol}//${location.host}/preview?tid=${tid}`} />
}
const handleSaveTpl = () => {
confirm({
title: '确定要保存吗?',
content: <div className={styles.saveForm}>
<div className={styles.formIpt}>
<span>模版名称</span><Input ref={iptRef} />
</div>
<div className={styles.formIpt}>
<span>访问链接</span><Input disabled value="暂未开放,保存之后可以在模版库中访问" />
</div>
</div>,
okText: '保存',
cancelText: '取消',
onOk() {
let name = iptRef.current.state.value
req.post('/visible/tpl/save', { name, tpl: pointData }).then(res => {
console.log(res)
})
},
onCancel() {
console.log('Cancel');
},
});
}
const downLoadJson = () => {
const jsonStr = JSON.stringify(pointData)
const blob = new Blob([jsonStr], { type: "text/plain;charset=utf-8" })
saveAs(blob, "template.json")
}
const savePreview = () => {
const { tid } = props.location.query || ''
req.post('/visible/preview', { tid, tpl: pointData })
@ -128,24 +74,7 @@ const Container = memo((props) => {
return (
<div className={styles.editorWrap}>
<div className={styles.header}>
<div className={styles.logoArea}>
<div className={styles.backBtn}><ArrowLeftOutlined /></div>
<div className={styles.logo}>Dooring</div>
</div>
<div className={styles.controlArea}>
<div className={styles.tit}>H5可视化编辑器</div>
</div>
<div className={styles.btnArea}>
<Button type="primary" style={{marginRight: '9px'}}>使用模版库</Button>
<Button type="primary" style={{marginRight: '9px'}} onClick={handleSaveTpl} disabled={!pointData.length}><DownloadOutlined />保存</Button>
<Button style={{marginRight: '9px'}} title="下载json文件" onClick={downLoadJson} disabled={!pointData.length}><CopyOutlined /></Button>
<Popover placement="bottom" title={null} content={content} trigger="click">
<Button style={{marginRight: '9px'}} onClick={savePreview} disabled={!pointData.length}><MobileOutlined /></Button>
</Popover>
<Button onClick={toPreview} disabled={!pointData.length}>预览</Button>
</div>
</div>
<HeaderComponent pointData={pointData} location={props.location} />
<div className={styles.container}>
<div className={styles.list} >
<div className={styles.searchBar}>

View File

@ -0,0 +1,112 @@
import React, { useRef, memo } from 'react'
import { Button, Input, Popover, Modal } from 'antd'
import {
ArrowLeftOutlined,
MobileOutlined,
DownloadOutlined,
CopyOutlined
} from '@ant-design/icons'
import QRCode from 'qrcode.react'
import { saveAs } from 'file-saver'
import req from '@/utils/req'
import Code from '@/assets/code.png'
import styles from './index.less'
const { confirm } = Modal;
const isDev = process.env.NODE_ENV === 'development';
const HeaderComponent = memo((props) => {
const { pointData, location } = props
const iptRef = useRef(null)
const toPreview = () => {
localStorage.setItem('pointData', JSON.stringify(pointData))
savePreview()
setTimeout(() => {
window.open(isDev ? `/preview?tid=${props.location.query.tid}` : `http://io.nainor.com/h5_plus/preview?tid=${props.location.query.tid}`)
}, 600)
}
const content = () => {
const { tid } = location.query || ''
return <QRCode value={`${window.location.protocol}//${window.location.host}/h5_plus/preview?tid=${tid}`} />
}
const handleSaveTpl = () => {
confirm({
title: '确定要保存吗?',
content: <div className={styles.saveForm}>
<div className={styles.formIpt}>
<span>模版名称</span><Input ref={iptRef} />
</div>
<div className={styles.formIpt}>
<span>访问链接</span><Input disabled value="暂未开放,保存之后可以在模版库中访问" />
</div>
</div>,
okText: '保存',
cancelText: '取消',
onOk() {
let name = iptRef.current.state.value
req.post('/visible/tpl/save', { name, tpl: pointData }).then(res => {
console.log(res)
})
},
onCancel() {
console.log('Cancel');
},
});
}
const useTemplate = () => {
Modal.info({
title: '该功能正在升级,可以关注下方公众号实时查看动态',
content: (
<div style={{textAlign: 'center'}}>
<img src={Code} alt="趣谈前端" style={{width: "180px"}} />
</div>
),
okText: '客官知道啦'
})
}
const downLoadJson = () => {
const jsonStr = JSON.stringify(pointData)
const blob = new Blob([jsonStr], { type: "text/plain;charset=utf-8" })
saveAs(blob, "template.json")
}
const toLogin = () => {
const { tid } = props.location.query || ''
window.location.href = `/h5_plus/login?tid=${tid}`
}
const savePreview = () => {
const { tid } = props.location.query || ''
req.post('/visible/preview', { tid, tpl: pointData })
}
return (
<div className={styles.header}>
<div className={styles.logoArea}>
<div className={styles.backBtn} onClick={toLogin}><ArrowLeftOutlined /></div>
<div className={styles.logo}>Dooring</div>
</div>
<div className={styles.controlArea}>
<div className={styles.tit}>H5可视化编辑器</div>
</div>
<div className={styles.btnArea}>
<Button type="primary" style={{marginRight: '9px'}} onClick={useTemplate}>使用模版库</Button>
<Button type="primary" style={{marginRight: '9px'}} onClick={handleSaveTpl} disabled={!pointData.length}><DownloadOutlined />保存</Button>
<Button style={{marginRight: '9px'}} title="下载json文件" onClick={downLoadJson} disabled={!pointData.length}><CopyOutlined /></Button>
<Popover placement="bottom" title={null} content={content} trigger="click">
<Button style={{marginRight: '9px'}} onClick={savePreview} disabled={!pointData.length}><MobileOutlined /></Button>
</Popover>
<Button onClick={toPreview} disabled={!pointData.length}>预览</Button>
</div>
</div>
)
})
export default HeaderComponent

View File

@ -0,0 +1,42 @@
.header {
position: relative;
z-index: 10;
padding-left: 30px;
padding-right: 30px;
display: flex;
align-items: center;
height: 80px;
background: #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
.logoArea {
width: 300px;
.backBtn {
display: inline-block;
padding: 12px 10px;
margin-right: 26px;
background-color: rgba(222, 224, 230, 0.3);
cursor: pointer;
}
.logo {
display: inline-block;
width: 105px;
font-size: 24px;
font-weight: bold;
img {
width: 100%;
}
}
}
.controlArea {
flex: 1;
text-align: center;
.tit {
font-size: 18px;
color: #000;
}
}
.btnArea {
width: 400px;
text-align: right;
}
}

View File

@ -41,7 +41,28 @@ export default {
}
},
effects: {
// 更新一条数据模型信息
// *modifyDataModel({ payload }, { call, put }) {
// const modifyDataModel = yield call(mesService.modifyDataModel, payload)
// const activate = yield call(mesService.activateModifiedTableDataModel, modifyDataModel.dataModelId)
// const responseMessage = yield call(mesService.getDetailDataModel, { dataModelId: activate.dataModelId, showDataModelFieldFlag: true })
// yield put({
// type: 'receiveDetailDataModel',
// payload: responseMessage && responseMessage
// })
// },
// 创建一条数据模型
// *createDataModel({ payload }, { call, put }) {
// const responseMessage = yield call(mesService.createDataModel, payload)
// if (responseMessage.dataModelId) {
// router.push({
// pathname: '/dataModel/view',
// query: { id: responseMessage.dataModelId }
// })
// }
// },
},
subscriptions: {
setup({ dispatch, history }) {

View File

@ -34,7 +34,7 @@ const PreviewPage = memo((props) => {
useEffect(() => {
const { tid } = props.location.query
req.get('/visible/preview/get', { params: { tid } }).then(res => {
setPointData(res)
setPointData(res.map(item => ({...item, point: {...item.point, isDraggable: false, isResizable: false } })))
}).catch(err => {
setTimeout(() => {
window.close()

View File

@ -0,0 +1,26 @@
.loginWrap {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background: #f0f0f0 url(../../assets/login_bg.png) center center;
background-size: cover;
.tit {
font-size: 35px;
text-align: center;
padding-bottom: 20px;
margin-bottom: 36px;
border-bottom: 1px solid #f0f0f0;
}
.formWrap {
margin-left: auto;
margin-right: auto;
width: 520px;
padding: 22px 0 20px;
background-color: #fff;
box-shadow: 0 0 20px rgba(0,0,0,.1);
border-radius: 6px;
}
}

70
src/pages/login/index.tsx Normal file
View File

@ -0,0 +1,70 @@
import { Form, Input, Button, Checkbox } from 'antd';
import http from '@/utils/req';
import { history } from 'umi';
import styles from './index.less';
const layout = {
labelCol: { span: 6 },
wrapperCol: { span: 16 },
};
const tailLayout = {
wrapperCol: { offset: 6, span: 16 },
};
const Login = (props) => {
const onFinish = values => {
http.post('/login', {...values}).then(res => {
localStorage.setItem('token', res.token)
localStorage.setItem('user', values.username)
history.push('/')
})
};
const onFinishFailed = errorInfo => {
console.log('Failed:', errorInfo);
};
return (
<div className={styles.loginWrap}>
<Form
{...layout}
name="login"
className={styles.formWrap}
initialValues={{ remember: true }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
>
<div className={styles.tit}>Doring开放平台<span style={{marginLeft: '20px',fontSize: '18px',color: '#06c'}}></span></div>
<Form.Item
label="用户名"
name="username"
rules={[{ required: true, message: '请输入用户名!' }]}
>
<Input />
</Form.Item>
<Form.Item
label="密码"
name="password"
rules={[{ required: true, message: '请输入密码!' }]}
>
<Input.Password />
</Form.Item>
<Form.Item {...tailLayout}>
<Button type="primary" htmlType="submit" block>
</Button>
</Form.Item>
<Form.Item {...tailLayout}>
<Button block onClick={() => props.history.push(`/editor?tid=${props.location.query.tid}`)}>
使
</Button>
</Form.Item>
</Form>
</div>
);
};
export default Login

View File

@ -4,7 +4,8 @@ import { message } from 'antd'
const isDev = process.env.NODE_ENV === 'development'
const instance = axios.create({
baseURL: 'xxxxxxx',
// 服务器地址需要自己配置和开发
baseURL: isDev ? 'http://localhost:3000/xxx' : 'http://xxxxx',
timeout: 10000,
withCredentials: true
});
@ -24,8 +25,8 @@ instance.interceptors.request.use(function (config) {
// 添加响应拦截器
instance.interceptors.response.use(function (response) {
// 对响应数据做点什么, 这里是自定义的信息头,用来给前端说明展示的信息
if(response.headers['x-x-x'] === 'xxx') {
// 对响应数据做点什么
if(response.headers['x-show-msg'] === 'zxzk_msg_200') {
message.success(response.data.msg);
}
return response.data.result;