init: drip-table-generator 1.0.0

This commit is contained in:
helloqian12138 2021-12-13 18:07:28 +08:00
parent c31c4cd41c
commit 34600cc194
47 changed files with 4116 additions and 12 deletions

View File

@ -5,15 +5,17 @@
"packages/*"
],
"scripts": {
"build": "yarn run build:drip-table && yarn run build:drip-table-driver-antd",
"build": "yarn run build:drip-table && yarn run build:drip-table-driver-antd && yarn run build:drip-table-generator",
"build:drip-table": "lerna run build --stream --scope=drip-table",
"build:drip-table-driver-antd": "lerna run build --stream --scope=drip-table-driver-antd",
"build:drip-table-generator": "lerna run build --stream --scope=drip-table-generator",
"changelog": "lerna-changelog",
"clean": "lerna clean -y",
"lint": "yarn run lint:git && yarn run lint:drip-table && yarn run lint:drip-table-driver-antd",
"lint": "yarn run lint:git && yarn run lint:drip-table && yarn run lint:drip-table-driver-antd && yarn run lint:drip-table-generator",
"lint:git": "sh ./bin/gitlint.sh || exit 1",
"lint:drip-table": "lerna run lint --stream --scope=drip-table",
"lint:drip-table-driver-antd": "lerna run lint --stream --scope=drip-table-driver-antd"
"lint:drip-table-driver-antd": "lerna run lint --stream --scope=drip-table-driver-antd",
"lint:drip-table-generator": "lerna run lint --stream --scope=drip-table-generator"
},
"devDependencies": {
"@rollup/plugin-eslint": "^8.0.1",

View File

@ -0,0 +1,98 @@
const path = require('path');
const rules = {
"no-new-func": "off",
"no-undefined": "error",
"no-void": "off",
"react/jsx-no-bind": "off",
"react/prop-types": "off",
"react/sort-comp": "off",
"unicorn/no-array-callback-reference": "off",
"unicorn/no-array-for-each": "off",
"unicorn/no-array-reduce": "off",
"unicorn/prefer-switch": "off",
};
const extensions = [".js", ".jsx", ".jx", ".ts", ".tsx", ".tx"];
// http://eslint.org/docs/user-guide/configuring
module.exports = {
root: true,
parser: "babel-eslint",
parserOptions: {
ecmaVersion: 6,
ecmaFeatures: {
modules: true,
jsx: true,
legacyDecorators: true,
experimentalObjectRestSpread: true,
},
sourceType: "module",
},
env: {
browser: true,
node: true,
es6: true,
},
extends: [
"lvmcn/javascript/react",
],
plugins: [
"react",
"import",
"unicorn",
"unused-imports",
],
settings: {
"import/resolver": {
alias: {
map: [
['@', path.resolve(__dirname, './src')],
],
extensions: extensions,
},
node: {
extensions: extensions,
},
},
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"],
},
react: {
version: "detect",
},
},
noInlineConfig: true,
rules,
overrides: [
{
files: ["*.ts", "*.tsx", "*.tx"],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 6,
ecmaFeatures: {
modules: true,
jsx: true,
legacyDecorators: true,
experimentalObjectRestSpread: true,
},
sourceType: "module",
project: "./tsconfig.json",
},
extends: [
"lvmcn/typescript/react",
],
rules,
},
{
files: ["*.d.ts"],
rules: {
"react/no-typos": "off",
"@typescript-eslint/no-unused-vars": "off",
},
},
],
ignorePatterns: [
"/.eslintrc.js",
"/dist",
],
};

View File

@ -0,0 +1,54 @@
import { IBundleOptions } from 'father-build/src/types';
import commonjs from 'rollup-plugin-commonjs';
const options: IBundleOptions = {
cjs: { type: 'rollup' },
esm: {
type: 'rollup',
importLibToEs: true,
},
cssModules: {
generateScopedName: 'drip-table-generator-[local]',
},
extractCSS: true,
extraBabelPlugins: [
[
'import',
{
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
},
'antd',
],
[
'import',
{
libraryName: '@ant-design/icons',
libraryDirectory: 'lib/icons',
camel2DashComponentName: false,
},
'@ant-design/icons',
],
],
extraRollupPlugins: [
// fix error "'isFragment' is not exported by node_modules/react-is/index.js"
// https://github.com/mui-org/material-ui/issues/18791#issuecomment-574275944
commonjs({
include: '../../node_modules/**',
namedExports: {
'../../node_modules/react-is/index.js': [
'isFragment',
'isMemo',
'ForwardRef',
],
},
}),
],
pkgs: [
'drip-table',
'drip-table-generator',
],
};
export default options;

View File

