init: drip-table 1.0.0

This commit is contained in:
Emil Zhai 2021-12-09 17:15:46 +08:00
parent bf71c821a3
commit 8b267309c0
40 changed files with 14840 additions and 0 deletions

9
.fatherrc.ts Executable file
View File

@ -0,0 +1,9 @@
export default {
esm: 'rollup',
disableTypeCheck: false,
cjs: { type: 'babel', lazy: true },
autoPkgs: false,
pkgs: [
'drip-table',
],
};

4
.gitignore vendored
View File

@ -17,3 +17,7 @@
/packages/*/dist
/packages/*/dist-visualizer
/packages/*/dist-visualizer-css
# temporary files
/.tsconfig-lint.json
/packages/*/.tsconfig-lint.json

35
.stylelintrc.js Normal file
View File

@ -0,0 +1,35 @@
/**
* 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) 2021 JD Network Technology Co., Ltd.
*/
module.exports = {
"extends": "stylelint-config-standard",
"plugins": [],
"rules": {
"at-rule-empty-line-before": ["always", {
except: ["inside-block", "blockless-after-same-name-blockless", "first-nested"],
ignore: ["blockless-after-blockless"],
ignoreAtRules: ["array", "of", "at-rules", "at-root"],
}],
"at-rule-no-unknown": null,
"color-hex-length": "long",
"comment-empty-line-before": ["always", {
ignore: ["after-comment", "stylelint-commands"],
}],
"max-nesting-depth": null,
"no-empty-source": null,
"no-descending-specificity": null,
"number-leading-zero": "never",
"selector-id-pattern": /^\$?[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u,
"selector-max-compound-selectors": null,
"selector-no-qualifying-type": null,
"selector-pseudo-class-no-unknown": [true, {
ignorePseudoClasses: ["global"],
}],
},
"ignoreDisables": true,
};

16
bin/gitlint.sh Normal file
View File

@ -0,0 +1,16 @@
#!/bin/sh
changed=$(git diff --cached --name-only)
if [ -z "$changed" ]; then
exit 0
fi
echo $changed | xargs egrep '^[><=]{7}( |$)' -H -I --line-number
# If the egrep command has any hits - echo a warning and exit with non-zero status.
if [ $? = 0 ]; then
echo "WARNING: You have merge markers in the above files. Fix them before committing."
echo " If these markers are intentional, you can force the commit with the --no-verify argument."
exit 1
fi

84
bin/includes/env.sh Normal file
View File

@ -0,0 +1,84 @@
#!/bin/bash
# ----------------------------------
# Colors
# ----------------------------------
if test -t 1; then
if test -n "$(tput colors)" && test $(tput colors) -ge 8; then
NOCOLOR="\033[0m"
RED="\033[0;31m"
GREEN="\033[0;32m"
ORANGE="\033[0;33m"
BLUE="\033[0;34m"
PURPLE="\033[0;35m"
CYAN="\033[0;36m"
LIGHTGRAY="\033[0;37m"
DARKGRAY="\033[1;30m"
LIGHTRED="\033[1;31m"
LIGHTGREEN="\033[1;32m"
YELLOW="\033[1;33m"
LIGHTBLUE="\033[1;34m"
LIGHTPURPLE="\033[1;35m"
LIGHTCYAN="\033[1;36m"
WHITE="\033[1;37m"
fi
fi
# ----------------------------------
# npm or yarn
# ----------------------------------
if [ "${NPM}" = "" ]; then
NPM='yarn'
echo "> using ${LIGHTPURPLE}${NPM}${NOCOLOR} as building command tool."
echo ''
fi
# ----------------------------------
# url encode / decode
# ----------------------------------
urlencode() {
# urlencode <string>
old_lc_collate=$LC_COLLATE
LC_COLLATE=C
local length="${#1}"
i=0
while [ "$i" -lt "${length}" ]; do
local c="${1:$i:1}"
case $c in
[a-zA-Z0-9.~_-]) printf '%s' "$c" ;;
*) printf '%%%02X' "'$c" ;;
esac
i=$(( i + 1 ))
done
LC_COLLATE=$old_lc_collate
}
urldecode() {
# urldecode <string>
local url_encoded="${1//+/ }"
printf '%b' "${url_encoded//%/\\x}"
}
# ----------------------------------
# echo
# ----------------------------------
echoeol() {
echo " "
}
echoline() {
# echostep <string>
s="${*}"
echo "> ${s}"
}
echocmd() {
# echocmd <string>
cmd="${*}"
echo "${LIGHTRED}\$${NOCOLOR} ${cmd}"
${cmd} || exit 1
}

43
bin/tslint.sh Normal file
View File

@ -0,0 +1,43 @@
#!/bin/bash -e
__DIR__=$(dirname "$0")
PACKAGE_NAME=${1}
# includes
. ${__DIR__}/includes/env.sh
# create tsc lint config file
TMP=.tsconfig-lint.json
cat >${TMP} <<EOF
{
"extends": "./tsconfig.json",
"compilerOptions": {
"listFiles": false
},
"include": [
EOF
for file in "$@"; do
echo " \"$file\"," >> ${TMP}
done
cat >>${TMP} <<EOF
"**/*.d.ts"
]
}
EOF
# run tsc lint
${NPM} run tslint:exec > ./node_modules/.tslint.log
TSC_ERROR=`tail -n +5 ./node_modules/.tslint.log`
rm -f ./node_modules/.tslint.log
# remove tsc config file
rm -f ${TMP}
# emit error if exists
if [ "${TSC_ERROR}" != "" ]; then
echoeol
echo "${RED}⛔ Typescript complier lint (tsc) checksum failed!${NOCOLOR}"
echoeol
echo "${TSC_ERROR}"
echoeol
exit 1
fi

6
lerna.json Executable file
View File

@ -0,0 +1,6 @@
{
"packages": ["packages/*"],
"version": "independent",
"npmClient": "yarn",
"useWorkspaces": true
}

54
package.json Executable file
View File

@ -0,0 +1,54 @@
{
"name": "root",
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"build": "yarn run build:drip-table",
"build:drip-table": "lerna run build --stream --scope=drip-table",
"changelog": "lerna-changelog",
"clean": "lerna clean -y",
"lint": "yarn run gitlint && lerna run lint --stream --scope=drip-table",
"gitlint": "sh ./bin/gitlint.sh || exit 1"
},
"devDependencies": {
"@ant-design/icons": "^4.7.0",
"@babel/plugin-proposal-optional-chaining": "^7.6.0",
"@rollup/plugin-eslint": "^8.0.1",
"@types/cheerio": "0.22.22",
"@types/react-window": "^1.8.5",
"@typescript-eslint/eslint-plugin": "4.33.0",
"@typescript-eslint/parser": "4.33.0",
"babel-eslint": "^10.1.0",
"drip-table": "link:packages/drip-table",
"eslint-config-lvmcn": "0.0.30",
"eslint-formatter-pretty": "4.0.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-import-resolver-webpack": "^0.13.1",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-promise": "^5.1.1",
"eslint-plugin-react": "^7.27.0",
"eslint-plugin-unicorn": "^38.0.1",
"eslint-plugin-unused-imports": "^1.1.1",
"father-build": "1.17.2",
"lerna": "3.22.1",
"lerna-changelog": "1.0.1",
"lint-staged": "10.0.7",
"react": "17.0.2",
"stylelint": "13.8.0",
"stylelint-config-standard": "20.0.0",
"stylelint-formatter-pretty": "2.1.1",
"tsc-alias": "^1.4.2",
"typescript": "^4.4.4"
},
"files": [
"README.*",
"dist"
],
"homepage": "https://drip-table.jd.com/",
"license": "MIT",
"bugs": {
"url": "https://coding.jd.com/drip/drip-table/issues"
}
}

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,30 @@
import { IBundleOptions } from 'father-build';
import path from 'path';
import eslint from '@rollup/plugin-eslint';
const options: IBundleOptions = {
cjs: { type: 'rollup' },
esm: {
type: 'rollup',
importLibToEs: true,
},
cssModules: true,
extractCSS: true,
extraBabelPlugins: [],
extraRollupPlugins: [{
before: "babel",
plugins: [
eslint({
configFile: path.resolve(__dirname, '.eslintrc.js'),
}),
],
}],
pkgs: [
'drip-table',
],
preCommit: {
eslint: true,
},
};
export default options;

