Merge pull request #33 from MrXujiang/yehuozhiliwork

Yehuozhiliwork
This commit is contained in:
yehuozhili 2020-09-23 22:52:05 +08:00 committed by GitHub
commit bf7ca0fc98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
200 changed files with 22442 additions and 47756 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -1,3 +1,3 @@
{ {
"extends": "eslint-config-umi" "extends": "react-app"
} }

3
.gitignore vendored
View File

@ -102,3 +102,6 @@ dist
# TernJS port file # TernJS port file
.tern-port .tern-port
.umi/
.umi-production/

View File

@ -6,26 +6,34 @@ export default defineConfig({
loading: '@/components/LoadingCp', loading: '@/components/LoadingCp',
}, },
dva: { dva: {
immer: true immer: true,
}, },
devtool: 'source-map',
antd: {}, antd: {},
sass: { title: '趣谈前端-h5-dooring',
implementation: require('node-sass'),
},
title: '趣谈前端-h5-visible-tool',
exportStatic: {}, exportStatic: {},
base: 'h5_plus', base: 'h5_plus',
publicPath: '/h5_plus/', publicPath: '/h5_plus/',
outputPath: '../server/static/h5_plus', outputPath: '../server/static/h5_plus',
routes: [ routes: [
{ {
exact: false,
path: '/', path: '/',
// component: '@/layouts', component: '@/layouts/index',
routes: [ routes: [
{
path: '/',
component: '../pages/home',
},
{ {
path: '/editor', path: '/editor',
component: '../pages/editor', component: '../pages/editor',
}, },
{
path: '/ide',
component: '../pages/ide',
},
{ {
path: '/login', path: '/login',
component: '../pages/login', component: '../pages/login',
@ -37,22 +45,19 @@ export default defineConfig({
{ {
path: '/preview', path: '/preview',
component: '../pages/editor/preview', component: '../pages/editor/preview',
} },
] ],
} },
], ],
theme: { theme: {
"primary-color": "#2F54EB", 'primary-color': '#2F54EB',
// "btn-primary-bg": "#2F54EB" // "btn-primary-bg": "#2F54EB"
}, },
extraBabelPlugins: [ extraBabelPlugins: [['import', { libraryName: 'zarm', style: true }]],
['import', { libraryName: "zarm", style: true }],
],
// sass: {}, // sass: {},
alias: { alias: {
components: path.resolve(__dirname, 'src/components/'), components: path.resolve(__dirname, 'src/components/'),
utils: path.resolve(__dirname, 'src/utils/'), utils: path.resolve(__dirname, 'src/utils/'),
assets: path.resolve(__dirname, 'src/assets/') assets: path.resolve(__dirname, 'src/assets/'),
} },
}); });

29315
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,30 @@
{ {
"private": true, "name": "h5-dooring",
"version": "1.3.0",
"description": "H5-Dooring是一款功能强大开源免费的H5可视化页面配置解决方案致力于提供一套简单方便、专业可靠、无限可能的H5落地页最佳实践。技术栈以react为主 后台采用nodejs开发。",
"private": false,
"author": {
"name": "徐小夕",
"email": "xujiang156@qq.com",
"url": "http://io.nainor.com/h5_visible"
},
"keywords": [
"h5 editor",
"h5",
"react",
"antd",
"react-dnd",
"web visible"
],
"contributors": [
"徐小夕 <xujiang156@qq.com> (https://github.com/MrXujiang))",
"yehuozhili <yehuozhili@outlook.com> (https://github.com/yehuozhili))"
],
"scripts": { "scripts": {
"start": "umi dev", "start": "umi dev",
"build": "umi build", "build": "umi build",
"server": "node server.js",
"postinstall": "umi generate tmp",
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'", "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
"test": "umi-test", "test": "umi-test",
"test:coverage": "umi-test --coverage" "test:coverage": "umi-test --coverage"
@ -18,32 +40,77 @@
"prettier --parser=typescript --write" "prettier --parser=typescript --write"
] ]
}, },
"homepage": "http://io.nainor.com/h5_visible",
"repository": {
"type": "git",
"url": "git+https://github.com/MrXujiang/h5-Dooring.git"
},
"bugs": {
"url": "https://github.com/MrXujiang/h5-Dooring/issues"
},
"dependencies": { "dependencies": {
"@ant-design/charts": "^0.9.9",
"@ant-design/icons": "^4.2.1", "@ant-design/icons": "^4.2.1",
"@antv/f2": "^3.7.7",
"@umijs/plugin-sass": "^1.1.1", "@umijs/plugin-sass": "^1.1.1",
"@umijs/preset-react": "1.x", "@umijs/preset-react": "1.x",
"@umijs/test": "^3.0.12", "@umijs/test": "^3.2.19",
"antd": "^4.2.3", "antd": "^4.2.3",
"antd-img-crop": "^3.10.0", "antd-img-crop": "^3.10.0",
"axios": "^0.19.2", "axios": "^0.19.2",
"babel-plugin-import": "^1.13.0", "chatbot-antd": "^0.6.0",
"codemirror": "^5.57.0",
"file-saver": "^2.0.2", "file-saver": "^2.0.2",
"lint-staged": "^10.0.7",
"node-sass": "^4.14.1",
"prettier": "^1.19.1",
"qrcode.react": "^1.0.0", "qrcode.react": "^1.0.0",
"react": "^16.12.0", "react": "^16.12.0",
"react-codemirror2": "^7.2.1",
"react-color": "^2.18.1", "react-color": "^2.18.1",
"react-dnd": "^11.1.3", "react-dnd": "^11.1.3",
"react-dnd-html5-backend": "^11.1.3", "react-dnd-html5-backend": "^11.1.3",
"react-dom": "^16.12.0", "react-dom": "^16.12.0",
"react-draggable": "^4.4.3", "react-draggable": "^4.4.3",
"react-grid-layout": "^1.0.0", "react-grid-layout": "^1.0.0",
"sass-loader": "^9.0.3", "react-hotkeys-hook": "^2.3.1",
"umi": "^3.0.12", "react-text-loop": "^2.3.0",
"redux-undo": "^1.0.1",
"socket.io-client": "^2.3.0",
"umi": "^3.2.19",
"video-react": "^0.14.1", "video-react": "^0.14.1",
"xlsx": "^0.16.7",
"yorkie": "^2.0.0", "yorkie": "^2.0.0",
"zarm": "^2.5.1" "zarm": "^2.5.1"
},
"license": "MIT",
"devDependencies": {
"@types/qrcode.react": "^1.0.1",
"@types/classnames": "^2.2.10",
"@types/codemirror": "^0.0.98",
"@types/events": "^3.0.0",
"@types/file-saver": "^2.0.1",
"@types/node": "^14.6.2",
"@types/react-color": "^3.0.4",
"@types/react-grid-layout": "^1.1.0",
"@types/redux-logger": "^3.0.8",
"@types/xlsx": "^0.0.36",
"@typescript-eslint/eslint-plugin": "4.1.1",
"@typescript-eslint/parser": "4.1.1",
"babel-eslint": "10.x",
"babel-plugin-import": "^1.13.0",
"eslint": "6.x",
"eslint-config-react-app": "^5.2.1",
"eslint-plugin-flowtype": "4.x",
"eslint-plugin-import": "2.x",
"eslint-plugin-jsx-a11y": "6.x",
"eslint-plugin-react": "7.x",
"eslint-plugin-react-hooks": "2.x",
"koa": "^2.13.0",
"koa-body": "^4.2.0",
"koa-logger": "^3.2.1",
"koa-static": "^5.0.0",
"koa2-cors": "^2.0.6",
"lint-staged": "^10.0.7",
"prettier": "^1.19.1",
"redux-logger": "^3.0.6",
"sass-loader": "^9.0.3",
"typescript": "^4.0.2"
} }
} }

226
readme.md
View File