@ -0,0 +1,103 @@
# DripTableGenerator Development Guide
`DripTableGenerator` is a visual tool for producing configuration data that meets the `JSON Schema` standard in order to sent to `DripTable` for rendering a table and columns.
## Directory
- update continually
```
├── src // Source code
├── components // Common components that drip-table-generator depends on
│ ├── Draggable // draggable div
│ ├── Highlight // Highlight component
│ └── RichText // HTML character rendering component
├── custom-components // custom component
│ └── ArrayComponent // ArrayList component
├── layout // layout
│ ├── attribute-layout.tsx // attribute form panel
│ ├── component-layout.tsx // component list
│ ├── configs.ts // global configs
│ ├── custom-form.tsx // driver of attribute form
│ ├── editable-layout.tsx // edit table columns
│ ├── preview-layout.tsx // preview table
│ └── tool-layout.tsx // tools
├── store // states manager
│ ├── custom-hook.ts // setState
│ └── index.ts // export states
├── table-components // configuration of table components
│ ├── configs.ts // common configuration
│ ├── index.ts // export configuration
│ ├── links.ts // links component configuration
│ ├── picture.ts // picture component configuration
│ ├── render-html.ts // custom HTML render component configuration
│ └── text.ts // text component configuration
├── utils
│ └── common.ts // common functions
├── context.ts // global context
├── drip-table-generator.tsx // entry
├── hooks.ts // global states
├── index.module.less // global styles
├── index.ts // export
├── shims-styles.d.ts // style type definition
├── shims-window.d.ts // global variations definition
├── typing.ts // generator props type definition
└── wrapper.ts // generator app root element
```
## Development
### Preparation
1. Install dependencies
```sh
lerna bootstrap
```
2. run project
```sh
yarn start
```
### Steps
1. Fork.
2. create a new branch which shall be named to express the features briefly.
3. make your own changes and submit.
4. create a pull request.
### Cautions
- Use React.memo to wrap a functional component, and compare props to update the wrapped component.
- Merge related states instead of using too many useStates.
- Do not use arrow functions to assign to event functions as values directly. (Reference: [How to read an often-changing value from useCallback?](https://reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback))
- Either class component or functional component while defining a component as much as possible.
## Release
add owner in npm depository for the first time.
```sh
npm owner add [username] drip-table-generator
```
### Version number modification rules
- Major version number(1): Required. Modify when there are major changes in the functional modules, such as adding multiple modules or changing the architecture. Whether to modify this version number is determined by the project manager.
- Sub version number(2): Required. Modify when the function has a certain increase or change, such as adding authority management, adding custom views, etc. Whether to modify this version number is determined by the project manager.
- Phase version number(3): Required. Generally, a revised version is released when a bug is fixed or a small change is made. The time interval is not limited. When a serious bug is fixed, a revised version can be released. Whether to modify this version number is determined by the project manager.
- Date version number(051021): Optional. The date version number is used to record the date of the modification of the project, and the modification needs to be recorded every day. Whether to modify this version number is determined by developers.
- Greek letter version number(beta): Optional. This version number is used to mark which phrase of development the current software is in, and it needs to be modified when the software enters to another phrase. Whether to modify this version number is determined by the project manager.
```sh
git commit -m 'release: drip-table-generator 1.2.3'
lerna run build --stream --scope=drip-table-generator
cd packages/drip-table-generator
npm publish --access=public
```

View File

@ -0,0 +1,104 @@
# DripTableGenerator 开发文档
DripTableGenerator 是一个可视化的 DripTable `JSON Schema` 配置数据的生成工具。
## 目录结构
- 持续更新
```
├── src // 源代码
│ ├── components // drip-table-generator 依赖的通用组件
│ │ ├── Draggable // 拖拽组件
│ │ ├── Highlight // 高亮组件
│ │ └── RichText // HTML 字符渲染组件
│ ├── custom-components // 属性面板依赖的自定义组件
│ │ └── ArrayComponent // 列表组件
│ ├── layout // 页面布局
│ │ ├── attribute-layout.tsx // 属性面板
│ │ ├── component-layout.tsx // 组件列表面板
│ │ ├── configs.ts // 全局属性配置
│ │ ├── custom-form.tsx // 属性面板底层驱动表单
│ │ ├── editable-layout.tsx // 可编辑列表面板
│ │ ├── preview-layout.tsx // 预览列表面板
│ │ └── tool-layout.tsx // 工具栏
│ ├── store // 全局数据流管理
│ │ ├── custom-hook.ts // setState 实现
│ │ └── index.ts // 全局状态定义和修改函数定义
│ ├── table-components // 属性面板配置
│ │ ├── configs.ts // 通用属性配置
│ │ ├── index.ts // 属性导出
│ │ ├── links.ts // 链接组件属性配置
│ │ ├── picture.ts // 图片组件属性配置
│ │ ├── render-html.ts // 自定义 HTML 渲染组件属性配置
│ │ └── text.ts // 文本组件属性配置
│ ├── utils // 通用函数
│ │ └── common.ts // 常用通用函数
│ ├── context.ts // 全局上下文导出
│ ├── drip-table-generator.tsx // 入口文件
│ ├── hooks.ts // 全局属性状态管理
│ ├── index.module.less // 全局样式
│ ├── index.ts // 导出
│ ├── shims-styles.d.ts // 样式类型定义
│ ├── shims-window.d.ts // 全局变量类型定义
│ ├── typing.ts // drip-table-generator 类型定义
│ └── wrapper.ts // 入口文件
```
## 开发
### 开发准备
1. 安装依赖
```sh
lerna bootstrap
```
2. 启动项目
```sh
yarn start
```
### 开发步骤
1. Fork。
2. 创建一个新分支,分支名表达改动内容即可。
3. 改动,并提交。
4. 创建 PR 请求。
### 注意事项
- 每个函数式组件 export 之前使用 React.memo 包裹,浅比较 props 来更新包裹的组件
- 尽量合并相关的 state不要使用多个 useState。
- 事件函数不要用箭头函数直接赋值。(参考:[如何从 useCallback 读取一个经常变化的值?](https://react.docschina.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback))
- 单个组件中不要同时使用 class component 与 function component。
## 发布
npm 添加用户名,仅第一次添加即可
```sh
npm owner add [username] drip-table-generator
```
### 版本号定修改规则
- 主版本号(1):必填,当功能模块有较大的变动,比如增加多个模块或者整体架构发生变化。此版本号由项目决定是否修改。
- 子版本号(2):必填,当功能有一定的增加或变化,比如增加了对权限控制、增加自定义视图等功能。此版本号由项目决定是否修改。
- 阶段版本号(3):必填,一般是 Bug 修复或是一些小的变动要经常发布修订版时间间隔不限修复一个严重的bug即可发布一个修订版。此版本号由项目经理决定是否修改。
- 日期版本号(051021): 可选,用于记录修改项目的当前日期,每天对项目的修改都需要更改日期版本号。此版本号由开发人员决定是否修改。
- 希腊字母版本号(beta): 可选,此版本号用于标注当前版本的软件处于哪个开发阶段,当软件进入到另一个阶段时需要修改此版本号。此版本号由项目决定是否修改。
```sh
git commit -m 'release: drip-table-generator 1.2.3'
lerna run build --stream --scope=drip-table-generator
cd packages/drip-table-generator
npm publish --access=public
```

View File

@ -0,0 +1,68 @@
{
"name": "drip-table-generator",
"version": "1.0.0",
"description": "A visualization tool for generating schema of drip-table.",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"typings": "dist/index.d.ts",
"scripts": {
"build": "father-build && tsc-alias -p tsconfig.json",
"analyze": "ANALYZE=1 dumi dev",
"prepare": "yarn build",
"postpublish": "git push --tags",
"lint": "yarn run eslint && yarn run tslint && yarn run stylelint",
"lint:fix": "yarn run eslint:fix && yarn run stylelint:fix",
"tslint": "sh ../../bin/tslint.sh **/*.ts",
"tslint:commit": "sh ./bin/tslint.sh",
"tslint:exec": "tsc --project .tsconfig-lint.json --skipLibCheck --noEmit",
"eslint": "eslint \"src/**/*.{js,jsx,ts,tsx,json}\" --format pretty",
"eslint:fix": "eslint \"src/**/*.{js,jsx,ts,tsx,json}\" --format pretty --fix",
"eslint:commit": "eslint --format pretty",
"stylelint": "stylelint \"src/**/*.{less,sass,scss,css,vue}\" --custom-formatter=../../node_modules/stylelint-formatter-pretty",
"stylelint:fix": "stylelint \"src/**/*.{less,sass,scss,css,vue}\" --custom-formatter=../../node_modules/stylelint-formatter-pretty --fix",
"stylelint:commit": "stylelint --custom-formatter=../../node_modules/stylelint-formatter-pretty"
},
"dependencies": {
"@monaco-editor/react": "^4.2.1",
"cheerio": "1.0.0-rc.3",
"lodash": "^4.4.2",
"rc-color-picker": "^1.2.6",
"typescript": "^4.4.2",
"viewerjs": "^1.10.0"
},
"devDependencies": {
"drip-table": "link:../drip-table",
"drip-table-driver-antd": "link:../drip-table-driver-antd",
"babel-plugin-import": "1.13.0",
"father-build": "^1.20.4",
"rollup-plugin-commonjs": "^10.1.0",
"tsc-alias": "^1.4.2",
"typescript": "^4.4.4"
},
"peerDependencies": {
"@ant-design/icons": "^4.3.0",
"drip-table": ">=1.0.0",
"drip-table-driver-antd": ">=1.0.0",
"antd": ">=4.9.4",
"react": ">=16.9.0"
},
"keywords": [
"DripTableGenerator",
"TableGenerator",
"Generator",
"DripTable",
"Drip Design",
"React",
"Json Schema"
],
"files": [
"README.*",
"dist"
],
"homepage": "https://drip-table.jd.com",
"license": "MIT",
"bugs": {
"url": "https://github.com/JDFED/drip-table/issues"
}
}

View File

@ -0,0 +1,25 @@
.array-component {
&-container {
width: 420px;
}
&-form-container {
border: 1px solid #efefef;
border-radius: 2px;
padding: 0 12px;
margin: 6px 0;
}
&-left-container {
display: inline-block;
vertical-align: middle;
width: ~'calc(100% - 72px)';
}
&-right-container {
display: inline-block;
vertical-align: middle;
width: 72px;
text-align: center;
}
}

View File

@ -0,0 +1,191 @@
import React from 'react';
import { Button, Row, Col, Popover, Radio, InputNumber, Switch, Input, Select, AutoComplete } from 'antd';
import { LabeledValue, SelectValue } from 'antd/lib/select';
import { MinusCircleOutlined, PlusCircleOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import RichText from '@/components/RichText';
import { DTGComponentPropertySchema } from '@/typing';
import styles from './index.module.less';
interface Props {
schema: DTGComponentPropertySchema;
fieldOptions?: { label: string; value: string }[];
value?: Record<string, unknown>[];
onChange?: (value: Record<string, unknown>[]) => void;
}
export default class ArrayComponent extends React.PureComponent<Props> {
private changeColumnItem(key: string, value: unknown, index: number) {
const currentValue = this.props.value?.slice() || [];
if (!currentValue[index]) {
currentValue[index] = {};
}
currentValue[index][key] = value;
this.props.onChange?.(currentValue);
}
private renderAttributeComponent(schema: DTGComponentPropertySchema, index: number, parentIndex: number) {
const currentValue = (this.props.value || [])[parentIndex] || {};
const options = schema['ui:props']?.options as LabeledValue[] || this.props.fieldOptions || [];
if (schema['ui:type'] === 'radio') {
return (
<Radio.Group
style={{ width: '100%' }}
defaultValue={schema.default}
value={currentValue[schema.name]}
onChange={e => this.changeColumnItem(schema.name, e.target.value, parentIndex)}
>
{
options?.map(
(option, i) =>
<Radio key={i} value={option.value}>{ option.label }</Radio>,
)
}
</Radio.Group>
);
}
if (schema['ui:type'] === 'input') {
return (
<Input
style={{ width: '100%' }}
defaultValue={schema.default as string}
value={currentValue[schema.name] as string}
onChange={e => this.changeColumnItem(schema.name, e.target.value, parentIndex)}
/>
);
}
if (schema['ui:type'] === 'text') {
return (
<Input.TextArea
style={{ width: '100%' }}
autoSize={schema['ui:autoSize']}
defaultValue={schema.default as string}
value={currentValue[schema.name] as string}
onChange={e => this.changeColumnItem(schema.name, e.target.value, parentIndex)}
/>
);
}
if (schema['ui:type'] === 'auto-complete') {
return (
<AutoComplete
style={{ width: '100%' }}
defaultValue={schema.default as string}
value={currentValue[schema.name] as string}
options={options}
onChange={value => this.changeColumnItem(schema.name, value, parentIndex)}
/>
);
}
if (schema['ui:type'] === 'number') {
return (
<InputNumber
style={{ width: '100%' }}
min={schema['ui:minium']}
max={schema['ui:maximum']}
step={schema['ui:step']}
defaultValue={Number(schema.default)}
value={Number(currentValue[schema.name])}
onChange={value => this.changeColumnItem(schema.name, Number(value), parentIndex)}
/>
);
}
if (schema['ui:type'] === 'switch') {
const value = typeof currentValue[schema.name] === 'undefined' ? schema.default : currentValue[schema.name];
return (
<Switch
checked={value as boolean}
checkedChildren={schema['ui:checkedContent']}
unCheckedChildren={schema['ui:unCheckedContent']}
onChange={checked => this.changeColumnItem(schema.name, checked, parentIndex)}
/>
);
}
if (schema['ui:type'] === 'select') {
return (
<Select
showSearch
style={{ width: '100%' }}
mode={schema['ui:mode']}
defaultValue={schema.default as SelectValue}
value={currentValue[schema.name] as SelectValue}
options={options}
onChange={value => this.changeColumnItem(schema.name, value, parentIndex)}
/>
);
}
return null;
}
private renderAttributeItem(schema: DTGComponentPropertySchema, index: number, parentIndex: number) {
return (
<Row key={index} style={{ lineHeight: '32px', margin: '6px 0' }}>
<Col span={8}>
{ schema['ui:title'] }
{
schema['ui:description']
? (
<Popover content={<RichText html={schema['ui:description'].title} />}>
<QuestionCircleOutlined style={{ marginLeft: '12px', cursor: 'pointer' }} />
</Popover>
)
: null
}
</Col>
<Col span={16}>{ this.renderAttributeComponent(schema, index, parentIndex) }</Col>
</Row>
);
}
public renderFormItem(item: unknown, index: number) {
return (
<div className={styles['array-component-form-container']} key={index}>
<div className={styles['array-component-left-container']}>
{ (this.props.schema['ui:props']?.items as DTGComponentPropertySchema[])
.map((schema, i) => this.renderAttributeItem(schema, i, index)) }
</div>
<div className={styles['array-component-right-container']}>
<Button
icon={<PlusCircleOutlined />}
shape="circle"
size="small"
onClick={() => {
const currentValue = this.props.value?.slice() || [];
currentValue.splice(index + 1, 0, { paramName: '', prefix: '', suffix: '' });
this.props.onChange?.(currentValue);
}}
/>
<Button
icon={<MinusCircleOutlined />}
shape="circle"
size="small"
onClick={() => {
const currentValue = this.props.value?.slice() || [];
currentValue.splice(index, 1);
this.props.onChange?.(currentValue);
}}
/>
</div>
</div>
);
}
public render() {
return (
<div className={styles['array-component-container']}>
{ (this.props.value || []).map((item, index) => this.renderFormItem(item, index)) }
<Button
icon={<PlusOutlined />}
onClick={() => {
const value: Record<string, unknown>[] = [
...this.props.value || [],
{},
];
this.props.onChange?.(value);
}}
>
</Button>
</div>
);
}
}

View File

@ -0,0 +1,15 @@
.trigger {
border: 1px solid #999999;
display: inline-block;
padding: 2px;
border-radius: 2px;
user-select: none;
width: 90px;
height: 32px;
cursor: pointer;
box-shadow: 0 0 0 2px #ffffff inset;
}
:global(.rc-color-picker-wrap) {
display: flex !important;
}

View File

@ -0,0 +1,76 @@
/**
* This file is part of the drip-table project.
* @link : https://drip-table.jd.com/
* @author : helloqian12138 (johnhello12138@163.com)
* @modifier : helloqian12138 (johnhello12138@163.com)
* @copyright: Copyright (c) 2020 JD Network Technology Co., Ltd.
*/
import React from 'react';
import ColorPicker from 'rc-color-picker';
import { DTGComponentPropertySchema } from '@/typing';
import 'rc-color-picker/assets/index.css';
import styles from './index.module.css';
interface Props {
schema: DTGComponentPropertySchema;
fieldOptions?: { label: string; value: string }[];
value?: string;
onChange?: (value: string) => void;
}
export default class ColorPickerComponent extends React.PureComponent<Props> {
/**
* alpha通道值()16
*
* @private
* @param {number} alpha alpha通道值()
* @returns {string} 16
*
* @memberof ColorPickerComponent
*/
private alphaToHex(alpha: number) {
const alphaDec = Math.floor((alpha / 100) * 255);
return alphaDec.toString(16);
}
/**
* 16alpha通道值()
*
* @private
* @param {string} color 16
* @returns {number} alpha通道值()
*
* @memberof ColorPickerComponent
*/
private colorToAlpha(color?: string) {
if (!color) { return 100; }
if (color.length === 5) {
const num = Number.parseInt(color.slice(4), 16);
return Math.floor((num / 15) * 100);
}
if (color.length === 9) {
const num = Number.parseInt(color.slice(7), 16);
return Math.floor((num / 255) * 100);
}
return 100;
}
public render() {
const uiProps = this.props.schema['ui:props'] || {};
return (
<ColorPicker
placement="bottomLeft"
alpha={this.colorToAlpha(this.props.value)}
defaultAlpha={100}
color={this.props.value}
defaultColor={uiProps.defaultColor || '#000000'}
onChange={(colors) => {
this.props.onChange?.(colors.color + this.alphaToHex(colors.alpha));
}}
>
<span className={styles.trigger} />
</ColorPicker>
);
}
}

View File

@ -0,0 +1,14 @@
/**
* This file is part of the drip-table project.
* @link : https://drip-table.jd.com/
* @author : helloqian12138 (johnhello12138@163.com)
* @modifier : helloqian12138 (johnhello12138@163.com)
* @copyright: Copyright (c) 2020 JD Network Technology Co., Ltd.
*/
import ArrayComponent from './ArrayComponent';
import ColorPicker from './ColorPicker';
export default {
ArrayComponent,
ColorPicker,
};

View File

@ -0,0 +1,416 @@
/**
* This file is part of the drip-table project.
* @link : https://drip-table.jd.com/
* @author : helloqian12138 (johnhello12138@163.com)
* @modifier : helloqian12138 (johnhello12138@163.com)
* @copyright: Copyright (c) 2020 JD Network Technology Co., Ltd.
*/
import React, { Component, ReactNode } from 'react';
import { Form, Input, Switch, Cascader, Button, Select, Radio, Checkbox, InputNumber, Popover, message, AutoComplete } from 'antd';
import { StringDataSchema } from 'drip-table';
import MonacoEditor from '@monaco-editor/react';
import { QuestionCircleOutlined } from '@ant-design/icons';
import { LabeledValue, SelectValue } from 'antd/lib/select';
import { DTGComponentPropertySchema } from '@/typing';
import ExtraComponents from './components';
import RichText from '@/components/RichText';
import 'rc-color-picker/assets/index.css';
type CheckboxGroupProps = React.ComponentProps<typeof Checkbox.Group>;
type CascaderProps = React.ComponentProps<typeof Cascader>;
interface Props<T> {
configs: DTGComponentPropertySchema[];
data?: T;
key?: string;
extendKeys?: string[];
encodeData: (formData: { [key: string]: unknown }) => T;
onChange?: (data?: T) => void;
}
interface State {
formValues: { [key: string]: unknown };
helpMsg: { [key: string]: string };
}
export default class CustomForm<T> extends Component<Props<T>, State> {
public constructor(props: Props<T>) {
super(props);
this.state = {
formValues: {},
helpMsg: {},
};
}
public componentDidMount() {
this.setState({ formValues: this.decodeData(this.props.data) });
}
public componentDidUpdate(prepProps: Props<T>) {
const key = this.props.key || '$id';
const preId = prepProps.data ? prepProps.data[key] : '';
const thisId = this.props.data ? this.props.data[key] : '';
if (preId !== thisId) {
this.setState({ formValues: this.decodeData(this.props.data), helpMsg: {} });
}
}
public decodeData(material?: T) {
const obj: { [key: string]: unknown } = {};
// 注入 defaultValue
this.props.configs.forEach((item) => {
if (typeof item.default !== 'undefined') { obj[item.name] = item.default; }
});
if (material) {
Object.keys(material)
.filter(key => (this.props.extendKeys ? !this.props.extendKeys.includes(key) : true))
.forEach((key) => { obj[key] = material[key]; });
if (this.props.extendKeys) {
this.props.extendKeys.forEach(extKey =>
Object.keys(material[extKey] || {})
.forEach((key) => { obj[`${extKey}.${key}`] = (material[extKey] || {})[key]; }));
}
}
return obj;
}
private visible(config: DTGComponentPropertySchema) {
const { formValues } = this.state;
if (typeof config.visible === 'function') {
return config.visible(formValues[config.name], formValues);
} if (typeof config.visible === 'string') {
const visible = new Function('formData', config.visible);
return visible(formValues);
} if (typeof config.visible === 'boolean') {
return config.visible;
}
return true;
}
public async submitData() {
const { formValues, helpMsg } = this.state;
let count = 0;
const configs = this.props.configs.filter(config => this.visible(config));
await configs.forEach(async (cfg) => {
const msg = cfg.validate ? await cfg.validate(formValues[cfg.name] as string) : '';
if (msg) {
helpMsg[cfg.name] = msg;
count += 1;
}
});
this.setState({ helpMsg });
if (count <= 0) {
return this.props.encodeData(formValues);
}
return void 0;
}
public async changeData() {
const data = await this.submitData();
if (data && this.props.onChange) {
this.props.onChange(data);
}
}
public renderFormComponent(config: DTGComponentPropertySchema) {
const { formValues, helpMsg } = this.state;
const uiProps = config['ui:props'] || {};
if (config['ui:type'] === 'input') {
return (
<Input
value={formValues[config.name] as string}
placeholder={uiProps.placeholder as string}
disabled={uiProps.disabled as boolean}
style={{ width: 420, ...uiProps.style }}
onChange={(e) => {
const value = e.target.value;
formValues[config.name] = (config as StringDataSchema).transform?.includes('trim') ? value.trim() : value;
this.setState({ formValues }, () => { this.changeData(); });
if (config.validate) {
const res = config.validate(value);
(res instanceof Promise ? res : Promise.resolve(res))
.then((msg) => {
helpMsg[config.name] = msg || '';
this.setState({ helpMsg });
return msg;
})
.catch((error) => { throw error; });
}
}}
/>
);
}
if (config['ui:type'] === 'text') {
return (
<Input.TextArea
value={formValues[config.name] as string}
placeholder={uiProps.placeholder as string}
disabled={uiProps.disabled as boolean}
style={{ width: 420, ...uiProps.style }}
autoSize={{
minRows: uiProps.minRows as number,
maxRows: uiProps.maxRows as number,
}}
onChange={(e) => {
const value = e.target.value;
formValues[config.name] = (config as StringDataSchema).transform?.includes('trim') ? value.trim() : value;
this.setState({ formValues }, () => { this.changeData(); });
if (config.validate) {
const res = config.validate(value);
(res instanceof Promise ? res : Promise.resolve(res))
.then((msg) => {
helpMsg[config.name] = msg || '';
this.setState({ helpMsg });
return msg;
})
.catch((error) => { throw error; });
}
}}
/>
);
}
if (config['ui:type'] === 'auto-complete') {
return (
<AutoComplete
value={formValues[config.name] as string}
placeholder={uiProps.placeholder as string}
disabled={uiProps.disabled as boolean}
style={{ width: 420, ...uiProps.style }}
options={uiProps.options as LabeledValue[]}
onChange={(value) => {
formValues[config.name] = (config as StringDataSchema).transform?.includes('trim') ? value.trim() : value;
this.setState({ formValues }, () => { this.changeData(); });
if (config.validate) {
const res = config.validate(value);
(res instanceof Promise ? res : Promise.resolve(res))
.then((msg) => {
helpMsg[config.name] = msg || '';
this.setState({ helpMsg });
return msg;
})
.catch((error) => { throw error; });
}
}}
/>
);
}
if (config['ui:type'] === 'switch') {
const checkedValue = uiProps.checkedValue ? formValues[config.name] === uiProps.checkedValue : formValues[config.name] as boolean;
return (
<Switch
checked={checkedValue}
checkedChildren={uiProps.checkedChildren as ReactNode || '是'}
unCheckedChildren={uiProps.uncheckedChildren as ReactNode || '否'}
onChange={(checked: boolean) => {
let value: boolean | string | number = checked;
if (uiProps.checkedValue && uiProps.unCheckedValue) {
value = checked ? uiProps.checkedValue as string | number : uiProps.unCheckedValue as string | number;
}
formValues[config.name] = value;
this.setState({ formValues }, () => { this.changeData(); });
}}
/>
);
}
if (config['ui:type'] === 'number') {
return (
<InputNumber
value={formValues[config.name] as number}
max={uiProps.max as number}
min={uiProps.min as number}
precision={uiProps.precision as number}
onChange={(checked) => {
formValues[config.name] = checked;
this.setState({ formValues }, () => { this.changeData(); });
}}
/>
);
}
if (config['ui:type'] === 'checkbox') {
return (
<Checkbox.Group
defaultValue={config.default as CheckboxGroupProps['defaultValue']}
value={formValues[config.name] as CheckboxGroupProps['value']}
onChange={(value) => {
formValues[config.name] = value;
this.setState({ formValues }, () => { this.changeData(); });
}}
>
{ (uiProps.options as CheckboxGroupProps['options'])?.map((option, i) => {
if (typeof option === 'string') {
option = { label: option, value: option };
}
return (
<Checkbox key={i} value={option.value} style={option.style} disabled={option.disabled}>{ option.label }</Checkbox>
);
}) }
</Checkbox.Group>
);
}
if (config['ui:type'] === 'radio') {
return (
<Radio.Group
defaultValue={config.default}
value={formValues[config.name] as number | string}
onChange={(e) => {
formValues[config.name] = e.target.value;
this.setState({ formValues }, () => { this.changeData(); });
}}
>
{ (uiProps.options as CheckboxGroupProps['options'])?.map((option, i) => {
if (typeof option === 'string') {
option = { label: option, value: option };
}
return (<Radio key={i} value={option.value} style={option.style} disabled={option.disabled}>{ option.label }</Radio>);
}) }
</Radio.Group>
);
}
if (config['ui:type'] === 'select') {
return (
<Select
showSearch
allowClear={uiProps.allowClear as boolean}
style={{ width: 420, ...uiProps.style }}
mode={uiProps.mode as 'multiple' | 'tags'}
defaultValue={config.default as SelectValue}
value={formValues[config.name] as SelectValue}
options={(uiProps.options as { label: string; value: string }[] || []).map(v => ({ label: v.label, value: v.value }))}
onChange={(value) => {
formValues[config.name] = value;
this.setState({ formValues }, () => { this.changeData(); });
}}
/>
);
}
if (config['ui:type'] === 'cascader') {
return (
<Cascader
options={uiProps.options as CascaderProps['options']}
defaultValue={config.default as CascaderProps['defaultValue']}
value={formValues[config.name] as CascaderProps['value']}
displayRender={uiProps.displayRender as CascaderProps['displayRender']}
disabled={uiProps.disabled as boolean}
style={{ width: 420, ...uiProps.style }}
onChange={(value) => {
formValues[config.name] = value;
this.setState({ formValues }, () => { this.changeData(); });
}}
/>
);
}
if (config['ui:type'] === 'code-editor') {
let codeStr = formValues[config.name] as string;
let marks: { message: string }[] = [];
return (
<div style={{ position: 'relative' }}>
<div style={{ margin: '8px 0', position: 'absolute', top: '-42px', right: '2px' }}>
<Button onClick={() => {
if (marks.length <= 0) {
formValues[config.name] = codeStr;
this.setState({ formValues }, () => { this.changeData(); });
} else {
message.error(marks.map(item => item.message).join('\n'));
}
}}
>
</Button>
</div>
<MonacoEditor
width="100%"
height={356}
language="javascript"
theme="vs-dark"
value={codeStr}
onChange={(value) => { codeStr = value || ''; }}
onValidate={(markers) => {
marks = markers;
}}
/>
</div>
);
}
if (config['ui:type'].startsWith('custom::')) {
const ComponentName = config['ui:type'].replace('custom::', '');
const CustomComponent = ExtraComponents[ComponentName] || config['ui:externalComponent'];
return (
<CustomComponent
schema={config}
value={formValues[config.name] as Record<string, string> | Record<string, string>[]}
onChange={(value) => {
formValues[config.name] = value;
this.setState({ formValues }, () => {
this.changeData();
});
}}
{...uiProps}
/>
);
}
return null;
}
public renderTitleLabel(config: DTGComponentPropertySchema) {
const titleFragment = (
<span style={{ marginRight: '6px' }}>
{ config['ui:title'] }
</span>
);
if (config['ui:description']) {
return (
<div style={config['ui:titleStyle']}>
{ titleFragment }
<Popover
content={<RichText html={config['ui:description'].title} />}
trigger={config['ui:description'].trigger}
>
<QuestionCircleOutlined />
</Popover>
</div>
);
}
return (
<div style={config['ui:titleStyle']}>{ titleFragment }</div>
);
}
public renderFormItem(config: DTGComponentPropertySchema) {
const { helpMsg } = this.state;
const labelCol = 6;
const wrapperCol = config['ui:type'] === 'code-editor' ? 24 : 16;
const formItemLayout = {
labelCol: { xs: { span: labelCol }, sm: { span: labelCol } },
wrapperCol: { xs: { span: wrapperCol }, sm: { span: wrapperCol } },
};
const key = config.name;
const visible = this.visible(config);
if (!visible) { return null; }
return (
<Form.Item
key={key}
label={this.renderTitleLabel(config)}
colon={false}
validateStatus={helpMsg[key] ? 'error' : 'success'}
help={helpMsg[key]}
required={config.required}
style={config['ui:wrapperStyle']}
{...formItemLayout}
>
{ this.renderFormComponent(config) }
</Form.Item>
);
}
public render() {
const { configs } = this.props;
return (
<div>
{ configs.map(item => this.renderFormItem(item)) }
</div>
);
}
}

View File

@ -0,0 +1,12 @@
.draggable {
&-droppedcontent {
border: 0px solid transparent;
}
&-droppingcontent {
border: 1px dashed rgb(10, 123, 222);
box-shadow: 0 0 8px rgb(10, 123, 222, 0.8);
padding: 6px 6px 6px 6px;
background-color: #fafafa;
border-radius: 5%;
}
}

View File

@ -0,0 +1,170 @@
import React, { Component } from 'react';
import styles from './index.module.less';
interface Data {
key: number;
sort: number;
width?: string | number;
}
interface Props<T extends Data> {
value: T[];
codeKey: string;
style: React.CSSProperties;
isAcceptAdd?: boolean;
render: (item: T) => JSX.Element;
onChange: (data: T[]) => void;
}
interface States {
uuid: string;
}
export default class Draggable<T extends Data> extends Component<Props<T>, States> {
public constructor(props: Props<T>) {
super(props);
this.state = {
uuid: this.guid(),
};
}
public S4() {
return Math.trunc((1 + Math.random()) * 0x10000).toString(16).slice(1);
}
public guid() {
return `${this.S4() + this.S4()}-${this.S4()}-${this.S4()}-${this.S4()}-${this.S4()}${this.S4()}${this.S4()}`;
}
public onDragStart(event: React.DragEvent<HTMLDivElement>, sort: number, code: string, uuid: string, item: T) {
event.dataTransfer.setData('code', code);
event.dataTransfer.setData('uuid', uuid);
event.dataTransfer.setData('item', JSON.stringify(item));
event.dataTransfer.setData('sort', String(sort));
}
public onDragEnter = (event: React.DragEvent<HTMLDivElement>) => {
const ele = event.target as Element;
if (ele.className?.includes && ele.className.includes(styles['draggable-droppedcontent'])) {
ele.className = styles['draggable-droppingcontent'];
}
}
public onDragLeave = (event: React.DragEvent<HTMLDivElement>) => {
const ele = event.target as Element;
if (ele.className?.includes && ele.className.includes(styles['draggable-droppingcontent'])) {
ele.className = styles['draggable-droppedcontent'];
}
}
public compare() {
return (a: T, b: T) => {
if (a.key < b.key) {
return -1;
} if (a.key > b.key) {
return 1;
}
return 0;
};
}
public onDrop(
event: React.DragEvent<HTMLDivElement>,
droppedSort: number,
data: T[],
droppedUuid: string,
codeKey: string,
) {
event.preventDefault();
const code = event.dataTransfer.getData('code');
const uuid = event.dataTransfer.getData('uuid');
const sort = Number(event.dataTransfer.getData('sort'));
if (uuid === droppedUuid) {
if (sort < droppedSort) {
data.map((item) => {
if (item[codeKey] === Number(code)) {
item.key = droppedSort;
} else if (item.key > sort && item.key < droppedSort + 1) {
item.key -= 1;
}
return item;
});
} else {
data = data.map((item) => {
if (item[codeKey] === Number(code)) {
item.key = droppedSort;
} else if (item.key > droppedSort - 1 && item.key < sort) {
item.key += 1;
}
return item;
});
}
} else if (this.props.isAcceptAdd) {
const draggedItem = JSON.parse(event.dataTransfer.getData('item'));
if (!data.some(item => item[codeKey] === draggedItem[codeKey])) {
const maxSort = Math.max(...data.map(item => item.key));
data.forEach((item) => {
if (droppedSort === maxSort) {
draggedItem.key = droppedSort + 1;
} else if (item.sort > droppedSort) {
draggedItem.key = droppedSort + 1;
item.key += 1;
}
});
data.push(draggedItem);
}
}
this.props.onChange(data);
const element = event.target as Element;
if (element.className.includes(styles['draggable-droppingcontent'])) {
element.className = styles['draggable-droppedcontent'];
}
}
// 生成拖拽组件
public createDraggleComponent(
data: T[],
style: React.CSSProperties,
uuid: string,
renderCell: (item: T) => JSX.Element,
codeKey: string,
) {
return data.sort(this.compare()).map(item => (
<div
key={item[codeKey]}
className={styles['draggable-droppedcontent']}
style={style}
draggable
onDragEnter={this.onDragEnter}
onDragLeave={this.onDragLeave}
onDragStart={e => this.onDragStart(e, item.key, String(item[codeKey]), uuid, item)}
onDrop={e => this.onDrop(e, item.key, data, uuid, codeKey)}
onDragOver={e => e.preventDefault()}
>
{ renderCell(item) }
</div>
));
}
public render() {
const { value, codeKey, style, render } = this.props;
const { uuid } = this.state;
let containerWidth = 4;
const minWidth = Number(style.width) || 0;
for (const item of value) {
const width = typeof item.width === 'number'
? item.width
: Number(String(item.width).replace(/(px|%|r?em|pt|vw|cm|in|pc)$/gui, '')) || 0;
containerWidth += Math.max(minWidth, width + 2, 120);
}
return (
<div style={{
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
width: `${containerWidth}px`,
}}
>
{ this.createDraggleComponent(value, style, uuid, render, codeKey) }
</div>
);
}
}

View File

@ -0,0 +1,97 @@
/*
* This file is part of the drip-table project.
* @link : https://drip-table.jd.com/
* @author : Emil Zhai (root@derzh.com)
* @modifier : Emil Zhai (root@derzh.com)
* @copyright: Copyright (c) 2020 JD Network Technology Co., Ltd.
*/
import React from 'react';
export interface HighlightProps {
content: string;
keywords: string[];
color?: string;
tagName?: keyof React.ReactHTML;
tagAttrs?: Record<string, unknown>;
}
interface ContentPart {
highlight: boolean;
text: string;
}
/**
*
*
* @export
* @class Highlight
* @extends {React.Component<HighlightProps, {}>}
*/
export default class Highlight extends React.PureComponent<HighlightProps> {
private get contents(): ContentPart[] {
const contents: ContentPart[] = [];
let { content, keywords } = this.props;
keywords = keywords
.filter(kw => kw)
.sort((s1, s2) => s2.length - s1.length);
if (keywords.length > 0) {
while (content) {
const [keyword, index] = this.searchString(content, keywords);
if (keyword) {
if (index > 0) {
contents.push({
highlight: false,
text: content.slice(0, Math.max(0, index)),
});
}
contents.push({
highlight: true,
text: keyword,
});
content = content.slice(index + keyword.length);
} else {
contents.push({
highlight: false,
text: content,
});
content = '';
}
}
}
return contents;
}
/**
*
*
* @private
* @param {string} content
* @param {string[]} keywords
* @returns {[string, number]} ,
*
* @memberOf Highlight
*/
private searchString(content: string, keywords: string[]): [string, number] {
let keyword = '';
let foundIndex = -1;
for (const kw of keywords) {
const index = content.indexOf(kw);
if (index >= 0 && (index < foundIndex || foundIndex === -1)) {
keyword = kw;
foundIndex = index;
}
}
return [keyword, foundIndex];
}
public render(): JSX.Element {
const { color = 'red', tagName = 'span', tagAttrs } = this.props;
const children = this.contents.map((content, i) => (
content.highlight
? <span key={i} style={{ color }}>{ content.text }</span>
: <span key={i}>{ content.text }</span>
));
return React.createElement(tagName, tagAttrs, children);
}
}

View File

@ -0,0 +1,369 @@
/*
* This file is part of the drip-table project.
* @link : https://drip-table.jd.com/
* @author : Emil Zhai (root@derzh.com)
* @modifier : Emil Zhai (root@derzh.com)
* @copyright: Copyright (c) 2020 JD Network Technology Co., Ltd.
*/
import React, { CSSProperties } from 'react';
import cheerio from 'cheerio';
import ViewerJS from 'viewerjs';
import Highlight, { HighlightProps } from '../Highlight';
import 'viewerjs/dist/viewer.css';
interface RichTextProps {
html: string;
tagNames?: (keyof React.ReactDOM)[];
singleLine?: boolean;
maxLength?: number;
highlight?: Omit<HighlightProps, 'content' | 'tagName'>;
style?: CSSProperties;
wrapperClass?: string;
}
const SAFE_TAG_NAME: NonNullable<RichTextProps['tagNames']> = [
'a',
'abbr',
'address',
'area',
'article',
'aside',
'audio',
'b',
'base',
'bdi',
'bdo',
'big',
'blockquote',
// 'body',
'br',
'button',
// 'canvas',
'caption',
'cite',
'code',
'col',
'colgroup',
'data',
'datalist',
'dd',
'del',
'details',
'dfn',
'dialog',
'div',
'dl',
'dt',
'em',
'embed',
'fieldset',
'figcaption',
'figure',
'footer',
// 'form',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'head',
'header',
'hgroup',
'hr',
'html',
'i',
// 'iframe',
'img',
// 'input',
'ins',
'kbd',
'keygen',
'label',
'legend',
'li',
// 'link',
'main',
'map',
'mark',
'menu',
'menuitem',
'meta',
'meter',
'nav',
// 'noscript',
'object',
'ol',
'optgroup',
'option',
'output',
'p',
'param',
'picture',
'pre',
'progress',
'q',
'rp',
'rt',
'ruby',
's',
'samp',
'slot',
// 'script',
'section',
'select',
'small',
'source',
'span',
'strong',
'style',
'sub',
'summary',
'sup',
'table',
'template',
'tbody',
'td',
'textarea',
'tfoot',
'th',
'thead',
'time',
'title',
'tr',
'track',
'u',
'ul',
'var',
'video',
'wbr',
'webview',
'animate',
'circle',
'clipPath',
'defs',
'desc',
'ellipse',
'feBlend',
'feColorMatrix',
'feComponentTransfer',
'feComposite',
'feConvolveMatrix',
'feDiffuseLighting',
'feDisplacementMap',
'feDistantLight',
'feDropShadow',
'feFlood',
'feFuncA',
'feFuncB',
'feFuncG',
'feFuncR',
'feGaussianBlur',
'feImage',
'feMerge',
'feMergeNode',
'feMorphology',
'feOffset',
'fePointLight',
'feSpecularLighting',
'feSpotLight',
'feTile',
'feTurbulence',
'filter',
'foreignObject',
'g',
'image',
'line',
'linearGradient',
'marker',
'mask',
'metadata',
'path',
'pattern',
'polygon',
'polyline',
'radialGradient',
'rect',
'stop',
'svg',
'switch',
'symbol',
'text',
'textPath',
'tspan',
'use',
'view',
];
interface ReducerRenderValue {
elements: (JSX.Element | string)[];
maxLength: number;
tagNames: NonNullable<RichTextProps['tagNames']>;
singleLine: NonNullable<RichTextProps['singleLine']>;
highlight: RichTextProps['highlight'];
}
/**
*
*
* @export
* @class RichText
* @extends {React.PureComponent<RichTextProps>}
*/
export default class RichText extends React.PureComponent<RichTextProps> {
private viewer!: ViewerJS;
/**
*
*
* @private
* @param {Record<string, unknown>} originAttr
* @param {string[]} excludeKeys
* @returns {Record<string, unknown>}
*
* @memberOf RichText
*/
private filterAttrs(originAttr: Record<string, unknown>, excludeKeys: string[]) {
const attr = {};
Object.keys(originAttr).filter(k => !excludeKeys.includes(k)).forEach((key) => {
attr[key] = originAttr[key];
});
return attr;
}
/**
*
*
* @private
* @param {ReducerRenderValue} prevVal
* @param {CheerioElement} el
* @param {number} key
* @param {number} maxLength
* @returns {ReducerRenderValue}
*
* @memberOf RichText
*/
private reducerRenderEl = (prevVal: ReducerRenderValue, el: cheerio.Element, key: number): ReducerRenderValue => {
const { tagNames, singleLine, maxLength, highlight } = prevVal;
if (el.type === 'text') {
let text = el.data || '';
if (singleLine) {
text = text.replace(/[\r\n]/ug, '$nbsp');
}
if (prevVal.maxLength >= 0) {
text = text.slice(0, Math.max(0, prevVal.maxLength));
if (text.length > 0 && text.length === prevVal.maxLength) {
text += '...';
}
prevVal.maxLength = Math.max(prevVal.maxLength - text.length, 0);
}
prevVal.elements.push(
highlight
? React.createElement(Highlight, { ...highlight, key, content: text })
: text,
);
return prevVal;
}
if (tagNames.includes(el.tagName as never)) {
const tagName = el.tagName as NonNullable<HighlightProps['tagName']>;
const { attribs = {}, children } = el;
const style: React.CSSProperties = {};
if (attribs.style) {
attribs.style.split(';').forEach((s: string) => {
const [k, v] = s.split(':');
if (v) {
style[k.trim().replace(/-([a-z])/ug, (_: string, c: string) => c.toUpperCase())] = v.trim();
}
});
}
if (tagName === 'img') {
style.maxWidth = '100%';
}
if (singleLine) {
if (tagName === 'br') {
prevVal.elements.push(<span key={key}>&nbsp;</span>);
return prevVal;
}
style.display = 'inline';
}
const props: Record<string, unknown> = {
...this.filterAttrs(attribs, ['key', 'class', 'onclick']),
key,
style,
className: attribs.class,
src: attribs.src,
width: attribs.width,
height: attribs.height,
onClick: new Function(attribs.onclick),
};
if (tagName === 'a') {
props.href = attribs.href;
props.target = attribs.target;
}
let content: (JSX.Element | string | null)[] | undefined;
if (children) {
const res = children.reduce<ReducerRenderValue>(this.reducerRenderEl, {
elements: [],
maxLength,
tagNames,
singleLine,
highlight,
});
if (res.elements.length > 0) {
content = res.elements;
}
prevVal.maxLength = res.maxLength;
}
prevVal.elements.push(React.createElement(tagName, props, content));
return prevVal;
}
return prevVal;
}
/**
*
*
* @private
* @param {HTMLDivElement} el
* @returns {void}
*
* @memberOf RichText
*/
private onRef = (el: HTMLDivElement | null) => {
if (!el) {
return;
}
if (this.viewer) {
this.viewer.destroy();
}
this.viewer = new ViewerJS(el);
}
public componentDidUpdate() {
if (this.viewer) {
this.viewer.update();
}
}
public render(): JSX.Element | null {
const { html, maxLength = -1, tagNames = SAFE_TAG_NAME, singleLine = false, highlight, style, wrapperClass } = this.props;
const $ = cheerio.load(html);
const body = $('body')[0];
return (
<div ref={this.onRef} style={style} className={wrapperClass}>
{
body && body.type === 'tag'
? body.children.reduce<ReducerRenderValue>(this.reducerRenderEl, {
elements: [],
maxLength,
tagNames,
singleLine,
highlight,
}).elements
: void 0
}
</div>
);
}
}

View File

@ -0,0 +1,7 @@
import { createContext } from 'react';
import { DripTableRecordTypeBase } from 'drip-table';
import { DripTableGeneratorProps } from '@/typing';
export type IDripTableGeneratorContext<RecordType extends DripTableRecordTypeBase> = DripTableGeneratorProps<RecordType>;
export const Ctx = createContext<IDripTableGeneratorContext<Record<string, unknown>>>({});

View File

@ -0,0 +1,73 @@
import React, { useState, forwardRef, useImperativeHandle, useRef } from 'react';
import { ConfigProvider } from 'antd';
import { ColumnConfig, DripTableRecordTypeBase, DripTableSchema } from 'drip-table';
import zhCN from 'antd/lib/locale/zh_CN';
import { Ctx } from '@/context';
import { DripTableGeneratorProps } from '@/typing';
import { defaultState, GlobalStore } from '@/store';
import Wrapper from './wrapper';
import 'antd/dist/antd.less';
export interface RootState {
getSchemaValue: () => null;
}
const useTableRoot = (props, store, wrapper) => {
const [state, setState] = store;
const getSchemaValue = (): DripTableSchema => {
if (wrapper.current) {
const currentState = wrapper.current.getState();
return {
$schema: 'http://json-schema.org/draft/2019-09/schema#',
configs: {
...currentState.globalConfigs,
},
columns: currentState.columns.map(item => ({ ...item, key: void 0, sort: void 0 })) as ColumnConfig[],
};
}
return {
$schema: 'http://json-schema.org/draft/2019-09/schema#',
configs: {
...state.globalConfigs,
},
columns: state.columns.map(item => ({ ...item, key: void 0, sort: void 0 })) as ColumnConfig[],
};
};
const context = {
...props,
getSchemaValue,
setGlobalData: setState,
};
return context;
};
const Container = <RecordType extends DripTableRecordTypeBase>(props: DripTableGeneratorProps<RecordType>, ref) => {
const wrapper = useRef({});
const initialState = defaultState();
const store = useState(initialState);
const context = useTableRoot(props, store, wrapper);
useImperativeHandle(ref, () => context);
const WrapperRef = forwardRef<unknown, DripTableGeneratorProps<RecordType> & { store: GlobalStore }>(Wrapper);
return (
<ConfigProvider locale={zhCN}>
<Ctx.Provider {...props} value={context}>
<WrapperRef ref={wrapper} {...props} store={store} />
</Ctx.Provider>
</ConfigProvider>
);
};
const DripTableGeneratorProvider = forwardRef(Container);
export default DripTableGeneratorProvider;

View File

@ -0,0 +1,20 @@
import { useReducer, useContext, SetStateAction } from 'react';
import { Ctx } from './context';
// 使用最顶层组件的 props
export const useGlobalData = () => useContext(Ctx);
/**
* 使
* @param initState
* @returns [, ]
*/
export const useState = <T>(initState: T): [T, (action: SetStateAction<Partial<T>>) => void] => useReducer(
(state: T, action: SetStateAction<Partial<T>>): T => {
const data = typeof action === 'function'
? action(state)
: action;
return { ...state, ...data };
},
initState,
);

View File

@ -0,0 +1,45 @@
* {
font-size: 14px;
--drip-table-primary-color: rgb(59, 130, 246);
--drip-table-background-color: rgb(239, 246, 255);
--drip-table-border-color: rgba(59, 130, 246, .5);
}
.wrapper {
display: flex;
overflow-x: auto;
}
.layout {
&-right {
&-wrapper {
width: calc(100% - 280px);
height: 100%;
border-left: 1px solid rgb(243, 244, 246);
margin-left: -1px;
min-width: 442px;
overflow-x: auto;
&.preview {
height: auto;
overflow-y: auto;
}
}
&-title {
display: flex;
align-items: center;
justify-content: flex-start;
border-bottom: 1px solid #eeeeee;
padding: .5rem;
font-size: 1.25rem;
line-height: 1.75rem;
font-weight: 500;
& > span {
font-size: 1.25rem;
}
}
}
}

View File

@ -0,0 +1,3 @@
export * from './typing';
export { default } from './drip-table-generator';

View File

@ -0,0 +1,32 @@
.attributes {
&-wrapper {
padding: 12px 0;
}
&-container {
padding: 16px;
border-top: 1px solid #eeeeee;
}
&-form-panel {
margin-left: 16px;
font-size: 1.25rem;
color: #999999;
min-width: 560px;
:global(.ant-result-icon > span > svg) {
width: 4.25rem;
height: 4.25rem;
}
:global(.ant-result-title > div) {
font-size: 1.72rem;
}
}
&-code-panel {
font-size: 1.25rem;
color: #999999;
width: 100%;
}
}

View File

@ -0,0 +1,245 @@
/**
* This file is part of the drip-table project.
* @link : https://drip-table.jd.com/
* @author : helloqian12138 (johnhello12138@163.com)
* @modifier : helloqian12138 (johnhello12138@163.com)
* @copyright: Copyright (c) 2020 JD Network Technology Co., Ltd.
*/
import React from 'react';
import { Result, Tabs } from 'antd';
import { ExclamationCircleTwoTone } from '@ant-design/icons';
import { DripTableSchema } from 'drip-table';
import MonacoEditor from '@monaco-editor/react';
import debounce from 'lodash/debounce';
import { useGlobalData } from '@/hooks';
import { DripTableComponentConfig, DTGComponentPropertySchema } from '@/typing';
import { DripTableColumn, globalActions, GlobalStore } from '@/store';
import CustomForm from '@/components/CustomForm';
import components from '@/table-components';
import { GlobalAttrFormConfigs } from '../configs';
import styles from './index.module.less';
type GlobalSchema = DripTableSchema['configs'];
interface Props {
parentHeight: React.CSSProperties['width'];
customComponentPanel: {
mode: 'add' | 'replace';
components: DripTableComponentConfig[];
} | undefined;
}
const { TabPane } = Tabs;
const AttributeLayout = (props: Props & { store: GlobalStore }) => {
const { dataFields } = useGlobalData();
const [activeKey, setActiveKey] = React.useState('0');
const [state, actions] = props.store;
const store = { state, setState: actions };
const [code, setCode] = React.useState(JSON.stringify(state.dataSource, null, 4));
const getActiveKey = () => {
if (activeKey === '0') {
return state.currentColumn ? '1' : '2';
}
return activeKey;
};
const getComponents = () => {
let componentsToUse = components;
if (props.customComponentPanel) {
const customComponents = props.customComponentPanel.components;
componentsToUse = props.customComponentPanel.mode === 'add' ? [...components, ...customComponents] : [...customComponents];
}
return [...componentsToUse];
};
const encodeGlobalConfigs = (formData: { [key: string]: unknown }): GlobalSchema => ({
bordered: formData.bordered as boolean,
size: formData.size as 'small' | 'middle' | 'large' | undefined,
ellipsis: formData.ellipsis as boolean,
header: formData.header
? {
title: formData['header.title.type'] === 'title'
? {
type: formData['header.title.type'] as 'title',
title: formData['header.title.title'] as string,
span: formData['header.title.span'] as number,
position: 'topLeft',
}
: void 0,
search: formData['header.search.type'] === 'search'
? {
type: formData['header.search.type'] as 'search',
span: formData['header.search.wrapperWidth'] as number,
position: 'topCenter',
placeholder: formData['header.search.placeholder'],
allowClear: formData['header.search.allowClear'],
searchBtnText: formData['header.search.searchBtnText'],
searchStyle: { float: 'right', width: formData['header.search.width'] as string },
defaultSelectedKey: formData['header.search.defaultSelectedKey'],
typeOptions: formData['header.search.typeVisible'] ? formData['header.search.typeOptions'] : void 0,
}
: void 0,
}
: false,
pagination: formData.pagination
? {
pageSize: formData['pagination.pageSize'] as number,
position: formData['pagination.position'] as 'bottomLeft' | 'bottomCenter' | 'bottomRight',
}
: false,
} as GlobalSchema);
const filterAttrs = (originObj: Record<string, unknown>, excludeKeys: string[]) => {
const obj = {};
Object.keys(originObj).filter(k => !excludeKeys.includes(k)).forEach((key) => {
obj[key] = originObj[key];
});
return obj;
};
const encodeColumnConfigs = (formData: { [key: string]: unknown }) => {
const uiProps = {};
const dataProps = {};
Object.keys(formData).forEach((key) => {
if (key.startsWith('ui:props.')) {
uiProps[key.replace('ui:props.', '')] = formData[key];
} else {
dataProps[key] = formData[key];
}
});
const columnConfig = getComponents().find(item => item['ui:type'] === state.currentColumn?.['ui:type']);
return {
...filterAttrs(dataProps, ['ui:props', 'ui:type', 'type', 'name', 'dataIndex', 'title', 'width', 'group']),
$id: state.currentColumn?.$id || '',
dataIndex: formData.dataIndex as string | string[],
title: formData.title as string,
width: formData.width as string,
'ui:type': state.currentColumn?.['ui:type'],
'ui:props': { ...uiProps },
type: columnConfig ? columnConfig.type : state.currentColumn?.type,
} as DripTableColumn;
};
/** TODO: 报错逻辑后续优化 */
function submitDataWithoutThrottle() {
try {
state.dataSource = JSON.parse(code);
globalActions.updateDataSource(store);
} catch (error) {
console.error(error);
}
}
const submitTableData = debounce(submitDataWithoutThrottle, 1000);
const renderGlobalForm = () => (
<CustomForm<GlobalSchema>
configs={GlobalAttrFormConfigs}
data={state.globalConfigs}
encodeData={encodeGlobalConfigs}
onChange={(data) => {
state.globalConfigs = { ...data };
globalActions.updateGlobalConfig(store);
}}
/>
);
const renderColumnForm = () => {
if (!state.currentColumn) {
return (
<Result
icon={<ExclamationCircleTwoTone />}
title={<div style={{ color: '#999' }}></div>}
/>
);
}
const columnConfig = getComponents().find(schema => schema['ui:type'] === state.currentColumn?.['ui:type']);
columnConfig?.attrSchema.forEach((schema) => {
const uiProps = schema['ui:props'];
if (!uiProps) {
return;
}
if (uiProps.from === 'dataSource') {
uiProps.options = Object.keys(state.dataSource[0] || {})
.map(key => ({ label: key, value: key }));
} else if (uiProps.from === 'dataFields') {
uiProps.options = dataFields?.map(key => ({ label: key, value: key })) || [];
}
if (uiProps.items) {
(uiProps.items as DTGComponentPropertySchema[])?.forEach((item, index) => {
const itemUiProps = item['ui:props'];
if (!itemUiProps) {
return;
}
if (itemUiProps.from === 'dataSource') {
itemUiProps.options = Object.keys(state.dataSource[0] || {})
.map(key => ({ label: key, value: key }));
} else if (itemUiProps.from === 'dataFields') {
itemUiProps.options = dataFields?.map(key => ({ label: key, value: key })) || [];
}
});
}
});
return (
<CustomForm<DripTableColumn>
configs={columnConfig ? columnConfig.attrSchema || [] : []}
data={state.currentColumn}
encodeData={encodeColumnConfigs}
extendKeys={['ui:props']}
onChange={(data) => {
state.currentColumn = Object.assign(state.currentColumn, data);
const idx = state.columns.findIndex(item => item.$id === state.currentColumn?.$id);
if (idx > -1 && state.currentColumn) {
state.columns[idx] = state.currentColumn;
}
globalActions.editColumns(store);
globalActions.checkColumn(store);
}}
/>
);
};
const tabHeight = typeof props.parentHeight === 'number' ? `${props.parentHeight - 320}px` : `calc(${props.parentHeight} - 320px)`;
return (
<div className={styles['attributes-wrapper']}>
<div className={styles['attributes-container']}>
<Tabs activeKey={getActiveKey()} type="card" onChange={(key) => { setActiveKey(key); }}>
<TabPane tab="属性配置" key="1" style={{ height: tabHeight, overflow: 'auto' }}>
<div className={styles['attributes-form-panel']}>
{ renderColumnForm() }
</div>
</TabPane>
<TabPane tab="全局设置" key="2" style={{ height: tabHeight, overflow: 'auto' }}>
<div className={styles['attributes-form-panel']}>
{ renderGlobalForm() }
</div>
</TabPane>
<TabPane tab="表格数据" key="3" style={{ height: tabHeight, overflow: 'auto' }}>
<div className={styles['attributes-code-panel']}>
<MonacoEditor
width="100%"
height={400}
language="json"
theme="vs-dark"
value={code || ''}
onChange={(value) => { setCode(value || ''); submitTableData(); }}
/>
</div>
</TabPane>
</Tabs>
</div>
</div>
);
};
export default AttributeLayout;

View File

@ -0,0 +1,113 @@
.layout {
&-left {
&-wrapper {
min-width: 280px;
width: 280px;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
border-right: 1px solid rgb(243, 244, 246);
.group-title {
border-bottom: 1px solid #eeeeee;
padding-top: .5rem;
padding-bottom: .25rem;
padding-left: .5rem;
font-size: 1.125rem;
line-height: 1.75rem;
}
}
&-title {
border-bottom: 1px solid #eeeeee;
display: flex;
align-items: center;
justify-content: space-between;
padding: .5rem;
font-size: 1.25rem;
line-height: 1.75rem;
font-weight: 500;
}
&-components-container {
padding: .75rem;
table {
width: 100%;
.components-line {
&__gutter {
height: 8px;
}
}
}
}
}
}
.component {
&-container {
width: 100%;
padding: 8px;
position: relative;
display: inline-block;
cursor: pointer;
& > div {
height: 28px;
border-radius: 4px;
vertical-align: top;
padding: 4px;
font-weight: bold;
align-items: center;
color: var(--drip-table-primary-color);
display: flex;
justify-content: space-between;
border-width: 1px;
background-color: var(--drip-table-background-color);
border: 4px solid transparent;
}
& > div:hover {
color: rgb(255, 255, 255);
background-color: var(--drip-table-primary-color);
box-shadow:
rgb(255, 255, 255) 0 0 0 0,
var(--drip-table-border-color) 0 0 0 3px,
rgba(0, 0, 0, 0%) 0 0 0 0;
cursor: pointer;
border: 4px solid var(--drip-table-border-color);
.component-icon {
fill: rgb(255, 255, 255);
}
}
}
&-icon {
fill: var(--drip-table-primary-color);
height: 20px;
width: 24px;
display: inline-block;
vertical-align: top;
svg {
width: 18px;
height: 18px;
margin: 1px;
}
}
img {
width: 18px;
height: 18px;
}
span {
line-height: 20px;
display: inline-block;
font-size: 14px;
text-align: center;
vertical-align: top;
}
}

View File

@ -0,0 +1,150 @@
/**
* This file is part of the drip-table project.
* @link : https://drip-table.jd.com/
* @author : helloqian12138 (johnhello12138@163.com)
* @modifier : helloqian12138 (johnhello12138@163.com)
* @copyright: Copyright (c) 2020 JD Network Technology Co., Ltd.
*/
import React from 'react';
import chunk from 'lodash/chunk';
import { DripTableProps, DripTableRecordTypeBase } from 'drip-table';
import { globalActions, GlobalStore } from '@/store';
import { DripTableComponentConfig } from '@/typing';
import { mockId } from '@/utils';
import RichText from '@/components/RichText';
import components from '@/table-components';
import { defaultComponentIcon } from '../configs';
import styles from './index.module.less';
interface Props {
width: React.CSSProperties['width'];
customComponentPanel: {
mode: 'add' | 'replace';
components: DripTableComponentConfig[];
} | undefined;
}
const ComponentLayout = (props: Props & { store: GlobalStore }) => {
const getGroups = () => {
let groups = [
'基础组件',
'自定义组件',
];
if (props.customComponentPanel) {
const customGroups = props.customComponentPanel.components.map(item => item.group);
if (props.customComponentPanel.mode === 'add') {
const theSet = new Set([...groups, ...customGroups]);
groups = [...theSet];
} else {
groups = [...customGroups];
}
}
return groups;
};
const getComponents = (groupName) => {
let componentsToUse = components;
if (props.customComponentPanel) {
const customComponents = props.customComponentPanel.components;
componentsToUse = props.customComponentPanel.mode === 'add' ? [...components, ...customComponents] : [...customComponents];
}
return [...componentsToUse].filter(item => item.group === groupName);
};
const [state, actions] = props.store;
const store = { state, setState: actions };
const componentCell = (item: DripTableComponentConfig) => (
<div
className={styles['component-container']}
onClick={() => {
const columnSchema: DripTableProps<DripTableRecordTypeBase>['schema']['columns'][number] & { $id: string; group: string } = {
$id: `${item.$id}_${mockId()}`,
dataIndex: '',
title: item.title,
group: '',
width: void 0,
description: '',
'ui:type': item['ui:type'],
'ui:props': {},
type: item.type || 'null',
};
item.attrSchema.forEach((schema) => {
if (typeof schema.default !== 'undefined') {
if (schema.name.startsWith('ui:props')) {
const key = schema.name.replace('ui:props.', '');
columnSchema['ui:props'][key] = schema.default;
} else {
columnSchema[schema.name] = schema.default;
}
}
});
state.columns.push({
...columnSchema,
key: state.columns.length + 1, // +1 是因为从1排序的
sort: state.columns.length + 1,
});
globalActions.editColumns(store);
}}
>
<div>
{ item.icon && item.icon.startsWith('base64')
? <div className={styles['component-icon']} style={{ backgroundImage: `url(${item.icon})` }} />
: <RichText html={item.icon || defaultComponentIcon} wrapperClass={styles['component-icon']} /> }
<span>{ item.title }</span>
</div>
</div>
);
return (
<div className={styles['layout-left-wrapper']} style={{ width: props.width }}>
<div className={styles['layout-left-title']}></div>
<table style={{ width: '100%' }}>
{ getGroups().map((name, index) => (
<React.Fragment key={index}>
<thead key={`thead.${index}`}>
<tr><td colSpan={2}><div className={styles['group-title']}>{ name }</div></td></tr>
</thead>
<tbody key={`tbody.${index}`}>
{ chunk(getComponents(name), 2).map((items, i, componentsInLayout) => (
<React.Fragment key={i}>
{
i === 0
? (
<tr key={`groupItem.group.${index}.${i}.gutter.before`}>
<td colSpan={2}>
<div className={styles['components-line__gutter']} />
</td>
</tr>
)
: null
}
<tr key={`groupItem.group.${index}.${i}`} className={styles['components-line']}>
{ items.map(item => <td key={item.$id}>{ componentCell(item) }</td>) }
</tr>
{
i === componentsInLayout.length - 1
? (
<tr key={`groupItem.group.${index}.${i}.gutter.after`}>
<td colSpan={2}>
<div className={styles['components-line__gutter']} />
</td>
</tr>
)
: null
}
</React.Fragment>
)) }
</tbody>
</React.Fragment>
)) }
</table>
</div>
);
};
export default ComponentLayout;

View File

@ -0,0 +1,219 @@
/**
* This file is part of the drip-table project.
* @link : https://drip-table.jd.com/
* @author : helloqian12138 (johnhello12138@163.com)
* @modifier : helloqian12138 (johnhello12138@163.com)
* @copyright: Copyright (c) 2020 JD Network Technology Co., Ltd.
*/
import { DTGComponentPropertySchema } from '../typing';
export const GlobalAttrFormConfigs: DTGComponentPropertySchema[] = [
{
name: 'size',
'ui:title': '表格尺寸',
'ui:type': 'radio',
'ui:props': {
options: [
{ label: '默认', value: 'default' },
{ label: '大号', value: 'large' },
{ label: '中等', value: 'middle' },
{ label: '小号', value: 'small' },
],
},
type: 'string',
default: 'default',
},
{
name: 'innerBordered',
'ui:title': '是否展示内部边框',
'ui:type': 'switch',
'ui:props': {},
type: 'boolean',
default: false,
},
{
name: 'ellipsis',
'ui:title': '是否平均列宽',
'ui:type': 'switch',
'ui:props': {},
type: 'boolean',
default: false,
},
{
name: 'bordered',
'ui:title': '是否展示边框',
'ui:type': 'switch',
'ui:props': {},
type: 'boolean',
default: false,
},
{
name: 'header',
'ui:title': '是否展示标题栏',
'ui:type': 'switch',
'ui:props': {},
type: 'boolean',
default: false,
},
{
name: 'header.title.type',
'ui:title': '标题栏是否展示标题',
'ui:type': 'switch',
'ui:props': {
checkedValue: 'title',
unCheckedValue: 'null',
},
type: 'string',
visible: (value: unknown, formData?: Record<string, unknown>) => formData?.header === true,
},
{
name: 'header.title.title',
'ui:title': '标题名称',
'ui:type': 'input',
'ui:props': {
placeholder: '请输入标题',
},
type: 'string',
visible: (value: unknown, formData?: Record<string, unknown>) => formData?.header === true && formData['header.title.type'] === 'title',
},
{
name: 'header.title.width',
'ui:title': '标题宽度',
'ui:type': 'number',
'ui:props': {
minimum: 1,
maximum: 24,
},
type: 'number',
visible: (value: unknown, formData?: Record<string, unknown>) => formData?.header === true && formData['header.title.type'] === 'title',
},
{
name: 'header.search.type',
'ui:title': '标题栏是否展示搜索',
'ui:type': 'switch',
'ui:props': {
checkedValue: 'search',
unCheckedValue: 'null',
},
type: 'string',
visible: (value: unknown, formData?: Record<string, unknown>) => formData?.header === true,
},
{
name: 'header.search.wrapperWidth',
'ui:title': '搜索栏宽度',
'ui:type': 'number',
'ui:props': {
minimum: 1,
maximum: 24,
},
type: 'number',
visible: (value: unknown, formData?: Record<string, unknown>) => formData?.header === true && formData['header.search.type'] === 'search',
},
{
name: 'header.search.placeholder',
'ui:title': '搜索框提示文案',
'ui:type': 'input',
'ui:props': {
placeholder: '请输入',
},
type: 'string',
visible: (value: unknown, formData?: Record<string, unknown>) => formData?.header === true && formData['header.search.type'] === 'search',
},
{
name: 'header.search.searchBtnText',
'ui:title': '搜索按钮文案',
'ui:type': 'input',
'ui:props': {
placeholder: '请输入',
},
type: 'string',
visible: (value: unknown, formData?: Record<string, unknown>) => formData?.header === true && formData['header.search.type'] === 'search',
},
{
name: 'header.search.width',
'ui:title': '搜索框宽度',
'ui:type': 'input',
'ui:props': {
placeholder: '请输入',
},
type: 'string',
visible: (value: unknown, formData?: Record<string, unknown>) => formData?.header === true && formData['header.search.type'] === 'search',
},
{
name: 'header.search.allowClear',
'ui:title': '搜索框清除按钮',
'ui:type': 'switch',
'ui:props': {},
type: 'boolean',
visible: (value: unknown, formData?: Record<string, unknown>) => formData?.header === true && formData['header.search.type'] === 'search',
},
{
name: 'header.search.typeVisible',
'ui:title': '是否展示选择框',
'ui:type': 'switch',
'ui:props': {},
type: 'boolean',
visible: (value: unknown, formData?: Record<string, unknown>) => formData?.header === true && formData['header.search.type'] === 'search',
},
{
name: 'header.search.typeOptions',
'ui:title': '选择框配置',
'ui:type': 'custom::ArrayComponent',
'ui:props': {
items: [
{
name: 'label',
'ui:title': '文案',
'ui:type': 'input',
default: '',
},
{
name: 'value',
'ui:title': '值',
'ui:type': 'input',
default: '',
},
],
},
default: [],
type: 'array',
visible: (value: unknown, formData?: Record<string, unknown>) => formData?.header === true && formData['header.search.type'] === 'search' && formData['header.search.typeVisible'] === true,
},
{
name: 'pagination',
'ui:title': '是否展示分页',
'ui:type': 'switch',
'ui:props': {},
type: 'boolean',
default: false,
},
{
name: 'pagination.pageSize',
'ui:title': '分页大小',
'ui:type': 'number',
'ui:props': {
placeholder: '请输入',
},
type: 'number',
minimum: 1,
visible: (value: unknown, formData?: Record<string, unknown>) => formData?.pagination === true,
},
{
name: 'pagination.position',
'ui:title': '分页位置',
'ui:type': 'radio',
'ui:props': {
options: [
{ label: '左下角', value: 'bottomLeft' },
{ label: '正下方', value: 'bottomCenter' },
{ label: '右下角', value: 'bottomRight' },
],
},
type: 'string',
visible: (value: unknown, formData?: Record<string, unknown>) => formData?.pagination === true,
},
];
export const defaultComponentIcon = `
<svg t="1627123986728" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3506" width="48" height="48"><path d="M917.376 243.456L500.8 4.8a34.144 34.144 0 0 0-34.336 0.096l0.16-0.096L50.112 243.52a33.952 33.952 0 0 0-17.088 29.376v477.44c0 12.16 6.528 23.296 17.088 29.44l416.512 238.72a34.88 34.88 0 0 0 34.336-0.064l-0.16 0.096 416.576-238.72c10.272-5.952 17.088-16.896 17.088-29.44V272.928c0-12.544-6.816-23.488-16.928-29.344l-0.16-0.096z m-51.264 487.36l-382.4 219.136-382.336-219.136V292.48l382.336-219.136 382.4 219.136v438.272zM198.784 360.512a33.76 33.76 0 0 0 12.384 46.304l0.16 0.096 237.824 136.32V812.8c0 18.816 15.232 33.92 34.176 33.92h0.256a33.92 33.92 0 0 0 33.92-33.92v-269.184l238.656-136.832a33.92 33.92 0 0 0 12.48-46.528l0.096 0.16a34.4 34.4 0 0 0-46.88-12.32l0.16-0.096-238.272 136.512-238.208-136.512a34.464 34.464 0 0 0-46.624 12.384l-0.096 0.16z" p-id="3507"></path></svg>
`;

View File

@ -0,0 +1,48 @@
.column {
width: 120px;
border-radius: 2px;
border: 2px dashed #f3f3f3;
&:hover,
&:global(.checked) {
border-radius: 2px;
border: 2px solid #f3f3f3;
cursor: pointer;
}
&:global(.checked) {
border-color: var(--drip-table-border-color);
}
.column-title {
padding: 8px;
word-break: break-all;
color: #343036;
font-weight: bold;
text-align: center;
background: #fafafa;
border-bottom: 1px solid #e9e9e9;
transition: background .3s ease;
font-size: 12px;
height: 35px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.close-icon {
position: absolute;
top: -6px;
right: -6px;
cursor: pointer;
z-index: 1;
}
}
.table-cell {
height: 120px;
text-align: center;
vertical-align: middle;
display: table-cell;
position: relative;
}

View File

@ -0,0 +1,139 @@
/**
* This file is part of the drip-table project.
* @link : https://drip-table.jd.com/
* @author : helloqian12138 (johnhello12138@163.com)
* @modifier : helloqian12138 (johnhello12138@163.com)
* @copyright: Copyright (c) 2020 JD Network Technology Co., Ltd.
*/
import React from 'react';
import { Empty } from 'antd';
import { CloseCircleTwoTone } from '@ant-design/icons';
import { builtInComponents, ColumnConfig, DripTableDriver, DripTableRecordTypeBase, DripTableProps } from 'drip-table';
import DripTableDriverAntDesign from 'drip-table-driver-antd';
import { globalActions, GlobalStore, DripTableColumn } from '@/store';
import Draggable from '@/components/Draggable';
import styles from './index.module.less';
import { get } from '@/utils';
interface Props<RecordType extends DripTableRecordTypeBase> {
driver: DripTableDriver<RecordType>;
customComponents: DripTableProps<RecordType>['components'] | undefined;
}
const EditableTable = <RecordType extends DripTableRecordTypeBase>(props: Props<RecordType> & { store: GlobalStore }) => {
const [state, actions] = props.store;
const store = { state, setState: actions };
const previewComponentRender = (column: DripTableColumn) => {
const customComponents = {};
const componentsList = Object.values(props.customComponents || {});
componentsList.forEach((item) => {
Object.keys(item).forEach((key) => { customComponents[key] = item[key]; });
});
const DripTableComponent = column['ui:type'].startsWith('custom::')
? customComponents[column['ui:type'].replace('custom::', '')]
: builtInComponents[column['ui:type']];
const hasRecord = !(!state.dataSource || state.dataSource.length <= 0);
const record = state.dataSource[0] || {} as Record<string, unknown>;
const value = column.dataIndex ? get(record, column.dataIndex) : record;
const errorBoundary = () => {
let color = '#F00';
let message = '未知错误';
if (!DripTableComponent) {
color = '#F00';
message = '未知组件';
} else if (!hasRecord) {
color = '#c9c9c9';
message = '暂无数据';
}
return (
<div style={{ color }}>{ message }</div>
);
};
return (
<div style={{ height: '120px', overflow: 'auto' }}>
<div className={styles['table-cell']} style={{ width: column.width || 120 }}>
{ DripTableComponent && hasRecord
? (
<DripTableComponent
driver={props.driver || DripTableDriverAntDesign}
value={value as unknown}
data={record as Record<string, unknown>}
schema={{ ...column, ...(column['ui:props'] || {}) as Record<string, unknown> } as unknown as ColumnConfig}
preview={{}}
/>
)
: errorBoundary() }
</div>
</div>
);
};
const renderTableCell = (col: DripTableColumn) => {
const isCurrent = state.currentColumn && state.currentColumn.$id === col.$id;
let width = String(col.width).trim() || '120';
if ((/^[0-9]+$/gui).test(width)) {
width += 'px';
}
return (
<div
style={{ width }}
className={`${styles.column} ${isCurrent ? 'checked' : ''}`}
onClick={() => {
state.currentColumn = isCurrent ? void 0 : col;
globalActions.checkColumn(store);
}}
>
<div className={styles['column-title']}>{ col.title }</div>
{ previewComponentRender(col) }
{ isCurrent && (
<CloseCircleTwoTone
className={styles['close-icon']}
twoToneColor="#ff4d4f"
onClick={() => {
const index = state.columns.findIndex(item => item.$id === state.currentColumn?.$id);
if (index > -1) {
state.columns.splice(index, 1);
for (let i = index; i < state.columns.length; i++) {
state.columns[i].key = i + 1;
state.columns[i].sort = i + 1;
}
state.currentColumn = void 0;
globalActions.editColumns(store);
globalActions.checkColumn(store);
}
}}
/>
) }
</div>
);
};
return (
<div style={{ padding: '12px 0 12px 12px', overflowX: 'auto' }}>
{
state.columns && state.columns.length > 0
? (
<Draggable<DripTableColumn>
value={(state.columns || []) as DripTableColumn[]}
codeKey="sort"
style={{ position: 'relative' }}
onChange={(data) => {
state.columns = [...data];
globalActions.editColumns(store);
}}
render={renderTableCell}
/>
)
: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无表格配置" />
}
</div>
);
};
export default EditableTable;

View File

@ -0,0 +1,3 @@
.table-preview-wrapper {
padding: 16px;
}

View File

@ -0,0 +1,45 @@
/**
* This file is part of the drip-table project.
* @link : https://drip-table.jd.com/
* @author : helloqian12138 (johnhello12138@163.com)
* @modifier : helloqian12138 (johnhello12138@163.com)
* @copyright: Copyright (c) 2020 JD Network Technology Co., Ltd.
*/
import React from 'react';
import DripTable, { ColumnConfig, DripTableDriver, DripTableProps, DripTableRecordTypeBase, DripTableSchema } from 'drip-table';
import DripTableDriverAntDesign from 'drip-table-driver-antd';
import { GlobalStore } from '@/store';
import styles from './index.module.less';
interface Props<RecordType extends DripTableRecordTypeBase> {
driver: DripTableDriver<RecordType>;
customComponents: DripTableProps<RecordType>['components'];
}
const PreviewTable = <RecordType extends DripTableRecordTypeBase>(props: Props<RecordType> & { store: GlobalStore }) => {
const [state] = props.store;
const schema: DripTableSchema = {
$schema: 'http://json-schema.org/draft/2019-09/schema#',
configs: {
...state.globalConfigs,
},
columns: state.columns as ColumnConfig[],
};
const totalPage = state.globalConfigs?.pagination && state.globalConfigs?.pagination.pageSize ? state.dataSource.length : 1;
return (
<div className={styles['table-preview-wrapper']}>
<DripTable<RecordType>
driver={(props.driver || DripTableDriverAntDesign)}
schema={schema}
total={totalPage}
dataSource={state.dataSource as RecordType[]}
components={props.customComponents}
/>
</div>
);
};
export default PreviewTable;

View File

@ -0,0 +1,122 @@
/**
* This file is part of the drip-table project.
* @link : https://drip-table.jd.com/
* @author : helloqian12138 (johnhello12138@163.com)
* @modifier : helloqian12138 (johnhello12138@163.com)
* @copyright: Copyright (c) 2020 JD Network Technology Co., Ltd.
*/
import React, { useState } from 'react';
import { message, Button, Input, Modal } from 'antd';
import { ColumnConfig, DripTableSchema } from 'drip-table';
import { DripTableColumn, globalActions, GlobalStore } from '@/store';
import { useGlobalData } from '@/hooks';
const ToolLayout = (props: { store: GlobalStore }) => {
const globalData = useGlobalData();
const [state, actions] = props.store;
const store = { state, setState: actions };
const [modalStatus, setModalStatus] = useState('');
const [code, setCode] = useState('');
const getSchemaValue = (): DripTableSchema => ({
$schema: 'http://json-schema.org/draft/2019-09/schema#',
configs: {
...state.globalConfigs,
},
columns: state.columns.map(item => ({ ...item, key: void 0, sort: void 0 })) as ColumnConfig[],
});
/**
* Modal用来展示JSON Schema配置
* @returns {JSX.Element} React组件
*/
const renderSchemaModal = () => {
if (modalStatus !== 'export' && modalStatus !== 'import') {
return null;
}
const defaultValue = modalStatus === 'export'
? JSON.stringify(getSchemaValue(), null, 4)
: code || '';
return (
<Input.TextArea
style={{ minHeight: '560px' }}
value={defaultValue}
onChange={(e) => {
if (modalStatus === 'import') { setCode(e.target.value); }
}}
/>
);
};
return (
<React.Fragment>
<Button
style={{ margin: '0 12px' }}
size="small"
onClick={() => { globalActions.toogleEditMode(store); }}
>
{ state.isEdit ? '预览模式' : '编辑模式' }
</Button>
<Button
style={{ margin: '0 12px' }}
size="small"
onClick={() => setModalStatus('import')}
>
</Button>
<Button
style={{ margin: '0 12px' }}
size="small"
onClick={() => {
setModalStatus('export');
}}
>
</Button>
<Modal
width={720}
title={modalStatus === 'export' ? '导出数据' : '导入数据'}
visible={modalStatus === 'export' || modalStatus === 'import'}
cancelText={modalStatus === 'export' ? '确认' : '取消'}
okText={modalStatus === 'export' ? '复制文本' : '确认导入'}
onCancel={() => setModalStatus('')}
onOk={() => {
if (modalStatus === 'import') { // 导入解析
const value = (code || '').trim();
try {
const json: { configs: DripTableSchema['configs']; columns: DripTableColumn[] } = JSON.parse(value);
state.globalConfigs = json.configs;
state.columns = json.columns;
state.currentColumn = void 0;
} catch {
message.error('解析出错, 请传入正确的格式');
} finally {
globalActions.updateGlobalConfig(store);
}
} else { // 导出复制
const aux = document.createElement('input');
aux.setAttribute('value', JSON.stringify(getSchemaValue()));
document.body.append(aux);
aux.select();
document.execCommand('copy');
aux.remove();
if (globalData.onExportSchema) {
globalData.onExportSchema(getSchemaValue());
}
}
setModalStatus('');
setCode('');
}}
>
{ renderSchemaModal() }
</Modal>
</React.Fragment>
);
};
export default ToolLayout;

View File

@ -0,0 +1,33 @@
/**
* This file is part of the drip-table project.
* @link : https://drip-table.jd.com/
* @author : Emil Zhai (root@derzh.com)
* @modifier : Emil Zhai (root@derzh.com)
* @copyright: Copyright (c) 2020 JD Network Technology Co., Ltd.
*/
declare module '*.css' {
interface IClassNames {
[className: string]: string;
}
const classNames: IClassNames;
export = classNames;
}
declare module '*.less' {
interface IClassNames {
[className: string]: string;
}
const classNames: IClassNames;
export = classNames;
}
declare module '*.svg' {
const content: string;
export = content;
}
declare module '*.png' {
const content: string;
export = content;
}

View File

@ -0,0 +1,11 @@
/**
* This file is part of the drip-table project.
* @link : https://drip-table.jd.com/
* @author : Emil Zhai (root@derzh.com)
* @modifier : Emil Zhai (root@derzh.com)
* @copyright: Copyright (c) 2020 JD Network Technology Co., Ltd.
*/
declare interface Window {
mockCount: number;
}

View File

@ -0,0 +1,62 @@
/**
* This file is part of the drip-table project.
* @link : https://drip-table.jd.com/
* @author : helloqian12138 (johnhello12138@163.com)
* @modifier : helloqian12138 (johnhello12138@163.com)
* @copyright: Copyright (c) 2020 JD Network Technology Co., Ltd.
*/
import React from 'react';
function setState<StateType>(that) {
return (nextState: StateType) => {
that.state = { ...that.state, ...nextState };
that.subscriptions.forEach((subscription) => {
subscription(that.state);
});
};
}
function useCustom(that) {
return () => {
const [state, subscription] = React.useState(that.state);
React.useEffect(() => {
that.subscriptions.push(subscription);
return () => {
that.subscriptions = that.subscriptions.filter(item => item !== subscription);
};
}, []);
return [state, that.actions];
};
}
function mappingActions(store, actions) {
const associatedActions = {};
Object.keys(actions).forEach((key) => {
if (typeof actions[key] === 'function') {
associatedActions[key] = actions[key].bind(null, store);
}
if (typeof actions[key] === 'object') {
associatedActions[key] = mappingActions(store, actions[key]);
}
});
return associatedActions;
}
export type StoreType<StateType> = {
state: StateType;
subscriptions: [];
setState: (nextState: StateType) => void;
actions: Record<string, unknown>;
}
export default function useSharedState<StateType>(initState: StateType, actions) {
const store: StoreType<StateType> = {
state: initState,
subscriptions: [],
setState,
actions,
};
store.setState = setState<StateType>(store);
store.actions = mappingActions(store, actions);
return useCustom(store);
}

View File

@ -0,0 +1,76 @@
/**
* This file is part of the drip-table project.
* @link : https://drip-table.jd.com/
* @author : helloqian12138 (johnhello12138@163.com)
* @modifier : helloqian12138 (johnhello12138@163.com)
* @copyright: Copyright (c) 2020 JD Network Technology Co., Ltd.
*/
import { DripTableSchema, ColumnConfig } from 'drip-table';
import { DripTableComponentConfig } from '@/typing';
import useSharedState from './custom-hook';
export type DripTableColumn = ColumnConfig & {
$id: string;
key: number;
sort: number;
type: DripTableComponentConfig['type'];
}
export interface DripTableGeneratorState {
isEdit: boolean;
columns: DripTableColumn[];
currentColumn?: DripTableColumn;
globalConfigs: DripTableSchema['configs'];
/** 表格数据generator不需要知道数据格式是什么直接交给drip-table即可 */
dataSource: Record<string, unknown>[];
}
export const defaultState: () => DripTableGeneratorState = () => ({
isEdit: true,
/** 生成的列配置 */
columns: [],
/** 当前选中的列 */
currentColumn: void 0,
/** 配置项 */
globalConfigs: {
pagination: false,
},
/** 数据 */
dataSource: [],
});
export type GlobalStore = [DripTableGeneratorState, React.Dispatch<React.SetStateAction<DripTableGeneratorState>>];
export type GlobalStoreObject = {
state: DripTableGeneratorState;
setState: React.Dispatch<React.SetStateAction<DripTableGeneratorState>>;
};
export const globalActions: Record<string, (store?: GlobalStoreObject) => void> = {
toggleEditMode(store) {
store?.setState({ ...store.state, isEdit: !store.state.isEdit });
},
editColumns(store) {
store?.setState({ ...store.state, columns: [...store.state.columns] });
},
checkColumn(store) {
store?.setState({ ...store.state, currentColumn: store.state.currentColumn ? { ...store.state?.currentColumn } : void 0 });
},
updateDataSource(store) {
store?.setState({ ...store.state, dataSource: [...store.state.dataSource] });
},
updateGlobalConfig(store) {
store?.setState({ ...store.state, globalConfigs: { ...store.state.globalConfigs } });
},
};
export const setState = (originState: DripTableGeneratorState, states?: DripTableGeneratorState) => {
if (states) {
Object.keys(states).forEach((key) => { originState[key] = states[key]; });
}
};
const useStateHook = originState => useSharedState(originState, globalActions);
export default useStateHook;

View File

@ -0,0 +1,29 @@
/**
* This file is part of the drip-table project.
* @link : https://drip-table.jd.com/
* @author : helloqian12138 (johnhello12138@163.com)
* @modifier : helloqian12138 (johnhello12138@163.com)
* @copyright: Copyright (c) 2020 JD Network Technology Co., Ltd.
*/
import { DTGComponentPropertySchema } from '../typing';
export const basicColumnAttrComponents: DTGComponentPropertySchema[] = [
{
name: 'title',
'ui:title': '表头名称',
'ui:type': 'text',
type: 'string',
default: '',
},
{
name: 'width',
'ui:title': '表格列宽',
'ui:type': 'text',
'ui:description': {
title: '控制表格该列宽度默认单位为“px”支持手动指定单位后缀。',
trigger: 'hover',
type: 'icon',
},
type: 'string',
},
];

View File

@ -0,0 +1,22 @@
import { DripTableComponentConfig } from '../typing';
import text from './text';
import image from './picture';
import renderHtml from './render-html';
import links from './links';
import tag from './tag';
const baseComponentList: DripTableComponentConfig[] = [
text,
image,
links,
tag,
];
const customComponentList: DripTableComponentConfig[] = [
renderHtml,
];
export default [
...baseComponentList,
...customComponentList,
] as DripTableComponentConfig[];

View File

@ -0,0 +1,97 @@
import { DripTableComponentConfig } from '../typing';
import { basicColumnAttrComponents } from './configs';
export default {
$id: '$display_links',
'ui:type': 'links',
type: 'string',
group: '基础组件',
fieldKey: 'links_qywxDIIO',
title: '链接组件',
paramName: '',
default: '',
attrSchema: [
...basicColumnAttrComponents,
{
name: 'ui:props.mode',
'ui:title': '模式',
'ui:type': 'radio',
'ui:props': {
options: [
{ label: '单链接', value: 'single' },
{ label: '多链接', value: 'multiple' },
],
},
type: 'string',
default: 'single',
},
{
name: 'ui:props.label',
'ui:title': '链接文案',
'ui:type': 'text',
default: '',
visible: (_1: unknown[], formData: Record<string, unknown>) => formData['ui:props.mode'] === 'single',
},
{
name: 'ui:props.href',
'ui:title': '链接地址',
'ui:type': 'text',
default: '',
visible: (_1: unknown[], formData: Record<string, unknown>) => formData['ui:props.mode'] === 'single',
},
{
name: 'ui:props.target',
'ui:title': '链接打开方式',
'ui:type': 'text',
default: '',
visible: (_1: unknown[], formData: Record<string, unknown>) => formData['ui:props.mode'] === 'single',
},
{
name: 'ui:props.event',
'ui:title': '事件名称',
'ui:type': 'text',
default: '',
visible: (_1: unknown[], formData: Record<string, unknown>) => formData['ui:props.mode'] === 'single',
},
{
name: 'ui:props.operates',
'ui:title': '字段配置',
'ui:type': 'custom::ArrayComponent',
default: [],
visible: (_1: unknown[], formData: Record<string, unknown>) => formData['ui:props.mode'] === 'multiple',
'ui:props': {
items: [
{
name: 'label',
'ui:title': '链接文案',
'ui:type': 'input',
type: 'string',
default: '',
},
{
name: 'href',
'ui:title': '链接地址',
'ui:type': 'input',
type: 'string',
default: '',
},
{
name: 'target',
'ui:title': '链接打开方式',
'ui:type': 'input',
type: 'string',
default: '',
},
{
name: 'event',
'ui:title': '事件名称',
'ui:type': 'input',
type: 'string',
default: '',
},
],
},
},
],
icon: '<svg t="1627278690307" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6409" width="48" height="48"><path d="M341.333333 512a42.666667 42.666667 0 0 0 42.666667 42.666667h256a42.666667 42.666667 0 0 0 0-85.333334H384a42.666667 42.666667 0 0 0-42.666667 42.666667z" p-id="6410"></path><path d="M384 682.666667H307.626667A176.213333 176.213333 0 0 1 128 527.786667 170.666667 170.666667 0 0 1 298.666667 341.333333h85.333333a42.666667 42.666667 0 0 0 0-85.333333H307.626667a262.4 262.4 0 0 0-262.826667 222.293333A256 256 0 0 0 298.666667 768h85.333333a42.666667 42.666667 0 0 0 0-85.333333zM981.333333 479.573333A262.826667 262.826667 0 0 0 715.093333 256h-64.426666C616.106667 256 597.333333 275.2 597.333333 298.666667a42.666667 42.666667 0 0 0 42.666667 42.666666h76.373333A176.213333 176.213333 0 0 1 896 496.213333 170.666667 170.666667 0 0 1 725.333333 682.666667h-85.333333a42.666667 42.666667 0 0 0 0 85.333333h85.333333a256 256 0 0 0 256-288.426667z" p-id="6411"></path></svg>',
} as DripTableComponentConfig;

View File

@ -0,0 +1,47 @@
import { DripTableComponentConfig } from '../typing';
import { basicColumnAttrComponents } from './configs';
export default {
$id: '$table_picture',
'ui:type': 'image',
type: 'string',
group: '基础组件',
fieldKey: 'picture_qywxDIIO',
title: '图片组件',
paramName: '',
default: '',
attrSchema: [
...basicColumnAttrComponents,
{
name: 'dataIndex',
required: true,
'ui:title': '字段选择',
'ui:type': 'input',
type: 'string',
},
{
name: 'ui:props.imgWidth',
'ui:title': '图片宽度',
'ui:type': 'number',
'ui:minium': 0.01,
'ui:step': 0.01,
default: 100,
},
{
name: 'ui:props.imgHeight',
'ui:title': '图片高度',
'ui:type': 'number',
'ui:minium': 0.01,
'ui:step': 0.01,
default: 100,
},
{
name: 'ui:props.previewImg',
'ui:title': '是否预览图片',
'ui:type': 'switch',
type: 'boolean',
default: true,
},
],
icon: '<svg t="1627276571182" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4622" width="48" height="48"><path d="M422.912 404.672c0 46.72-37.888 84.672-84.672 84.672-46.656 0-84.672-37.888-84.672-84.672C253.568 357.824 291.584 320 338.24 320 385.024 320 422.912 357.824 422.912 404.672zM820.544 768c0 0-34.944-274.432-139.648-274.432-104.768 0-139.712 134.912-209.408 134.912-69.952 0-69.952-33.728-139.776-33.728C261.888 594.752 192 768 192 768L820.544 768 820.544 768zM896 128 128 128l0 704 768 0L896 128M896 64c35.392 0 64 28.672 64 64l0 704c0 35.392-28.608 64-64 64L128 896c-35.328 0-64-28.608-64-64L64 128c0-35.328 28.672-64 64-64L896 64 896 64z" p-id="4623"></path></svg>',
} as DripTableComponentConfig;

View File

@ -0,0 +1,28 @@
import { DripTableComponentConfig } from '../typing';
import { basicColumnAttrComponents } from './configs';
export default {
$id: '$table_renderhtml',
'ui:type': 'render-html',
type: 'string',
group: '自定义组件',
fieldKey: 'render-html',
title: '自定义',
paramName: '',
default: '',
attrSchema: [
...basicColumnAttrComponents,
{
name: 'ui:props.render',
'ui:title': '代码编辑',
'ui:type': 'code-editor',
'ui:props': {
minRow: 5,
},
'ui:wrapperStyle': { minWidth: 680 },
type: 'string',
default: 'return rec.name',
},
],
icon: '<svg t="1627277503592" class="icon" viewBox="0 0 1097 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5523" width="48" height="48"><path d="M352.585143 799.414857l-28.562286 28.562286a18.285714 18.285714 0 0 1-26.294857 0L31.451429 561.700571a18.285714 18.285714 0 0 1 0-26.294857l266.276571-266.276571a18.285714 18.285714 0 0 1 26.294857 0l28.562286 28.562286a18.285714 18.285714 0 0 1 0 26.294857L128 548.571429l224.585143 224.585142a18.285714 18.285714 0 0 1 0 26.294858z m337.700571-609.718857l-213.138285 737.718857a18.139429 18.139429 0 0 1-22.272 12.580572l-35.437715-9.728a18.505143 18.505143 0 0 1-12.580571-22.857143L619.995429 169.691429a18.139429 18.139429 0 0 1 22.272-12.580572l35.437714 9.728a18.505143 18.505143 0 0 1 12.580571 22.857143z m375.442286 372.004571L799.451429 827.977143a18.285714 18.285714 0 0 1-26.294858 0l-28.562285-28.562286a18.285714 18.285714 0 0 1 0-26.294857l224.585143-224.585143-224.585143-224.585143a18.285714 18.285714 0 0 1 0-26.294857l28.562285-28.562286a18.285714 18.285714 0 0 1 26.294858 0l266.276571 266.276572a18.285714 18.285714 0 0 1 0 26.294857z" fill="" p-id="5524"></path></svg>',
} as DripTableComponentConfig;

View File

@ -0,0 +1,101 @@
import { DripTableComponentConfig } from '../typing';
import { basicColumnAttrComponents } from './configs';
export default {
$id: '$table_tag',
'ui:type': 'tag',
type: 'string',
group: '基础组件',
fieldKey: 'tag_qywxDIIO',
title: '标签组件',
paramName: '',
default: '',
attrSchema: [
...basicColumnAttrComponents,
{
name: 'dataIndex',
required: true,
'ui:title': '字段选择',
'ui:type': 'auto-complete',
'ui:props': {
from: 'dataFields',
},
type: 'string',
},
{
name: 'prefix',
'ui:title': '前缀文案',
'ui:type': 'input',
type: 'string',
default: '',
},
{
name: 'suffix',
'ui:title': '后缀文案',
'ui:type': 'input',
type: 'string',
default: '',
},
{
name: 'color',
required: false,
'ui:title': '字体颜色',
'ui:type': 'custom::ColorPicker',
'ui:props': {},
type: 'string',
},
{
name: 'borderColor',
required: false,
'ui:title': '边框颜色',
'ui:type': 'custom::ColorPicker',
'ui:props': {},
type: 'string',
},
{
name: 'backgroundColor',
required: false,
'ui:title': '背景颜色',
'ui:type': 'custom::ColorPicker',
'ui:props': {},
type: 'string',
},
{
name: 'radius',
required: false,
'ui:title': '圆角半径',
'ui:type': 'number',
'ui:props': {
minimum: 0,
maximum: 24,
},
type: 'string',
},
{
name: 'tagOptions',
required: false,
'ui:title': '枚举值',
'ui:type': 'custom::ArrayComponent',
'ui:props': {
items: [
{
name: 'value',
'ui:title': '属性值',
'ui:type': 'input',
type: 'string',
default: '',
},
{
name: 'label',
'ui:title': '展示文案',
'ui:type': 'input',
type: 'string',
default: '',
},
],
},
type: 'string',
},
],
icon: '<svg t="1637755462372" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2309" width="14" height="14"><path d="M323.008 786.752c-52.928 0-96-43.072-96-96s43.072-96 96-96 96 43.072 96 96S375.936 786.752 323.008 786.752zM323.008 658.752c-17.632 0-32 14.336-32 32s14.368 32 32 32 32-14.336 32-32S340.64 658.752 323.008 658.752z" p-id="2310"></path><path d="M416.096 927.072 284.224 927.072c-159.936 0-186.912-59.232-186.912-192l0-140.8c0-74.272 14.304-96.256 70.72-150.976l327.04-319.904c36.576-35.488 105.888-35.392 142.304-0.096l263.072 256.032c18.336 17.792 28.864 43.552 28.864 70.656 0 27.296-10.656 53.28-29.248 71.264l-290.016 294.592C544.544 880.416 497.216 927.072 416.096 927.072zM566.24 159.488c-10.496 0-20.16 3.52-26.528 9.696l-327.04 319.936c-49.952 48.48-51.36 54.528-51.36 105.152l0 140.8c0 110.272 8.352 128 122.912 128l131.872 0c52.672 0 83.744-28.48 148.992-92.8l26.656-26.144 263.232-268.256c6.784-6.592 10.336-15.808 10.336-25.888 0-9.888-3.424-18.88-9.472-24.736l-263.072-256.032C586.432 163.04 576.736 159.488 566.24 159.488z" p-id="2311"></path></svg>',
} as DripTableComponentConfig;

View File

@ -0,0 +1,141 @@
import { DripTableComponentConfig } from '../typing';
import { basicColumnAttrComponents } from './configs';
export default {
$id: '$display_text',
'ui:type': 'text',
type: 'string',
group: '基础组件',
fieldKey: 'text_qywxDIIO',
title: '文本组件',
paramName: '',
default: '',
attrSchema: [
...basicColumnAttrComponents,
{
name: 'dataIndex',
required: true,
'ui:title': '字段选择',
'ui:type': 'select',
'ui:props': {
from: 'dataSource',
},
type: 'string',
visible: (_1: string, formData: Record<string, unknown>) => formData['ui:props.mode'] === 'single',
},
{
name: 'fontSize',
'ui:title': '字体大小',
'ui:type': 'number',
'ui:description': {
trigger: 'hover',
type: 'icon',
title: '控制表格该列默认字体大小默认单位为“px”支持手动指定单位后缀。',
},
default: 12,
min: 12,
},
{
name: 'ui:props.maxRow',
'ui:title': '最大行数',
'ui:type': 'number',
'ui:description': {
trigger: 'hover',
type: 'icon',
title: '文字展示的最大行数,超过该行数则展示...',
},
min: 1,
},
{
name: 'ui:props.linHeight',
'ui:title': '行高',
'ui:type': 'number',
min: 1,
},
{
name: 'ui:props.mode',
'ui:title': '模式',
'ui:type': 'radio',
'ui:props': {
options: [
{ label: '单行文本', value: 'single' },
{ label: '多行文本', value: 'multiple' },
{ label: '自定义文本', value: 'custom' },
],
},
type: 'string',
default: 'single',
},
{
name: 'ui:props.prefix',
'ui:title': '前缀文案',
'ui:type': 'input',
default: '',
visible: (_1: string, formData: Record<string, unknown>) => formData['ui:props.mode'] === 'single',
},
{
name: 'ui:props.suffix',
'ui:title': '后缀文案',
'ui:type': 'input',
default: '',
visible: (_1: string, formData: Record<string, unknown>) => formData['ui:props.mode'] === 'single',
},
{
name: 'ui:props.params',
'ui:title': '字段配置',
'ui:type': 'custom::ArrayComponent',
'ui:props': {
items: [
{
name: 'prefix',
'ui:title': '前缀文案',
'ui:type': 'input',
type: 'string',
default: '',
},
{
name: 'dataIndex',
'ui:title': '字段选择',
'ui:type': 'select',
'ui:props': {
from: 'dataSource',
},
type: 'string',
},
{
name: 'suffix',
'ui:title': '后缀文案',
'ui:type': 'input',
default: '',
type: 'string',
},
],
},
default: [] as unknown[],
visible: (_1: unknown[], formData: Record<string, unknown>) => formData['ui:props.mode'] === 'multiple',
},
{
name: 'defaultValue',
'ui:title': '兜底文案',
'ui:type': 'input',
'ui:description': {
trigger: 'hover',
type: 'icon',
title: '当数据不下发时展示的兜底文案',
},
default: '',
visible: (_1: unknown[], formData: Record<string, unknown>) => formData['ui:props.mode'] === 'single' || formData['ui:mode'] === 'multiple',
},
{
name: 'ui:props.format',
'ui:title': '模板',
'ui:type': 'text',
'ui:props': {
minRows: 5,
},
default: '{{rec}}',
visible: (_1: unknown[], formData: Record<string, unknown>) => formData['ui:props.mode'] === 'custom',
},
],
icon: '<svg t="1627299991032" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1990" width="48" height="48"><path d="M640.136533 896a42.666667 42.666667 0 0 1-39.253333-25.6L341.469867 277.333333 82.056533 870.4a42.666667 42.666667 0 1 1-78.506666-34.133333l298.666666-682.666667a42.666667 42.666667 0 0 1 78.506667 0l298.666667 682.666667a42.666667 42.666667 0 0 1-22.186667 56.32 48.64 48.64 0 0 1-17.066667 3.413333z" p-id="1991"></path><path d="M469.469867 554.666667H213.469867a42.666667 42.666667 0 0 1 0-85.333334h256a42.666667 42.666667 0 0 1 0 85.333334zM981.469867 896a42.666667 42.666667 0 0 1-42.666667-42.666667v-256a42.666667 42.666667 0 0 0-42.666667-42.666666h-128a42.666667 42.666667 0 0 1 0-85.333334h128a128 128 0 0 1 128 128v256a42.666667 42.666667 0 0 1-42.666666 42.666667z" p-id="1992"></path><path d="M896.136533 896h-42.666666a128 128 0 0 1 0-256h128a42.666667 42.666667 0 0 1 42.666666 42.666667v85.333333a128 128 0 0 1-128 128z m-42.666666-170.666667a42.666667 42.666667 0 0 0 0 85.333334h42.666666a42.666667 42.666667 0 0 0 42.666667-42.666667v-42.666667z" p-id="1993"></path></svg>',
} as DripTableComponentConfig;

View File

@ -0,0 +1,59 @@
import { CSSProperties } from 'react';
import { DataSchema, DripTableSchema, EventLike, DripTableProps, DripTableDriver, DripTableRecordTypeBase } from 'drip-table';
/** 组件属性的表单配置项 */
export type DTGComponentPropertySchema = DataSchema & {
$id?: string;
name: string;
required?: boolean;
'ui:title': string;
'ui:description'?: {
type: 'icon' | 'text';
trigger: 'click' | 'hover';
title: string;
};
'ui:titleStyle'?: CSSProperties;
'ui:type': string;
'ui:props'?: Record<string, unknown> & { style?: CSSProperties; className?: string };
'ui:wrapperStyle'?: CSSProperties;
default?: unknown;
disabled?: boolean | string | ((value?: unknown, formData?: Record<string, unknown>) => boolean);
visible?: boolean | string | ((value?: unknown, formData?: Record<string, unknown>) => boolean);
validate?: (value?: unknown, formData?: Record<string, unknown>) => string | Promise<string> | string;
}
/** 组件配置项 */
export interface DripTableComponentConfig {
'$id': string;
/** 组件类型 */
'ui:type': string;
type: 'string' | 'number' | 'boolean' | 'object' | 'integer' | 'null' | 'array';
fieldKey?: string;
/** 模板名称 | 表头名称 */
title: string;
/** 组件所属分组 */
group: string;
dataIndex?: (string | number)[];
/** 属性表单配置 - 用于生成column的表单 */
attrSchema: DTGComponentPropertySchema[];
/** 展示用icon */
icon?: string;
}
export interface DripTableGeneratorProps<RecordType extends DripTableRecordTypeBase, CustomComponentEvent extends EventLike = never, Ext = unknown> {
style?: CSSProperties;
driver?: DripTableDriver<RecordType>;
showComponentLayout?: boolean;
componentLayoutStyle?: CSSProperties;
rightLayoutStyle?: CSSProperties;
showToolLayout?: boolean;
dataSource?: RecordType[];
dataFields?: string[];
schema?: DripTableSchema;
customComponents?: DripTableProps<RecordType, CustomComponentEvent, Ext>['components'];
customComponentPanel?: {
mode: 'add' | 'replace';
components: DripTableComponentConfig[];
};
onExportSchema?: (schema: DripTableSchema) => void;
}

View File

@ -0,0 +1,33 @@
/**
* This file is part of the drip-table project.
* @link : https://drip-table.jd.com/
* @author : helloqian12138 (johnhello12138@163.com)
* @modifier : helloqian12138 (johnhello12138@163.com)
* @copyright: Copyright (c) 2020 JD Network Technology Co., Ltd.
*/
window.mockCount = Number((Math.random() * 1000 + 4096).toFixed(0));
export const mockId = () => {
window.mockCount += 1;
return `${Date.now().toString(16)}-${window.mockCount.toString(16)}`;
};
/**
*
* @param data
* @param indexes
* @param defaultValue
* @returns
*/
export const get = (data: unknown, indexes: string | number | (string | number)[], defaultValue: unknown = void 0) => {
if (typeof data !== 'object' || !data) {
return void 0;
}
if (typeof indexes === 'string') {
return data[indexes];
}
if (Array.isArray(indexes)) {
return indexes.reduce((d, key) => (d ? d[key] : void 0), data);
}
return defaultValue;
};

View File

@ -0,0 +1,73 @@
import React, { useState, useImperativeHandle } from 'react';
import { DripTableRecordTypeBase } from 'drip-table';
import DripTableDriverAntDesign from 'drip-table-driver-antd';
import { useGlobalData } from './hooks';
import { defaultState, DripTableGeneratorState, GlobalStore, setState } from './store';
import AttributeLayout from './layout/attribute-layout';
import ComponentLayout from './layout/component-layout';
import EditableTable from './layout/editable-table';
import PreviewTable from './layout/preview-table';
import ToolLayout from './layout/tool-layout';
import { DripTableGeneratorProps } from './typing';
import styles from './index.module.less';
const Wrapper = <RecordType extends DripTableRecordTypeBase>(props: DripTableGeneratorProps<RecordType> & {
store: GlobalStore;
}, ref) => {
const {
style = {},
driver,
showComponentLayout = true,
componentLayoutStyle = {},
rightLayoutStyle = {},
showToolLayout = true,
dataSource = [],
schema,
customComponentPanel,
customComponents,
} = useGlobalData();
const initialData = { dataSource } as DripTableGeneratorState;
if (schema) {
initialData.globalConfigs = schema.configs;
initialData.columns = schema.columns.map((item, index) => ({ key: index, sort: index, ...item }));
}
const originState: DripTableGeneratorState = props.store ? props.store[0] : defaultState();
setState(originState, { ...initialData });
const store = useState(originState);
const [state] = store;
useImperativeHandle(ref, () => ({
getState: () => state,
}));
let leftLayoutWidth = '280px';
if (typeof componentLayoutStyle.width === 'number') {
leftLayoutWidth = `${componentLayoutStyle.width}px`;
} else if (typeof componentLayoutStyle.width === 'string') {
leftLayoutWidth = componentLayoutStyle.width;
}
return (
<div className={styles.wrapper} style={style}>
{ showComponentLayout && (
<ComponentLayout store={store} width={componentLayoutStyle.width} customComponentPanel={customComponentPanel} />
) }
<div
className={`${styles['layout-right-wrapper']} ${!state.isEdit ? styles.preview : ''}`}
style={showComponentLayout ? { width: `calc(100% - ${leftLayoutWidth})`, ...rightLayoutStyle } : {}}
>
<div className={styles['layout-right-title']}>
<span style={{ margin: '0 12px', fontWeight: 'bold' }}></span>
{ showToolLayout && <ToolLayout store={store} /> }
</div>
{ state.isEdit
? <EditableTable driver={driver || DripTableDriverAntDesign} customComponents={customComponents} store={store} />
: <PreviewTable driver={driver || DripTableDriverAntDesign} customComponents={customComponents} store={store} /> }
<AttributeLayout parentHeight={style.height} customComponentPanel={customComponentPanel} store={store} />
</div>
</div>
);
};
export default Wrapper;

View File

@ -0,0 +1,38 @@
{
"compilerOptions": {
"outDir": "dist",
"strict": true,
"module": "ESNext",
"target": "ESNext",
"lib": ["dom", "esnext"],
"moduleResolution": "node",
"sourceMap": true,
"declaration": true,
"esModuleInterop": true,
"isolatedModules": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"noImplicitReturns": true,
"importHelpers": true,
"listFiles": true,
"noEmit": false,
"removeComments": true,
"noImplicitThis": true,
"noImplicitAny": false,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"allowJs": true,
"baseUrl": "./",
"paths": {
"@/*": ["src/*"],
"@@/*": ["src/.umi/*"]
},
"types": ["node"],
"jsx": "react",
"jsxFactory": "React.createElement",
"noUnusedLocals": true,
"noUnusedParameters": false,
"allowSyntheticDefaultImports": true
},
"include": ["src"]
}

194
yarn.lock
View File

@ -212,7 +212,7 @@
dependencies:
"@babel/types" "^7.16.0"
"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.12.1", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.0":
"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.12.1", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.0.tgz#90538e60b672ecf1b448f5f4f5433d37e79a3ec3"
integrity sha512-kkH7sWzKPq0xt3H1n+ghb4xEMP8k0U7XV3kkB+ZGy69kDk2ySFW1qPi06sjKzFY3t1j6XbJSqr4mF9L7CYVyhg==
@ -2101,6 +2101,21 @@
npmlog "^4.1.2"
write-file-atomic "^2.3.0"
"@monaco-editor/loader@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.2.0.tgz#373fad69973384624e3d9b60eefd786461a76acd"
integrity sha512-cJVCG/T/KxXgzYnjKqyAgsKDbH9mGLjcXxN6AmwumBwa2rVFkwvGcUj1RJtD0ko4XqLqJxwqsN/Z/KURB5f1OQ==
dependencies:
state-local "^1.0.6"
"@monaco-editor/react@^4.2.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.3.1.tgz#d65bcbf174c39b6d4e7fec43d0cddda82b70a12a"
integrity sha512-f+0BK1PP/W5I50hHHmwf11+Ea92E5H1VZXs+wvKplWUWOfyMa1VVwqkJrXjRvbcqHL+XdIGYWhWNdi4McEvnZg==
dependencies:
"@monaco-editor/loader" "^1.2.0"
prop-types "^15.7.2"
"@mrmlnc/readdir-enhanced@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
@ -2707,6 +2722,13 @@ acorn@^7.4.0:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
add-dom-event-listener@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/add-dom-event-listener/-/add-dom-event-listener-1.1.0.tgz#6a92db3a0dd0abc254e095c0f1dc14acbbaae310"
integrity sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw==
dependencies:
object-assign "4.x"
agent-base@4, agent-base@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee"
@ -3210,6 +3232,14 @@ babel-plugin-dynamic-import-node@^2.3.3:
dependencies:
object.assign "^4.1.0"
babel-plugin-import@1.13.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-import/-/babel-plugin-import-1.13.0.tgz#c532fd533df9db53b47d4d4db3676090fc5c07a5"
integrity sha512-bHU8m0SrY89ub2hBBuYjbennOeH0YUYkVpH6jxKFk0uD8rhN+0jNHIPtXnac+Vn7N/hgkLGGDcIoYK7je3Hhew==
dependencies:
"@babel/helper-module-imports" "^7.0.0"
"@babel/runtime" "^7.0.0"
babel-plugin-istanbul@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz#df4ade83d897a92df069c4d9a25cf2671293c854"
@ -3249,6 +3279,14 @@ babel-plugin-react-require@3.1.1:
resolved "https://registry.yarnpkg.com/babel-plugin-react-require/-/babel-plugin-react-require-3.1.1.tgz#5c3d2564fa16b1e45212ed52519db147b1596106"
integrity sha512-XFz+B0dWx41fnGnugzCWn5rOgrDHb150N5gFhUfO3BgYDCT25o4sofRtd9uUfqUHoRu+t4/r5Cr2RMPIKuCt2g==
babel-runtime@6.x, babel-runtime@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
dependencies:
core-js "^2.4.0"
regenerator-runtime "^0.11.0"
bail@^1.0.0:
version "1.0.5"
resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776"
@ -3941,11 +3979,23 @@ compare-func@^2.0.0:
array-ify "^1.0.0"
dot-prop "^5.1.0"
component-classes@^1.2.5:
version "1.2.6"
resolved "https://registry.yarnpkg.com/component-classes/-/component-classes-1.2.6.tgz#c642394c3618a4d8b0b8919efccbbd930e5cd691"
integrity sha1-xkI5TDYYpNiwuJGe/Mu9kw5c1pE=
dependencies:
component-indexof "0.0.3"
component-emitter@^1.2.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
component-indexof@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/component-indexof/-/component-indexof-0.0.3.tgz#11d091312239eb8f32c8f25ae9cb002ffe8d3c24"
integrity sha1-EdCRMSI5648yyPJa6csAL/6NPCQ=
compute-scroll-into-view@^1.0.17:
version "1.0.17"
resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz#6a88f18acd9d42e9cf4baa6bec7e0522607ab7ab"
@ -4141,6 +4191,11 @@ core-js-compat@^3.18.0, core-js-compat@^3.19.1, core-js-compat@^3.6.2:
browserslist "^4.18.1"
semver "7.0.0"
core-js@^2.4.0:
version "2.6.12"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
core-util-is@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@ -4183,6 +4238,14 @@ cosmiconfig@^7.0.0:
path-type "^4.0.0"
yaml "^1.10.0"
create-react-class@15.x:
version "15.7.0"
resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.7.0.tgz#7499d7ca2e69bb51d13faf59bd04f0c65a1d6c1e"
integrity sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng==
dependencies:
loose-envify "^1.3.1"
object-assign "^4.1.1"
cross-spawn@^5.0.1:
version "5.1.0"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
@ -4217,6 +4280,14 @@ crypto-random-string@^1.0.0:
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=
css-animation@^1.3.2:
version "1.6.1"
resolved "https://registry.yarnpkg.com/css-animation/-/css-animation-1.6.1.tgz#162064a3b0d51f958b7ff37b3d6d4de18e17039e"
integrity sha512-/48+/BaEaHRY6kNQ2OIPzKf9A6g8WjZYjhiNDNuIVbsm5tXCGIAsHDjB4Xu1C4vXJtUWZo26O68OQkDpNBaPog==
dependencies:
babel-runtime "6.x"
component-classes "^1.2.5"
css-color-names@0.0.4, css-color-names@^0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
@ -4697,6 +4768,11 @@ dot-prop@^6.0.1:
dependencies:
is-obj "^2.0.0"
"drip-table-driver-antd@link:packages/drip-table-driver-antd":
version "1.0.1"
dependencies:
react-dom "^16.9.0"
"drip-table@link:packages/drip-table":
version "0.0.0"
uid ""
@ -6877,7 +6953,7 @@ is-promise@^2.1.0:
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
is-reference@^1.2.1:
is-reference@^1.1.2, is-reference@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7"
integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==
@ -7588,7 +7664,7 @@ lodash@4.17.20:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
lodash@4.17.21, lodash@^4.15.0, lodash@^4.17.12, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.2.1:
lodash@4.17.21, lodash@^4.15.0, lodash@^4.17.12, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.2.1, lodash@^4.4.2:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -7634,7 +7710,7 @@ longest@^1.0.1:
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
integrity sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@ -7686,7 +7762,7 @@ macos-release@^2.2.0:
resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.5.0.tgz#067c2c88b5f3fb3c56a375b2ec93826220fa1ff2"
integrity sha512-EIgv+QZ9r+814gjJj0Bt5vSLJLzswGmSUbUpbi9AIr/fsN2IWFBl2NucV9PAiek+U1STK468tEkxmVYUtuAN3g==
magic-string@^0.25.5, magic-string@^0.25.7:
magic-string@^0.25.2, magic-string@^0.25.5, magic-string@^0.25.7:
version "0.25.7"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051"
integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==
@ -8482,7 +8558,7 @@ oauth-sign@~0.9.0:
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
object-assign@4.x, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
@ -9555,7 +9631,7 @@ promzard@^0.3.0:
dependencies:
read "1"
prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@ -9668,6 +9744,13 @@ quick-lru@^4.0.1:
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
raf@^3.4.0:
version "3.4.1"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
dependencies:
performance-now "^2.1.0"
randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@ -9675,6 +9758,16 @@ randombytes@^2.1.0:
dependencies:
safe-buffer "^5.1.0"
rc-align@2.x:
version "2.4.5"
resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-2.4.5.tgz#c941a586f59d1017f23a428f0b468663fb7102ab"
integrity sha512-nv9wYUYdfyfK+qskThf4BQUSIadeI/dCsfaMZfNEoxm9HwOIioQ+LyqmMK6jWHAZQgOzMLaqawhuBXlF63vgjw==
dependencies:
babel-runtime "^6.26.0"
dom-align "^1.7.0"
prop-types "^15.5.8"
rc-util "^4.0.4"
rc-align@^4.0.0:
version "4.0.11"
resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-4.0.11.tgz#8198c62db266bc1b8ef05e56c13275bf72628a5e"
@ -9687,6 +9780,19 @@ rc-align@^4.0.0:
rc-util "^5.3.0"
resize-observer-polyfill "^1.5.1"
rc-animate@2.x:
version "2.11.1"
resolved "https://registry.yarnpkg.com/rc-animate/-/rc-animate-2.11.1.tgz#2666eeb6f1f2a495a13b2af09e236712278fdb2c"
integrity sha512-1NyuCGFJG/0Y+9RKh5y/i/AalUCA51opyyS/jO2seELpgymZm2u9QV3xwODwEuzkmeQ1BDPxMLmYLcTJedPlkQ==
dependencies:
babel-runtime "6.x"
classnames "^2.2.6"
css-animation "^1.3.2"
prop-types "15.x"
raf "^3.4.0"
rc-util "^4.15.3"
react-lifecycles-compat "^3.0.4"
rc-cascader@~2.3.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/rc-cascader/-/rc-cascader-2.3.1.tgz#2c72f76fc948ed58874d1beac66d2904d95f9123"
@ -9719,6 +9825,17 @@ rc-collapse@~3.1.0:
rc-util "^5.2.1"
shallowequal "^1.1.0"
rc-color-picker@^1.2.6:
version "1.2.6"
resolved "https://registry.yarnpkg.com/rc-color-picker/-/rc-color-picker-1.2.6.tgz#6cd516e46c6dd6f60a192590e1dc6080032b246d"
integrity sha512-AaC9Pg7qCHSy5M4eVbqDIaNb2FC4SEw82GOHB2C4R/+vF2FVa/r5XA+Igg5+zLPmAvBLhz9tL4MAfkRA8yWNJw==
dependencies:
classnames "^2.2.5"
prop-types "^15.5.8"
rc-trigger "1.x"
rc-util "^4.0.2"
tinycolor2 "^1.4.1"
rc-dialog@~8.6.0:
version "8.6.0"
resolved "https://registry.yarnpkg.com/rc-dialog/-/rc-dialog-8.6.0.tgz#3b228dac085de5eed8c6237f31162104687442e7"
@ -9983,6 +10100,18 @@ rc-tree@~5.3.0:
rc-util "^5.16.1"
rc-virtual-list "^3.4.1"
rc-trigger@1.x:
version "1.11.5"
resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-1.11.5.tgz#f88f9f84e0e79f8e0ef1c8d1bf8ac2208b715620"
integrity sha512-MBuUPw1nFzA4K7jQOwb7uvFaZFjXGd00EofUYiZ+l/fgKVq8wnLC0lkv36kwqM7vfKyftRo2sh7cWVpdPuNnnw==
dependencies:
babel-runtime "6.x"
create-react-class "15.x"
prop-types "15.x"
rc-align "2.x"
rc-animate "2.x"
rc-util "4.x"
rc-trigger@^5.0.0, rc-trigger@^5.0.4, rc-trigger@^5.1.2, rc-trigger@^5.2.10:
version "5.2.10"
resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-5.2.10.tgz#8a0057a940b1b9027eaa33beec8a6ecd85cce2b1"
@ -10003,6 +10132,17 @@ rc-upload@~4.3.0:
classnames "^2.2.5"
rc-util "^5.2.0"
rc-util@4.x, rc-util@^4.0.2, rc-util@^4.0.4, rc-util@^4.15.3:
version "4.21.1"
resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-4.21.1.tgz#88602d0c3185020aa1053d9a1e70eac161becb05"
integrity sha512-Z+vlkSQVc1l8O2UjR3WQ+XdWlhj5q9BMQNLk2iOBch75CqPfrJyGtcWMcnhRlNuDu0Ndtt4kLVO8JI8BrABobg==
dependencies:
add-dom-event-listener "^1.1.0"
prop-types "^15.5.10"
react-is "^16.12.0"
react-lifecycles-compat "^3.0.4"
shallowequal "^1.1.0"
rc-util@^5.0.1, rc-util@^5.0.6, rc-util@^5.0.7, rc-util@^5.12.0, rc-util@^5.14.0, rc-util@^5.15.0, rc-util@^5.16.1, rc-util@^5.2.0, rc-util@^5.2.1, rc-util@^5.3.0, rc-util@^5.4.0, rc-util@^5.5.0, rc-util@^5.5.1, rc-util@^5.6.1, rc-util@^5.7.0, rc-util@^5.8.0, rc-util@^5.9.4, rc-util@^5.9.8:
version "5.16.1"
resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.16.1.tgz#374db7cb735512f05165ddc3d6b2c61c21b8b4e3"
@ -10046,6 +10186,11 @@ react-is@^16.12.0, react-is@^16.8.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-window@^1.8.6:
version "1.8.6"
resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.6.tgz#d011950ac643a994118632665aad0c6382e2a112"
@ -10231,6 +10376,11 @@ regenerate@^1.4.2:
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
regenerator-runtime@^0.11.0:
version "0.11.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
regenerator-runtime@^0.13.4:
version "0.13.9"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
@ -10469,7 +10619,7 @@ resolve@1.17.0:
dependencies:
path-parse "^1.0.6"
resolve@^1.10.0, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.16.1, resolve@^1.17.0, resolve@^1.20.0, resolve@^1.3.2, resolve@^1.5.0, resolve@^1.8.1:
resolve@^1.10.0, resolve@^1.11.0, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.16.1, resolve@^1.17.0, resolve@^1.20.0, resolve@^1.3.2, resolve@^1.5.0, resolve@^1.8.1:
version "1.20.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
@ -10558,6 +10708,17 @@ rimraf@^3.0.2:
dependencies:
glob "^7.1.3"
rollup-plugin-commonjs@^10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz#417af3b54503878e084d127adf4d1caf8beb86fb"
integrity sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==
dependencies:
estree-walker "^0.6.1"
is-reference "^1.1.2"
magic-string "^0.25.2"
resolve "^1.11.0"
rollup-pluginutils "^2.8.1"
rollup-plugin-postcss@3.1.8:
version "3.1.8"
resolved "https://registry.yarnpkg.com/rollup-plugin-postcss/-/rollup-plugin-postcss-3.1.8.tgz#d1bcaf8eb0fcb0936e3684c22dd8628d13a82fd1"
@ -10599,7 +10760,7 @@ rollup-plugin-typescript2@0.29.0:
resolve "1.17.0"
tslib "2.0.1"
rollup-pluginutils@^2.8.2:
rollup-pluginutils@^2.8.1, rollup-pluginutils@^2.8.2:
version "2.8.2"
resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e"
integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==
@ -11062,6 +11223,11 @@ stable@^0.1.8:
resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
state-local@^1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/state-local/-/state-local-1.0.7.tgz#da50211d07f05748d53009bee46307a37db386d5"
integrity sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==
static-extend@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
@ -11636,6 +11802,11 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
tinycolor2@^1.4.1:
version "1.4.2"
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@ -12185,6 +12356,11 @@ viewerjs@1.7.1:
resolved "https://registry.yarnpkg.com/viewerjs/-/viewerjs-1.7.1.tgz#dba6198a6542942cd78904c780fba9fe9503a269"
integrity sha512-OQ6zYzIgyeAO1kutwdtwhTb3OTcThVWsjUV216cb4q5vnJFpsAaVmn+cQ3PhIXXrFZ6T60TwbH0DLVJ9ijhVPg==
viewerjs@^1.10.0:
version "1.10.2"
resolved "https://registry.yarnpkg.com/viewerjs/-/viewerjs-1.10.2.tgz#de16fa10668e4da6325969836a3264a046e3ef9a"
integrity sha512-v/4+OUF/71JthlvYuqfse+WGkMJO0LxvWiOLsAoxWw+RWjMdEBWn0ZQ5Mc+OhNIFd9uLhG62GzfFIplvix8odg==
vinyl-fs@3.0.3, vinyl-fs@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-3.0.3.tgz#c85849405f67428feabbbd5c5dbdd64f47d31bc7"