77
packages/drip-table/README.md Executable file
View File

@ -0,0 +1,77 @@
# Drip-Table Development Guide
`Drip-Table` is the core library of the solution for building dynamic tables. It's main ability is to render a dynamic table automatically when received data which conforms to the `JSON Schema` standard.
## Directory
- update continually
```
├── src // source code
├── components // common components that drip-table depends on
│ ├── ErrorBoundary // error notification component
│ ├── Highlight // highlight component
│ └── RichText // HTML character rendering component
├── drip-table // drip-table main component
│ ├─ components // built-in common components
│ | ├── image // picture display component
│ | ├── link // link operation component
│ | ├── render-html // custom HTML render component
│ | ├── text // text display component
│ | ├── components.ts // common type definition
│ | └── index.ts // export
| ├── header // header of table
| ├── utils // util functions
| └── virtual-table // virtual scrollable table
├── drip-table-provider // drip-table entry
├── context.ts // export global context
├── hooks.ts // global states
├── index.ts // export
├── shims-styles.d.ts // style type definition
└── types.ts // export type definition
```
## Development
### Steps
1. create a new branch that names to express the features simply.
2. coding in local branch.
3. add documents of new features in `docs/drip-table` directory.
4. add features in `docs/drip-table/changelog` file.
5. submit code and merge into `master` branch.
6. modify version infomation in `package.json` file.
7. submit and push code.
8. release new version.
### 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 assgin 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 definiting a component as much as possible.
## Release
add owner in npm depository for the first time.
```sh
npm owner add [username] drip-table
```
### 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 pharse of development the current software is in, and it needs to be modified when the software enters to another pharse. Whether to modify this version number is determined by the project manager.
```sh
git commit -m 'release: drip-table 1.2.3'
lerna run build --stream --scope=drip-table
cd packages/drip-table
npm publish --access=public
```

View File