@ -15,7 +15,7 @@
### ✨ [Demo](http://io.nainor.com/h5_plus/editor?tid=123456) ### ✨ [Demo](http://io.nainor.com/h5_plus/editor?tid=123456)
<img src="http://io.nainor.com/uploads/56_1741c466be0.png" alt="H5可视化编辑器" /> <img src="http://io.nainor.com/uploads/demo_1748b27f0e4.png" alt="H5可视化编辑器" />
## Author ## Author
@ -46,171 +46,13 @@ Give a ⭐️ if this project helped you!
* **@koa/router** 基于koa2的服务端路由中间件 * **@koa/router** 基于koa2的服务端路由中间件
* **ramda** 优秀的函数式js工具库 * **ramda** 优秀的函数式js工具库
## 需求分析 ### 预览功能
在思考需求分析之前我们先来看看**Dooring**的使用演示:
![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e7000035ac7f44898d3450c24468cc65~tplv-k3u1fbpfcp-zoom-1.image)
由上面的gif图我们可以分析出可视化编辑器主要有以下几部分组成
* 可拖拽的组件库 draggable components
* 盛放组件的画布 canvas
* 组件编辑器 FormEditor
* 头部工具栏 toolBar
可拖拽组件我们可以用社区比较火的**react-dnd****react-draggable**来实现,由于我们的画布是可拖拽可放大缩小的,所以这里需要对画布赋能,具体实现可参考下文。
其次就是H5编辑器部分这部分是核心功能后面我们会详细分析。还有就是预览生成预览链接保存**json**文件, 保存模版这些功能本质上是对我们**json**文件的操作,可是目前可视化搭建技术的常用手段之一。先来看看这些功能的演示:
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/593bdeae932e4e72a5d51249b866e65d~tplv-k3u1fbpfcp-zoom-1.image)
## 基础准备
我们的**h5页面可视化编辑器**采用**umi**来作为脚手架工具.
> **umi**是可扩展的企业级前端应用框架,以路由为基础的,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。然后配以生命周期完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求.
这样我们不会关注繁琐的工程配置细节, 可以直接在项目中使用 **antd****less** 这些方案, 并且集成了目前比较流行的**css module**, 可以方便我们在项目里对**css**进行模块化开发. umi创建项目的具体使用流程如下:
``` js
// 创建并进入工程目录
mkdir dooring && cd dooring
// 创建umi应用
yarn create @umijs/umi-app
// 安装依赖
yarn // 或者使用npm install
```
简单的三步走策略就能轻松搭建我们的项目工程, 是不是省去了很多麻烦? (在使用这些方式之前我们首先确保自己本地的**node** 版本是 10.13 或以上)
在项目创建完之后我们还需要安装可视化方面必备的第三方组件, 笔者调研社区精选组件之后采用了一下方案:
* **react-dnd** react拖拽组件
* **react-color** react颜色选择组件,用于H5编辑器的编辑颜色部分
* **react-draggable** 用于组件或者画布的拖拽移动
* **react.qrcode** 基于react的二维码生成组件, 能以react组件的方式生成二维码
以上组件在运行项目前大家可以自行安装.
## 介绍
在最好项目开发准备之后,我们就来开始设计我们的h5页面可视化编辑器**Dooring**.
### H5编辑器实现
H5可视化编辑器主要需要4个部分,在文章开头也分析过, 这里用图来巩固一下:
![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5dd41aec53134d558fe46401a5e58800~tplv-k3u1fbpfcp-zoom-1.image)
以上是最基本也是最核心的功能展示模型,接下来我们会一一将其拆解并逐个实现.
#### 实现原理
我们都知道, 目前比较流行的页面可视化搭建方案可以有如下几种:
* 在线编辑代码实现
* 在线编辑json实现
* 无代码化拖拽实现(底层基于json配置文件)
笔者做了一下优缺点对比图,如下:
| 方案 | 定制化程度 | 缺点 |
| :--------- | :--: | -----------: |
| 在线编辑代码 | 最高 | 使用成本高,对非技术人员不友好,效率低 |
| 在线编辑json | 较高 | 需要熟悉json,有一定使用成本, 对非技术人员不友好,效率一般 |
| 无代码化拖拽实现 | 高 | 使用成本低, 操作基本无门槛,效率较高 |
由以上分析来看, 为了开发一个低门槛, 对任何人适用的可视化编辑器, 笔者将采用第三种方案来实现, 目前市面上已有的产品也有很多, 比如说易企秀, 兔展, 百度H5等等. **实现原理其实还是基于json, 我们通过可视化的手段将自己配置的 页面转化为json数据,最后在基于json渲染器来动态生成H5站点**.
![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6aafb8a67c0844248b6122f8e021dfdb~tplv-k3u1fbpfcp-zoom-1.image)
#### 数据结构设计
为了提供组件的自定义能力,我们需要定义一套高可用的数据结构, 这样才能实现因组件需求变更而带来的维护性优势.
在开始设计数据结构之前我们先来拆解一下模块:
![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/de625f318fa247f3a0e3bd5048ed2266~tplv-k3u1fbpfcp-zoom-1.image)
不同的组件都对应不同的"编辑区域".我们需要设计一套统一的标准的配置来约定它, 这样对于表单编辑器的设计也非常有利, 具体拆解如下:
![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5ff742d89b464849898871d353f64855~tplv-k3u1fbpfcp-zoom-1.image)
经过以上分析之后, 笔者设计了类似下面的数据结构:
``` js
"Text": {
"editData": [
{
"key": "text",
"name": "文字",
"type": "Text"
},
{
"key": "color",
"name": "标题颜色",
"type": "Color"
},
{
"key": "fontSize",
"name": "字体大小",
"type": "Number"
},
{
"key": "align",
"name": "对齐方式",
"type": "Select",
"range": [
{
"key": "left",
"text": "左对齐"
},
{
"key": "center",
"text": "居中对齐"
},
{
"key": "right",
"text": "右对齐"
}
]
},
{
"key": "lineHeight",
"name": "行高",
"type": "Number"
}
],
"config": {
"text": "我是文本",
"color": "rgba(60,60,60,1)",
"fontSize": 18,
"align": "center",
"lineHeight": 2
}
}
```
通过这种标准化结构设计之后,我们可以很方便的实现我们所需要的编辑页面的功能, 并且后期扩展非常方便, 只需要往editData添加配置即可. 至于动态表单编辑器的实现,方案有很多, 笔者之前也写过相关的文章, 这里就不详细介绍了.
[基于react搭建一个通用的表单管理配置平台vue同](https://juejin.im/post/6844904137390292999)
#### 组件库设计
组件库设计考虑的一个重要的问题就是体积和渲染问题, 一旦组件库变的越来越多, 那意味着页面加载会非常慢,所以我们需要实现异步加载组件和代码分割的能力, umi提供了这样的功能,我们可以基于它提供的api去实现自己的额按需组件.
``` js
import { dynamic } from 'umi';
export default dynamic({
loader: async function() {
// 这里的注释 webpackChunkName 可以指导 webpack 将该组件 HugeA 以这个名字单独拆出去
const { default: HugeA } = await import(/* webpackChunkName: "external_A" */ './HugeA');
return HugeA;
},
});
```
通过以上的方式来定义包裹我们的每一个组件, 这样就能实现按需加载了, 但是最好的建议是不需要每个组件都按需加载和拆包,对于**标题**, **通知栏**,**页头**,**页脚**这些组件, 我们完全可以把它放在一个组里,这样不但对不会影响加载速度, 还能减少一定的http请求.
笔者这里简单举一个组件实现的例子,方便大家理解:
``` js
const Header = memo((props) => {
const {
bgColor,
logo,
logoText,
fontSize,
color
} = props
return <header className={styles.header} style={{backgroundColor: bgColor}}>
<div className={styles.logo}>
<img src={logo && logo[0].url} alt={logoText} />
</div>
<div className={styles.title} style={{fontSize, color}}>{ logoText }</div>
</header>
})
```
上面的Header组件的props属性完全是由我们之前设计的json结构来定义的在用户编辑的过程中将收据收集并传给Header组件。最后一步是将这些组件动态传给**dynamic**组件, 这块在上文也介绍过了,大家可以根据自己的实现来做动态化渲染。
### 实现预览功能
预览功能这块比较简单, 我们只需要将用户生成的json数据丢进H5渲染器中即可, 这里我们需要做一个渲染页面单独用来预览组件. 先来看看几个预览效果: 预览功能这块比较简单, 我们只需要将用户生成的json数据丢进H5渲染器中即可, 这里我们需要做一个渲染页面单独用来预览组件. 先来看看几个预览效果:
<img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/582e67cb0a874bee947efb05af622a55~tplv-k3u1fbpfcp-zoom-1.image" alt="h5-editor" width="375px" /> <img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/582e67cb0a874bee947efb05af622a55~tplv-k3u1fbpfcp-zoom-1.image" alt="h5-editor" width="375px" />
<br />
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7c50d89f770f4ae5b4c5c4ec9f0052c1~tplv-k3u1fbpfcp-zoom-1.image" alt="h5-editor" width="375px" /> <img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7c50d89f770f4ae5b4c5c4ec9f0052c1~tplv-k3u1fbpfcp-zoom-1.image" alt="h5-editor" width="375px" />
前面的渲染器原理已经介绍了, 这里就不一一介绍了,感兴趣的可以交流讨论. 前面的渲染器原理已经介绍了, 这里就不一一介绍了,感兴趣的可以交流讨论.
### 实现在线下载功能 ### 实现在线下载功能
@ -222,7 +64,7 @@ FileSaver.saveAs(blob, "hello world.txt");
``` ```
以上代码可以实现将传入的数据下载为txt文件, 如果是Blob, 是不是还能在线下载图片, html呢? 答案是肯定的, 所以我们的下载任务采用该方案来实现. 以上代码可以实现将传入的数据下载为txt文件, 如果是Blob, 是不是还能在线下载图片, html呢? 答案是肯定的, 所以我们的下载任务采用该方案来实现.
### 后端部分实现 ### 后端部分
后端部分由于涉及的知识点比较多, 不是本文考虑的重点, 所以这里大致提几个点, 大家可以用完全不同的技术来实现后台服务, 比如说**PHP**, **Java**, **Python**或者**Egg**. 笔者这里采用的是**koa**. 主要实现功能如下: 后端部分由于涉及的知识点比较多, 不是本文考虑的重点, 所以这里大致提几个点, 大家可以用完全不同的技术来实现后台服务, 比如说**PHP**, **Java**, **Python**或者**Egg**. 笔者这里采用的是**koa**. 主要实现功能如下:
* 保存模板 * 保存模板
* 真机原理的数据源存储 * 真机原理的数据源存储
@ -235,40 +77,70 @@ FileSaver.saveAs(blob, "hello world.txt");
模式基本一致. 模式基本一致.
## wiki(参考文档)
* [H5可视化编辑器(H5 Dooring)介绍](https://github.com/MrXujiang/h5-Dooring/wiki/H5%E5%8F%AF%E8%A7%86%E5%8C%96%E7%BC%96%E8%BE%91%E5%99%A8(H5-Dooring)%E4%BB%8B%E7%BB%8D)
* [Form Editor(动态表单设计器)](https://github.com/MrXujiang/h5-Dooring/wiki/Form-Editor(%E5%8A%A8%E6%80%81%E8%A1%A8%E5%8D%95%E8%AE%BE%E8%AE%A1%E5%99%A8))
* [基于f2实现移动端可视化编辑器(dooring升级版)](https://github.com/MrXujiang/h5-Dooring/wiki/%E5%9F%BA%E4%BA%8Ef2%E5%AE%9E%E7%8E%B0%E7%A7%BB%E5%8A%A8%E7%AB%AF%E5%8F%AF%E8%A7%86%E5%8C%96%E7%BC%96%E8%BE%91%E5%99%A8(dooring%E5%8D%87%E7%BA%A7%E7%89%88))
## 已完成功能 ## 已完成功能
* 1. 组件库拖拽和显示 1. 组件库拖拽和显示
* 2. 组件库动态编辑 2. 组件库动态编辑
* 3. H5页面预览功能 3. H5页面预览功能
* 4. 保存H5页面配置文件 4. 保存H5页面配置文件
* 5. 保存为模版 5. 保存为模版
* 6. 移动端跨端适配 6. 移动端跨端适配
* 7. 媒体组件 7. 媒体组件
8. 在线下载网站代码功能
9. 添加typescript支持
10. 表单设计器/自定义表单组件
11. 可视化组件Chart实现
12. 在线编程模块(Mini Web IDE)
13. 新增图表组件 面积图,折线图, 饼图
## 正在完成功能 ## 正在完成功能
* 添加模版库模块 * 升级模版库
* 添加在线下载网站代码功能 * 丰富组件库组件,添加可视化组件如折线图, 饼图, 面积图等
* 丰富组件库组件,添加可视化组件
* 添加配置交互功能 * 添加配置交互功能
* 组件细分和代码优化 * 组件细分和代码优化
* 添加typescript支持和单元测试 * 单元测试
## Install(安装) ## Install(安装)
1. 下载代码
```sh
git clone https://github.com/MrXujiang/h5-Dooring.git
```
2. 进入项目目录
```sh
cd ./h5-Dooring
```
3. 安装依赖包
```sh ```sh
yarn install yarn install
``` ```
## Usage ## Usage
启动应用
```sh ```sh
yarn run start yarn run start
``` ```
## 更新日志
1. 添加在线编程模块在执行代码前先启动node服务 npm run server
2. 添加客服机器人模块[chatbot-antd](https://www.npmjs.com/package/chatbot-antd)
## 持续升级 ## 持续升级
正在升级1.1版本,敬请期待... 正在升级1.3版本,敬请期待...
## 赞助
开源不易, 有了您的赞助, 我们会做的更好~
## 技术反馈和交流 ## 技术反馈和交流
微信beautifulFront 微信beautifulFront
<img src="http://io.nainor.com/uploads/code_1741c445027.png" width="180px" /> <img src="http://io.nainor.com/uploads/code_1741c445027.png" width="180px" />
## 技术交流群
<img src="http://io.nainor.com/uploads/i_1749a2713f0.png" width="180px" />

50
server.js Normal file
View File

@ -0,0 +1,50 @@
const Koa = require('koa');
const { resolve } = require('path');
const staticServer = require('koa-static');
const koaBody = require('koa-body');
const cors = require('koa2-cors');
const logger = require('koa-logger');
const app = new Koa();
app.use(staticServer(resolve(__dirname, './static')));
app.use(koaBody());
app.use(logger());
// 设置跨域
app.use(
cors({
origin: function(ctx) {
if (ctx.url.indexOf('/dooring') > -1) {
return '*'; // 允许来自所有域名请求
}
return '';
},
exposeHeaders: ['WWW-Authenticate', 'Server-Authorization', 'x-test-code'],
maxAge: 5, // 该字段可选,用来指定本次预检请求的有效期,单位为秒
credentials: true,
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowHeaders: [
'Content-Type',
'Authorization',
'Accept',
'x-requested-with',
'Content-Encoding',
],
}),
);
let htmlStr = '';
app.use(async (ctx, next) => {
console.log(ctx.url);
if (ctx.url === '/dooring/render') {
htmlStr = ctx.request.body;
ctx.body = 'success';
} else if (ctx.url.indexOf('/html') === 0) {
ctx.type = 'html';
ctx.body = htmlStr;
}
});
app.listen(3000);

View File

@ -1,51 +0,0 @@
// @ts-nocheck
if (window.g_initWebpackHotDevClient) {
function tryApplyUpdates(onHotUpdateSuccess?: Function) {
// @ts-ignore
if (!module.hot) {
window.location.reload();
return;
}
function isUpdateAvailable() {
// @ts-ignore
return window.g_getMostRecentCompilationHash() !== __webpack_hash__;
}
// TODO: is update available?
// @ts-ignore
if (!isUpdateAvailable() || module.hot.status() !== 'idle') {
return;
}
function handleApplyUpdates(err: Error | null, updatedModules: any) {
if (err || !updatedModules || window.g_getHadRuntimeError()) {
window.location.reload();
return;
}
onHotUpdateSuccess?.();
if (isUpdateAvailable()) {
// While we were updating, there was a new update! Do it again.
tryApplyUpdates();
}
}
// @ts-ignore
module.hot.check(true).then(
function (updatedModules: any) {
handleApplyUpdates(null, updatedModules);
},
function (err: Error) {
handleApplyUpdates(err, null);
},
);
}
window.g_initWebpackHotDevClient({
tryApplyUpdates,
});
}

View File

@ -1,26 +0,0 @@
// @ts-nocheck
import { createBrowserHistory } from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/@umijs/runtime';
let options = {
"basename": "h5_plus"
};
if ((<any>window).routerBase) {
options.basename = (<any>window).routerBase;
}
// remove initial history because of ssr
let history: any = process.env.__IS_SERVER ? null : createBrowserHistory(options);
export const createHistory = (hotReload = false) => {
if (!hotReload) {
history = createBrowserHistory(options);
}
return history;
};
// 通常仅微前端场景需要调用这个 API
export const setCreateHistoryOptions = (newOpts: any = {}) => {
options = { ...options, ...newOpts };
};
export { history };

View File

@ -1,8 +0,0 @@
// @ts-nocheck
import { Plugin } from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/@umijs/runtime';
const plugin = new Plugin({
validKeys: ['modifyClientRenderOpts','patchRoutes','rootContainer','render','onRouteChange','dva','getInitialState','request',],
});
export { plugin };

View File

@ -1,210 +0,0 @@
/** Created by Umi Plugin **/
export interface IConfigFromPlugins {
routes?: {
/**
* Any valid URL path
*/
path?: string;
/**
* A React component to render only when the location matches.
*/
component?: string | (() => any);
wrappers?: string[];
/**
* navigate to a new location
*/
redirect?: string;
/**
* When true, the active class/style will only be applied if the location is matched exactly.
*/
exact?: boolean;
routes?: any[];
[k: string]: any;
}[];
history?: {
type?: "browser" | "hash" | "memory";
options?: {};
};
polyfill?: {
imports?: string[];
};
alias?: {};
analyze?: {
analyzerMode?: "server" | "static" | "disabled";
analyzerHost?: string;
analyzerPort?: any;
openAnalyzer?: boolean;
generateStatsFile?: boolean;
statsFilename?: string;
logLevel?: "info" | "warn" | "error" | "silent";
defaultSizes?: "stat" | "parsed" | "gzip";
[k: string]: any;
};
/**
* postcss autoprefixer, default flexbox: no-2009
*/
autoprefixer?: {};
base?: string;
chainWebpack?: () => any;
chunks?: string[];
/**
* more css-loader options see https://webpack.js.org/loaders/css-loader/#options
*/
cssLoader?: {
url?: boolean | (() => any);
import?: boolean | (() => any);
modules?: boolean | string | {};
sourceMap?: boolean;
importLoaders?: number;
onlyLocals?: boolean;
esModule?: boolean;
localsConvention?: "asIs" | "camelCase" | "camelCaseOnly" | "dashes" | "dashesOnly";
};
cssModulesTypescriptLoader?: {
mode?: "emit" | "verify";
};
cssnano?: {};
copy?: string[];
define?: {};
devScripts?: {};
/**
* devServer configs
*/
devServer?: {
/**
* devServer port, default 8000
*/
port?: number;
host?: string;
https?:
| {
key?: string;
cert?: string;
[k: string]: any;
}
| boolean;
headers?: {};
writeToDisk?: boolean | (() => any);
[k: string]: any;
};
devtool?: string;
/**
* Code splitting for performance optimization
*/
dynamicImport?: {
/**
* loading the component before loaded
*/
loading?: string;
};
exportStatic?: {
htmlSuffix?: boolean;
dynamicRoot?: boolean;
/**
* extra render paths only enable in ssr
*/
extraRoutePaths?: () => any;
};
externals?: {} | string | (() => any);
extraBabelPlugins?: any[];
extraBabelPresets?: any[];
extraPostCSSPlugins?: any[];
/**
* fork-ts-checker-webpack-plugin options see https://github.com/TypeStrong/fork-ts-checker-webpack-plugin#options
*/
forkTSChecker?: {
async?: boolean;
typescript?: boolean | {};
eslint?: {};
issue?: {};
formatter?: string | {};
logger?: {};
[k: string]: any;
};
hash?: boolean;
ignoreMomentLocale?: boolean;
inlineLimit?: number;
lessLoader?: {};
manifest?: {
fileName?: string;
publicPath?: "";
basePath?: string;
writeToFileEmit?: boolean;
};
mountElementId?: "";
mpa?: {};
nodeModulesTransform?: {
type?: "all" | "none";
exclude?: string[];
};
outputPath?: "";
plugins?: string[];
postcssLoader?: {};
presets?: string[];
proxy?: {};
publicPath?: string;
runtimePublicPath?: boolean;
ssr?: {
/**
* remove window.g_initialProps in html, to force execing Page getInitialProps functions
*/
forceInitial?: boolean;
/**
* disable serve-side render in umi dev mode.
*/
devServerRender?: boolean;
mode?: "stream" | "string";
/**
* static markup in static site
*/
staticMarkup?: boolean;
};
singular?: boolean;
styleLoader?: {};
targets?: {};
terserOptions?: {};
theme?: {};
runtimeHistory?: {};
favicon?: string;
headScripts?: any[];
links?: any[];
metas?: any[];
scripts?: any[];
styles?: any[];
title?: string;
mock?: {
exclude?: string[];
};
antd?: {
dark?: boolean;
compact?: boolean;
config?: {};
};
dva?: {
immer?: boolean;
hmr?: boolean;
skipModelValidate?: boolean;
extraModels?: string[];
};
locale?: {
default?: string;
useLocalStorage?: boolean;
baseNavigator?: boolean;
title?: boolean;
antd?: boolean;
baseSeparator?: string;
};
layout?: {};
request?: {
dataField?: "";
};
sass?: {
implementation?: any;
sassOptions?: {};
prependData?: string | (() => any);
sourceMap?: boolean;
webpackImporter?: boolean;
};
[k: string]: any;
}

View File

@ -1,18 +0,0 @@
// @ts-nocheck
import { plugin } from './plugin';
import * as Plugin_0 from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/src/.umi/plugin-dva/runtime.tsx';
import * as Plugin_1 from '../plugin-initial-state/runtime';
import * as Plugin_2 from '../plugin-model/runtime';
plugin.register({
apply: Plugin_0,
path: '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/src/.umi/plugin-dva/runtime.tsx',
});
plugin.register({
apply: Plugin_1,
path: '../plugin-initial-state/runtime',
});
plugin.register({
apply: Plugin_2,
path: '../plugin-model/runtime',
});

View File

@ -1,3 +0,0 @@
// @ts-nocheck
import 'core-js';
import 'regenerator-runtime/runtime';

View File

@ -1,38 +0,0 @@
// @ts-nocheck
import { ApplyPluginsType, dynamic } from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/@umijs/runtime';
import { plugin } from './plugin';
import LoadingComponent from '@/components/LoadingCp';
export function getRoutes() {
const routes = [
{
"path": "/",
"routes": [
{
"path": "/editor",
"component": dynamic({ loader: () => import(/* webpackChunkName: 'p__editor' */'/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/src/pages/editor'), loading: LoadingComponent}),
"exact": true
},
{
"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": "/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
}
]
}
];
// allow user to extend routes
plugin.applyPlugins({
key: 'patchRoutes',
type: ApplyPluginsType.event,
args: { routes },
});
return routes;
}

View File

@ -1,9 +0,0 @@
// @ts-nocheck
export { history, setCreateHistoryOptions } from './history';
export { plugin } from './plugin';
export * from '../plugin-dva/exports';
export * from '../plugin-dva/connect';
export * from '../plugin-initial-state/exports';
export * from '../plugin-model/useModel';
export * from '../plugin-request/request';
export * from '../plugin-helmet/exports';

View File

@ -1,83 +0,0 @@
// @ts-nocheck
import { IRoute } from '@umijs/core';
import { AnyAction } from 'redux';
import React from 'react';
import { EffectsCommandMap, SubscriptionAPI } from 'dva';
import { match } from 'react-router-dom';
import { Location, LocationState, History } from 'history';
export * from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/src/pages/editor/models/editorModal';
export interface Action<T = any> {
type: T
}
export type Reducer<S = any, A extends Action = AnyAction> = (
state: S | undefined,
action: A
) => S;
export type ImmerReducer<S = any, A extends Action = AnyAction> = (
state: S,
action: A
) => void;
export type Effect = (
action: AnyAction,
effects: EffectsCommandMap,
) => void;
/**
* @type P: Type of payload
* @type C: Type of callback
*/
export type Dispatch = <P = any, C = (payload: P) => void>(action: {
type: string;
payload?: P;
callback?: C;
[key: string]: any;
}) => any;
export type Subscription = (api: SubscriptionAPI, done: Function) => void | Function;
export interface Loading {
global: boolean;
effects: { [key: string]: boolean | undefined };
models: {
[key: string]: any;
};
}
/**
* @type P: Params matched in dynamic routing
*/
export interface ConnectProps<
P extends { [K in keyof P]?: string } = {},
S = LocationState,
T = {}
> {
dispatch?: Dispatch;
// https://github.com/umijs/umi/pull/2194
match?: match<P>;
location: Location<S> & { query: T };
history: History;
route: IRoute;
}
export type RequiredConnectProps<
P extends { [K in keyof P]?: string } = {},
S = LocationState,
T = {}
> = Required<ConnectProps<P, S, T>>
/**
* @type T: React props
* @type U: match props types
*/
export type ConnectRC<
T = {},
U = {},
S = {},
Q = {}
> = React.ForwardRefRenderFunction<any, T & RequiredConnectProps<U, S, Q>>;

View File

@ -1,68 +0,0 @@
// @ts-nocheck
import { Component } from 'react';
import { ApplyPluginsType } from 'umi';
import dva from 'dva';
// @ts-ignore
import createLoading from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/dva-loading/dist/index.esm.js';
import { plugin, history } from '../core/umiExports';
let app:any = null;
export function _onCreate(options = {}) {
const runtimeDva = plugin.applyPlugins({
key: 'dva',
type: ApplyPluginsType.modify,
initialValue: {},
});
app = dva({
history,
...(runtimeDva.config || {}),
// @ts-ignore
...(typeof window !== 'undefined' && window.g_useSSR ? { initialState: window.g_initialProps } : {}),
...(options || {}),
});
app.use(createLoading());
app.use(require('/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/dva-immer/dist/index.js')());
(runtimeDva.plugins || []).forEach((plugin:any) => {
app.use(plugin);
});
app.model({ namespace: 'editorModal', ...(require('/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/src/pages/editor/models/editorModal.js').default) });
return app;
}
export function getApp() {
return app;
}
export class _DvaContainer extends Component {
constructor(props: any) {
super(props);
// run only in client, avoid override server _onCreate()
if (typeof window !== 'undefined') {
_onCreate();
}
}
componentWillUnmount() {
let app = getApp();
app._models.forEach((model:any) => {
app.unmodel(model.namespace);
});
app._models = [];
try {
// 释放 appfor gc
// immer 场景 app 是 read-only 的,这里 try catch 一下
app = null;
} catch(e) {
console.error(e);
}
}
render() {
const app = getApp();
app.router(() => this.props.children);
return app.start()();
}
}

View File

@ -1,4 +0,0 @@
// @ts-nocheck
export { connect, useDispatch, useStore, useSelector } from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/dva';
export { getApp as getDvaApp } from './dva';

View File

@ -1,8 +0,0 @@
// @ts-nocheck
import React from 'react';
import { _DvaContainer, getApp, _onCreate } from './dva';
export function rootContainer(container) {
return React.createElement(_DvaContainer, null, container);
}

View File

@ -1,3 +0,0 @@
// @ts-nocheck
// @ts-ignore
export { Helmet } from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/react-helmet';

View File

@ -1,26 +0,0 @@
// @ts-nocheck
import React, { useRef, useEffect } from 'react';
import { useModel } from '../plugin-model/useModel';
if (typeof useModel !== 'function') {
throw new Error('[plugin-initial-state]: useModel is not a function, @umijs/plugin-model is required.')
}
interface Props {
children: React.ReactNode;
}
export default (props: Props) => {
const { children } = props;
const appLoaded = useRef(false);
const { loading = false } = useModel('@@initialState') || {};
useEffect(()=>{
if(!loading){
appLoaded.current = true
}
}, [loading])
// initial state loading 时,阻塞渲染
if (loading && !appLoaded.current) {
return null;
}
return children;
};

View File

@ -1,7 +0,0 @@
// @ts-nocheck
// @ts-ignore
import { InitialState as InitialStateType } from '../plugin-initial-state/models/initialState';
export type InitialState = InitialStateType;
export const __PLUGIN_INITIAL_STATE = 1;

View File

@ -1,2 +0,0 @@
// @ts-nocheck
export default () => ({ loading: false, refresh: () => {} })

View File

@ -1,13 +0,0 @@
// @ts-nocheck
import React from 'react';
import Provider from './Provider';
export function rootContainer(container: React.ReactNode) {
return React.createElement(
// 这里的 plugin-initial-state 不能从 constant 里取,里面有 path 依赖
// 但 webpack-5 没有 node 补丁(包括 path
Provider,
null,
container,
);
}

View File

@ -1,39 +0,0 @@
// @ts-nocheck
import React from 'react';
import initialState from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/src/.umi/plugin-initial-state/models/initialState';
// @ts-ignore
import Dispatcher from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/@umijs/plugin-model/lib/helpers/dispatcher';
// @ts-ignore
import Executor from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/@umijs/plugin-model/lib/helpers/executor';
// @ts-ignore
import { UmiContext } from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/@umijs/plugin-model/lib/helpers/constant';
export const models = { '@@initialState': initialState, };
export type Model<T extends keyof typeof models> = {
[key in keyof typeof models]: ReturnType<typeof models[T]>;
};
export type Models<T extends keyof typeof models> = Model<T>[T]
const dispatcher = new Dispatcher!();
const Exe = Executor!;
export default ({ children }: { children: React.ReactNode }) => {
return (
<UmiContext.Provider value={dispatcher}>
{
Object.entries(models).map(pair => (
<Exe key={pair[0]} namespace={pair[0]} hook={pair[1] as any} onUpdate={(val: any) => {
const [ns] = pair as [keyof typeof models, any];
dispatcher.data[ns] = val;
dispatcher.update(ns);
}} />
))
}
{children}
</UmiContext.Provider>
)
}

View File

@ -1,12 +0,0 @@
// @ts-nocheck
/* eslint-disable import/no-dynamic-require */
import React from 'react';
import Provider from './Provider';
export function rootContainer(container: React.ReactNode) {
return React.createElement(
Provider,
null,
container,
);
}

View File

@ -1,53 +0,0 @@
// @ts-nocheck
import { useState, useEffect, useContext, useRef } from 'react';
// @ts-ignore
import isEqual from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/fast-deep-equal/index.js';
// @ts-ignore
import { UmiContext } from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/@umijs/plugin-model/lib/helpers/constant';
import { Model, models } from './Provider';
export type Models<T extends keyof typeof models> = Model<T>[T]
export function useModel<T extends keyof Model<T>>(model: T): Model<T>[T]
export function useModel<T extends keyof Model<T>, U>(model: T, selector: (model: Model<T>[T]) => U): U
export function useModel<T extends keyof Model<T>, U>(
namespace: T,
updater?: (model: Model<T>[T]) => U
) : typeof updater extends undefined ? Model<T>[T] : ReturnType<NonNullable<typeof updater>>{
type RetState = typeof updater extends undefined ? Model<T>[T] : ReturnType<NonNullable<typeof updater>>
const dispatcher = useContext<any>(UmiContext);
const updaterRef = useRef(updater);
updaterRef.current = updater;
const [state, setState] = useState<RetState>(
() => updaterRef.current ? updaterRef.current(dispatcher.data![namespace]) : dispatcher.data![namespace]
);
const stateRef = useRef<any>(state);
stateRef.current = state;
useEffect(() => {
const handler = (e: any) => {
if(updater && updaterRef.current){
const currentState = updaterRef.current(e);
const previousState = stateRef.current
if(!isEqual(currentState, previousState)){
setState(currentState);
}
} else {
setState(e);
}
}
try {
dispatcher.callbacks![namespace]!.add(handler);
} catch (e) {
dispatcher.callbacks![namespace] = new Set();
dispatcher.callbacks![namespace]!.add(handler);
}
return () => {
dispatcher.callbacks![namespace]!.delete(handler);
}
}, [namespace]);
return state;
};

View File

@ -1,274 +0,0 @@
// @ts-nocheck
/**
* Base on https://github.com/umijs//Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/umi-request
*/
import {
extend,
Context,
RequestOptionsInit,
OnionMiddleware,
RequestOptionsWithoutResponse,
RequestMethod,
RequestOptionsWithResponse,
RequestResponse,
RequestInterceptor,
ResponseInterceptor,
} from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/umi-request';
// @ts-ignore
import { ApplyPluginsType } from 'umi';
import { history, plugin } from '../core/umiExports';
import { message, notification } from 'antd';
import useUmiRequest, { UseRequestProvider } from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/@ahooksjs/use-request';
import {
BaseOptions,
BasePaginatedOptions,
BaseResult,
CombineService,
LoadMoreFormatReturn,
LoadMoreOptions,
LoadMoreOptionsWithFormat,
LoadMoreParams,
LoadMoreResult,
OptionsWithFormat,
PaginatedFormatReturn,
PaginatedOptionsWithFormat,
PaginatedParams,
PaginatedResult,
} from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/@ahooksjs/use-request/lib/types';
type ResultWithData<T = any> = { data?: T; [key: string]: any };
function useRequest<
R = any,
P extends any[] = any,
U = any,
UU extends U = any
>(
service: CombineService<R, P>,
options: OptionsWithFormat<R, P, U, UU>,
): BaseResult<U, P>;
function useRequest<R extends ResultWithData = any, P extends any[] = any>(
service: CombineService<R, P>,
options?: BaseOptions<R['data'], P>,
): BaseResult<R['data'], P>;
function useRequest<R extends LoadMoreFormatReturn = any, RR = any>(
service: CombineService<RR, LoadMoreParams<R>>,
options: LoadMoreOptionsWithFormat<R, RR>,
): LoadMoreResult<R>;
function useRequest<
R extends ResultWithData<LoadMoreFormatReturn | any> = any,
RR extends R = any
>(
service: CombineService<R, LoadMoreParams<R['data']>>,
options: LoadMoreOptions<RR['data']>,
): LoadMoreResult<R['data']>;
function useRequest<R = any, Item = any, U extends Item = any>(
service: CombineService<R, PaginatedParams>,
options: PaginatedOptionsWithFormat<R, Item, U>,
): PaginatedResult<Item>;
function useRequest<Item = any, U extends Item = any>(
service: CombineService<
ResultWithData<PaginatedFormatReturn<Item>>,
PaginatedParams
>,
options: BasePaginatedOptions<U>,
): PaginatedResult<Item>;
function useRequest(service: any, options: any = {}) {
return useUmiRequest(service, {
formatResult: result => result?.data,
requestMethod: (requestOptions: any) => {
if (typeof requestOptions === 'string') {
return request(requestOptions);
}
if (typeof requestOptions === 'object') {
const { url, ...rest } = requestOptions;
return request(url, rest);
}
throw new Error('request options error');
},
...options,
});
}
export interface RequestConfig extends RequestOptionsInit {
errorConfig?: {
errorPage?: string;
adaptor?: (resData: any, ctx: Context) => ErrorInfoStructure;
};
middlewares?: OnionMiddleware[];
requestInterceptors?: RequestInterceptor[];
responseInterceptors?: ResponseInterceptor[];
}
export enum ErrorShowType {
SILENT = 0,
WARN_MESSAGE = 1,
ERROR_MESSAGE = 2,
NOTIFICATION = 4,
REDIRECT = 9,
}
interface ErrorInfoStructure {
success: boolean;
data?: any;
errorCode?: string;
errorMessage?: string;
showType?: ErrorShowType;
traceId?: string;
host?: string;
[key: string]: any;
}
interface RequestError extends Error {
data?: any;
info?: ErrorInfoStructure;
request?: Context['req'];
response?: Context['res'];
}
const DEFAULT_ERROR_PAGE = '/exception';
let requestMethodInstance: RequestMethod;
const getRequestMethod = () => {
if (requestMethodInstance) {
// request method 已经示例化
return requestMethodInstance;
}
// runtime 配置可能应为依赖顺序的问题在模块初始化的时候无法获取,所以需要封装一层在异步调用后初始化相关方法
// 当用户的 app.ts 中依赖了该文件的情况下就该模块的初始化时间就会被提前,无法获取到运行时配置
const requestConfig: RequestConfig = plugin.applyPlugins({
key: 'request',
type: ApplyPluginsType.modify,
initialValue: {},
});
const errorAdaptor =
requestConfig.errorConfig?.adaptor || (resData => resData);
requestMethodInstance = extend({
errorHandler: (error: RequestError) => {
// @ts-ignore
if (error?.request?.options?.skipErrorHandler) {
throw error;
}
let errorInfo: ErrorInfoStructure | undefined;
if (error.name === 'ResponseError' && error.data && error.request) {
const ctx: Context = {
req: error.request,
res: error.response,
};
errorInfo = errorAdaptor(error.data, ctx);
error.message = errorInfo?.errorMessage || error.message;
error.data = error.data;
error.info = errorInfo;
}
errorInfo = error.info;
if (errorInfo) {
const errorMessage = errorInfo?.errorMessage;
const errorCode = errorInfo?.errorCode;
const errorPage =
requestConfig.errorConfig?.errorPage || DEFAULT_ERROR_PAGE;
switch (errorInfo?.showType) {
case ErrorShowType.SILENT:
// do nothing
break;
case ErrorShowType.WARN_MESSAGE:
message.warn(errorMessage);
break;
case ErrorShowType.ERROR_MESSAGE:
message.error(errorMessage);
break;
case ErrorShowType.NOTIFICATION:
notification.open({
message: errorMessage,
});
break;
case ErrorShowType.REDIRECT:
// @ts-ignore
history.push({
pathname: errorPage,
query: { errorCode, errorMessage },
});
// redirect to error page
break;
default:
message.error(errorMessage);
break;
}
} else {
message.error(error.message || 'Request error, please retry.');
}
throw error;
},
...requestConfig,
});
// 中间件统一错误处理
// 后端返回格式 { success: boolean, data: any }
// 按照项目具体情况修改该部分逻辑
requestMethodInstance.use(async (ctx, next) => {
await next();
const { req, res } = ctx;
// @ts-ignore
if (req.options?.skipErrorHandler) {
return;
}
const { options } = req;
const { getResponse } = options;
const resData = getResponse ? res.data : res;
const errorInfo = errorAdaptor(resData, ctx);
if (errorInfo.success === false) {
// 抛出错误到 errorHandler 中处理
const error: RequestError = new Error(errorInfo.errorMessage);
error.name = 'BizError';
error.data = resData;
error.info = errorInfo;
throw error;
}
});
// Add user custom middlewares
const customMiddlewares = requestConfig.middlewares || [];
customMiddlewares.forEach(mw => {
requestMethodInstance.use(mw);
});
// Add user custom interceptors
const requestInterceptors = requestConfig.requestInterceptors || [];
const responseInterceptors = requestConfig.responseInterceptors || [];
requestInterceptors.map(ri => {
requestMethodInstance.interceptors.request.use(ri);
});
responseInterceptors.map(ri => {
requestMethodInstance.interceptors.response.use(ri);
});
return requestMethodInstance;
};
interface RequestMethodInUmi<R = false> {
<T = any>(
url: string,
options: RequestOptionsWithResponse & { skipErrorHandler?: boolean },
): Promise<RequestResponse<T>>;
<T = any>(
url: string,
options: RequestOptionsWithoutResponse & { skipErrorHandler?: boolean },
): Promise<T>;
<T = any>(
url: string,
options?: RequestOptionsInit & { skipErrorHandler?: boolean },
): R extends true ? Promise<RequestResponse<T>> : Promise<T>;
}
const request: RequestMethodInUmi = (url: any, options: any) => {
const requestMethod = getRequestMethod();
return requestMethod(url, options);
};
export { request, useRequest, UseRequestProvider };

View File

@ -1,59 +0,0 @@
// @ts-nocheck
import './core/polyfill';
import '@@/core/devScripts';
import { plugin } from './core/plugin';
import './core/pluginRegister';
import { createHistory } from './core/history';
import { ApplyPluginsType } from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/@umijs/runtime';
import { renderClient } from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/@umijs/renderer-react/dist/index.js';
import { getRoutes } from './core/routes';
require('../global.css');
const getClientRender = (args: { hot?: boolean; routes?: any[] } = {}) => plugin.applyPlugins({
key: 'render',
type: ApplyPluginsType.compose,
initialValue: () => {
const opts = plugin.applyPlugins({
key: 'modifyClientRenderOpts',
type: ApplyPluginsType.modify,
initialValue: {
routes: args.routes || getRoutes(),
plugin,
history: createHistory(args.hot),
isServer: process.env.__IS_SERVER,
dynamicImport: true,
rootElement: 'root',
defaultTitle: `趣谈前端-h5-visible-tool`,
},
});
return renderClient(opts);
},
args,
});
const clientRender = getClientRender();
export default clientRender();
window.g_umi = {
version: '3.2.16',
};
// hot module replacement
// @ts-ignore
if (module.hot) {
// @ts-ignore
module.hot.accept('./core/routes', () => {
const ret = require('./core/routes');
if (ret.then) {
ret.then(({ getRoutes }) => {
getClientRender({ hot: true, routes: getRoutes() })();
});
} else {
getClientRender({ hot: true, routes: ret.getRoutes() })();
}
});
}

22
src/app.tsx Normal file
View File

@ -0,0 +1,22 @@
import { createLogger } from 'redux-logger';
import { message } from 'antd';
import undoable, { StateWithHistory } from 'redux-undo';
import { Reducer, AnyAction } from 'redux';
import { isDev } from './utils/tool';
export const dva = {
config: {
onAction: isDev ? createLogger() : undefined,
onError(e: Error) {
message.error(e.message, 3);
},
onReducer: (reducer: Reducer<any, AnyAction>) => {
let undoReducer = undoable(reducer);
return function(state: StateWithHistory<any>, action: AnyAction) {
let newState = undoReducer(state, action);
let router = newState.present.router ? newState.present.router : newState.present.routing;
return { ...newState, router: router };
};
},
},
};

BIN
src/assets/area.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
src/assets/chart copy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
src/assets/chart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
src/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
src/assets/line.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
src/assets/pie.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -1,34 +0,0 @@
import { memo } from 'react'
import { BackToTop, Icon } from 'zarm'
const themeObj = {
simple: { bgColor: '#fff', color: '#999' },
black: { bgColor: '#000', color: '#fff' },
danger: { bgColor: '#ff5050', color: '#fff' },
primary: { bgColor: '#00bc71', color: '#fff' },
blue: { bgColor: '#06c', color: '#fff' }
}
const BackTop = memo((props) => {
const {
theme = 'simple'
} = props
return <BackToTop>
<div style={{
width: 48,
height: 48,
lineHeight: '48px',
textAlign: 'center',
backgroundColor: themeObj[theme].bgColor,
color: themeObj[theme].color,
fontSize: 20,
borderRadius: 30,
boxShadow: '0 2px 10px 0 rgba(0, 0, 0, 0.2)',
cursor: 'pointer',
}}>
<Icon type="arrow-top" />
</div>
</BackToTop>
})
export default BackTop

View File

@ -0,0 +1,36 @@
import { memo } from 'react';
import { BackToTop, Icon } from 'zarm';
import React from 'react';
const themeObj = {
simple: { bgColor: '#fff', color: '#999' },
black: { bgColor: '#000', color: '#fff' },
danger: { bgColor: '#ff5050', color: '#fff' },
primary: { bgColor: '#00bc71', color: '#fff' },
blue: { bgColor: '#06c', color: '#fff' },
};
const BackTop = memo((props: { theme: keyof typeof themeObj }) => {
const { theme = 'simple' } = props;
return (
<BackToTop>
<div
style={{
width: 48,
height: 48,
lineHeight: '48px',
textAlign: 'center',
backgroundColor: themeObj[theme].bgColor,
color: themeObj[theme].color,
fontSize: 20,
borderRadius: 30,
boxShadow: '0 2px 10px 0 rgba(0, 0, 0, 0.2)',
cursor: 'pointer',
}}
>
<Icon type="arrow-top" />
</div>
</BackToTop>
);
});
export default BackTop;

View File

@ -7,4 +7,4 @@
img { img {
width: 100%; width: 100%;
} }
} }

View File

@ -0,0 +1,53 @@
import React, { memo } from 'react';
import { Carousel } from 'zarm';
import styles from './index.less';
import { ICarouselConfig } from './schema';
const XCarousel = memo((props: ICarouselConfig) => {
const { direction, swipeable, autoPlay, isTpl, imgList, tplImg } = props;
const contentRender = () => {
return imgList.map((item, i) => {
return (
<div className={styles.carousel__item__pic} key={+i}>
<img src={item.imgUrl.length > 0 ? item.imgUrl[0].url : ''} alt="" />
</div>
);
});
};
return (
<>
{isTpl ? (
<div className={styles.carousel__item__pic}>
<img src={tplImg} alt="" />
</div>
) : (
<div
style={{
overflow: 'hidden',
position: 'absolute',
width: `${props.baseWidth}%`,
height: `${props.baseHeight}%`,
borderRadius: props.baseRadius,
transform: `translate(${props.baseLeft}px,${props.baseTop}px)
scale(${props.baseScale / 100})
rotate(${props.baseRotate}deg)`,
}}
>
<Carousel
onChange={index => {
// console.log(`onChange: ${index}`);
}}
direction={direction}
swipeable={swipeable}
autoPlay={autoPlay}
loop
>
{contentRender()}
</Carousel>
</div>
)}
</>
);
});
export default XCarousel;

View File

@ -0,0 +1,106 @@
import {
IDataListConfigType,
INumberConfigType,
IRadioConfigType,
ISwitchConfigType,
TDataListDefaultType,
TRadioDefaultType,
TSwitchDefaultType,
} from '@/components/PanelComponents/FormEditor/types';
import { baseConfig, baseDefault, ICommonBaseType } from '../../common';
export type CarouselDirectionKeyType = 'down' | 'left';
export type TCarouselEditData = Array<
| INumberConfigType
| IRadioConfigType<CarouselDirectionKeyType>
| ISwitchConfigType
| IDataListConfigType
>;
export interface ICarouselConfig extends ICommonBaseType {
direction: TRadioDefaultType<CarouselDirectionKeyType>;
swipeable: TSwitchDefaultType;
autoPlay: TSwitchDefaultType;
imgList: TDataListDefaultType;
tplImg: string;
}
export interface ICarouselSchema {
editData: TCarouselEditData;
config: ICarouselConfig;
}
const Carousel: ICarouselSchema = {
editData: [
...baseConfig,
{
key: 'direction',
name: '方向',
type: 'Radio',
range: [
{
key: 'down',
text: '从上到下',
},
{
key: 'left',
text: '从左到右',
},
],
},
{
key: 'swipeable',
name: '是否可拖拽',
type: 'Switch',
},
{
key: 'autoPlay',
name: '是否自动播放',
type: 'Switch',
},
{
key: 'imgList',
name: '图片列表',
type: 'DataList',
},
],
config: {
direction: 'left',
swipeable: false,
autoPlay: false,
imgList: [
{
id: '1',
title: '趣谈小课1',
desc: '致力于打造优质小课程',
link: 'xxxxx',
imgUrl: [
{
uid: '001',
name: 'image.png',
status: 'done',
url: 'http://io.nainor.com/uploads/1_1740bd7c3dc.png',
},
],
},
{
id: '2',
title: '趣谈小课1',
desc: '致力于打造优质小课程',
link: 'xxxxx',
imgUrl: [
{
uid: '001',
name: 'image.png',
status: 'done',
url: 'http://io.nainor.com/uploads/2_1740bd8d525.png',
},
],
},
],
...baseDefault,
tplImg: 'http://io.nainor.com/uploads/carousal_17442e1420f.png',
},
};
export default Carousel;

View File

@ -0,0 +1,5 @@
const template = {
type: 'Carousel',
h: 82,
};
export default template;

View File

@ -0,0 +1,53 @@
import React, { memo } from 'react';
import { IFooterConfig } from './schema';
const Footer = memo((props: IFooterConfig) => {
const { bgColor, text, color, align, fontSize, height } = props;
console.log(props);
return (
<>
{props.isTpl && (
<footer
style={{
backgroundColor: bgColor,
color,
fontSize,
textAlign: align,
height,
lineHeight: height + 'px',
}}
>
{text}
</footer>
)}
{!props.isTpl && (
<div
style={{
overflow: 'hidden',
position: 'absolute',
width: `${props.baseWidth}%`,
height: `${props.baseHeight}%`,
borderRadius: props.baseRadius,
transform: `translate(${props.baseLeft}px,${props.baseTop}px)
scale(${props.baseScale / 100})
rotate(${props.baseRotate}deg)`,
}}
>
<footer
style={{
backgroundColor: bgColor,
color,
fontSize,
textAlign: align,
height,
lineHeight: height + 'px',
}}
>
{text}
</footer>
</div>
)}
</>
);
});
export default Footer;

View File

@ -0,0 +1,90 @@
import {
IColorConfigType,
INumberConfigType,
ISelectConfigType,
ITextConfigType,
TColorDefaultType,
TNumberDefaultType,
TSelectDefaultType,
TTextDefaultType,
} from '@/components/PanelComponents/FormEditor/types';
import { baseConfig, baseDefault, ICommonBaseType } from '../../common';
export type TfooterSelectKeyType = 'left' | 'center' | 'right';
export type TFooterEditData = Array<
IColorConfigType | INumberConfigType | ITextConfigType | ISelectConfigType<TfooterSelectKeyType>
>;
export interface IFooterConfig extends ICommonBaseType {
bgColor: TColorDefaultType;
text: TTextDefaultType;
color: TColorDefaultType;
align: TSelectDefaultType<TfooterSelectKeyType>;
fontSize: TNumberDefaultType;
height: TNumberDefaultType;
}
export interface IFooterSchema {
editData: TFooterEditData;
config: IFooterConfig;
}
const Footer: IFooterSchema = {
editData: [
...baseConfig,
{
key: 'bgColor',
name: '背景色',
type: 'Color',
},
{
key: 'height',
name: '高度',
type: 'Number',
},
{
key: 'text',
name: '文字',
type: 'Text',
},
{
key: 'fontSize',
name: '字体大小',
type: 'Number',
},
{
key: 'color',
name: '文字颜色',
type: 'Color',
},
{
key: 'align',
name: '对齐方式',
type: 'Select',
range: [
{
key: 'left',
text: '左对齐',
},
{
key: 'center',
text: '居中对齐',
},
{
key: 'right',
text: '右对齐',
},
],
},
],
config: {
bgColor: 'rgba(0,0,0,1)',
text: '页脚Footer',
color: 'rgba(255,255,255,1)',
align: 'center',
fontSize: 16,
height: 48,
...baseDefault,
},
};
export default Footer;

View File

@ -0,0 +1,5 @@
const template = {
type: 'Footer',
h: 24,
};
export default template;

View File

@ -0,0 +1,96 @@
import { Input, Cell, DateSelect, Radio, Select } from 'zarm';
import styles from './baseForm.less';
import React from 'react';
import {
baseFormDateTpl,
baseFormMyRadioTpl,
baseFormMySelectTpl,
baseFormNumberTpl,
baseFormTextAreaTpl,
baseFormTextTpl,
baseFormUnionType,
} from '@/components/PanelComponents/FormEditor/types';
// 维护表单控件, 提高form渲染性能
type TBaseForm = {
[key in baseFormUnionType]: any;
};
const BaseForm: TBaseForm = {
Text: (props: baseFormTextTpl & { onChange: (v: string | undefined) => void }) => {
const { label, placeholder, onChange } = props;
return (
<Cell title={label}>
<Input clearable type="text" placeholder={placeholder} onChange={onChange} />
</Cell>
);
},
Textarea: (props: baseFormTextAreaTpl & { onChange: (v: string | undefined) => void }) => {
const { label, placeholder, onChange } = props;
return (
<Cell title={label}>
<Input
type="text"
rows={3}
autoHeight
showLength
placeholder={placeholder}
onChange={onChange}
/>
</Cell>
);
},
Number: (props: baseFormNumberTpl & { onChange: (v: string | undefined | number) => void }) => {
const { label, placeholder, onChange } = props;
return (
<Cell title={label}>
<Input type="number" placeholder={placeholder} onChange={onChange} />
</Cell>
);
},
MyRadio: (props: baseFormMyRadioTpl & { onChange: (v: string | undefined | number) => void }) => {
const { label, options, onChange } = props;
return (
<div className={styles.radioWrap}>
<div className={styles.radioTitle}>{label}</div>
<Cell>
<Radio.Group onChange={onChange}>
{options.map((item, i) => {
return (
<Radio value={item.value} key={i} className={styles.radioItem}>
{item.label}
</Radio>
);
})}
</Radio.Group>
</Cell>
</div>
);
},
Date: (props: baseFormDateTpl & { onChange: (v: Date) => void }) => {
const { label, placeholder, onChange } = props;
return (
<Cell title={label}>
<DateSelect
placeholder={placeholder}
mode="date"
min="1949-05-15"
max="2100-05-15"
onOk={onChange}
/>
</Cell>
);
},
MySelect: (
props: baseFormMySelectTpl & { onChange: ((v: Record<string, any>) => void) | undefined },
) => {
const { label, options, onChange } = props;
return (
<Cell title={label}>
<Select dataSource={options} onOk={onChange} />
</Cell>
);
},
};
export default BaseForm;

View File

@ -0,0 +1,10 @@
.radioWrap {
margin-bottom: 10px;
.radioTitle {
padding: 6px 16px;
font-size: 15px;
}
.radioItem {
margin-top: 10px;
}
}

View File

@ -0,0 +1,14 @@
.formWrap {
margin: 10px;
padding: 20px 16px;
border-radius: 6px;
background-color: #fff;
box-shadow: 0 2px 6px #f0f0f0;
.title {
padding-bottom: 20px;
text-align: center;
font-size: 18px;
}
.formContent {
}
}

View File

@ -0,0 +1,104 @@
import React, { memo, useCallback } from 'react';
import { Button } from 'zarm';
import BaseForm from './BaseForm';
import styles from './index.less';
import { IFormConfig } from './schema';
const FormComponent = (props: IFormConfig) => {
const { title, bgColor, fontSize, titColor, btnColor, btnTextColor, api, formControls } = props;
const formData: Record<string, any> = {};
const handleChange = useCallback(
(item, v) => {
if (item.options) {
formData[item.label] = v[0].label;
return;
}
formData[item.label] = v;
},
[formData],
);
const handleSubmit = () => {
if (api) {
fetch(api, {
body: JSON.stringify(formData),
cache: 'no-cache',
headers: {
'content-type': 'application/json',
},
method: 'POST',
mode: 'cors',
});
}
};
return (
<>
{props.isTpl && (
<div className={styles.formWrap} style={{ backgroundColor: bgColor }}>
{title && (
<div className={styles.title} style={{ fontSize, color: titColor }}>
{title}
</div>
)}
<div className={styles.formContent}>
{formControls.map(item => {
const FormItem = BaseForm[item.type];
return <FormItem onChange={handleChange.bind(this, item)} {...item} key={item.id} />;
})}
<div style={{ textAlign: 'center', padding: '16px 0' }}>
<Button
theme="primary"
size="sm"
block
onClick={handleSubmit}
style={{ backgroundColor: btnColor, borderColor: btnColor, color: btnTextColor }}
>
</Button>
</div>
</div>
</div>
)}
{!props.isTpl && (
<div
className={styles.formWrap}
style={{
backgroundColor: bgColor,
overflow: 'hidden',
position: 'absolute',
width: `${props.baseWidth}%`,
height: `${props.baseHeight}%`,
borderRadius: props.baseRadius,
transform: `translate(${props.baseLeft}px,${props.baseTop}px)
scale(${props.baseScale / 100})
rotate(${props.baseRotate}deg)`,
}}
>
{title && (
<div className={styles.title} style={{ fontSize, color: titColor }}>
{title}
</div>
)}
<div className={styles.formContent}>
{formControls.map(item => {
const FormItem = BaseForm[item.type];
return <FormItem onChange={handleChange.bind(this, item)} {...item} key={item.id} />;
})}
<div style={{ textAlign: 'center', padding: '16px 0' }}>
<Button
theme="primary"
size="sm"
block
onClick={handleSubmit}
style={{ backgroundColor: btnColor, borderColor: btnColor, color: btnTextColor }}
>
</Button>
</div>
</div>
</div>
)}
</>
);
};
export default memo(FormComponent);

View File

@ -0,0 +1,111 @@
import {
IColorConfigType,
IFormItemsConfigType,
INumberConfigType,
ITextConfigType,
TColorDefaultType,
TFormItemsDefaultType,
TNumberDefaultType,
TTextDefaultType,
} from '@/components/PanelComponents/FormEditor/types';
import { baseConfig, baseDefault, ICommonBaseType } from '../../common';
export type TFormEditData = Array<
ITextConfigType | INumberConfigType | IColorConfigType | ITextConfigType | IFormItemsConfigType
>;
export interface IFormConfig extends ICommonBaseType {
title: TTextDefaultType;
fontSize: TNumberDefaultType;
titColor: TColorDefaultType;
bgColor: TColorDefaultType;
btnColor: TColorDefaultType;
btnTextColor: TColorDefaultType;
api: TTextDefaultType;
formControls: TFormItemsDefaultType;
}
export interface IFormSchema {
editData: TFormEditData;
config: IFormConfig;
}
const Form: IFormSchema = {
editData: [
...baseConfig,
{
key: 'title',
name: '标题',
type: 'Text',
},
{
key: 'fontSize',
name: '标题大小',
type: 'Number',
},
{
key: 'titColor',
name: '标题颜色',
type: 'Color',
},
{
key: 'bgColor',
name: '背景色',
type: 'Color',
},
{
key: 'btnColor',
name: '按钮颜色',
type: 'Color',
},
{
key: 'btnTextColor',
name: '按钮文本颜色',
type: 'Color',
},
{
key: 'api',
name: '表单Api地址',
type: 'Text',
},
{
key: 'formControls',
name: '表单控件',
type: 'FormItems',
},
],
config: {
title: '表单定制组件',
fontSize: 18,
titColor: 'rgba(60,60,60,1)',
bgColor: 'rgba(255,255,255,1)',
btnColor: 'rgba(129,173,173,1)',
btnTextColor: 'rgba(255,255,255,1)',
api: '',
formControls: [
{
id: '1',
type: 'Text',
label: '姓名',
placeholder: '请输入姓名',
},
{
id: '2',
type: 'Number',
label: '年龄',
placeholder: ' 请输入年龄',
},
{
id: '4',
type: 'MySelect',
label: '爱好',
options: [
{ label: '选项一', value: '1' },
{ label: '选项二', value: '2' },
{ label: '选项三', value: '3' },
],
},
],
...baseDefault,
},
};
export default Form;

View File

@ -0,0 +1,5 @@
const template = {
type: 'Form',
h: 172,
};
export default template;

View File

@ -21,23 +21,3 @@
color: #fff; color: #fff;
} }
} }
.list {
margin: 20px auto;
width: 94%;
.sourceList {
.sourceItem {
display: flex;
align-items: center;
margin-bottom: 16px;
.imgWrap {
}
.content {
margin-left: 12px;
.tit {
line-height: 2;
}
}
}
}
}