@ -0,0 +1,78 @@
# Drip-Table 开发文档
`Drip-Table` 是动态列表解决方案的核心库,其主要能力是支持符合 `JSON Schema` 标准的数据自动渲染列表内容。
## 目录结构
- 持续更新
```
├── src // 源代码
├── components // drip-table 依赖的通用组件
│ ├── ErrorBoundary // 错误提示组件
│ ├── Highlight // 高亮组件
│ └── RichText // HTML 字符渲染组件
├── drip-table // drip-table 主要渲染逻辑
│ ├─ components // 内置通用组件
│ | ├── image // 图片展示组件
│ | ├── link // 链接操作组件
│ | ├── render-html // 自定义 HTML 渲染组件
│ | ├── text // 文本展示组件
│ | ├── components.ts // 组件通用类型定义
│ | └── index.ts // 组件导出
| ├── header // 表格头部渲染
| ├── utils // 工具函数库
| └── virtual-table // 虚拟滚动表格
├── drip-table-provider // drip-table 入口文件
├── context.ts // 全局上下文导出
├── hooks.ts // 全局属性状态管理
├── index.ts // 导出
├── shims-styles.d.ts // 样式类型定义
└── types.ts // 全局变量类型定义
```
## 开发
### 开发步骤
1. 新建本地分支,命名可大概说明需求新特性;
2. 在本地分支上修改对应代码;
3. 在 `docs/drip-table` 目录下添加相应的文档说明;
4. 在 `docs/drip-table/changelog` 目录下修改版本更改说明;
5. 提交代码,合并至 `master` 分支;
6. 修改 `package.json` 修改版本信息;
7. 提交代码;
8. 打包发布;
### 注意事项
- 每个函数式组件 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
```
### 版本号定修改规则
- 主版本号(1):必填,当功能模块有较大的变动,比如增加多个模块或者整体架构发生变化。此版本号由项目决定是否修改。
- 子版本号(2):必填,当功能有一定的增加或变化,比如增加了对权限控制、增加自定义视图等功能。此版本号由项目决定是否修改。
- 阶段版本号(3):必填,一般是问题修复或是一些小的变动,要经常发布修订版,时间间隔不限,修复一个严重的问题即可发布一个修订版。此版本号由项目经理决定是否修改。
- 日期版本号(051021): 可选,用于记录修改项目的当前日期,每天对项目的修改都需要更改日期版本号。此版本号由开发人员决定是否修改。
- 希腊字母版本号(beta): 可选,此版本号用于标注当前版本的软件处于哪个开发阶段,当软件进入到另一个阶段时需要修改此版本号。此版本号由项目决定是否修改。
```sh
git commit -m 'release: drip-table 1.2.3'
lerna run build --stream --scope=drip-table
cd packages/drip-table
npm publish --access=public
```

View File

@ -0,0 +1,64 @@
{
"name": "drip-table",
"version": "1.0.0",
"description": "A tiny and powerful enterprise-class solution for building tables.",
"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"
},
"gitHooks": {
"pre-commit": "lint-staged"
},
"peerDependencies": {
"react": ">=16.9.0"
},
"dependencies": {
"cheerio": "1.0.0-rc.3",
"lodash": "4.17.20",
"lodash.get": "^4.4.2",
"rc-resize-observer": "^1.0.1",
"react-dom": "^16.9.0",
"react-window": "^1.8.6",
"viewerjs": "1.7.1"
},
"devDependencies": {
"@types/cheerio": "0.22.22",
"@types/react-window": "^1.8.5",
"father-build": "1.17.2",
"typescript": "^4.4.4"
},
"keywords": [
"DripTable",
"Render",
"TableRender",
"Drip Design",
"Json Schema",
"React"
],
"files": [
"*.md",
"dist"
],
"homepage": "https://drip-table.jd.com/",
"license": "MIT",
"bugs": {
"url": "https://coding.jd.com/drip/drip-table/issues"
}
}

View File

@ -0,0 +1,42 @@
/*
* 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) 2021 JD Network Technology Co., Ltd.
*/
import React, { ErrorInfo } from 'react';
import { DripTableRecordTypeBase, DripTableDriver } from '@/types';
class ErrorBoundary<RecordType extends DripTableRecordTypeBase> extends React.Component<
{ driver: DripTableDriver<RecordType> },
{ hasError: boolean; errorInfo: string }
> {
public state = { hasError: false, errorInfo: '' };
public static getDerivedStateFromError(error: Error) {
return { hasError: true, errorInfo: error.message };
}
public componentDidCatch(error: unknown, errorInfo: ErrorInfo) {
console.error(error, errorInfo);
}
public render() {
if (this.state.hasError) {
const Result = this.props.driver.components.Result;
// You can render any custom fallback UI
return (
<Result
status="error"
title="Something went wrong."
extra={this.state.errorInfo}
/>
);
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@ -0,0 +1,97 @@
/*
* This file is part of the drip-table project.
* @link : https://ace.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,298 @@
/*
* This file is part of the drip-table project.
* @link : https://ace.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 '@/components/Highlight';
import 'viewerjs/dist/viewer.css';
interface RichTextProps {
html: string;
tagNames?: (keyof React.ReactHTML)[];
singleLine?: boolean;
maxLength?: number;
highlight?: Omit<HighlightProps, 'content' | 'tagName'>;
style?: CSSProperties;
}
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',
];
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 {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 filterAttrs = (rec: Record<string, unknown>, excludeKeys: string[]) =>
Object.fromEntries(Object.entries(rec)
.filter(([k, v]) => !excludeKeys.includes(k)));
const props: Record<string, unknown> = {
...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 } = this.props;
const $ = cheerio.load(html);
const body = $('body')[0];
return (
<div ref={this.onRef} style={style}>
{
body && body.type === 'tag'
? body.children.reduce<ReducerRenderValue>(this.reducerRenderEl, {
elements: [],
maxLength,
tagNames,
singleLine,
highlight,
}).elements
: void 0
}
</div>
);
}
}

View File

@ -0,0 +1,45 @@
/**
* 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) 2021 JD Network Technology Co., Ltd.
*/
import { createContext } from 'react';
export interface IDripTableContext {
readonly _CTX_SOURCE: 'CONTEXT' | 'PROVIDER';
loading: boolean;
api: CallableFunction | CallableFunction[] | null;
tab: number; // 如果api是数组需要在最顶层感知tab来知道到底点击搜索调用的是啥api
extraData: null; // 需要用到的 dataSource 以外的扩展返回值
pagination: {
current: number;
total: number;
pageSize: number;
};
tableSize: 'default';
checkPassed: boolean;
selectedRowKeys: React.Key[];
setTableState: CallableFunction;
}
export const DripTableContext = createContext<IDripTableContext>({
loading: false,
api: null,
tab: 0,
extraData: null,
pagination: {
current: 1,
total: 0,
pageSize: 10,
},
tableSize: 'default',
checkPassed: true,
selectedRowKeys: [],
_CTX_SOURCE: 'CONTEXT',
setTableState: () => false,
});
export const DripTableStoreContext = createContext({});

View File

@ -0,0 +1,109 @@
/*
* 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) 2021 JD Network Technology Co., Ltd.
*/
import React, { forwardRef, useImperativeHandle } from 'react';
import { DripTableRecordTypeBase } from '@/types';
import { useState, useTable } from '@/hooks';
import { DripTableContext, IDripTableContext } from '@/context';
import DripTable, { DripTableProps } from '@/drip-table';
/**
*
*/
export interface DripTableContainerHandle extends IDripTableContext {
/**
*
*
* @param indexes
*/
select: (indexes: number[]) => void;
}
// 组件提供给外部的公共接口
const createTableContext = <RecordType extends DripTableRecordTypeBase>(props: DripTableProps<RecordType>): DripTableContainerHandle => {
const initialState = useTable();
const [state, setState] = useState(initialState);
const select = (indexes: number[]) => {
let selectedKeys: React.Key[] = [];
const { dataSource, rowKey } = props;
if (dataSource && rowKey) {
indexes.forEach((index) => {
const data = dataSource[index];
if (data) {
const value = data[rowKey];
const key = typeof value === 'string' || typeof value === 'number'
? value
: index;
selectedKeys.push(key);
}
});
} else {
selectedKeys = [...indexes];
}
setState({ selectedRowKeys: selectedKeys });
};
const handler: DripTableContainerHandle = {
...state,
setTableState: setState,
select,
_CTX_SOURCE: 'PROVIDER', // context 来源于 drip-table-provider
};
return handler;
};
export interface DripTableProviderProps {}
const findChildReactNode = (children: React.ReactNode | undefined, filter: (child: React.ReactNode) => boolean): React.ReactNode => {
const stack = [children];
while (stack.length > 0) {
const child = stack.pop();
if (Array.isArray(child)) {
stack.push(...child);
} else if (child && filter(child)) {
return child;
}
}
return void 0;
};
const DripTableContainer: React.ForwardRefRenderFunction<DripTableContainerHandle, React.PropsWithChildren<DripTableProviderProps>> = (props, ref) => {
const tableChild = findChildReactNode(
props.children,
(child) => {
const childType = child && typeof child === 'object' && 'type' in child ? child.type : void 0;
const childName = typeof childType === 'function' ? childType.name : childType;
return childName === DripTable.name;
},
);
const tableProps: DripTableProps<DripTableRecordTypeBase> | undefined = tableChild && typeof tableChild === 'object' && 'type' in tableChild
? tableChild.props
: void 0;
if (tableProps) {
const ConfigProvider = tableProps.driver.components.ConfigProvider;
const context = createTableContext(tableProps);
useImperativeHandle(ref, () => context);
return (
<ConfigProvider locale={tableProps?.driver.locale}>
<DripTableContext.Provider {...props} value={context} />
</ConfigProvider>
);
}
return <div />;
};
const DripTableProvider = forwardRef(DripTableContainer);
const withTable = <T extends DripTableRecordTypeBase>(Component: React.FC<DripTableProps<T>>) => (props: DripTableProps<T>) => (
<DripTableProvider>
<Component {...props} />
</DripTableProvider>
);
export { DripTableProvider, withTable };

View File

@ -0,0 +1,72 @@
/**
* 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) 2021 JD Network Technology Co., Ltd.
*/
import { DripTableDriver, EventLike } from '@/types';
import { DripTableBuiltInComponentEvent } from '.';
export interface DripTableComponentSchema {
/** 唯一标识,不做展示用 */
$id: string;
/** 组件类型唯一标识 */
type: string;
/** 表头,组件名 */
title: string;
/** 表格列宽 */
width?: string | number;
/** 表格列对齐 */
align?: 'left' | 'center' | 'right';
/** 列数据在数据项中对应的路径,支持通过数组查询嵌套路径 */
dataIndex: string | string[];
default?: string;
/** 表头说明 */
description?: string;
/** 是否固定列 */
fixed?: boolean;
}
export interface DripTableComponentProps<
RecordType,
Schema extends DripTableComponentSchema = DripTableComponentSchema,
ComponentEvent extends EventLike = never,
Ext = unknown,
> {
/**
*
*/
driver: DripTableDriver<RecordType>;
/**
*
*/
schema: Schema;
/**
* `list[i]`
*/
data: RecordType;
/**
* `data[schema.dataIndex]`
*/
value: unknown;
/**
*
*/
ext?: Ext;
/**
*
*/
preview?: boolean | {
/**
*
*/
columnRenderer?: (columnSchema: Schema, columnElement: JSX.Element) => JSX.Element;
};
onClick?: (name: string) => void;
/**
*
*/
fireEvent: (event: ComponentEvent | DripTableBuiltInComponentEvent) => void;
}

View File

@ -0,0 +1,58 @@
/**
* This file is part of the drip-table project.
* @link : https://ace.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 { get } from '@/drip-table/utils';
import { DripTableComponentProps, DripTableComponentSchema } from '../component';
export interface DTCImageSchema extends DripTableComponentSchema {
'ui:type': 'image';
/** 兜底图案 */
noDataUrl?: string;
popover?: boolean;
previewImg?: boolean;
// fullScreen?: boolean;
imgWidth?: number;
imgHeight?: number;
}
interface DTCImageProps<RecordType> extends DripTableComponentProps<RecordType, DTCImageSchema> { }
interface DTCImageState { }
export default class DTCImage<RecordType> extends React.PureComponent<DTCImageProps<RecordType>, DTCImageState> {
private DEFAULT_IMG = 'data:image/png;base64,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';
private get value() {
const schema = this.props.schema;
const dataIndex = schema.dataIndex;
return get(this.props.data, dataIndex, '');
}
public render() {
const { popover, imgWidth, imgHeight, noDataUrl, previewImg } = this.props.schema;
const Popover = this.props.driver.components.Popover;
const Image = this.props.driver.components.Image;
const imgFragment = (
<Image
width={imgWidth}
height={imgHeight}
src={this.value}
preview={this.props.preview ? false : previewImg}
fallback={noDataUrl || this.DEFAULT_IMG}
/>
);
return popover && !this.props.preview
? (
<Popover content={(<img src={this.value} />)}>
{ imgFragment }
</Popover>
)
: imgFragment;
}
}

View File

@ -0,0 +1,26 @@
/**
* This file is part of the drip-table project.
* @link : https://ace.jd.com/
* @author : helloqian12138 (johnhello12138@163.com)
* @modifier : helloqian12138 (johnhello12138@163.com)
* @copyright: Copyright (c) 2020 JD Network Technology Co., Ltd.
*/
import DTCImage from './image';
import DTCLink, { DTCLinkEvent } from './link';
import DTCRenderHTML from './render-html';
import DTCText from './text';
import DTCTag from './tag';
export type { DripTableComponentProps, DripTableComponentSchema } from './component';
export type DripTableBuiltInComponentEvent =
| DTCLinkEvent;
const DripTableBuiltInComponents = {
image: DTCImage,
links: DTCLink,
text: DTCText,
tag: DTCTag,
'render-html': DTCRenderHTML,
};
export default DripTableBuiltInComponents;

View File

@ -0,0 +1,103 @@
/*
* 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) 2021 JD Network Technology Co., Ltd.
*/
import React from 'react';
import { DripTableComponentProps, DripTableComponentSchema } from '../component';
export interface DTCLinkSchema extends DripTableComponentSchema {
'ui:type': 'link';
mode: 'single' | 'multiple';
name?: string;
label?: string;
href?: string;
event?: string;
target?: string;
operates?: {
name?: string;
label?: string;
href?: string;
event?: string;
target?: string;
}[];
}
export interface DTCLinkEvent {
type: 'drip-link-click';
payload: string;
}
interface DTCLinkProps<RecordType> extends DripTableComponentProps<RecordType, DTCLinkSchema> { }
interface DTCLinkState {}
export default class DTCLink<RecordType> extends React.PureComponent<DTCLinkProps<RecordType>, DTCLinkState> {
private get configured() {
const schema = this.props.schema;
if (schema.mode === 'multiple') {
if (schema.operates) {
return true;
}
return false;
}
if (schema.mode === 'single' && (schema.href || schema.event)) {
return true;
}
return false;
}
public render(): JSX.Element {
const { schema } = this.props;
if (!this.configured) {
return <div style={{ color: 'red' }}></div>;
}
if (schema.mode === 'single') {
const event = schema.event;
if (event) {
return (
<a
onClick={() => {
if (this.props.preview) {
return;
}
this.props.fireEvent({ type: 'drip-link-click', payload: event });
}}
>
{ schema.label }
</a>
);
}
return <a href={schema.href} target={schema.target}>{ schema.label }</a>;
}
return (
<div>
{
schema.operates?.map((config, index) => {
const event = config.event;
if (event) {
return (
<a
style={{ marginRight: '5px' }}
key={config.name || index}
onClick={() => {
if (this.props.preview) {
return;
}
this.props.fireEvent({ type: 'drip-link-click', payload: event });
}}
>
{ config.label }
</a>
);
}
return <a style={{ marginRight: '5px' }} key={config.name || index} href={config.href} target={config.target}>{ config.label }</a>;
})
}
</div>
);
}
}

View File

@ -0,0 +1,39 @@
/**
* This file is part of the drip-table project.
* @link : https://ace.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';
import RichText from '@/components/RichText';
import { DripTableComponentProps, DripTableComponentSchema } from '../component';
export interface DTCRenderHTMLSchema extends DripTableComponentSchema {
'ui:type': 'render-html';
paramName: string | string[];
render: string;
}
interface DTCRenderHTMLProps<RecordType> extends DripTableComponentProps<RecordType, DTCRenderHTMLSchema> { }
interface DTCRenderHTMLState { }
export default class DTCRenderHTML<RecordType> extends React.PureComponent<DTCRenderHTMLProps<RecordType>, DTCRenderHTMLState> {
public render(): JSX.Element {
const { data, schema } = this.props;
try {
const html = new Function('rec', schema.render)(data);
if (typeof html === 'object') {
return (
<div>{ Object.prototype.toString.call(html) }</div>
);
}
return <RichText html={html || ''} />;
} catch (error) {
console.error(error);
}
return <div style={{ color: 'red' }}></div>;
}
}

View File

@ -0,0 +1,68 @@
/**
* This file is part of the drip-table project.
* @link : https://ace.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 { get } from '@/drip-table/utils';
import { DripTableComponentProps, DripTableComponentSchema } from '../component';
export interface DTCTagSchema extends DripTableComponentSchema {
'ui:type': 'tag';
/** 模式 */
// mode?: 'single' | 'multi';
/** 字体颜色 */
color?: string;
/** 边框颜色 */
borderColor?: string;
/** 背景色 */
backgroundColor?: string;
/** 圆角半径 */
radius?: number;
/** 前缀 */
prefix?: string;
/** 后缀 */
subfix?: string;
/** 静态文案 */
content?: string;
/** 枚举 */
tagOptions?: { label: string; value: string | number }[];
}
interface DTCTagProps<RecordType> extends DripTableComponentProps<RecordType, DTCTagSchema> { }
interface DTCTagState { }
export default class DTCTag<RecordType> extends React.PureComponent<DTCTagProps<RecordType>, DTCTagState> {
private get value() {
const schema = this.props.schema;
const dataIndex = schema.dataIndex;
return get(this.props.data, dataIndex, '');
}
public render() {
const Tag = this.props.driver.components.Tag;
const schema = this.props.schema;
const value = this.value;
return (
<div>
{ schema.prefix || '' }
<Tag
color={schema.color}
style={{
color: schema.color,
borderColor: schema.borderColor,
backgroundColor: schema.backgroundColor,
borderRadius: schema.radius,
}}
>
{ schema.content || schema.tagOptions?.find(item => item.value === value)?.label || value }
</Tag>
{ schema.subfix || '' }
</div>
);
}
}

View File

@ -0,0 +1,24 @@
/**
* This file is part of the drip-table project.
* @link : https://ace.jd.com/
* @author : Emil Zhai (root@derzh.com)
* @modifier : Emil Zhai (root@derzh.com)
* @copyright: Copyright (c) 2020 JD Network Technology Co., Ltd.
*/
.word-break {
word-break: break-word;
}
.word-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
}
.max-row {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
cursor: pointer;
}

View File

@ -0,0 +1,197 @@
/**
* This file is part of the drip-table project.
* @link : https://ace.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';
import { DripTableComponentProps, DripTableComponentSchema } from '../component';
import styles from './index.module.less';
export interface DTCTextSchema extends DripTableComponentSchema {
'ui:type': 'text';
/** 字体大小 */
fontSize?: string;
/** 最大行数,超出展示... */
maxRow?: number;
/** 行高 */
lineHeight?: number;
/** 展示模式:单行文本、多行文本、自定义文本 */
mode?: 'single' | 'multiple' | 'custom';
/** 自定义文本 -- 渲染格式/代码 */
format?: string;
/** 兜底文案 */
noDataValue?: string;
/** 前缀文案 */
prefix?: string;
/** 后缀文案 */
subfix?: string;
enumValue?: string[];
enumLabel?: string[];
tooltip?: boolean | string;
ellipsis?: boolean;
/** 字段配置 */
params?: {
dataIndex: string;
/** 前缀文案 */
prefix?: string;
/** 后缀文案 */
subfix?: string;
enumValue?: string[];
enumLabel?: string[];
tooltip?: boolean | string;
}[];
}
interface DTCTextProps<RecordType> extends DripTableComponentProps<RecordType, DTCTextSchema> { }
interface DTCTextState {}
export default class DTCText<RecordType> extends React.PureComponent<DTCTextProps<RecordType>, DTCTextState> {
private get configured() {
const schema = this.props.schema;
if (schema.mode === 'custom') {
if (schema.format) {
return true;
}
return false;
}
if (schema.mode === 'multiple') {
if (schema.params) {
return schema.params.length > 0;
}
return false;
}
if (schema.mode === 'single') {
if (typeof schema.dataIndex === 'object') {
return Object.keys(schema.dataIndex).length > 0;
}
return !!schema.dataIndex;
}
return false;
}
private get fontSize() {
let fontSize = String(this.props.schema.fontSize || '').trim();
if ((/^[0-9]+$/uig).test(fontSize)) {
fontSize += 'px';
}
return fontSize;
}
public get classNames() {
const schema = this.props.schema;
let classNames = styles['word-break'];
if (schema.ellipsis) {
classNames += ` ${styles['word-ellipsis']}`;
}
if (schema.maxRow) {
classNames += ` ${styles['max-row']}`;
}
return classNames;
}
private get lineHeight() {
return this.props.schema.lineHeight || 1.5;
}
public get styles(): React.CSSProperties {
const schema = this.props.schema;
const wrapperStyles: React.CSSProperties = {
fontSize: this.fontSize,
lineHeight: this.lineHeight,
};
if (schema.maxRow) {
wrapperStyles.WebkitLineClamp = schema.maxRow;
wrapperStyles.maxHeight = `${schema.maxRow * this.lineHeight}em`;
}
return wrapperStyles;
}
public render(): JSX.Element {
const { schema, data } = this.props;
const { mode,
dataIndex,
noDataValue,
format,
prefix,
subfix,
params,
enumValue,
enumLabel,
tooltip } = schema;
if (!this.configured) {
return <div style={{ color: 'red' }}></div>;
}
if (mode === 'custom') {
return (
<pre style={{ fontSize: this.fontSize }}>
{
(format || '')
.replace(/\{\{(.+?)\}\}/uig, (s, s1) => {
try {
const text = new Function('rec', `return ${s1}`)(data);
if (typeof text === 'string') {
return text;
}
return JSON.stringify(text);
} catch {}
return '';
})
}
</pre>
);
}
if (mode === 'single') {
const noDataStr = noDataValue || '';
let value = data[dataIndex as string];
if (enumValue && enumLabel) {
const index = enumValue.indexOf(value);
value = enumLabel[index];
}
const contentStr = `${prefix || ''} ${value || noDataStr} ${subfix || ''}`;
const Tooltip = this.props.driver.components.Tooltip;
return (
<div style={this.styles} className={this.classNames}>
{ tooltip
? (
<Tooltip title={typeof tooltip === 'string' ? tooltip : contentStr} placement="top">
{ contentStr }
</Tooltip>
)
: contentStr }
</div>
);
}
if (mode === 'multiple') {
const noDataStr = noDataValue || '';
const Tooltip = this.props.driver.components.Tooltip;
return (
<div style={this.styles} className={this.classNames}>
{ (params || []).map((config, i) => {
let value = data[config.dataIndex];
if (enumValue && enumLabel) {
const index = enumValue.indexOf(value);
value = enumLabel[index];
}
const contentStr = `${config.prefix || ''} ${value || noDataStr} ${config.subfix || ''}`;
return (
<div key={i}>
{ tooltip
? (
<Tooltip title={typeof tooltip === 'string' ? tooltip : contentStr} placement="top">
{ contentStr }
</Tooltip>
)
: contentStr }
</div>
);
}) }
</div>
);
}
return <div />;
}
}

View File

@ -0,0 +1,25 @@
/**
* 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) 2021 JD Network Technology Co., Ltd.
*/
.search-container {
display: flex;
}
.header-title {
margin-bottom: 0;
line-height: 32px;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-break: break-all;
}
.search-select {
min-width: 72px;
}

View File

@ -0,0 +1,166 @@
/*
* 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) 2021 JD Network Technology Co., Ltd.
*/
import React, { useState } from 'react';
import { DripTableDriver, DripTableRecordTypeBase } from '@/types';
import { DripTableProps } from '@/index';
import RichText from '@/components/RichText';
import styles from './index.module.css';
type Config = {
type: 'title' | 'search' | 'addButton' | 'null';
/**
* 0-24
*/
span: number;
/**
* stringpx也可以是%
*/
width?: number | string;
position: 'topLeft' | 'topCenter' | 'topRight';
};
interface TitleConfig extends Config {
type: 'title';
title: string;
html?: boolean;
}
interface SearchConfig extends Config {
type: 'search';
placeholder?: string;
allowClear?: boolean;
searchBtnText?: string;
searchStyle?: React.CSSProperties;
searchClassName?: string;
size?: 'large' | 'middle' | 'small';
searchKeys?: { label: string; value: number | string }[];
searchKeyValue?: number | string;
props?: Record<string, unknown>;
}
interface AddButtonConfig extends Config {
type: 'addButton';
showIcon?: boolean;
addBtnText?: string;
addBtnStyle?: React.CSSProperties;
addBtnClassName?: string;
}
interface NullConfig extends Config {
type: 'null';
}
export interface DripTableHeaderProps<RecordType extends DripTableRecordTypeBase> {
driver: DripTableDriver<RecordType>;
title?: TitleConfig | NullConfig;
search?: SearchConfig | NullConfig;
addButton?: AddButtonConfig | NullConfig;
onSearch?: DripTableProps<RecordType>['onSearch'];
onAddButtonClick?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
}
const Header = <RecordType extends DripTableRecordTypeBase>(props: DripTableHeaderProps<RecordType>) => {
const Button = props.driver.components.Button;
const Col = props.driver.components.Col;
const Input = props.driver.components.Input;
const PlusOutlined = props.driver.icons.PlusOutlined;
const Row = props.driver.components.Row;
const Select = props.driver.components.Select;
const TableSearch = props.driver.components.TableSearch;
const [searchStr, setSearchStr] = useState('');
const [searchKey, setSearchKey] = useState<SearchConfig['searchKeyValue']>(void 0);
if (!props.title && !props.search && !props.addButton) {
return null;
}
const renderColumnContent = (config: TitleConfig | AddButtonConfig | SearchConfig | NullConfig) => {
if (config.type === 'title') {
return config.html
? <RichText html={config.title} />
: <h3 className={styles['header-title']}>{ config.title }</h3>;
}
if (config.type === 'search') {
if (TableSearch) {
return (
<TableSearch
{...config.props}
driver={props.driver}
onSearch={(searchParams) => { props.onSearch?.(searchParams); }}
/>
);
}
return (
<div style={config.searchStyle} className={`${styles['search-container']} ${config.searchClassName}`}>
{ config.searchKeys && (
<Select
defaultValue={config.searchKeyValue}
className={styles['search-select']}
value={searchKey}
onChange={value => setSearchKey(value)}
>
{ config.searchKeys.map((item, i) => <Select.Option key={i} value={item.value}>{ item.label }</Select.Option>) }
</Select>
) }
<Input.Search
allowClear={config.allowClear}
placeholder={config.placeholder}
enterButton={config.searchBtnText || true}
size={config.size}
value={searchStr}
onChange={e => setSearchStr(e.target.value.trim())}
onSearch={(value) => { props.onSearch?.({ key: searchKey, value }); }}
/>
</div>
);
}
if (config.type === 'addButton') {
return (
<Button
type="primary"
icon={config.showIcon && <PlusOutlined />}
style={config.addBtnStyle}
className={config.addBtnClassName}
onClick={props.onAddButtonClick}
>
{ config.addBtnText }
</Button>
);
}
return null;
};
const renderColumn = (position: 'topLeft' | 'topCenter' | 'topRight') => {
const config = [props.title, props.search, props.addButton].find(item => item && item.position === position);
if (!config) {
return <Col span={0} />;
}
const span = config.span || 8;
return (
<Col span={span} style={{ width: config.width }}>
{ renderColumnContent(config) }
</Col>
);
};
return (
<div className={styles['header-container']}>
<Row>
{ renderColumn('topLeft') }
{ renderColumn('topCenter') }
{ renderColumn('topRight') }
</Row>
</div>
);
};
export default Header;

View File

@ -0,0 +1,45 @@
/**
* 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) 2021 JD Network Technology Co., Ltd.
*/
.drip-table-wrapper {
background: #ffffff;
padding: 0 24px;
width: 100%;
}
.mb2 {
margin-bottom: .5rem;
}
.mr {
margin-right: 8px;
}
.flex {
display: flex;
}
.justify-end {
justify-content: flex-end;
}
.w-100 {
width: 100%;
}
.header-container {
margin: 12px auto;
}
.header-container.left {
margin-left: 0;
}
.header-container.right {
margin-right: 0;
}

View File

@ -0,0 +1,285 @@
/*
* 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) 2021 JD Network Technology Co., Ltd.
*/
import React, { useRef } from 'react';
import { ColumnConfig, DripTableDriver, DripTableReactComponentProps, DripTableRecordTypeBase, DripTableSchema, EventLike } from '@/types';
import { useState, useTable } from '@/hooks';
import ErrorBoundary from '@/components/ErrorBoundary';
import Header from './header';
import VirtualTable from './virtual-table';
import DripTableBuiltInComponents, { DripTableBuiltInComponentEvent, DripTableComponentProps, DripTableComponentSchema } from './components';
import styles from './index.module.css';
export interface DripTableProps<RecordType extends DripTableRecordTypeBase, CustomComponentEvent extends EventLike = never, Ext = unknown> {
/**
*
*/
driver: DripTableDriver<RecordType>;
/**
*
*/
className?: string;
/**
*
*/
style?: React.CSSProperties;
/**
* Schema
*/
schema: DripTableSchema;
/**
*
*/
rowKey?: string;
/**
*
*/
dataSource: RecordType[];
/**
*
*/
selectedRowKeys?: React.Key[];
/**
*
*/
ext?: Ext;
/**
*
*/
total?: number;
/**
*
*/
currentPage?: number;
/**
*
*/
loading?: boolean;
/**
*
*/
components?: {
[libName: string]: {
[componentName: string]:
new (props: DripTableComponentProps<RecordType, DripTableComponentSchema, CustomComponentEvent, Ext>)
=> React.PureComponent<DripTableComponentProps<RecordType, DripTableComponentSchema, CustomComponentEvent, Ext>>;
};
};
/** 生命周期 */
componentDidMount?: () => void;
componentDidUpdate?: () => void;
componentWillUnmount?: () => void;
/**
*
*/
onRowClick?: (record: RecordType | RecordType[], index?: number | string | (number | string)[]) => void;
/**
*
*/
onRowDoubleClick?: (record: RecordType | RecordType[], index?: number | string | (number | string)[]) => void;
/**
*
*/
onSelectionChange?: (selectedKeys: React.Key[], selectedRows: RecordType[]) => void;
/**
*
*/
onSearch?: (searchParams: { key?: number | string; value: string } | Record<string, unknown>) => void;
/**
*
*/
onAddButtonClick?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
/**
*
*/
onFilter?: (column: ColumnConfig) => void;
/**
* /
*/
onPageChange?: (currentPage: number, pageSize: number) => void;
/**
*
*/
onEvent?: (event: DripTableBuiltInComponentEvent | CustomComponentEvent, record: RecordType, index: number) => void;
}
const DripTable = <RecordType extends Record<string, unknown>, CustomComponentEvent extends EventLike = never, Ext = unknown>
(props: DripTableProps<RecordType, CustomComponentEvent, Ext>): JSX.Element => {
const Table = props.driver.components?.Table;
const Popover = props.driver.components?.Popover;
const QuestionCircleOutlined = props.driver.icons?.QuestionCircleOutlined;
type TableColumn = NonNullable<DripTableReactComponentProps<typeof Table>['columns']>[number];
const initialState = useTable();
const paginationConfig = props.schema?.configs?.pagination || {} as { current: number; total: number; pageSize: number };
initialState.pagination.pageSize = paginationConfig.pageSize || 10;
const [tableState, setTableState] = initialState._CTX_SOURCE === 'CONTEXT' ? useState(initialState) : [initialState, initialState.setTableState];
const rootRef = useRef<HTMLDivElement>(null); // ProTable组件的ref
const {
rowKey = '$id',
} = props;
const dataSource = props.dataSource.map((item, index) => ({
...item,
[rowKey]: typeof item[rowKey] === 'undefined' ? index : item[rowKey],
}));
/**
*
* @param schema Schema
* @returns
*/
const renderGenerator = (schema: ColumnConfig): (value: unknown, record: RecordType, index: number) => JSX.Element | string | null => {
const BuiltInComponent = DripTableBuiltInComponents[schema['ui:type']] as new() => React.PureComponent<DripTableComponentProps<RecordType>>;
if (BuiltInComponent) {
return (value, record, index) => (
<BuiltInComponent
driver={props.driver}
value={value}
data={record}
schema={{ ...schema, ...schema['ui:props'] }}
ext={props.ext}
fireEvent={event => props.onEvent?.(event, record, index)}
/>
);
}
const [libName, componentName] = schema['ui:type'].split('::');
if (libName && componentName) {
const ExtraComponent = props.components?.[libName]?.[componentName];
if (ExtraComponent) {
return (value, record, index) => (
<ExtraComponent
driver={props.driver}
value={value}
data={record}
schema={schema}
ext={props.ext}
fireEvent={event => props.onEvent?.(event, record, index)}
/>
);
}
}
return value => JSON.stringify(value);
};
/**
* Schema
* @param schemaColumn Schema Column
* @returns
*/
const columnGenerator = (schemaColumn: ColumnConfig): TableColumn => {
let width = String(schemaColumn.width).trim();
if ((/^[0-9]+$/uig).test(width)) {
width += 'px';
}
const column: TableColumn = {
width,
align: schemaColumn.align,
title: schemaColumn.title,
dataIndex: schemaColumn.dataIndex,
fixed: schemaColumn.fixed,
};
if (schemaColumn.description) {
column.title = (
<div>
<span style={{ marginRight: '6px' }}>{ schemaColumn.title }</span>
<Popover placement="top" title="" content={schemaColumn.description}>
<QuestionCircleOutlined />
</Popover>
</div>
);
}
if (props.schema.configs.ellipsis) {
column.ellipsis = true;
}
if (!column.render) {
column.render = renderGenerator(schemaColumn);
}
return column;
};
const tableProps: Parameters<DripTableDriver<RecordType>['components']['Table']>[0] = {
rowKey,
columns: props.schema.columns?.map(columnGenerator) || [],
dataSource,
pagination: props.schema.configs.pagination === false
? false as const
: {
onChange: (page, pageSize) => {
if (pageSize === void 0) {
pageSize = tableState.pagination.pageSize;
}
setTableState({ pagination: { ...tableState.pagination, current: page, pageSize } });
props.onPageChange?.(page, pageSize);
},
size: props.schema.configs.pagination?.size === void 0 ? 'small' : props.schema.configs.pagination.size,
pageSize: tableState.pagination.pageSize,
total: props.total === void 0 ? dataSource.length : props.total,
current: props.currentPage || tableState.pagination.current,
position: [props.schema.configs.pagination?.position || 'bottomRight'],
showLessItems: props.schema.configs.pagination?.showLessItems,
showQuickJumper: props.schema.configs.pagination?.showQuickJumper,
showSizeChanger: props.schema.configs.pagination?.showSizeChanger,
},
loading: props.loading,
size: props.schema.configs.size,
bordered: props.schema.configs.bordered,
innerBordered: props.schema.configs.innerBordered,
// ellipsis: schema.configs.ellipsis,
sticky: props.schema.configs.isVirtualList ? false : props.schema.configs.sticky,
rowSelection: props.schema.configs.rowSelection && !props.schema.configs.isVirtualList
? {
selectedRowKeys: props.selectedRowKeys || tableState.selectedRowKeys,
onChange: (selectedKeys, selectedRows) => {
setTableState({ selectedRowKeys: [...selectedKeys] });
props.onSelectionChange?.(selectedKeys, selectedRows);
},
}
: void 0,
};
return (
<ErrorBoundary driver={props.driver}>
<div
className={`${styles['drip-table-wrapper']} ${props.className || ''}`}
style={props.style}
ref={rootRef}
>
{
props.schema.configs.header
? (
<Header
{...(typeof props.schema.configs.header !== 'boolean' ? props.schema.configs.header : {})}
driver={props.driver}
onSearch={props.onSearch}
onAddButtonClick={props.onAddButtonClick}
/>
)
: null
}
{
props.schema.configs.isVirtualList
? (
<VirtualTable
{...tableProps}
driver={props.driver}
scroll={{ y: props.schema.configs.scrollY || 300, x: '100vw' }}
/>
)
: <Table {...tableProps} />
}
</div>
</ErrorBoundary>
);
};
export default DripTable;

View File

@ -0,0 +1,27 @@
/**
* 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) 2021 JD Network Technology Co., Ltd.
*/
/**
*
* @param data
* @param indexes
* @param defaultValue
* @returns
*/
export const get = (data: unknown, indexes: string | string[], 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,14 @@
/**
* This file is part of the drip-table launch.
* @link : https://drip-table.jd.com/
* @author : Emil Zhai (root@derzh.com)
* @modifier : Emil Zhai (root@derzh.com)
* @copyright: Copyright (c) 2021 JD Network Technology Co., Ltd.
*/
.virtual-table-cell {
box-sizing: border-box;
padding: 12px 8px;
border-bottom: 1px solid #f0f0f0;
overflow-wrap: break-word;
}

View File

@ -0,0 +1,161 @@
import React, { useState, useEffect, useRef } from 'react';
import { VariableSizeGrid } from 'react-window';
import ResizeObserver from 'rc-resize-observer';
import { DripTableDriver, DripTableRecordTypeBase } from '../..';
import styles from './index.module.css';
import { get } from '../utils';
// 根据size来控制行高
const rowHeightMap = {
small: 49,
middle: 54,
large: 88,
};
function VirtualTable<RecordType extends DripTableRecordTypeBase>(props: { driver: DripTableDriver<RecordType> } & Parameters<DripTableDriver<RecordType>['components']['Table']>[0]) {
const { columns = [], scroll, size, driver } = props;
const Table = driver.components.Table;
const [tableWidth, setTableWidth] = useState(0);
const rowHeight = rowHeightMap[size || 'middle'] || rowHeightMap.middle;
// 减去已经设定的宽度,剩下的宽度均分
const initWidthColumn = columns.filter(c => c.width && c.width !== 'undefined');
const widthColumnCount = columns.length - initWidthColumn.length;
const initWidth = initWidthColumn.reduce((summary, c) => summary + ((typeof c.width === 'string' ? Number.parseFloat(c.width) : c.width) || 0), 0);
const restWidth = tableWidth - initWidth;
// 如果当设定宽度大于table宽度则默认剩余平均宽度为100
const restWidthAvg = restWidth > 0 ? Math.floor(restWidth / widthColumnCount) : 100;
const mergedColumns = columns.map((column) => {
if (column.width && column.width !== 'undefined') {
if (typeof column.width === 'string') {
column.width = Number(column.width.replace('px', ''));
}
return column;
}
return {
...column,
width: restWidthAvg,
};
});
const fixedColumns = mergedColumns.filter(c => c.fixed);
const fixedColumnsWidth = fixedColumns.reduce((summary, c) => summary + ((typeof c.width === 'string' ? Number.parseFloat(c.width) : c.width) || 0), 0);
const gridRef = useRef<VariableSizeGrid>(null);
const fixedGridRef = useRef<VariableSizeGrid>(null);
// const [refConnector] = useState((): { scrollLeft: number } => {
// const connector = { scrollLeft: 0 };
// Object.defineProperty(connector, 'scrollLeft', {
// get: () => null,
// set: (scrollLeft: number) => { gridRef.current?.scrollTo({ scrollLeft }); },
// });
// return connector;
// });
const resetVirtualGrid = () => {
gridRef.current?.resetAfterIndices({
columnIndex: 0,
rowIndex: 0,
shouldForceUpdate: true,
});
};
useEffect(() => resetVirtualGrid, [tableWidth]);
const renderVirtualList: NonNullable<Parameters<DripTableDriver<RecordType>['components']['Table']>[0]['components']>['body'] = (rawData, { scrollbarSize, ref, onScroll }) => {
// if (ref && 'current' in ref) {
// ref.current = refConnector;
// }
const totalHeight = rawData.length * rowHeight;
const renderCell = ({ columnIndex, rowIndex, style }: { columnIndex: number; rowIndex: number; style: React.CSSProperties }) => {
const columnItem = mergedColumns[columnIndex];
const dataItem = rawData[rowIndex];
const value = columnItem.dataIndex ? get(dataItem, columnItem.dataIndex) : dataItem;
return (
<div className={styles['virtual-table-cell']} style={style}>
{
columnItem.render
? columnItem.render(value, dataItem, rowIndex)
: String(value)
}
</div>
);
};
const scrollY = (typeof scroll?.y === 'string' ? Number.parseFloat(scroll?.y) : scroll?.y) || 0;
// 暂时用盖住的方式来展示背景色也强制白色层级999应该暂时满足了
return (
<div style={{ position: 'relative' }}>
{
fixedColumns.length > 0
? (
<VariableSizeGrid
ref={fixedGridRef}
style={{ overflowY: 'hidden', position: 'absolute', top: 0, left: 0, zIndex: 999, width: fixedColumnsWidth, background: '#fff' }}
className="virtual-grid"
columnCount={fixedColumns.length}
columnWidth={(index: number) => {
const width = Number.parseFloat(String(fixedColumns[index].width)) || 0;
return totalHeight > scrollY && index === fixedColumns.length - 1
? width - scrollbarSize - 1
: width;
}}
height={scrollY}
rowCount={rawData.length}
rowHeight={() => rowHeight}
width={fixedColumnsWidth}
>
{ renderCell }
</VariableSizeGrid>
)
: null
}
<VariableSizeGrid
ref={gridRef}
className="virtual-grid"
columnCount={mergedColumns.length}
columnWidth={(index: number) => {
const { width } = mergedColumns[index];
return totalHeight > scrollY && index === mergedColumns.length - 1
? (width as number) - scrollbarSize - 1
: (width as number);
}}
height={scrollY}
rowCount={rawData.length}
rowHeight={() => rowHeight}
width={tableWidth}
onScroll={({ scrollLeft, scrollTop, scrollUpdateWasRequested }: { scrollLeft: number; scrollTop: number; scrollUpdateWasRequested: boolean }) => {
onScroll({ scrollLeft });
if (!scrollUpdateWasRequested) {
fixedGridRef.current?.scrollTo({ scrollLeft: 0, scrollTop });
}
}}
>
{ renderCell }
</VariableSizeGrid>
</div>
);
};
return (
<ResizeObserver
onResize={({ width }) => {
setTableWidth(width);
}}
>
<Table
{...props}
columns={mergedColumns}
components={{
body: renderVirtualList,
}}
/>
</ResizeObserver>
);
}
export default VirtualTable;

View File

@ -0,0 +1,31 @@
/**
* 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) 2021 JD Network Technology Co., Ltd.
*/
import { useReducer, useContext, SetStateAction } from 'react';
import { DripTableContext, DripTableStoreContext } from './context';
// 使用最顶层组件的 setState
export const useTable = () => useContext(DripTableContext);
// 组件最顶层传入的所有props
export const useStore = () => useContext(DripTableStoreContext);
/**
* 使
* @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,6 @@
export * from './drip-table-provider';
export * from './types';
export { default as builtInComponents } from './drip-table/components';
export type { DripTableComponentProps, DripTableComponentSchema } from './drip-table/components';
export type { DripTableProps } from './drip-table';
export { default } from './drip-table';

View File

@ -0,0 +1,33 @@
/**
* This file is part of the drip-table project.
* @link : https://ace.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,292 @@
/**
* 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) 2021 JD Network Technology Co., Ltd.
*/
import React from 'react';
import { DripTableHeaderProps } from './drip-table/header';
import { DripTableComponentSchema } from './drip-table/components';
export interface StringDataSchema {
type: 'string';
maxLength?: number;
minLength?: number;
pattern?: string;
default?: string;
enumValue?: string[];
enumLabel?: string[];
transform?: ('trim' | 'toUpperCase' | 'toLowerCase')[];
}
export interface NumberDataSchema {
type: 'number' | 'integer';
minimum?: number;
exclusiveMinimum?: number;
maximum?: number;
exclusiveMaximum?: number;
default?: number;
enumValue?: number[];
enumLabel?: string[];
}
export interface BooleanDataSchema {
type: 'boolean';
default?: boolean;
checkedValue?: string | number;
uncheckedValue?: string | number;
}
export interface NullDataSchema {
type: 'null';
default?: undefined | null;
}
export interface ObjectDataSchema {
type: 'object';
default?: Record<string, unknown>;
properties?: {
[key: string]: DataSchema;
};
}
export interface ArrayDataSchema {
type: 'array';
items?: DataSchema | DataSchema[];
default?: unknown[];
}
export type DataSchema =
| StringDataSchema
| NumberDataSchema
| BooleanDataSchema
| ObjectDataSchema
| NullDataSchema
| ArrayDataSchema;
export interface UISchema {
/**
* `命名空间::组件名称` components
*
*/
'ui:type': string;
'ui:props': {
[key: string]: unknown;
};
}
export type ColumnConfig = DripTableComponentSchema & UISchema & DataSchema & {
/**
*
*/
filtered?: boolean | {
/**
* icons antd icon
*/
icon?: string;
options?: { label: string; value: string }[];
resetText?: string;
confirmText?: string;
};
/** 是否支持排序 */
sorter?: boolean | {
ascendIcon?: string;
descendIcon?: string;
};
}
export interface DripTableSchema {
'$schema': 'http://json-schema.org/draft/2019-09/schema#'
| 'http://json-schema.org/draft-07/schema#'
| 'http://json-schema.org/draft-06/schema#'
| 'http://json-schema.org/draft-04/schema#';
configs: {
/** 是否展示表格边框 */
bordered?: boolean;
/** 是否展示表格内部边框 */
innerBordered?: boolean;
/** 是否展示搜索栏以及配置 */
header?: boolean | Omit<DripTableHeaderProps<DripTableRecordTypeBase>, 'driver' | 'onSearch' | 'onAddButtonClick'>;
/** 是否展示分页以及配置 */
pagination?: false | {
size?: 'small' | 'default';
pageSize: number;
position?: 'bottomLeft' | 'bottomCenter' | 'bottomRight';
showLessItems?: boolean;
showQuickJumper?: boolean;
showSizeChanger?: boolean;
};
size?: 'small' | 'middle' | 'large' | undefined;
/** 粘性头部 */
sticky?: boolean;
/** 是否支持选择栏 */
rowSelection?: boolean;
/** 是否平均列宽 */
ellipsis?: boolean;
/** 无数据提示 */
nodata?: {
image: string;
text: string;
};
/** 是否开启虚拟列表 */
isVirtualList?: boolean;
/** 虚拟列表滚动高度 */
scrollY?: number;
};
columns: ColumnConfig[];
}
export type DripTableRecordTypeBase = Record<string, unknown>;
export type DripTableReactComponent<P> = (props: React.PropsWithChildren<P>) => React.ReactElement | null;
export type DripTableReactComponentProps<T> = T extends DripTableReactComponent<infer P> ? P : never;
export interface DripTableDriver<RecordType> {
/**
*
*/
components: {
Button: DripTableReactComponent<{
style?: React.CSSProperties;
className?: string;
type?: 'primary';
icon?: React.ReactNode;
onClick?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
}>;
Col: DripTableReactComponent<{
style?: React.CSSProperties;
span?: number;
}>;
ConfigProvider: DripTableReactComponent<Record<string, unknown>>;
Image: DripTableReactComponent<{
width?: number;
height?: number;
src?: string;
preview?: boolean;
fallback?: string;
}>;
Input: {
Search: DripTableReactComponent<{
style?: React.CSSProperties;
allowClear?: boolean;
placeholder?: string;
enterButton?: string | true;
size?: 'large' | 'middle' | 'small';
value?: string;
onChange?: React.ChangeEventHandler<HTMLInputElement>;
onSearch?: (value: string) => void;
}>;
};
Popover: DripTableReactComponent<{
placement?: 'top';
title?: string;
content?: React.ReactNode;
}>;
Result: DripTableReactComponent<{
status?: 'error';
title?: string;
extra?: string;
}>;
Row: DripTableReactComponent<{
style?: React.CSSProperties;
}>;
Select: DripTableReactComponent<{
className?: string;
defaultValue?: string | number;
value?: string | number;
onChange?: (value: string | number) => void;
}> & {
Option: DripTableReactComponent<Record<string, unknown> & { value: string | number; children: React.ReactNode }>;
};
Table: DripTableReactComponent<{
rowKey?: string;
columns?: {
width?: string | number;
align?: 'left' | 'center' | 'right';
title?: string | JSX.Element;
dataIndex?: string | string[];
fixed?: boolean;
ellipsis?: boolean;
render?: (value: unknown, record: RecordType, rowIndex: number) => React.ReactNode;
}[];
dataSource?: RecordType[];
pagination?: false | {
onChange?: (page: number, pageSize?: number) => void;
size?: 'small' | 'default';
pageSize?: number;
total?: number;
current?: number;
position?: ('topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight')[];
showLessItems?: boolean;
showQuickJumper?: boolean;
showSizeChanger?: boolean;
};
loading?: boolean;
size?: 'large' | 'middle' | 'small';
bordered?: boolean;
innerBordered?: boolean;
sticky?: boolean;
rowSelection?: {
selectedRowKeys?: React.Key[];
onChange?: (selectedKeys: React.Key[], selectedRows: RecordType[]) => void;
};
scroll?: {
x?: string | number;
y?: string | number;
};
components?: {
body?: (data: readonly RecordType[], info: {
scrollbarSize: number;
ref: React.Ref<{
scrollLeft: number;
}>;
onScroll: (info: {
currentTarget?: HTMLElement;
scrollLeft?: number;
}) => void;
}) => React.ReactNode;
};
}>;
TableSearch?: DripTableReactComponent<Record<string, unknown> & {
driver: DripTableDriver<RecordType>;
onSearch: (searchParams: Record<string, unknown>) => void;
}>;
Tag: DripTableReactComponent<{
style?: React.CSSProperties;
color?: string;
}>;
Tooltip: DripTableReactComponent<{
title: React.ReactNode | (() => React.ReactNode);
placement?: 'top';
}>;
Typography: {
Text: DripTableReactComponent<{
style?: React.CSSProperties;
ellipsis?: boolean;
copyable?: boolean | {
text?: string;
onCopy?: () => void;
};
}>;
};
message: {
success: (message: string) => void;
};
};
/**
*
*/
icons: {
PlusOutlined: DripTableReactComponent<unknown>;
QuestionCircleOutlined: DripTableReactComponent<unknown>;
};
/**
*
*/
locale: unknown;
}
export type EventLike<T = { type: string }> = T extends { type: string } ? T : never;
export interface DripTableCustomEvent<TN> extends EventLike<{ type: 'custom' }> { name: TN }

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": true,
"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"]
}

21
tsconfig.json Normal file
View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"strict": true,
"target": "es5",
"moduleResolution": "node",
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"declaration": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"baseUrl": ".",
"paths": {
"drip-table": ["./packages/drip-table/src"]
}
},
"rules": {
"react/prop-types": 0
},
"exclude": ["node_modules", "dist"]
}

11920
yarn.lock Normal file

File diff suppressed because it is too large Load Diff