View File

@ -0,0 +1,47 @@
import { memo } from 'react';
import styles from './index.less';
import React from 'react';
import { IHeaderConfig } from './schema';
const Header = memo((props: IHeaderConfig) => {
const { bgColor, logo, logoText, fontSize, color } = props;
return (
<>
{props.isTpl && (
<header className={styles.header} style={{ backgroundColor: bgColor }}>
<div className={styles.logo}>
<img src={logo && logo[0].url} alt={logoText} />
</div>
<div className={styles.title} style={{ fontSize, color }}>
{logoText}
</div>
</header>
)}
{!props.isTpl && (
<header
className={styles.header}
style={{
backgroundColor: bgColor,
overflow: 'hidden',
position: 'absolute',
width: `${props.baseWidth}%`,
height: `${props.baseHeight}%`,
borderRadius: props.baseRadius,
transform: `translate(${props.baseLeft}px,${props.baseTop}px)
scale(${props.baseScale / 100})
rotate(${props.baseRotate}deg)`,
}}
>
<div className={styles.logo}>
<img src={logo && logo[0].url} alt={logoText} />
</div>
<div className={styles.title} style={{ fontSize, color }}>
{logoText}
</div>
</header>
)}
</>
);
});
export default Header;

View File

@ -0,0 +1,84 @@
import {
IColorConfigType,
INumberConfigType,
ITextConfigType,
IUploadConfigType,
TColorDefaultType,
TNumberDefaultType,
TTextDefaultType,
TUploadDefaultType,
} from '@/components/PanelComponents/FormEditor/types';
import { baseConfig, baseDefault, ICommonBaseType } from '../../common';
export type THeaderEditData = Array<
IColorConfigType | INumberConfigType | IUploadConfigType | ITextConfigType
>;
export interface IHeaderConfig extends ICommonBaseType {
bgColor: TColorDefaultType;
logo: TUploadDefaultType;
logoText: TTextDefaultType;
fontSize: TNumberDefaultType;
color: TColorDefaultType;
height: TNumberDefaultType;
}
export interface IHeaderSchema {
editData: THeaderEditData;
config: IHeaderConfig;
}
const Header: IHeaderSchema = {
editData: [
...baseConfig,
{
key: 'bgColor',
name: '背景色',
type: 'Color',
},
{
key: 'height',
name: '高度',
type: 'Number',
},
{
key: 'logo',
name: 'logo',
type: 'Upload',
isCrop: true,
cropRate: 1000 / 618,
},
{
key: 'logoText',
name: 'logo文字',
type: 'Text',
},
{
key: 'color',
name: '文字颜色',
type: 'Color',
},
{
key: 'fontSize',
name: '文字大小',
type: 'Number',
},
],
config: {
bgColor: 'rgba(0,0,0,1)',
logo: [
{
uid: '001',
name: 'image.png',
status: 'done',
url: 'http://io.nainor.com/uploads/3_1740be8a482.png',
},
],
logoText: '页头Header',
fontSize: 20,
color: 'rgba(255,255,255,1)',
height: 50,
...baseDefault,
},
};
export default Header;

View File

@ -0,0 +1,5 @@
const template = {
type: 'Header',
h: 28,
};
export default template;

View File

@ -0,0 +1,364 @@
export type AntdIconType =
| 'max'
| 'required'
| 'default'
| 'high'
| 'low'
| 'disabled'
| 'start'
| 'open'
| 'media'
| 'hidden'
| 'cite'
| 'data'
| 'dir'
| 'form'
| 'label'
| 'slot'
| 'span'
| 'style'
| 'summary'
| 'title'
| 'pattern'
| 'async'
| 'defer'
| 'manifest'
| 'color'
| 'content'
| 'size'
| 'wrap'
| 'multiple'
| 'height'
| 'rotate'
| 'translate'
| 'width'
| 'prefix'
| 'src'
| 'children'
| 'key'
| 'list'
| 'step'
| 'aria-label'
| 'spin'
| 'accept'
| 'acceptCharset'
| 'action'
| 'allowFullScreen'
| 'allowTransparency'
| 'alt'
| 'as'
| 'autoComplete'
| 'autoFocus'
| 'autoPlay'
| 'capture'
| 'cellPadding'
| 'cellSpacing'
| 'charSet'
| 'challenge'
| 'checked'
| 'classID'
| 'cols'
| 'colSpan'
| 'controls'
| 'coords'
| 'crossOrigin'
| 'dateTime'
| 'download'
| 'encType'
| 'formAction'
| 'formEncType'
| 'formMethod'
| 'formNoValidate'
| 'formTarget'
| 'frameBorder'
| 'headers'
| 'href'
| 'hrefLang'
| 'htmlFor'
| 'httpEquiv'
| 'integrity'
| 'keyParams'
| 'keyType'
| 'kind'
| 'loop'
| 'marginHeight'
| 'marginWidth'
| 'maxLength'
| 'mediaGroup'
| 'method'
| 'min'
| 'minLength'
| 'muted'
| 'name'
| 'nonce'
| 'noValidate'
| 'optimum'
| 'placeholder'
| 'playsInline'
| 'poster'
| 'preload'
| 'readOnly'
| 'rel'
| 'reversed'
| 'rows'
| 'rowSpan'
| 'sandbox'
| 'scope'
| 'scoped'
| 'scrolling'
| 'seamless'
| 'selected'
| 'shape'
| 'sizes'
| 'srcDoc'
| 'srcLang'
| 'srcSet'
| 'target'
| 'type'
| 'useMap'
| 'value'
| 'wmode'
| 'defaultChecked'
| 'defaultValue'
| 'suppressContentEditableWarning'
| 'suppressHydrationWarning'
| 'accessKey'
| 'className'
| 'contentEditable'
| 'contextMenu'
| 'draggable'
| 'id'
| 'lang'
| 'spellCheck'
| 'tabIndex'
| 'radioGroup'
| 'role'
| 'about'
| 'datatype'
| 'inlist'
| 'property'
| 'resource'
| 'typeof'
| 'vocab'
| 'autoCapitalize'
| 'autoCorrect'
| 'autoSave'
| 'itemProp'
| 'itemScope'
| 'itemType'
| 'itemID'
| 'itemRef'
| 'results'
| 'security'
| 'unselectable'
| 'inputMode'
| 'is'
| 'aria-activedescendant'
| 'aria-atomic'
| 'aria-autocomplete'
| 'aria-busy'
| 'aria-checked'
| 'aria-colcount'
| 'aria-colindex'
| 'aria-colspan'
| 'aria-controls'
| 'aria-current'
| 'aria-describedby'
| 'aria-details'
| 'aria-disabled'
| 'aria-dropeffect'
| 'aria-errormessage'
| 'aria-expanded'
| 'aria-flowto'
| 'aria-grabbed'
| 'aria-haspopup'
| 'aria-hidden'
| 'aria-invalid'
| 'aria-keyshortcuts'
| 'aria-labelledby'
| 'aria-level'
| 'aria-live'
| 'aria-modal'
| 'aria-multiline'
| 'aria-multiselectable'
| 'aria-orientation'
| 'aria-owns'
| 'aria-placeholder'
| 'aria-posinset'
| 'aria-pressed'
| 'aria-readonly'
| 'aria-relevant'
| 'aria-required'
| 'aria-roledescription'
| 'aria-rowcount'
| 'aria-rowindex'
| 'aria-rowspan'
| 'aria-selected'
| 'aria-setsize'
| 'aria-sort'
| 'aria-valuemax'
| 'aria-valuemin'
| 'aria-valuenow'
| 'aria-valuetext'
| 'dangerouslySetInnerHTML'
| 'onCopy'
| 'onCopyCapture'
| 'onCut'
| 'onCutCapture'
| 'onPaste'
| 'onPasteCapture'
| 'onCompositionEnd'
| 'onCompositionEndCapture'
| 'onCompositionStart'
| 'onCompositionStartCapture'
| 'onCompositionUpdate'
| 'onCompositionUpdateCapture'
| 'onFocus'
| 'onFocusCapture'
| 'onBlur'
| 'onBlurCapture'
| 'onChange'
| 'onChangeCapture'
| 'onBeforeInput'
| 'onBeforeInputCapture'
| 'onInput'
| 'onInputCapture'
| 'onReset'
| 'onResetCapture'
| 'onSubmit'
| 'onSubmitCapture'
| 'onInvalid'
| 'onInvalidCapture'
| 'onLoad'
| 'onLoadCapture'
| 'onError'
| 'onErrorCapture'
| 'onKeyDown'
| 'onKeyDownCapture'
| 'onKeyPress'
| 'onKeyPressCapture'
| 'onKeyUp'
| 'onKeyUpCapture'
| 'onAbort'
| 'onAbortCapture'
| 'onCanPlay'
| 'onCanPlayCapture'
| 'onCanPlayThrough'
| 'onCanPlayThroughCapture'
| 'onDurationChange'
| 'onDurationChangeCapture'
| 'onEmptied'
| 'onEmptiedCapture'
| 'onEncrypted'
| 'onEncryptedCapture'
| 'onEnded'
| 'onEndedCapture'
| 'onLoadedData'
| 'onLoadedDataCapture'
| 'onLoadedMetadata'
| 'onLoadedMetadataCapture'
| 'onLoadStart'
| 'onLoadStartCapture'
| 'onPause'
| 'onPauseCapture'
| 'onPlay'
| 'onPlayCapture'
| 'onPlaying'
| 'onPlayingCapture'
| 'onProgress'
| 'onProgressCapture'
| 'onRateChange'
| 'onRateChangeCapture'
| 'onSeeked'
| 'onSeekedCapture'
| 'onSeeking'
| 'onSeekingCapture'
| 'onStalled'
| 'onStalledCapture'
| 'onSuspend'
| 'onSuspendCapture'
| 'onTimeUpdate'
| 'onTimeUpdateCapture'
| 'onVolumeChange'
| 'onVolumeChangeCapture'
| 'onWaiting'
| 'onWaitingCapture'
| 'onAuxClick'
| 'onAuxClickCapture'
| 'onClick'
| 'onClickCapture'
| 'onContextMenu'
| 'onContextMenuCapture'
| 'onDoubleClick'
| 'onDoubleClickCapture'
| 'onDrag'
| 'onDragCapture'
| 'onDragEnd'
| 'onDragEndCapture'
| 'onDragEnter'
| 'onDragEnterCapture'
| 'onDragExit'
| 'onDragExitCapture'
| 'onDragLeave'
| 'onDragLeaveCapture'
| 'onDragOver'
| 'onDragOverCapture'
| 'onDragStart'
| 'onDragStartCapture'
| 'onDrop'
| 'onDropCapture'
| 'onMouseDown'
| 'onMouseDownCapture'
| 'onMouseEnter'
| 'onMouseLeave'
| 'onMouseMove'
| 'onMouseMoveCapture'
| 'onMouseOut'
| 'onMouseOutCapture'
| 'onMouseOver'
| 'onMouseOverCapture'
| 'onMouseUp'
| 'onMouseUpCapture'
| 'onSelect'
| 'onSelectCapture'
| 'onTouchCancel'
| 'onTouchCancelCapture'
| 'onTouchEnd'
| 'onTouchEndCapture'
| 'onTouchMove'
| 'onTouchMoveCapture'
| 'onTouchStart'
| 'onTouchStartCapture'
| 'onPointerDown'
| 'onPointerDownCapture'
| 'onPointerMove'
| 'onPointerMoveCapture'
| 'onPointerUp'
| 'onPointerUpCapture'
| 'onPointerCancel'
| 'onPointerCancelCapture'
| 'onPointerEnter'
| 'onPointerEnterCapture'
| 'onPointerLeave'
| 'onPointerLeaveCapture'
| 'onPointerOver'
| 'onPointerOverCapture'
| 'onPointerOut'
| 'onPointerOutCapture'
| 'onGotPointerCapture'
| 'onGotPointerCaptureCapture'
| 'onLostPointerCapture'
| 'onLostPointerCaptureCapture'
| 'onScroll'
| 'onScrollCapture'
| 'onWheel'
| 'onWheelCapture'
| 'onAnimationStart'
| 'onAnimationStartCapture'
| 'onAnimationEnd'
| 'onAnimationEndCapture'
| 'onAnimationIteration'
| 'onAnimationIterationCapture'
| 'onTransitionEnd'
| 'onTransitionEndCapture'
| 'twoToneColor';

View File

@ -0,0 +1,27 @@
import React, { memo } from 'react';
import * as Icon from '@ant-design/icons';
import IconImg from 'assets/icon.png';
import { AntdIconProps } from '@ant-design/icons/lib/components/AntdIcon';
import { AntdIconType } from './icon';
import { IIconConfig } from './schema';
interface IconType extends IIconConfig {
isTpl?: boolean;
}
const XIcon = memo((props: IconType) => {
const { color, size, type, spin, isTpl } = props;
const MyIcon: React.ForwardRefExoticComponent<Pick<AntdIconProps, AntdIconType> &
React.RefAttributes<HTMLSpanElement>> = Icon[type];
return isTpl ? (
<div style={{ textAlign: 'center' }}>
<img style={{ verticalAlign: '-20px', width: '82px' }} src={IconImg} alt={type} />
</div>
) : (
<MyIcon twoToneColor={color} style={{ fontSize: size }} spin={spin} />
);
});
export default XIcon;

View File

@ -0,0 +1,144 @@
import {
ICardPickerConfigType,
IColorConfigType,
INumberConfigType,
ISwitchConfigType,
TCardPickerDefaultType,
TColorDefaultType,
TNumberDefaultType,
TSwitchDefaultType,
} from '@/components/PanelComponents/FormEditor/types';
export type TIconEditData = Array<
IColorConfigType | INumberConfigType | ISwitchConfigType | ICardPickerConfigType<IconTypes>
>;
export interface IIconConfig {
color: TColorDefaultType;
size: TNumberDefaultType;
spin: TSwitchDefaultType;
type: TCardPickerDefaultType<IconTypes>;
}
export interface IIconSchema {
editData: TIconEditData;
config: IIconConfig;
}
export type IconTypes =
| 'AccountBookTwoTone'
| 'AlertTwoTone'
| 'ApiTwoTone'
| 'AppstoreTwoTone'
| 'AudioTwoTone'
| 'BankTwoTone'
| 'BellTwoTone'
| 'BookTwoTone'
| 'BugTwoTone'
| 'BuildTwoTone'
| 'BulbTwoTone'
| 'CalculatorTwoTone'
| 'CalendarTwoTone'
| 'CameraTwoTone'
| 'CarTwoTone'
| 'CarryOutTwoTone'
| 'CiCircleTwoTone'
| 'CloudTwoTone'
| 'CodeTwoTone'
| 'CrownTwoTone'
| 'CustomerServiceTwoTone'
| 'DollarCircleTwoTone'
| 'EnvironmentTwoTone'
| 'ExperimentTwoTone'
| 'FireTwoTone'
| 'GiftTwoTone'
| 'InsuranceTwoTone'
| 'LikeTwoTone'
| 'LockTwoTone'
| 'MailTwoTone'
| 'MessageTwoTone'
| 'PhoneTwoTone'
| 'PictureTwoTone'
| 'PlaySquareTwoTone'
| 'RedEnvelopeTwoTone'
| 'ShopTwoTone'
| 'TrademarkCircleTwoTone'
| 'StarTwoTone'
| 'SafetyCertificateTwoTone'
| 'SettingTwoTone'
| 'RocketTwoTone';
const Icon: IIconSchema = {
editData: [
{
key: 'color',
name: '颜色',
type: 'Color',
},
{
key: 'size',
name: '大小',
type: 'Number',
},
{
key: 'spin',
name: '旋转动画',
type: 'Switch',
},
{
key: 'type',
name: '图标类型',
type: 'CardPicker',
icons: [
'AccountBookTwoTone',
'AlertTwoTone',
'ApiTwoTone',
'AppstoreTwoTone',
'AudioTwoTone',
'BankTwoTone',
'BellTwoTone',
'BookTwoTone',
'BugTwoTone',
'BuildTwoTone',
'BulbTwoTone',
'CalculatorTwoTone',
'CalendarTwoTone',
'CameraTwoTone',
'CarTwoTone',
'CarryOutTwoTone',
'CiCircleTwoTone',
'CloudTwoTone',
'CodeTwoTone',
'CrownTwoTone',
'CustomerServiceTwoTone',
'DollarCircleTwoTone',
'EnvironmentTwoTone',
'ExperimentTwoTone',
'FireTwoTone',
'GiftTwoTone',
'InsuranceTwoTone',
'LikeTwoTone',
'LockTwoTone',
'MailTwoTone',
'MessageTwoTone',
'PhoneTwoTone',
'PictureTwoTone',
'PlaySquareTwoTone',
'RedEnvelopeTwoTone',
'ShopTwoTone',
'TrademarkCircleTwoTone',
'StarTwoTone',
'SafetyCertificateTwoTone',
'SettingTwoTone',
'RocketTwoTone',
],
},
],
config: {
color: 'rgba(74,144,226,1)',
size: 36,
spin: false,
type: 'CarTwoTone',
},
};
export default Icon;

View File

@ -0,0 +1,5 @@
const template = {
type: 'Icon',
h: 23,
};
export default template;

View File

@ -0,0 +1,20 @@
import React, { memo } from 'react';
import { IImageConfig } from './schema';
const Image = memo((props: IImageConfig) => {
const { imgUrl, round = 0 } = props;
return (
<div
style={{
borderRadius: round,
width: '100%',
textAlign: 'center',
overflow: 'hidden',
position: 'absolute',
}}
>
<img src={imgUrl && imgUrl[0].url} alt="" style={{ width: '100%' }} />
</div>
);
});
export default Image;

View File

@ -0,0 +1,49 @@
import {
INumberConfigType,
IUploadConfigType,
TNumberDefaultType,
TUploadDefaultType,
} from '@/components/PanelComponents/FormEditor/types';
import { baseConfig, baseDefault, ICommonBaseType } from '../../common';
export type TImageEditData = Array<IUploadConfigType | INumberConfigType>;
export interface IImageConfig extends ICommonBaseType {
imgUrl: TUploadDefaultType;
round: TNumberDefaultType;
}
export interface IImageSchema {
editData: TImageEditData;
config: IImageConfig;
}
const Image: IImageSchema = {
editData: [
...baseConfig,
{
key: 'imgUrl',
name: '上传',
type: 'Upload',
isCrop: false,
},
{
key: 'round',
name: '圆角',
type: 'Number',
},
],
config: {
imgUrl: [
{
uid: '001',
name: 'image.png',
status: 'done',
url: 'http://io.nainor.com/uploads/4_1740bf4535c.png',
},
],
round: 0,
...baseDefault,
},
};
export default Image;

View File

@ -0,0 +1,5 @@
const template = {
type: 'Image',
h: 188,
};
export default template;

View File

@ -0,0 +1,19 @@
.list {
margin: 20px auto;
width: 94%;
.sourceList {
.sourceItem {
display: flex;
align-items: center;
margin-bottom: 16px;
.imgWrap {
}
.content {
margin-left: 12px;
.tit {
line-height: 2;
}
}
}
}
}

View File

@ -0,0 +1,110 @@
import React, { memo } from 'react';
import styles from './index.less';
import { IListConfig } from './schema';
const List = memo((props: IListConfig) => {
const { round, sourceData, imgSize, fontSize, color } = props;
return (
<>
{props.isTpl && (
<div className={styles.list}>
<div className={styles.sourceList}>
{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: parseFloat(imgSize),
height: imgSize,
objectFit: 'cover',
borderRadius: round,
}}
/>
</div>
<div className={styles.content}>
<a
className={styles.tit}
style={{ fontSize, color }}
href={item.link ? item.link : '#'}
>
{item.title}
<div
className={styles.desc}
style={{ fontSize: fontSize * 0.8, color: 'rgba(0,0,0, .3)' }}
>
{item.desc}
</div>
</a>
</div>
</div>
);
})}
</div>
</div>
)}
{!props.isTpl && (
<div
className={styles.list}
style={{
overflow: 'hidden',
position: 'absolute',
width: `${props.baseWidth}%`,
height: `${props.baseHeight}%`,
borderRadius: props.baseRadius,
transform: `translate(${props.baseLeft}px,${props.baseTop}px)
scale(${props.baseScale / 100})
rotate(${props.baseRotate}deg)`,
}}
>
<div className={styles.sourceList}>
{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: parseFloat(imgSize),
height: imgSize,
objectFit: 'cover',
borderRadius: round,
}}
/>
</div>
<div className={styles.content}>
<a
className={styles.tit}
style={{ fontSize, color }}
href={item.link ? item.link : '#'}
>
{item.title}
<div
className={styles.desc}
style={{ fontSize: fontSize * 0.8, color: 'rgba(0,0,0, .3)' }}
>
{item.desc}
</div>
</a>
</div>
</div>
);
})}
</div>
</div>
)}
</>
);
});
export default List;

View File

@ -0,0 +1,119 @@
import {
IColorConfigType,
IDataListConfigType,
INumberConfigType,
ISelectConfigType,
TColorDefaultType,
TDataListDefaultType,
TNumberDefaultType,
TSelectDefaultType,
} from '@/components/PanelComponents/FormEditor/types';
import { baseConfig, baseDefault, ICommonBaseType } from '../../common';
export type TListSelectKeyType = '60' | '80' | '100' | '120' | '150';
export type TListEditData = Array<
IColorConfigType | IDataListConfigType | INumberConfigType | ISelectConfigType<TListSelectKeyType>
>;
export interface IListConfig extends ICommonBaseType {
sourceData: TDataListDefaultType;
round: TNumberDefaultType;
imgSize: TSelectDefaultType<TListSelectKeyType>;
fontSize: TNumberDefaultType;
color: TColorDefaultType;
}
export interface IListSchema {
editData: TListEditData;
config: IListConfig;
}
const List: IListSchema = {
editData: [
...baseConfig,
{
key: 'sourceData',
name: '数据源',
type: 'DataList',
},
{
key: 'round',
name: '圆角',
type: 'Number',
},
{
key: 'imgSize',
name: '图片大小',
type: 'Select',
range: [
{
key: '60',
text: '60 x 60',
},
{
key: '80',
text: '80 x 80',
},
{
key: '100',
text: '100 x 100',
},
{
key: '120',
text: '120 x 120',
},
{
key: '150',
text: '150 x 150',
},
],
},
{
key: 'fontSize',
name: '文字大小',
type: 'Number',
},
{
key: 'color',
name: '文字颜色',
type: 'Color',
},
],
config: {
sourceData: [
{
id: '1',
title: '趣谈小课',
desc: '致力于打造优质小课程',
link: 'xxxxx',
imgUrl: [
{
uid: '001',
name: 'image.png',
status: 'done',
url: 'http://io.nainor.com/uploads/1_1740c6fbcd9.png',
},
],
},
{
id: '2',
title: '趣谈小课',
desc: '致力于打造优质小课程',
link: 'xxxxx',
imgUrl: [
{
uid: '002',
name: 'image.png',
status: 'done',
url: 'http://io.nainor.com/uploads/1_1740c6fbcd9.png',
},
],
},
],
round: 0,
imgSize: '80',
fontSize: 16,
color: 'rgba(153,153,153,1)',
...baseDefault,
},
};
export default List;

View File

@ -0,0 +1,5 @@
const template = {
type: 'List',
h: 110,
};
export default template;

View File

@ -0,0 +1,15 @@
import React, { memo } from 'react';
import styles from './index.less';
import { ILongTextConfig } from './schema';
const LongText = memo((props: ILongTextConfig) => {
const { text, fontSize, color, indent, lineHeight, textAlign } = props;
return (
<div
className={styles.textWrap}
style={{ color, textIndent: indent + 'px', fontSize, lineHeight, textAlign }}
>
{text}
</div>
);
});
export default LongText;

View File

@ -0,0 +1,92 @@
import {
IColorConfigType,
INumberConfigType,
ISelectConfigType,
ITextAreaConfigType,
TColorDefaultType,
TNumberDefaultType,
TSelectDefaultType,
TTextAreaDefaultType,
} from '@/components/PanelComponents/FormEditor/types';
export type TLongTextSelectKeyType = 'left' | 'center' | 'right';
export type TLongTextEditData = Array<
| ITextAreaConfigType
| IColorConfigType
| INumberConfigType
| ISelectConfigType<TLongTextSelectKeyType>
>;
export interface ILongTextConfig {
text: TTextAreaDefaultType;
color: TColorDefaultType;
fontSize: TNumberDefaultType;
indent: TNumberDefaultType;
lineHeight: TNumberDefaultType;
textAlign: TSelectDefaultType<TLongTextSelectKeyType>;
}
export interface ILongTextSchema {
editData: TLongTextEditData;
config: ILongTextConfig;
}
const LongText: ILongTextSchema = {
editData: [
{
key: 'text',
name: '文字',
type: 'TextArea',
},
{
key: 'color',
name: '标题颜色',
type: 'Color',
},
{
key: 'fontSize',
name: '字体大小',
type: 'Number',
},
{
key: 'indent',
name: '首行缩进',
type: 'Number',
range: [0, 100],
},
{
key: 'textAlign',
name: '对齐方式',
type: 'Select',
range: [
{
key: 'left',
text: '左对齐',
},
{
key: 'center',
text: '居中对齐',
},
{
key: 'right',
text: '右对齐',
},
],
},
{
key: 'lineHeight',
name: '行高',
type: 'Number',
step: 0.1,
},
],
config: {
text: '我是长文本有一段故事dooring可视化编辑器无限可能赶快来体验吧骚年们奥利给~',
color: 'rgba(60,60,60,1)',
fontSize: 14,
indent: 20,
lineHeight: 1.8,
textAlign: 'left',
},
};
export default LongText;

View File

@ -0,0 +1,5 @@
const template = {
type: 'LongText',
h: 36,
};
export default template;

View File

@ -0,0 +1,13 @@
import { NoticeBar } from 'zarm';
import React, { memo } from 'react';
import { INoticeConfig } from './schema';
const Notice = memo((props: INoticeConfig) => {
const { text, speed, theme, isClose = false } = props;
return (
<NoticeBar theme={theme === 'default' ? undefined : theme} closable={isClose} speed={speed}>
<span style={{ color: 'inherit' }}>{text}</span>
</NoticeBar>
);
});
export default Notice;

View File

@ -0,0 +1,81 @@
import {
INumberConfigType,
ISelectConfigType,
ISwitchConfigType,
ITextConfigType,
TNumberDefaultType,
TSelectDefaultType,
TSwitchDefaultType,
TTextDefaultType,
} from '@/components/PanelComponents/FormEditor/types';
export type TNoticeSelectKeyType = 'default' | 'warning' | 'primary' | 'success' | 'danger';
export type TNoticeEditData = Array<
ITextConfigType | INumberConfigType | ISelectConfigType<TNoticeSelectKeyType> | ISwitchConfigType
>;
export interface INoticeConfig {
text: TTextDefaultType;
speed: TNumberDefaultType;
theme: TSelectDefaultType<TNoticeSelectKeyType>;
isClose: TSwitchDefaultType;
}
export interface INoticeSchema {
editData: TNoticeEditData;
config: INoticeConfig;
}
const Notice: INoticeSchema = {
editData: [
{
key: 'text',
name: '文本',
type: 'Text',
},
{
key: 'speed',
name: '滚动速度',
type: 'Number',
},
{
key: 'theme',
name: '主题',
type: 'Select',
range: [
{
key: 'default',
text: '默认',
},
{
key: 'warning',
text: '警告',
},
{
key: 'primary',
text: '主要',
},
{
key: 'success',
text: '成功',
},
{
key: 'danger',
text: '危险',
},
],
},
{
key: 'isClose',
name: '是否可关闭',
type: 'Switch',
},
],
config: {
text: '通知栏: 趣谈前端上线啦',
speed: 50,
theme: 'warning',
isClose: false,
},
};
export default Notice;

View File

@ -0,0 +1,5 @@
const template = {
type: 'Notice',
h: 20,
};
export default template;

View File

@ -0,0 +1,14 @@
import React, { memo } from 'react';
import { IQrcodeConfig } from './schema';
const Qrcode = memo((props: IQrcodeConfig) => {
const { qrcode, text, color, fontSize = 14 } = props;
return (
<div style={{ width: '240px', margin: '16px auto' }}>
<img src={qrcode && qrcode[0].url} alt={text} style={{ width: '100%' }} />
<div style={{ textAlign: 'center', color, fontSize, paddingTop: '8px' }}>{text}</div>
</div>
);
});
export default Qrcode;

View File

@ -0,0 +1,67 @@
import {
IColorConfigType,
INumberConfigType,
ITextConfigType,
IUploadConfigType,
TColorDefaultType,
TNumberDefaultType,
TTextDefaultType,
TUploadDefaultType,
} from '@/components/PanelComponents/FormEditor/types';
export type TQrcodeEditData = Array<
IUploadConfigType | ITextConfigType | IColorConfigType | INumberConfigType
>;
export interface IQrcodeConfig {
qrcode: TUploadDefaultType;
text: TTextDefaultType;
color: TColorDefaultType;
fontSize: TNumberDefaultType;
}
export interface IQrcodeSchema {
editData: TQrcodeEditData;
config: IQrcodeConfig;
}
const Qrcode: IQrcodeSchema = {
editData: [
{
key: 'qrcode',
name: '二维码',
type: 'Upload',
isCrop: true,
cropRate: 1,
},
{
key: 'text',
name: '文字',
type: 'Text',
},
{
key: 'color',
name: '文字颜色',
type: 'Color',
},
{
key: 'fontSize',
name: '文字大小',
type: 'Number',
},
],
config: {
qrcode: [
{
uid: '001',
name: 'image.png',
status: 'done',
url: 'http://io.nainor.com/uploads/code_173e1705e0c.png',
},
],
text: '二维码',
color: 'rgba(153,153,153,1)',
fontSize: 14,
},
};
export default Qrcode;

View File

@ -0,0 +1,5 @@
const template = {
type: 'Qrcode',
h: 150,
};
export default template;

View File

@ -0,0 +1,64 @@
import React, { useEffect, useRef } from 'react';
import { Tabs } from 'zarm';
import styles from './index.less';
import { ITabConfig } from './schema';
const { Panel } = Tabs;
const XTab = (props: ITabConfig) => {
const { tabs = ['分类一', '分类二'], activeColor, color, fontSize, sourceData } = props;
const tabWrapRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (tabWrapRef.current) {
let res = tabWrapRef.current.querySelector('.za-tabs__line') as HTMLElement;
if (res) {
res.style.backgroundColor = activeColor;
}
}
}, [activeColor]);
return (
<div className={styles.tabWrap} ref={tabWrapRef}>
<Tabs
scrollThreshold={3}
onChange={i => {
console.log(i);
}}
>
{tabs.map((item, i) => {
return (
<Panel title={item} key={i}>
<div className={styles.content}>
{sourceData
.filter(item => item.type === i)
.map((item, i) => {
return (
<div className={styles.item} key={i}>
<a className={styles.imgWrap} href={item.link} title={item.desc}>
<img
src={
item.imgUrl[0]
? item.imgUrl[0].url
: 'http://io.nainor.com/uploads/01_173e15d3493.png'
}
alt={item.title}
/>
<div className={styles.title} style={{ fontSize, color }}>
{item.title}
</div>
</a>
</div>
);
})}
</div>
</Panel>
);
})}
</Tabs>
</div>
);
};
export default XTab;

View File

@ -0,0 +1,118 @@
import {
IColorConfigType,
IDataListConfigType,
IMutiTextConfigType,
INumberConfigType,
TColorDefaultType,
TDataListDefaultType,
TMutiTextDefaultType,
TNumberDefaultType,
} from '@/components/PanelComponents/FormEditor/types';
export type TTabEditData = Array<
IMutiTextConfigType | IColorConfigType | INumberConfigType | IDataListConfigType
>;
export interface ITabConfig {
tabs: TMutiTextDefaultType;
color: TColorDefaultType;
activeColor: TColorDefaultType;
fontSize: TNumberDefaultType;
imgSize: TNumberDefaultType;
sourceData: TDataListDefaultType;
}
export interface ITabSchema {
editData: TTabEditData;
config: ITabConfig;
}
const Tab: ITabSchema = {
editData: [
{
key: 'tabs',
name: '项目类别',
type: 'MutiText',
},
{
key: 'activeColor',
name: '激活颜色',
type: 'Color',
},
{
key: 'color',
name: '文字颜色',
type: 'Color',
},
{
key: 'fontSize',
name: '文字大小',
type: 'Number',
},
{
key: 'imgSize',
name: '图片大小',
type: 'Number',
},
{
key: 'sourceData',
name: '数据源',
type: 'DataList',
},
],
config: {
tabs: ['类别一', '类别二'],
color: 'rgba(153,153,153,1)',
activeColor: 'rgba(0,102,204,1)',
fontSize: 16,
imgSize: 100,
sourceData: [
{
id: '1',
title: '趣谈小课1',
desc: '致力于打造优质小课程',
link: 'xxxxx',
type: 0,
imgUrl: [
{
uid: '001',
name: 'image.png',
status: 'done',
url: 'http://io.nainor.com/uploads/1_1740c6fbcd9.png',
},
],
},
{
id: '2',
title: '趣谈小课2',
desc: '致力于打造优质小课程',
link: 'xxxxx',
type: 0,
imgUrl: [
{
uid: '001',
name: 'image.png',
status: 'done',
url: 'http://io.nainor.com/uploads/2_1740c7033a9.png',
},
],
},
{
id: '3',
title: '趣谈小课3',
desc: '致力于打造优质小课程',
link: 'xxxxx',
type: 1,
imgUrl: [
{
uid: '001',
name: 'image.png',
status: 'done',
url: 'http://io.nainor.com/uploads/1_1740c6fbcd9.png',
},
],
},
],
},
};
export default Tab;

View File

@ -0,0 +1,5 @@
const template = {
type: 'Tab',
h: 130,
};
export default template;

View File

@ -0,0 +1,13 @@
import React, { memo } from 'react';
import styles from './index.less';
import { ITextConfig } from './schema';
const Text = memo((props: ITextConfig) => {
const { align, text, fontSize, color, lineHeight } = props;
return (
<div className={styles.textWrap} style={{ color, textAlign: align, fontSize, lineHeight }}>
{text}
</div>
);
});
export default Text;

View File

@ -0,0 +1,78 @@
import {
IColorConfigType,
INumberConfigType,
ISelectConfigType,
ITextConfigType,
TColorDefaultType,
TNumberDefaultType,
TSelectDefaultType,
TTextDefaultType,
} from '@/components/PanelComponents/FormEditor/types';
export type TTextSelectKeyType = 'left' | 'right' | 'center';
export type TTextEditData = Array<
ITextConfigType | IColorConfigType | INumberConfigType | ISelectConfigType<TTextSelectKeyType>
>;
export interface ITextConfig {
text: TTextDefaultType;
color: TColorDefaultType;
fontSize: TNumberDefaultType;
align: TSelectDefaultType<TTextSelectKeyType>;
lineHeight: TNumberDefaultType;
}
export interface ITextSchema {
editData: TTextEditData;
config: ITextConfig;
}
const Text: ITextSchema = {
editData: [
{
key: 'text',
name: '文字',
type: 'Text',
},
{
key: 'color',
name: '标题颜色',
type: 'Color',
},
{
key: 'fontSize',
name: '字体大小',
type: 'Number',
},
{
key: 'align',
name: '对齐方式',
type: 'Select',
range: [
{
key: 'left',
text: '左对齐',
},
{
key: 'center',
text: '居中对齐',
},
{
key: 'right',
text: '右对齐',
},
],
},
{
key: 'lineHeight',
name: '行高',
type: 'Number',
},
],
config: {
text: '我是文本',
color: 'rgba(60,60,60,1)',
fontSize: 18,
align: 'center',
lineHeight: 2,
},
};
export default Text;

View File

@ -0,0 +1,5 @@
const template = {
type: 'Text',
h: 20,
};
export default template;

View File

@ -0,0 +1,28 @@
import Carousel from './Carousel/schema';
import Footer from './Footer/schema';
import Form from './Form/schema';
import Header from './Header/schema';
import Icon from './Icon/schema';
import Image from './Image/schema';
import List from './List/schema';
import LongText from './LongText/schema';
import Notice from './Notice/schema';
import Qrcode from './Qrcode/schema';
import Tab from './Tab/schema';
import Text from './Text/schema';
const basicSchema = {
Carousel,
Footer,
Form,
Header,
Icon,
Image,
List,
LongText,
Notice,
Qrcode,
Tab,
Text,
};
export default basicSchema;

View File

@ -0,0 +1,32 @@
import Carousel from './Carousel/template';
import Footer from './Footer/template';
import Form from './Form/template';
import Header from './Header/template';
import Icon from './Icon/template';
import Image from './Image/template';
import List from './List/template';
import LongText from './LongText/template';
import Notice from './Notice/template';
import Qrcode from './Qrcode/template';
import Tab from './Tab/template';
import Text from './Text/template';
const basicTemplate = [
Carousel,
Footer,
Form,
Header,
Icon,
Image,
List,
LongText,
Notice,
Qrcode,
Tab,
Text,
];
const BasicTemplate = basicTemplate.map(v => {
return { ...v, category: 'base' };
});
export default BasicTemplate;

View File

@ -0,0 +1,21 @@
import React, { memo } from 'react';
import { Player, BigPlayButton } from 'video-react';
import './index.css';
import { IVideoConfig } from './schema';
const VideoPlayer = memo((props: IVideoConfig) => {
const { poster, url } = props;
return (
<div>
<Player
playsInline
poster={poster[0].url}
src={url || 'https://gossv.vcg.com/cmsUploadVideo/creative/1移轴/7月移轴.mp4'}
>
<BigPlayButton position="center" />
</Player>
</div>
);
});
export default VideoPlayer;

View File

@ -0,0 +1,45 @@
import {
ITextConfigType,
IUploadConfigType,
TTextDefaultType,
TUploadDefaultType,
} from '@/components/PanelComponents/FormEditor/types';
export type TVideoEditData = Array<IUploadConfigType | ITextConfigType>;
export interface IVideoConfig {
poster: TUploadDefaultType;
url: TTextDefaultType;
}
export interface IVideoSchema {
editData: TVideoEditData;
config: IVideoConfig;
}
const Video: IVideoSchema = {
editData: [
{
key: 'poster',
name: '视频封面',
type: 'Upload',
},
{
key: 'url',
name: '视频链接',
type: 'Text',
},
],
config: {
poster: [
{
uid: '001',
name: 'image.png',
status: 'done',
url: 'http://io.nainor.com/uploads/1_1740c6fbcd9.png',
},
],
url: '',
},
};
export default Video;

View File

@ -0,0 +1,5 @@
const template = {
type: 'Video',
h: 107,
};
export default template;

View File

@ -0,0 +1,6 @@
import Video from './Video/schema';
const mediaSchema = {
Video,
};
export default mediaSchema;

View File

@ -0,0 +1,9 @@
import Video from './Video/template';
const mediaTemplate = [Video];
const MediaTemplate = mediaTemplate.map(v => {
return { ...v, category: 'media' };
});
export default MediaTemplate;

View File

@ -0,0 +1,13 @@
.chartWrap {
position: relative;
width: 100%;
.chartTitle {
text-align: center;
}
img {
width: 100%;
}
canvas {
width: 100%;
}
}

View File

@ -0,0 +1,76 @@
import { Chart } from '@antv/f2';
import React, { memo, useEffect, useRef } from 'react';
// import { uuid } from 'utils/tool';
import AreaImg from '@/assets/area.png';
import styles from './index.less';
import { IChartConfig } from './schema';
interface XChartProps extends IChartConfig {
isTpl: boolean;
}
const XLine = (props: XChartProps) => {
const { isTpl, data, color, size, paddingTop, title } = props;
const chartRef = useRef(null);
useEffect(() => {
if (!isTpl) {
const chart = new Chart({
el: chartRef.current || undefined,
pixelRatio: window.devicePixelRatio, // 指定分辨率
});
// step 2: 处理数据
const dataX = data.map(item => ({ ...item, value: Number(item.value), a: '1' }));
// Step 2: 载入数据源
chart.source(dataX, {
percent: {
formatter: function formatter(val) {
return val * 100 + '%';
},
},
});
chart.tooltip({
showCrosshairs: true,
});
chart.scale({
name: {
range: [0, 1],
},
value: {
tickCount: 5,
min: 0,
},
});
chart.axis('name', {
label: function label(text, index, total) {
const textCfg: any = {};
if (index === 0) {
textCfg.textAlign = 'left';
} else if (index === total - 1) {
textCfg.textAlign = 'right';
}
return textCfg;
},
});
chart.area().position('name*value');
chart.line().position('name*value');
chart.render();
}
}, [data, isTpl]);
return (
<div className={styles.chartWrap}>
<div className={styles.chartTitle} style={{ color, fontSize: size, paddingTop }}>
{title}
</div>
{isTpl ? <img src={AreaImg} alt="dooring chart" /> : <canvas ref={chartRef}></canvas>}
</div>
);
};
export default memo(XLine);

View File

@ -0,0 +1,82 @@
import {
IColorConfigType,
INumberConfigType,
ITableConfigType,
ITextConfigType,
TColorDefaultType,
TNumberDefaultType,
TTableDefaultType,
TTextDefaultType,
} from '@/components/PanelComponents/FormEditor/types';
export type TChartEditData = Array<
ITextConfigType | INumberConfigType | IColorConfigType | ITableConfigType
>;
export interface IChartConfig {
title: TTextDefaultType;
size: TNumberDefaultType;
color: TColorDefaultType;
paddingTop: TNumberDefaultType;
data: TTableDefaultType;
}
export interface IChartSchema {
editData: TChartEditData;
config: IChartConfig;
}
const Chart: IChartSchema = {
editData: [
{
key: 'title',
name: '标题',
type: 'Text',
},
{
key: 'size',
name: '标题大小',
type: 'Number',
},
{
key: 'color',
name: '标题颜色',
type: 'Color',
},
{
key: 'paddingTop',
name: '上边距',
type: 'Number',
},
{
key: 'data',
name: '数据源',
type: 'Table',
},
],
config: {
title: '面积图',
size: 14,
color: 'rgba(0,0,0,1)',
paddingTop: 10,
data: [
{
name: 'A',
value: 20,
},
{
name: 'B',
value: 60,
},
{
name: 'C',
value: 20,
},
{
name: 'D',
value: 80,
},
],
},
};
export default Chart;

View File

@ -0,0 +1,5 @@
const template = {
type: 'Area',
h: 108,
};
export default template;

View File

@ -0,0 +1,13 @@
.chartWrap {
position: relative;
width: 100%;
.chartTitle {
text-align: center;
}
img {
width: 100%;
}
canvas {
width: 100%;
}
}

View File

@ -0,0 +1,49 @@
import { Chart } from '@antv/f2';
import React, { memo, useEffect, useRef } from 'react';
// import { uuid } from 'utils/tool';
import ChartImg from '@/assets/chart.png';
import styles from './index.less';
import { IChartConfig } from './schema';
interface XChartProps extends IChartConfig {
isTpl: boolean;
}
const XChart = (props: XChartProps) => {
const { isTpl, data, color, size, paddingTop, title } = props;
const chartRef = useRef(null);
useEffect(() => {
if (!isTpl) {
const chart = new Chart({
el: chartRef.current || undefined,
pixelRatio: window.devicePixelRatio, // 指定分辨率
});
// step 2: 处理数据
const dataX = data.map(item => ({ ...item, value: Number(item.value) }));
// Step 2: 载入数据源
chart.source(dataX);
// Step 3创建图形语法绘制柱状图由 genre 和 sold 两个属性决定图形位置genre 映射至 x 轴sold 映射至 y 轴
chart
.interval()
.position('name*value')
.color('name');
// Step 4: 渲染图表
chart.render();
}
}, [data, isTpl]);
return (
<div className={styles.chartWrap}>
<div className={styles.chartTitle} style={{ color, fontSize: size, paddingTop }}>
{title}
</div>
{isTpl ? <img src={ChartImg} alt="dooring chart" /> : <canvas ref={chartRef}></canvas>}
</div>
);
};
export default memo(XChart);

Some files were not shown because too many files have changed in this diff Show More