├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .github
└── workflows
│ └── publish-lastest.yml
├── .gitignore
├── .husky
├── commit-msg
└── pre-commit
├── .markdownlint.json
├── .markdownlintignore
├── .prettierrc.js
├── .stylelintignore
├── .stylelintrc.js
├── CHANGELOG.md
├── LICENSE
├── README.md
├── abc.json
├── build.json
├── build.lowcode.js
├── commitlint.config.js
├── demo
├── 00_basic.md
├── O10_p.md
├── O11_text.md
├── O1_dashboard.md
├── O2_portal.md
├── O3_portal-n2.md
├── O4_page-form.md
├── O5_page-table.md
├── O6_workplace.md
├── O8_break-points.md
└── O9_block.md
├── f2elint.config.js
├── global.d.ts
├── jest.config.js
├── lowcode
├── common
│ ├── divider.tsx
│ ├── hotkeys.ts
│ ├── keybindingService.ts
│ ├── split
│ │ ├── auto-block.ts
│ │ ├── auto-cell.ts
│ │ └── block-resize-map.ts
│ └── util.ts
├── default-schema.ts
├── index.scss
├── meta.ts
├── metas
│ ├── block.ts
│ ├── cell.tsx
│ ├── col.ts
│ ├── enhance
│ │ ├── callbacks.ts
│ │ └── experimentals.ts
│ ├── fixed-container.ts
│ ├── fixed-point.ts
│ ├── grid.ts
│ ├── nav-aside.ts
│ ├── p.ts
│ ├── page-aside.ts
│ ├── page-content.ts
│ ├── page-footer.ts
│ ├── page-header.ts
│ ├── page-nav.ts
│ ├── page.ts
│ ├── pro-card.ts
│ ├── row.ts
│ ├── section.ts
│ └── setter
│ │ ├── background.ts
│ │ ├── flex-column.tsx
│ │ ├── flex-row.tsx
│ │ ├── flex.tsx
│ │ ├── gap.ts
│ │ ├── height.ts
│ │ ├── min-height.ts
│ │ ├── operations.ts
│ │ ├── padding.ts
│ │ ├── tooltip-label.tsx
│ │ └── width.ts
├── names.ts
└── view.tsx
├── package.json
├── src
├── block.tsx
├── cell.tsx
├── col.tsx
├── common
│ ├── constant.ts
│ └── context.ts
├── fixed-container.tsx
├── fixed-point.tsx
├── grid.tsx
├── hooks
│ ├── use-combine-ref.ts
│ ├── use-flex-class-names.ts
│ └── use-guid.ts
├── index.scss
├── index.ts
├── p.tsx
├── page
│ ├── aside.tsx
│ ├── content.tsx
│ ├── index.tsx
│ ├── nav.tsx
│ ├── page-footer.tsx
│ └── page-header.tsx
├── row.tsx
├── scss
│ ├── block.scss
│ ├── grid.scss
│ ├── header-footer.scss
│ ├── p.scss
│ ├── page.scss
│ ├── row-col-cell.scss
│ ├── section.scss
│ ├── space.scss
│ ├── text.scss
│ └── variable.scss
├── section.tsx
├── space.tsx
├── text.tsx
├── types.ts
└── utils
│ └── index.ts
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 | quote_type = single
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | build/
3 | dist/
4 | **/*.min.js
5 | **/*-min.js
6 | **/*.bundle.js
7 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | 'eslint-config-ali/typescript/react',
4 | 'prettier',
5 | 'prettier/@typescript-eslint',
6 | 'prettier/react',
7 | ],
8 | };
9 |
--------------------------------------------------------------------------------
/.github/workflows/publish-lastest.yml:
--------------------------------------------------------------------------------
1 | name: publish-latest
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 | branches: [master]
8 | types: ['closed']
9 |
10 | jobs:
11 | build-and-publish-npm:
12 | runs-on: ubuntu-latest
13 |
14 | strategy:
15 | matrix:
16 | node-version: [14.x]
17 |
18 | steps:
19 | - uses: actions/checkout@v3
20 | - name: Use Node.js ${{ matrix.node-version }}
21 | uses: actions/setup-node@v3
22 | with:
23 | node-version: 16
24 | registry-url: 'https://registry.npmjs.org'
25 | - run: yarn install
26 | - run: npm publish
27 | env:
28 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | lowcode_es/
2 | lowcode_lib/
3 | # See https://help.github.com/ignore-files/ for more about ignoring files.
4 |
5 | # dependencies
6 | node_modules/
7 |
8 | # production
9 | build/
10 | dist/
11 | tmp/
12 | lib/
13 | es/
14 | .tmp/
15 | .idea/
16 |
17 | # misc
18 | .happypack
19 | .DS_Store
20 | *.swp
21 | *.dia~
22 |
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 |
28 | yarn.lock
29 | package-lock.json
30 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx commitlint --edit
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 |
--------------------------------------------------------------------------------
/.markdownlint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "markdownlint-config-ali"
3 | }
4 |
--------------------------------------------------------------------------------
/.markdownlintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | build/
3 | dist/
4 |
5 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 100,
3 | tabWidth: 2,
4 | semi: true,
5 | singleQuote: true,
6 | trailingComma: 'all',
7 | arrowParens: 'always',
8 | };
9 |
--------------------------------------------------------------------------------
/.stylelintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | build/
3 | dist/
4 | **/*.min.css
5 | **/*-min.css
6 | **/*.bundle.css
7 |
8 |
--------------------------------------------------------------------------------
/.stylelintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: 'stylelint-config-ali',
3 | };
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ### [2.4.1](https://github.com/alibaba-fusion/layout/compare/v2.4.0...v2.4.1) (2023-10-12)
6 |
7 |
8 | ### Bug Fixes
9 |
10 | * 修复自由布局错误和不能拖拽的问题 ([a4c7576](https://github.com/alibaba-fusion/layout/commit/a4c757627ac417523a725b1fa331eed7797ff533))
11 |
12 | ## [2.4.0](https://github.com/alibaba-fusion/layout/compare/v2.3.0...v2.4.0) (2023-08-30)
13 |
14 |
15 | ### Features
16 |
17 | * 去除自然布局的 disableBehaviors, 使其可以被删除 ([798418c](https://github.com/alibaba-fusion/layout/commit/798418cff9fb4876d626b7cc3de0df5aa4d496cf))
18 |
19 | ## [2.3.0](https://github.com/alibaba-fusion/layout/compare/v2.2.4...v2.3.0) (2023-08-01)
20 |
21 |
22 | ### Features
23 |
24 | * 补齐css 变量默认值 ([75ccc12](https://github.com/alibaba-fusion/layout/commit/75ccc129c6a5db8684a6ee9e69ba572225df0575))
25 |
26 | ### [2.2.4](https://github.com/alibaba-fusion/layout/compare/v2.2.3...v2.2.4) (2023-07-28)
27 |
28 | ### [2.2.3](https://github.com/alibaba-fusion/layout/compare/v2.2.2...v2.2.3) (2023-07-28)
29 |
30 | ### [2.2.2](https://github.com/alibaba-fusion/layout/compare/v2.2.1...v2.2.2) (2023-07-27)
31 |
32 |
33 | ### Bug Fixes
34 |
35 | * lowcode_es/view 内 src 引用报错 ([1f33a1f](https://github.com/alibaba-fusion/layout/commit/1f33a1f1ef7e0dda6c627235f289c66f98e96d1d))
36 |
37 | ### [2.2.1](https://github.com/alibaba-fusion/layout/compare/v2.2.0...v2.2.1) (2023-07-27)
38 |
39 | ## 2.2.0 (2023-07-27)
40 |
41 |
42 | ### Features
43 |
44 | * 调整 peer ([594f5ef](https://github.com/alibaba-fusion/layout/commit/594f5ef1cb7ccc462f29098ffeb6427052fe8a8f))
45 | * 调整使用文档链接 ([5b3dfca](https://github.com/alibaba-fusion/layout/commit/5b3dfcacbac7cdd1e4d038a336a60f6b650b0093))
46 | * 段落 verMargin 更名为 hasVerSpacing ([6484ee8](https://github.com/alibaba-fusion/layout/commit/6484ee82026ce31f4459ea2542b4b130da79c5f0))
47 | * 上传新的开源版本 ([2f3af5c](https://github.com/alibaba-fusion/layout/commit/2f3af5cd6e5cd6b13a3ed3b6abfe224459a3a294))
48 | * 升级 build-plugin-lowcode,适配 lowcode 打包 ([d82a6c6](https://github.com/alibaba-fusion/layout/commit/d82a6c663a8e0dd7d7f64417243e9eca8f7d86f7))
49 | * 使用 lodash-es ([42172a3](https://github.com/alibaba-fusion/layout/commit/42172a35cf93de4c8e42338ad1a379d5cbd48dbd))
50 | * 移除 tab 功能 ([e7a21ed](https://github.com/alibaba-fusion/layout/commit/e7a21ed3818adcad9dc9b4b7b26e3ddd17b3813f))
51 | * 移除低代码分页配置 ([5b4efc4](https://github.com/alibaba-fusion/layout/commit/5b4efc4395278593820f940786d5b4a9e43a52d1))
52 | * 移除x-if 判断 ([0c20e48](https://github.com/alibaba-fusion/layout/commit/0c20e48a2806c30ccc21fb3fe736636113ad85fa))
53 | * 优化部分 section 和 page 的代码 ([6c9cbc9](https://github.com/alibaba-fusion/layout/commit/6c9cbc98d218a7ef1881e9d33df651bba7017db9))
54 | * 自动引入 css ([f0d2b08](https://github.com/alibaba-fusion/layout/commit/f0d2b08997ca68127dcdd80ff4c553dcae09ebf9))
55 | * 最大支持 24 栅栏 ([a0544de](https://github.com/alibaba-fusion/layout/commit/a0544de80c82442e526d97f5d6bf72d01366b75f))
56 | * add adaptors for design components ([1526f4a](https://github.com/alibaba-fusion/layout/commit/1526f4a9ccb596eaf73ca1f356587bb426d50904))
57 | * add demo ([3c2907f](https://github.com/alibaba-fusion/layout/commit/3c2907f34fb689fcb03240c7f75c7bd04b50ecc0))
58 | * add tabletColSpan phoneColSpan for responsive ([431712c](https://github.com/alibaba-fusion/layout/commit/431712c9af349f17b5fd12f9bda703fccc7831cf))
59 | * add ts ([40a305e](https://github.com/alibaba-fusion/layout/commit/40a305e321645f12a80be06833a68843b8279f92))
60 | * add types for AliLowcodeEngine ([2ba5718](https://github.com/alibaba-fusion/layout/commit/2ba57186a916e43b2e1135713889f5ea23b18f26))
61 | * alias import ([9b92166](https://github.com/alibaba-fusion/layout/commit/9b921665c80ddddefe1d1736392586ac278004a4))
62 | * fix conflict ([969a3c5](https://github.com/alibaba-fusion/layout/commit/969a3c5723edec0bc33c4283b5710dfea811f1f2))
63 | * fix conflict ([888a3ea](https://github.com/alibaba-fusion/layout/commit/888a3ea9d98d9d95aae1b487296df970b896d15b))
64 | * lowcode ([c4a3f99](https://github.com/alibaba-fusion/layout/commit/c4a3f9959c66b3f5f10b15869769559c5a6ecc40))
65 | * p, space 支持数值类型的 spacing 和 size 传入 ([d629c79](https://github.com/alibaba-fusion/layout/commit/d629c79445f5cc3b3ef160d07595213c48cfe0ab))
66 | * Row/Col/Cell 支持height ([f55273a](https://github.com/alibaba-fusion/layout/commit/f55273a9741263bdf3dd30c3f87e20065a97a394))
67 | * suport lowcodeEngine 2.0 ([33d9299](https://github.com/alibaba-fusion/layout/commit/33d9299837928f65c303ebe155157213cfae3806))
68 | * update lock.json ([5057bc7](https://github.com/alibaba-fusion/layout/commit/5057bc70e8dfe27132cb602b2c1aa0f43fcf19cc))
69 | * update version of lowcode-engine and lowcode-materials ([087c58a](https://github.com/alibaba-fusion/layout/commit/087c58a510c5c61863ede54f8b55c08337940784))
70 |
71 |
72 | ### Bug Fixes
73 |
74 | * header 不支持 noBottomPadding ([255c115](https://github.com/alibaba-fusion/layout/commit/255c1154b6d4eda957106828c7c9d1749ca832e8))
75 | * 部分场景下布局渲染异常 ([4831f6d](https://github.com/alibaba-fusion/layout/commit/4831f6d4d3e776a87a63b6df203cd081e3f71264))
76 | * 低代码引擎升级导致的容器组件内小剪刀无法使用 ([7460625](https://github.com/alibaba-fusion/layout/commit/7460625bab4d45f7113f8e9c6d082542c2f51196))
77 | * 断点调整错误 ([768beb4](https://github.com/alibaba-fusion/layout/commit/768beb48c302388a02dea3cd527214e98a6e3ac8))
78 | * 构建产物 types 路径无效 ([0971d41](https://github.com/alibaba-fusion/layout/commit/0971d41599df8290ba321db6cfa011bd9fe5ccc9))
79 | * 类名错误 ([d4f1e4c](https://github.com/alibaba-fusion/layout/commit/d4f1e4c4977657c4e7c459f5902fa403687ed613))
80 | * 拖拽修改容器高度,导致容器变空白 ([04674ed](https://github.com/alibaba-fusion/layout/commit/04674ed43cb0692e390dfdd3418d6bf77f116c46))
81 | * 拖拽修改容器高度,导致容器变空白 ([d5383fe](https://github.com/alibaba-fusion/layout/commit/d5383fe65a9b53c85b34b176267b8b54b254c3b8))
82 | * 选中容器时,布局页面会闪一下 ([1e52924](https://github.com/alibaba-fusion/layout/commit/1e52924f58a7bc1dcdb98de4299dce265b574bbe))
83 | * 自由容器 flex 布局 ([9a4e642](https://github.com/alibaba-fusion/layout/commit/9a4e642da44e1f2d1ddf53fccae846c7619edf44))
84 | * add callback ([d06e895](https://github.com/alibaba-fusion/layout/commit/d06e895e989c569b20c11f769f6a74c060a2dde1))
85 | * css 链接访问不到 ([2f72a47](https://github.com/alibaba-fusion/layout/commit/2f72a47f840529841dd18f09587b714c56d6907d))
86 | * divider disappear ([5f504bf](https://github.com/alibaba-fusion/layout/commit/5f504bf7a9ee7b3763769ca6b15f8b515f38a360))
87 | * fix shortcut key failure problem ([bd231b9](https://github.com/alibaba-fusion/layout/commit/bd231b9ead32a88f1f4932660c08fe1b36b6de17))
88 | * fix the problem that free nodes cannot be dragged ([1bec76e](https://github.com/alibaba-fusion/layout/commit/1bec76e137996a197fdab9aa4ab90d7f020eb80d))
89 | * onChangeSelection 后剪刀位置异常 ([8d02e6a](https://github.com/alibaba-fusion/layout/commit/8d02e6a990c485bbe8fd8e13c3ca3cc18ce3271c))
90 | * props initial value ([2c6c619](https://github.com/alibaba-fusion/layout/commit/2c6c6190dbd8d48386e1f1cff2bc133f60a39846))
91 | * remove isTab config ([d10e659](https://github.com/alibaba-fusion/layout/commit/d10e659bb6022d381e75e21f8c76363853ff0183))
92 | * set device and auto set columns ([04ca451](https://github.com/alibaba-fusion/layout/commit/04ca45160adb6e86402d0629c959987ef4c6289a))
93 | * switch PureComponent to Component ([49fb228](https://github.com/alibaba-fusion/layout/commit/49fb22885615053e966bbd4aec9d8a8b1414abec))
94 |
95 | ## 2.0.0
96 |
97 | - 各组件 `_typeMark` 标记统一更名为 `typeMark`
98 | - `Cell` 增加模式属性 `height`, 支持直接固定高度
99 | - `Page` 上移除 Tab 模式
100 | - `P` 属性 `verMargin` 更名为 `hasVerSpacing`, `spacing` 支持传入数值
101 | - `Row/Col/Cell` 支持直接设置 `height` 属性
102 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019-present Alibaba Group Holding Limited, https://www.alibabagroup.com/
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/abc.json:
--------------------------------------------------------------------------------
1 | {
2 | "assets": {
3 | "type": "builder",
4 | "builder": {
5 | "name": "@ali/builder-fie4-component"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/build.json:
--------------------------------------------------------------------------------
1 | {
2 | "library": "AlifdLayout",
3 | "libraryTarget": "umd",
4 | "plugins": ["build-plugin-component", "build-plugin-fusion"],
5 | "alias": {
6 | "@": "./src"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/build.lowcode.js:
--------------------------------------------------------------------------------
1 | const { name, version } = require('./package.json');
2 | const { library } = require('./build.json');
3 |
4 | const baseRenderUrl =
5 | process && process.argv && process.argv.includes('start')
6 | ? '.'
7 | : `https://unpkg.com/${name}@${version}`;
8 |
9 | module.exports = {
10 | alias: {
11 | '@': './src',
12 | },
13 | plugins: [
14 | [
15 | '@alifd/build-plugin-lowcode',
16 | {
17 | library,
18 | staticResources: {
19 | engineCoreCssUrl:
20 | 'https://alifd.alicdn.com/npm/@alilc/lowcode-engine@1.1.3-beta.4/dist/css/engine-core.css',
21 | engineExtCssUrl:
22 | 'https://alifd.alicdn.com/npm/@alilc/lowcode-engine-ext@1.0.5-beta.10/dist/css/engine-ext.css',
23 | engineCoreJsUrl:
24 | 'https://alifd.alicdn.com/npm/@alilc/lowcode-engine@1.1.3-beta.4/dist/js/engine-core.js',
25 | engineExtJsUrl:
26 | 'https://alifd.alicdn.com/npm/@alilc/lowcode-engine-ext@1.0.5-beta.10/dist/js/engine-ext.js',
27 | },
28 | extraAssets: [
29 | 'https://alifd.alicdn.com/npm/@alilc/lowcode-materials@1.0.6/dist/assets.json',
30 | 'https://g.alicdn.com/code/npm/@alife/fusion-ui/0.1.7/build/lowcode/assets-prod.json',
31 | ],
32 | renderUrls: [`${baseRenderUrl}/dist/${library}.js`, `${baseRenderUrl}/dist/${library}.css`],
33 | noParse: true,
34 | singleComponent: true,
35 | },
36 | ],
37 | ],
38 | };
39 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['ali'],
3 | };
4 |
--------------------------------------------------------------------------------
/demo/00_basic.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 基本结构
3 | order: 1
4 | ---
5 |
6 | 自然布局主要解决页面内容区(main 区域)的布局,不解决 Shell 层级,一般每个业务线都会有自己现成的吊顶和布局。
7 |
8 | ```jsx
9 | import React, { Component } from 'react';
10 | import ReactDOM from 'react-dom';
11 | import { Icon } from '@alifd/next';
12 | import { Page, Section, Block, Row, Col, Cell, P, Text } from '@alifd/layout';
13 |
14 | import '@alifd/theme-3/variables.css';
15 | import '@alifd/theme-3/index.scss';
16 |
17 | const { Header, Content, Footer, Nav, Aside } = Page;
18 |
19 | const App = () => {
20 | return (
21 | <>
22 |
23 |
24 |
25 | Header
26 | |
27 |
28 |
29 | }>
30 |
31 | Block
32 | |
33 |
34 |
35 |
36 | Block
37 | |
38 |
39 |
40 |
41 | Block
42 | |
43 |
44 |
45 |
46 |
47 |
48 | Block
49 | |
50 |
51 |
52 |
53 |
54 | Footer
55 | |
56 |
57 |
58 |
59 |
60 |
61 |
62 | Header
63 | |
64 |
65 |
66 |
67 |
68 |
69 | Nav
70 | |
71 |
72 |
73 |
74 |
75 |
76 | Aside
77 | |
78 |
79 |
80 |
85 |
86 |
87 |
88 | Block
89 | |
90 |
91 |
92 |
93 |
94 |
95 | Footer
96 | |
97 |
98 |
99 | >
100 | );
101 | };
102 |
103 | ReactDOM.render( , mountNode);
104 | ```
105 |
--------------------------------------------------------------------------------
/demo/O10_p.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 段落
3 | order: 9
4 | ---
5 |
6 | 通过段落组织文本及各类 inline 模式的元素,自动元素间左右和上下间隙。
7 |
8 | ```jsx
9 | import React, { Component, useState } from 'react';
10 | import ReactDOM from 'react-dom';
11 | import { Button, Breadcrumb, Radio, Icon, Tag, Switch } from '@alifd/next';
12 | import { Page, Section, Block, Row, Col, Grid, Cell, Space, P, Text } from '@alifd/layout';
13 |
14 | import '@alifd/theme-3/variables.css';
15 | import '@alifd/theme-3/index.scss';
16 |
17 | const { Header } = Page;
18 |
19 | const App = () => {
20 | const [align, setAlign] = useState('left');
21 | const [spacing, setSpacing] = useState('medium');
22 | const [hasVerSpacing, setHasVerSpacing] = useState(true);
23 |
24 | return (
25 |
26 |
36 |
37 |
38 | Fusion 简介
39 |
40 |
41 | Fusion
42 | 是一套企业级中后台UI的解决方案,致力于解决设计师与前端在产品体验一致性、工作协同、开发效率方面的问题。通过协助业务线构建设计系统,提供系统化工具协助设计师前端使用设计系统,下游提供一站式设计项目协作平台;打通互联网产品从设计到开发的工作流。
43 |
44 |
45 |
46 |
47 | Fusion Design
48 | 产品创建于2015年底,阿里巴巴集团中台战略背景下,由国际UED(现国际用户体验事业部)与B2B技术部成立中台DPL项目。从国际UED,天猫,商家等各类业务形态中抽象解构,通过一套设计系统协议提升
49 | 设计与开发效率
50 | ,以统一的物料分发工具提升团队协同能力,借助灵活的在线样式配置支撑业务的设计创新。
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | 水平对齐方式:
59 |
60 |
87 |
88 |
89 | 内容间隙(spacing):
90 |
109 |
110 |
111 | 启用元素垂直间距:
112 |
113 |
114 | |
115 |
116 |
117 |
118 | Alibaba
119 |
120 |
121 | Fusion
122 |
123 |
124 | Design
125 |
126 |
127 |
128 | 企业级的中后台设计系统解决方案
129 |
130 |
131 |
132 | 研发文档
133 |
134 |
135 | 设计模式
136 |
137 |
138 | 观看视频
139 |
140 |
141 |
142 | Star
143 |
144 |
145 | |
146 |
147 |
148 |
149 |
150 |
151 | 实付订单
152 |
153 | 实付款:
154 |
155 | ¥35.00
156 |
157 |
158 | 订单编号:39876619
159 |
160 |
161 |
162 |
163 | );
164 | };
165 |
166 | ReactDOM.render(
167 |
,
170 | mountNode,
171 | );
172 | ```
173 |
174 | ```css
175 | .mock-iframe {
176 | border: 3px solid black;
177 | border-radius: 12px;
178 | overflow: hidden;
179 | }
180 | ```
181 |
--------------------------------------------------------------------------------
/demo/O11_text.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 字体
3 | order: 10
4 | ---
5 |
6 | 通过段落组织文本及各类 inline 模式的元素,自动元素间左右和上下间隙。
7 |
8 | ```jsx
9 | import React, { Component, useState } from 'react';
10 | import ReactDOM from 'react-dom';
11 | import { Button, Breadcrumb, Radio, Icon } from '@alifd/next';
12 | import { Page, Section, Block, Row, Col, Grid, Cell, P, Text } from '@alifd/layout';
13 |
14 | import '@alifd/theme-3/variables.css';
15 | import '@alifd/theme-3/index.scss';
16 |
17 | const { Header } = Page;
18 |
19 | const App = () => {
20 | const [align, setAlign] = useState('left');
21 |
22 | return (
23 |
24 |
34 |
35 |
36 |
37 | 段落中的纯文本
38 |
39 |
40 |
41 |
42 | 默认
43 | 标记
44 | 代码
45 | 加粗
46 | 下划线
47 | 删除线
48 |
49 | |
50 |
51 |
52 |
53 | 赤
54 | 橙
55 | 黄
56 | 绿
57 | 蓝
58 | 靛
59 | 紫
60 |
61 |
62 | 赤
63 | 橙
64 | 黄
65 | 绿
66 | 蓝
67 | 靛
68 | 紫
69 |
70 |
71 |
72 |
73 |
74 | 标题 1
75 | 标题 2
76 | 标题 3
77 | 标题 4
78 | 标题 5
79 | 标题 6
80 | 正文 1
81 | 正文 2
82 | 水印
83 | 超小字体
84 | |
85 |
86 |
87 |
88 | );
89 | };
90 |
91 | ReactDOM.render(
92 | ,
95 | mountNode,
96 | );
97 | ```
98 |
99 | ```css
100 | .mock-iframe {
101 | border: 3px solid black;
102 | border-radius: 12px;
103 | overflow: hidden;
104 | }
105 | ```
106 |
--------------------------------------------------------------------------------
/demo/O3_portal-n2.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 案例3 - 门户2
3 | order: 2
4 | ---
5 |
6 | 居中对齐 demo
7 |
8 | ```jsx
9 | import React, { Component } from 'react';
10 | import ReactDOM from 'react-dom';
11 | import { Box, Button, Tab } from '@alifd/next';
12 | import { Page, Block, Row, Col, Cell, Section, P, Text } from '@alifd/layout';
13 |
14 | import '@alifd/theme-3/variables.css';
15 |
16 | class App extends Component {
17 | render() {
18 | return (
19 |
20 |
21 |
30 |
31 |
32 |
33 |
34 | 平台服务能力介绍
35 |
36 | |
37 |
38 |
39 |
40 |
44 |
45 |
46 |
50 | Fusion Design Pro
51 |
52 | |
53 |
54 |
55 |
59 |
60 |
61 |
65 | Deep Design Pro
66 |
67 | |
68 |
69 |
70 |
74 |
75 |
76 |
80 | InTiger Design
81 |
82 | |
83 |
84 |
85 |
86 |
87 |
88 | 营销创意服务
89 |
90 | |
91 |
92 |
93 |
94 |
95 | 内容标签
96 |
97 |
98 |
99 |
100 | 内容标签
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | );
110 | }
111 | }
112 |
113 | ReactDOM.render(
114 | ,
117 | mountNode,
118 | );
119 | ```
120 |
121 | ```css
122 | .mock-body-portal {
123 | border: 3px solid black;
124 | border-radius: 12px;
125 | overflow: hidden;
126 | --page-max-content-width: 1000px;
127 | }
128 | ```
129 |
--------------------------------------------------------------------------------
/demo/O5_page-table.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 案例5 - 表格
3 | order: 5
4 | ---
5 |
6 | 表格相关的布局
7 |
8 | ```jsx
9 | import React, { Component } from 'react';
10 | import ReactDOM from 'react-dom';
11 | import { Button, Table, Pagination, Icon, Breadcrumb } from '@alifd/next';
12 | import { Page, Section, Block, Row, Col, P, Cell, Text } from '@alifd/layout';
13 |
14 | import '@alifd/theme-3/variables.css';
15 |
16 | const { Header } = Page;
17 |
18 | const dataSourceGen = () => {
19 | const result = [];
20 | for (let i = 0; i < 5; i++) {
21 | result.push({
22 | title: { name: `Quotation for 1PCS Nano ${3 + i}.0 controller compatible` },
23 | id: 100306660940 + i,
24 | time: 2000 + i,
25 | });
26 | }
27 | return result;
28 | };
29 | const cellRender = (value, index, record) => {
30 | return (
31 |
32 |
33 | 删除
34 |
35 |
36 | 编辑
37 |
38 |
39 | );
40 | };
41 |
42 | const columns = new Array(4).fill({
43 | dataIndex: 'data',
44 | title: 'Data',
45 | width: 200,
46 | });
47 | columns.unshift({
48 | dataIndex: 'id',
49 | title: 'Id',
50 | width: 100,
51 | lock: 'left',
52 | });
53 | columns.push({
54 | dataIndex: 'state',
55 | title: 'State',
56 | width: 200,
57 | });
58 | columns.push({
59 | title: 'Action',
60 | width: 100,
61 | align: 'center',
62 | cell: () => delete ,
63 | lock: 'right',
64 | });
65 |
66 | const dataSource = [
67 | {
68 | id: 30000,
69 | data: '$13.02',
70 | state: 'normal',
71 | },
72 | {
73 | id: 30001,
74 | data: '$16.02',
75 | state: 'normal',
76 | },
77 | {
78 | id: 30002,
79 | data: '$63.0002',
80 | state: 'error',
81 | },
82 | ];
83 |
84 | const orderList = [
85 | {
86 | id: 1,
87 | name: '蓝瓶咖啡线下体验店室内设计1',
88 | state: '进行中',
89 | level: 'high',
90 | },
91 | {
92 | id: 2,
93 | name: '双12投放 Banner',
94 | state: '进行中',
95 | level: 'high',
96 | },
97 | {
98 | id: 3,
99 | name: 'Global 大促活动',
100 | state: '进行中',
101 | level: 'high',
102 | },
103 | {
104 | id: 4,
105 | name: 'Banner 拓展',
106 | state: '进行中',
107 | level: 'middle',
108 | },
109 | {
110 | id: 5,
111 | name: '类目市场宣传设计',
112 | state: '待处理',
113 | level: 'low',
114 | },
115 | {
116 | id: 6,
117 | name: '类目市场宣传设计',
118 | state: '待处理',
119 | level: 'low',
120 | },
121 | {
122 | id: 7,
123 | name: '类目市场宣传设计',
124 | state: '待处理',
125 | level: 'low',
126 | },
127 | ];
128 | const timeLineList = [
129 | {
130 | planName: '财经周会',
131 | planAddress: '深圳 T4-4-1;杭州 7-4-9-N',
132 | planTime: '09:00',
133 | planDuaring: '2小时',
134 | },
135 | {
136 | planName: '财经周会',
137 | planAddress: '深圳 T4-4-1;杭州 7-4-9-N',
138 | planTime: '11:00',
139 | planDuaring: '2小时',
140 | },
141 | ];
142 | const colorMap = {
143 | high: 'red',
144 | middle: 'yellow',
145 | low: 'green',
146 | };
147 | const renderLevel = (text, index) => (
148 |
149 |
150 | {text}
151 |
152 |
153 | );
154 |
155 | class App extends Component {
156 | render() {
157 | return (
158 |
159 |
170 |
171 |
172 |
173 |
174 |
175 |
176 | 批量提交
177 | 批量删除
178 | 批量下载
179 |
180 | 帮助信息
181 |
182 | |
183 |
184 |
185 |
186 |
187 |
188 |
189 | |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 | |
199 |
200 |
201 | |
202 |
203 |
204 |
205 | {
209 | if (colIndex === 0) {
210 | return {
211 | colSpan: 1,
212 | rowSpan: 2,
213 | };
214 | }
215 | if (colIndex === columns.length - 1) {
216 | return {
217 | colSpan: 1,
218 | rowSpan: 3,
219 | };
220 | }
221 | }}
222 | >
223 | {columns.map((col, i) => {
224 | return ;
225 | })}
226 |
227 |
228 |
229 |
230 | );
231 | }
232 | }
233 |
234 | ReactDOM.render(
235 | ,
238 | mountNode,
239 | );
240 | ```
241 |
242 | ```css
243 | .mock-iframe {
244 | border: 3px solid black;
245 | border-radius: 12px;
246 | overflow: hidden;
247 | }
248 | ```
249 |
--------------------------------------------------------------------------------
/demo/O6_workplace.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 案例6 - 工作台
3 | order: 5
4 | ---
5 |
6 | 页面布局示例
7 |
8 | ```jsx
9 | import React, { Component } from 'react';
10 | import ReactDOM from 'react-dom';
11 | import {
12 | Breadcrumb,
13 | Avatar,
14 | Button,
15 | Divider,
16 | Balloon,
17 | Icon,
18 | Progress,
19 | Slider,
20 | Table,
21 | Tag,
22 | Calendar,
23 | Timeline,
24 | } from '@alifd/next';
25 | import { Page, Section, Block, Row, Col, Cell, P, Text } from '@alifd/layout';
26 |
27 | import '@alifd/theme-3/variables.css';
28 | import '@alifd/theme-3/index.scss';
29 |
30 | const { Header, Content, Footer, Nav, Aside } = Page;
31 |
32 | const orderList = [
33 | {
34 | id: 1,
35 | name: '蓝瓶咖啡线下体验店室内设计1',
36 | state: '进行中',
37 | level: 'high',
38 | },
39 | {
40 | id: 2,
41 | name: '双12投放 Banner',
42 | state: '进行中',
43 | level: 'high',
44 | },
45 | {
46 | id: 3,
47 | name: 'Global 大促活动',
48 | state: '进行中',
49 | level: 'high',
50 | },
51 | {
52 | id: 4,
53 | name: 'Banner 拓展',
54 | state: '进行中',
55 | level: 'middle',
56 | },
57 | {
58 | id: 5,
59 | name: '类目市场宣传设计',
60 | state: '待处理',
61 | level: 'low',
62 | },
63 | {
64 | id: 6,
65 | name: '类目市场宣传设计',
66 | state: '待处理',
67 | level: 'low',
68 | },
69 | {
70 | id: 7,
71 | name: '类目市场宣传设计',
72 | state: '待处理',
73 | level: 'low',
74 | },
75 | ];
76 | const timeLineList = [
77 | {
78 | planName: '财经周会',
79 | planAddress: '深圳 T4-4-1;杭州 7-4-9-N',
80 | planTime: '09:00',
81 | planDuaring: '2小时',
82 | },
83 | {
84 | planName: '财经周会',
85 | planAddress: '深圳 T4-4-1;杭州 7-4-9-N',
86 | planTime: '11:00',
87 | planDuaring: '2小时',
88 | },
89 | ];
90 | const colorMap = {
91 | high: 'red',
92 | middle: 'yellow',
93 | low: 'green',
94 | };
95 | const renderLevel = (text, index) => (
96 |
97 |
98 | {text}
99 |
100 |
101 | );
102 |
103 | const App = () => {
104 | return (
105 |
119 |
120 |
121 |
122 | Dashboard
123 | 工作台
124 |
125 |
126 |
127 |
131 | |
132 |
133 | 早上好, 潕量 !
134 |
135 | 美好的一天,从智能、创意、无缝的协作开始。我们将专注处理你专注的事情!
136 |
137 | |
138 |
139 |
140 | 项目数
141 | 56
142 | |
143 |
144 | 团队内排名
145 |
146 | 8/24
147 |
148 | |
149 |
150 | 项目数
151 | 56
152 | |
153 |
154 |
155 | |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
167 | |
168 |
169 |
170 | 阮小五 在 设计中台 新建项目 Fusion Design
171 | (「在」的间距丢了)
172 |
173 | 4小时前
174 | |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
185 | |
186 |
187 |
188 | 阮小五 将 新版本迭代 更新为已发布(lastchild 间距问题)
189 |
190 | 4小时前
191 | |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 | 共 2 个日程
200 |
201 |
202 | {timeLineList.map((item) => (
203 |
209 | {item.planTime}
210 | {item.planDuaring}
211 | >
212 | }
213 | />
214 | ))}
215 |
216 |
217 |
218 | ({
223 | children: (
224 |
225 | {record.name}
226 |
227 | ),
228 | }),
229 | columnProps: () => ({
230 | width: 330,
231 | }),
232 | titleAddons: () => 任务名称 ,
233 | }}
234 | >
235 |
236 |
237 |
238 |
239 |
240 |
241 |
248 |
249 | );
250 | };
251 |
252 | ReactDOM.render(
253 | ,
256 | mountNode,
257 | );
258 | ```
259 |
260 | ```css
261 | .mock-iframe {
262 | border: 3px solid black;
263 | border-radius: 12px;
264 | overflow: hidden;
265 | }
266 | ```
267 |
--------------------------------------------------------------------------------
/demo/O8_break-points.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 响应式和断点
3 | order: 7
4 | ---
5 |
6 | 通过断点,可以实现区块的响应式布局
7 |
8 | ```jsx
9 | import React, { useState, useEffect } from 'react';
10 | import ReactDOM from 'react-dom';
11 | import { Page, Section, Block, Cell, P, Text, BreakPoints } from '@alifd/layout';
12 | import { Table, Tag } from '@alifd/next';
13 | import { throttle } from 'lodash-es';
14 |
15 | import '@alifd/theme-3/variables.css';
16 |
17 | const cellProps = {
18 | align: 'center',
19 | verAlign: 'middle',
20 | style: { background: '#f2f2f2', height: 60 },
21 | };
22 |
23 | const breakPoints = [
24 | {
25 | width: 750,
26 | maxContentWidth: 750,
27 | numberOfColumns: 4,
28 | },
29 | {
30 | width: 960,
31 | maxContentWidth: 960,
32 | numberOfColumns: 8,
33 | },
34 | {
35 | width: 1200,
36 | maxContentWidth: 1200,
37 | numberOfColumns: 12,
38 | },
39 | {
40 | width: Infinity,
41 | maxContentWidth: 1200,
42 | numberOfColumns: 12,
43 | },
44 | ];
45 |
46 | const App = () => {
47 | const [availWidth, setAvailWidth] = useState(document.body.clientWidth);
48 | const [curBreakPoint, setBreakPoint] = useState(undefined);
49 |
50 | const resize = throttle(() => {
51 | setAvailWidth(document.body.clientWidth);
52 | }, 200);
53 |
54 | useEffect(() => {
55 | window.addEventListener('resize', resize, false);
56 |
57 | return () => {
58 | window.removeEventListener('resize', resize);
59 | };
60 | }, []);
61 |
62 | return (
63 | {
66 | setBreakPoint(bp);
67 | }}
68 | >
69 |
70 |
71 | 断点:
72 |
73 | 调整窗口尺寸,查看不同断点下的显示效果。 窗口宽度:
74 |
75 | {availWidth}px
76 |
77 |
78 |
79 | {
82 | if (curBreakPoint?.width === record?.width) {
83 | return {
84 | style: {
85 | background: '#BBDEFB',
86 | },
87 | };
88 | }
89 | return {};
90 | }}
91 | >
92 |
93 |
94 |
95 |
96 | |
97 |
98 |
99 |
100 |
101 | {Array.from(new Array(12)).map((_, key) => (
102 |
103 |
104 | span=1
105 | |
106 |
107 | ))}
108 |
109 |
110 |
111 |
112 |
113 | span=2
114 | |
115 |
116 |
117 |
118 | span=4
119 | |
120 |
121 |
122 |
123 | span=6
124 | |
125 |
126 |
127 |
128 | {Array.from(new Array(6)).map((_, index) => (
129 |
130 |
131 | span=4
132 | |
133 |
134 | ))}
135 |
136 |
137 | );
138 | };
139 |
140 | ReactDOM.render(
141 | ,
144 | mountNode,
145 | );
146 | ```
147 |
148 | ```css
149 | .mock-iframe {
150 | border: 3px solid black;
151 | border-radius: 12px;
152 | overflow: hidden;
153 | }
154 | ```
155 |
--------------------------------------------------------------------------------
/demo/O9_block.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 区块
3 | order: 8
4 | ---
5 |
6 | 区块的样子
7 |
8 | ```jsx
9 | import React, { Component } from 'react';
10 | import ReactDOM from 'react-dom';
11 | import {
12 | Avatar,
13 | Breadcrumb,
14 | Badge,
15 | Button,
16 | Balloon,
17 | Icon,
18 | Progress,
19 | ResponsiveGrid as RGrid,
20 | } from '@alifd/next';
21 | import { Page, Section, Block, Row, Col, P, Cell, Text } from '@alifd/layout';
22 |
23 | import '@alifd/theme-3/variables.css';
24 |
25 | const { Header } = Page;
26 |
27 | const MockBlock = (props) => {
28 | return (
29 |
30 | {props.children || '100% x 60'}
31 | |
32 | );
33 | };
34 |
35 | class App extends Component {
36 | render() {
37 | return (
38 |
39 |
48 |
49 |
53 | Simple Card
54 | SubTitle
55 | >
56 | }
57 | extra={link }
58 | >
59 |
60 | Lorem ipsum dolor sit amet, est viderer iuvaret perfecto et. Ne petentium quaerendum
61 | nec, eos ex recteque mediocritatem, ex usu assum legendos temporibus. Ius feugiat
62 | pertinacia an, cu verterem praesent quo.
63 |
64 |
65 |
66 |
70 | Simple Card
71 | SubTitle
72 | >
73 | }
74 | extra={link }
75 | divider
76 | >
77 |
78 | Lorem ipsum dolor sit amet, est viderer iuvaret perfecto et. Ne petentium quaerendum
79 | nec, eos ex recteque mediocritatem, ex usu assum legendos temporibus. Ius feugiat
80 | pertinacia an, cu verterem praesent quo.
81 |
82 |
83 |
84 |
85 |
89 |
90 |
91 |
92 | Simple Card
93 | SubTitle
94 | |
95 |
96 | link
97 | |
98 |
99 |
100 | Lorem ipsum dolor sit amet, est viderer iuvaret perfecto et. Ne petentium
101 | quaerendum nec, eos ex recteque mediocritatem, ex usu assum legendos temporibus.
102 | Ius feugiat pertinacia an, cu verterem praesent quo.
103 |
104 | |
105 |
106 |
107 |
108 |
109 |
110 | 基础
111 |
112 | }>
113 |
114 |
115 |
116 |
117 |
118 |
119 | 无标题
120 |
121 |
122 | 分割线&标题居中
123 |
124 |
128 |
132 | 标题
133 |
134 | }
135 | extra={ }
136 | >
137 |
138 |
139 |
140 |
141 | 透明模式
142 |
143 |
144 |
145 |
146 | );
147 | }
148 | }
149 |
150 | ReactDOM.render(
151 | ,
154 | mountNode,
155 | );
156 | ```
157 |
158 | ```css
159 | .mock-block-fullx60 {
160 | width: 100%;
161 | min-height: 60px;
162 | height: 100%;
163 | background: #f2f2f2;
164 | border: 1px dashed #ccc;
165 | }
166 | .mock-iframe {
167 | border: 3px solid black;
168 | border-radius: 12px;
169 | overflow: hidden;
170 | }
171 | ```
172 |
--------------------------------------------------------------------------------
/f2elint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | enableStylelint: true,
3 | enableMarkdownlint: true,
4 | enablePrettier: true,
5 | };
6 |
--------------------------------------------------------------------------------
/global.d.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IPublicApiSkeleton,
3 | IPublicApiHotkey,
4 | IPublicApiSetters,
5 | IPublicApiMaterial,
6 | IPublicApiEvent,
7 | IPublicApiProject,
8 | IPublicApiCommon,
9 | IPublicApiLogger,
10 | IPublicApiCanvas,
11 | IPublicApiPlugins,
12 | IPublicModelEngineConfig,
13 | } from '@alilc/lowcode-types';
14 |
15 | declare global {
16 | interface Window {
17 | AliLowCodeEngine: {
18 | skeleton: IPublicApiSkeleton;
19 | hotkey: IPublicApiHotkey;
20 | setters: IPublicApiSetters;
21 | config: IPublicModelEngineConfig;
22 | material: IPublicApiMaterial;
23 |
24 | /**
25 | * this event works globally, can be used between plugins and engine.
26 | */
27 | event: IPublicApiEvent;
28 | project: IPublicApiProject;
29 | common: IPublicApiCommon;
30 | plugins: IPublicApiPlugins;
31 | logger: IPublicApiLogger;
32 |
33 | /**
34 | * this event works within current plugin, on an emit locally.
35 | */
36 | pluginEvent: IPublicApiEvent;
37 | canvas: IPublicApiCanvas;
38 | };
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | setupFilesAfterEnv: ['/test/setupTests.js'],
3 | };
4 |
--------------------------------------------------------------------------------
/lowcode/common/hotkeys.ts:
--------------------------------------------------------------------------------
1 | import { splitNodeByDimension, autoPCellWithEnter, autoPCellWithTab } from './split/auto-cell';
2 | import { keybindingService } from './keybindingService';
3 | import { CELL, ROW, COL, BLOCK, P } from '../names';
4 |
5 | /**
6 | * 是否正在输入中
7 | * @returns
8 | */
9 | function isInLiveEditing() {
10 | return window.parent.AliLowCodeEngine.canvas?.isInLiveEditing;
11 | }
12 | /**
13 | * 是否focus在表单
14 | * @param e
15 | * @returns
16 | */
17 | function isFormEvent(e: KeyboardEvent | MouseEvent) {
18 | const t = e.target as HTMLFormElement;
19 | if (!t) {
20 | return false;
21 | }
22 |
23 | if (t.form || /^(INPUT|SELECT|TEXTAREA)$/.test(t.tagName)) {
24 | return true;
25 | }
26 | if (
27 | t instanceof HTMLElement &&
28 | /write/.test(window.getComputedStyle(t).getPropertyValue('-webkit-user-modify'))
29 | ) {
30 | return true;
31 | }
32 | return false;
33 | }
34 |
35 | export const registHotKeys = () => {
36 | // append a row
37 | keybindingService.bind({
38 | command: 'horizontalDividerCommands',
39 | keybinding: 's',
40 | components: [CELL, ROW, COL, BLOCK],
41 | cb: (node, e) => {
42 | if (isInLiveEditing() || isFormEvent(e)) {
43 | return;
44 | }
45 | splitNodeByDimension('v', node);
46 | },
47 | desc: 'append a row',
48 | });
49 | // preappend a row
50 | keybindingService.bind({
51 | command: 'preHorizontalDividerCommands',
52 | keybinding: 'shift+s',
53 | components: [CELL, ROW, COL, BLOCK],
54 | cb: (node) => {
55 | splitNodeByDimension('v', node, true);
56 | },
57 | desc: 'preappend a row',
58 | });
59 | // append a col
60 | keybindingService.bind({
61 | command: 'verticalDividerCommands',
62 | keybinding: 'w',
63 | components: [CELL, ROW, COL],
64 | cb: (node, e) => {
65 | if (isInLiveEditing() || isFormEvent(e)) {
66 | return;
67 | }
68 | splitNodeByDimension('h', node);
69 | },
70 | desc: 'append a col',
71 | });
72 | // preappend a col
73 | keybindingService.bind({
74 | command: 'preVerticalDividerCommands',
75 | keybinding: 'shift+w',
76 | components: [CELL, ROW, COL],
77 | cb: (node) => {
78 | splitNodeByDimension('h', node, true);
79 | },
80 | desc: 'preappend a col',
81 | });
82 |
83 | // 回车换行,是否有办法作用所有 P 元素内的组件?
84 | keybindingService.bind({
85 | command: 'enterPCommands',
86 | keybinding: 'enter',
87 | components: '*',
88 | cb: (node, e) => {
89 | // 正在输入则忽略
90 | if (isInLiveEditing() || isFormEvent(e)) {
91 | return;
92 | }
93 |
94 | if (node.parent.componentName === P) autoPCellWithEnter(node);
95 | },
96 | desc: 'Enter 换行, 把node后的所有组件带过去',
97 | });
98 |
99 | // tab push,是否有办法作用所有 P 元素内的组件?
100 | keybindingService.bind({
101 | command: 'tabPCommands',
102 | keybinding: 'tab',
103 | components: '*',
104 | cb: (node, e) => {
105 | // 正在输入则忽略
106 | if (isInLiveEditing() || isFormEvent(e)) {
107 | // console.log(/in tab/)
108 | return;
109 | }
110 |
111 | if (node.parent.componentName === P) autoPCellWithTab(node);
112 | },
113 | desc: 'Tab 换列, 把node后的所有组件带过去',
114 | });
115 |
116 | // f
117 | keybindingService.bind({
118 | command: 'fUpCommands',
119 | keybinding: 'f',
120 | components: '*',
121 | cb: (node, e) => {
122 | // 正在输入则忽略
123 | if (isInLiveEditing() || isFormEvent(e)) {
124 | // console.log(/in tab/)
125 | return;
126 | }
127 | if (node?.parent?.componentName === P && node.parent?.parent) {
128 | node.parent.parent.select();
129 | } else if (node.parent) {
130 | node.parent.select();
131 | }
132 | },
133 | desc: 'f 选择父组件',
134 | });
135 |
136 | keybindingService.bind({
137 | command: 'aOpenMaterialPanelCommands',
138 | keybinding: 'a',
139 | components: '*',
140 | cb: (node, e) => {
141 | // 正在输入则忽略
142 | if (isInLiveEditing() || isFormEvent(e)) {
143 | return;
144 | }
145 | window.parent.AliLowCodeEngine.skeleton.showPanel('componentsPane');
146 | },
147 | desc: 'a 打开物料面板',
148 | });
149 |
150 | // command+k 快速选中变成链接
151 | // keybindingService.bind({
152 | // command: 'textToLinkCommands',
153 | // keybinding: 'command+k',
154 | // components: '*',
155 | // cb: (node, e) => {
156 | // const selection = window.getSelection();
157 | // if (node.componentName === 'NextText' && isInLiveEditing() && selection.toString()) {
158 | // const range = selection.getRangeAt(0);
159 | // const text = selection.focusNode.textContent;
160 |
161 | // const firstText = text.slice(0, range.startOffset);
162 | // const selectedText = text.slice(range.startOffset, range.endOffset);
163 | // const lastText = text.slice(range.endOffset, text.length - 1);
164 |
165 | // console.log(firstText, selectedText, lastText);
166 | // }
167 |
168 | // // window.parent.AliLowCodeEngine.skeleton.showPanel('componentsPane');
169 | // },
170 | // desc: 'command+k 把选中部分的文字快速变成链接',
171 | // });
172 | };
173 |
--------------------------------------------------------------------------------
/lowcode/common/keybindingService.ts:
--------------------------------------------------------------------------------
1 | import { IPublicApiHotkey, IPublicApiProject } from '@alilc/lowcode-types';
2 |
3 | interface cbFunc {
4 | (node: any, e: KeyboardEvent): void;
5 | }
6 |
7 | export interface IKeyBinding {
8 | command: string;
9 | keybinding: string | string[];
10 | components: string | string[];
11 | cb: cbFunc;
12 | desc?: string;
13 | }
14 |
15 | class KeybindingService {
16 | hotkey: IPublicApiHotkey;
17 | project: IPublicApiProject;
18 | keybindingMap: IKeyBinding[];
19 |
20 | constructor() {
21 | const engine = window.parent.AliLowCodeEngine;
22 | this.hotkey = engine.hotkey;
23 | this.project = engine.project;
24 | this.keybindingMap = [];
25 | }
26 | bind(kb: IKeyBinding) {
27 | if (kb.command && this.keybindingMap.find((k) => k.command === kb.command)) {
28 | console.warn('KeybindingService Error[duplicated command]', kb.command);
29 | return;
30 | }
31 |
32 | this.keybindingMap.push(kb);
33 | this.hotkey.bind(kb.keybinding, (e: KeyboardEvent) => {
34 | const node = this.getSelectedNode();
35 | if (!node) {
36 | console.warn(`No node select on keydown: ${kb.keybinding}`);
37 | return;
38 | }
39 |
40 | if (!this.isValidNode(node, kb)) {
41 | console.warn(`Not valid node for keydown target: ${kb.keybinding}`);
42 | return;
43 | }
44 | kb.cb.apply(null, [node, e]);
45 | });
46 | }
47 | // unbind, hotkey 暂时不支持
48 | execCommand(command: string, node: any, ...rest: any[]) {
49 | const targetKb = this.keybindingMap.find((kb) => kb.command === command);
50 | if (!targetKb) {
51 | console.warn(`No command founded from keybindingMap: ${command}`);
52 | return;
53 | }
54 |
55 | if (!node || !this.isValidNode(node, targetKb)) {
56 | console.warn(`Not valid node target for command: ${command} ${node}`);
57 | return;
58 | }
59 |
60 | targetKb.cb.apply(null, [node, ...rest]);
61 | }
62 | private getSelectedNode() {
63 | return this.project.currentDocument?.selection.getNodes()[0];
64 | }
65 | private isValidNode(node: any, kb: IKeyBinding) {
66 | return kb.components === '*' || kb.components.includes(node.componentName);
67 | }
68 | }
69 |
70 | export const keybindingService = new KeybindingService();
71 |
--------------------------------------------------------------------------------
/lowcode/common/split/block-resize-map.ts:
--------------------------------------------------------------------------------
1 | const map: {
2 | [key: string]: {
3 | [key: string]: string;
4 | };
5 | } = {
6 | 12: {
7 | '0s': '6,6',
8 | '0d': '',
9 | },
10 | '2,10': {
11 | '1r': '3,9',
12 | '0d': '12',
13 | '1d': '12',
14 | '0s': '2,2,8',
15 | '1s': '4,4,4',
16 | },
17 | '3,9': {
18 | '1r': '4,8',
19 | '1l': '2,10',
20 | '0d': '12',
21 | '1d': '12',
22 | '0s': '2,2,8',
23 | '1s': '4,4,4',
24 | },
25 | '4,8': {
26 | '1r': '6,6',
27 | '1l': '3,9',
28 | '0d': '12',
29 | '1d': '12',
30 | '0s': '2,2,8',
31 | '1s': '4,4,4',
32 | },
33 | '6,6': {
34 | '1r': '8,4',
35 | '1l': '4,8',
36 | '0d': '12',
37 | '1d': '12',
38 | '0s': '3,3,6',
39 | '1s': '6,3,3',
40 | },
41 | '8,4': {
42 | '1r': '9,3',
43 | '1l': '6,6',
44 | '0d': '12',
45 | '1d': '12',
46 | '0s': '4,4,4',
47 | '1s': '8,2,2',
48 | },
49 | '9,3': {
50 | '1r': '10,2',
51 | '1l': '8,4',
52 | '0d': '12',
53 | '1d': '12',
54 | '0s': '4,4,4',
55 | '1s': '8,2,2',
56 | },
57 | '10,2': {
58 | '1l': '9,3',
59 | '0d': '12',
60 | '1d': '12',
61 | '0s': '4,4,4',
62 | '1s': '8,2,2',
63 | },
64 | '4,4,4': {
65 | '1l': '3,6,3',
66 | '1r': '6,3,3',
67 | '2l': '3,3,6',
68 | '2r': '3,6,3',
69 | '0d': '6,6',
70 | '1d': '6,6',
71 | '2d': '6,6',
72 | '0s': '3,3,3,3',
73 | '1s': '3,3,3,3',
74 | '2s': '3,3,3,3',
75 | },
76 | '6,3,3': {
77 | '1l': '4,4,4',
78 | '1r': '8,2,2',
79 | '2l': '4,4,4',
80 | '2r': '8,2,2',
81 | '0d': '6,6',
82 | '1d': '8,4',
83 | '2d': '8,4',
84 | '0s': '3,3,3,3',
85 | '1s': '6,2,2,2',
86 | '2s': '6,2,2,2',
87 | },
88 | '8,2,2': {
89 | '1l': '6,3,3',
90 | '2l': '6,3,3',
91 | '0d': '6,6',
92 | '1d': '8,4',
93 | '2d': '8,4',
94 | '0s': '3,3,3,3',
95 | '1s': '6,2,2,2',
96 | '2s': '6,2,2,2',
97 | },
98 | '3,6,3': {
99 | '1l': '2,8,2',
100 | '1r': '4,4,4',
101 | '2l': '4,4,4',
102 | '2r': '2,8,2',
103 | '0d': '8,4',
104 | '1d': '6,6',
105 | '2d': '4,8',
106 | '0s': '2,2,6,2',
107 | '1s': '3,3,3,3',
108 | '2s': '2,6,2,2',
109 | },
110 | '2,8,2': {
111 | '1r': '3,6,3',
112 | '2l': '3,6,3',
113 | '0d': '8,4',
114 | '1d': '6,6',
115 | '2d': '4,8',
116 | '0s': '2,2,6,2',
117 | '1s': '3,3,3,3',
118 | '2s': '2,6,2,2',
119 | },
120 | '3,3,6': {
121 | '1l': '2,2,8',
122 | '1r': '4,4,4',
123 | '2l': '2,2,8',
124 | '2r': '4,4,4',
125 | '0d': '4,8',
126 | '1d': '4,8',
127 | '2d': '6,6',
128 | '0s': '2,2,2,6',
129 | '1s': '2,2,2,6',
130 | '2s': '3,3,3,3',
131 | },
132 | '2,2,8': {
133 | '1r': '3,3,6',
134 | '2r': '3,3,6',
135 | '0d': '4,8',
136 | '1d': '4,8',
137 | '2d': '6,6',
138 | '0s': '2,2,2,6',
139 | '1s': '2,2,2,6',
140 | '2s': '3,3,3,3',
141 | },
142 | '3,3,3,3': {
143 | '1l': '2,6,2,2',
144 | '1r': '6,2,2,2',
145 | '2l': '2,2,6,2',
146 | '2r': '2,6,2,2',
147 | '3l': '2,2,2,6',
148 | '3r': '2,2,6,2',
149 | '0d': '4,4,4',
150 | '1d': '4,4,4',
151 | '2d': '4,4,4',
152 | '3d': '4,4,4',
153 | '0s': '2,2,2,4,2',
154 | '1s': '2,2,2,2,4',
155 | '2s': '4,2,2,2,2',
156 | '3s': '2,4,2,2,2',
157 | },
158 | '6,2,2,2': {
159 | '1l': '3,3,3,3',
160 | '2l': '3,3,3,3',
161 | '3l': '3,3,3,3',
162 | '0d': '4,4,4',
163 | '1d': '6,3,3',
164 | '2d': '6,3,3',
165 | '3d': '6,3,3',
166 | '0s': '4,2,2,2,2',
167 | '1s': '4,2,2,2,2',
168 | '2s': '4,2,2,2,2',
169 | '3s': '4,2,2,2,2',
170 | },
171 | '2,6,2,2': {
172 | '1r': '3,3,3,3',
173 | '2l': '3,3,3,3',
174 | '3l': '3,3,3,3',
175 | '0d': '6,3,3',
176 | '1d': '4,4,4',
177 | '2d': '3,6,3',
178 | '3d': '3,6,3',
179 | '0s': '2,2,4,2,2',
180 | '1s': '2,2,4,2,2',
181 | '2s': '2,4,2,2,2',
182 | '3s': '2,4,2,2,2',
183 | },
184 | '2,2,6,2': {
185 | '1r': '3,3,3,3',
186 | '2r': '3,3,3,3',
187 | '3l': '3,3,3,3',
188 | '0d': '3,6,3',
189 | '1d': '3,6,3',
190 | '2d': '4,4,4',
191 | '3d': '3,3,6',
192 | '0s': '2,2,2,4,2',
193 | '1s': '2,2,2,4,2',
194 | '2s': '2,2,4,2,2',
195 | '3s': '2,2,4,2,2',
196 | },
197 | '2,2,2,6': {
198 | '1r': '3,3,3,3',
199 | '2r': '3,3,3,3',
200 | '3r': '3,3,3,3',
201 | '0d': '3,3,6',
202 | '1d': '3,3,6',
203 | '2d': '3,3,6',
204 | '3d': '4,4,4',
205 | '0s': '2,2,2,2,4',
206 | '1s': '2,2,2,2,4',
207 | '2s': '2,2,2,2,4',
208 | '3s': '2,2,2,2,4',
209 | },
210 | '4,2,2,2,2': {
211 | '1l': '2,4,2,2,2',
212 | '2l': '2,2,4,2,2',
213 | '3l': '2,2,2,4,2',
214 | '4l': '2,2,2,2,4',
215 | '0d': '3,3,3,3',
216 | '1d': '3,3,3,3',
217 | '2d': '3,3,3,3',
218 | '3d': '3,3,3,3',
219 | '4d': '3,3,3,3',
220 | '0s': '2,2,2,2,2,2',
221 | '1s': '2,2,2,2,2,2',
222 | '2s': '2,2,2,2,2,2',
223 | '3s': '2,2,2,2,2,2',
224 | '4s': '2,2,2,2,2,2',
225 | },
226 | '2,4,2,2,2': {
227 | '1r': '4,2,2,2,2',
228 | '2l': '2,2,4,2,2',
229 | '3l': '2,2,2,4,2',
230 | '4l': '2,2,2,2,4',
231 | '0d': '3,3,3,3',
232 | '1d': '3,3,3,3',
233 | '2d': '3,3,3,3',
234 | '3d': '3,3,3,3',
235 | '4d': '3,3,3,3',
236 | '0s': '2,2,2,2,2,2',
237 | '1s': '2,2,2,2,2,2',
238 | '2s': '2,2,2,2,2,2',
239 | '3s': '2,2,2,2,2,2',
240 | '4s': '2,2,2,2,2,2',
241 | },
242 | '2,2,4,2,2': {
243 | '1r': '4,2,2,2,2',
244 | '2r': '2,4,2,2,2',
245 | '3l': '2,2,2,4,2',
246 | '4l': '2,2,2,2,4',
247 | '0d': '3,3,3,3',
248 | '1d': '3,3,3,3',
249 | '2d': '3,3,3,3',
250 | '3d': '3,3,3,3',
251 | '4d': '3,3,3,3',
252 | '0s': '2,2,2,2,2,2',
253 | '1s': '2,2,2,2,2,2',
254 | '2s': '2,2,2,2,2,2',
255 | '3s': '2,2,2,2,2,2',
256 | '4s': '2,2,2,2,2,2',
257 | },
258 | '2,2,2,4,2': {
259 | '1r': '4,2,2,2,2',
260 | '2r': '2,4,2,2,2',
261 | '3r': '2,2,4,2,2',
262 | '4l': '2,2,2,2,4',
263 | '0d': '3,3,3,3',
264 | '1d': '3,3,3,3',
265 | '2d': '3,3,3,3',
266 | '3d': '3,3,3,3',
267 | '4d': '3,3,3,3',
268 | '0s': '2,2,2,2,2,2',
269 | '1s': '2,2,2,2,2,2',
270 | '2s': '2,2,2,2,2,2',
271 | '3s': '2,2,2,2,2,2',
272 | '4s': '2,2,2,2,2,2',
273 | },
274 | '2,2,2,2,4': {
275 | '1r': '4,2,2,2,2',
276 | '2r': '2,4,2,2,2',
277 | '3r': '2,2,4,2,2',
278 | '4r': '2,2,2,4,2',
279 | '0d': '3,3,3,3',
280 | '1d': '3,3,3,3',
281 | '2d': '3,3,3,3',
282 | '3d': '3,3,3,3',
283 | '4d': '3,3,3,3',
284 | '0s': '2,2,2,2,2,2',
285 | '1s': '2,2,2,2,2,2',
286 | '2s': '2,2,2,2,2,2',
287 | '3s': '2,2,2,2,2,2',
288 | '4s': '2,2,2,2,2,2',
289 | },
290 | '2,2,2,2,2,2': {
291 | '0d': '4,2,2,2,2',
292 | '1d': '4,2,2,2,2',
293 | '2d': '2,4,2,2,2',
294 | '3d': '2,2,4,2,2',
295 | '4d': '2,2,2,4,2',
296 | '5d': '2,2,2,2,4',
297 | },
298 | };
299 |
300 | export default map;
301 |
--------------------------------------------------------------------------------
/lowcode/common/util.ts:
--------------------------------------------------------------------------------
1 | import { IPublicModelNode } from '@alilc/lowcode-types';
2 |
3 | const wrapWithProCard = (currentNode: IPublicModelNode) => {
4 | const subChildren = currentNode.children;
5 | const newSubChildren: IPublicModelNode[] = [];
6 |
7 | subChildren?.map((_item, i) => {
8 | const item = subChildren.get(i);
9 | item && newSubChildren.push(item);
10 | return false;
11 | });
12 |
13 | const hasCard = subChildren?.size === 1 && subChildren?.get(0)?.componentName === 'ProCard';
14 | if (!hasCard) {
15 | const newProCard = currentNode.document?.createNode({
16 | componentName: 'ProCard',
17 | title: '高级卡片',
18 | props: {
19 | title: '标题',
20 | },
21 | children: [],
22 | });
23 |
24 | currentNode.children?.importSchema([]);
25 | newProCard && currentNode.children?.insert(newProCard);
26 |
27 | for (const i in newSubChildren) {
28 | currentNode.children?.get(0)?.insertAfter(newSubChildren[i]);
29 | }
30 | }
31 | };
32 |
33 | const removeWrapProCard = (currentNode: IPublicModelNode) => {
34 | const subChildren = currentNode.children;
35 | const children0 = subChildren?.get(0);
36 |
37 | if (children0?.componentName === 'ProCard') {
38 | children0?.children?.map((item) => {
39 | currentNode.insertBefore(item, children0);
40 | return false;
41 | });
42 | children0.remove();
43 | }
44 | };
45 |
46 | export default {
47 | wrapWithProCard,
48 | removeWrapProCard,
49 | };
50 |
--------------------------------------------------------------------------------
/lowcode/default-schema.ts:
--------------------------------------------------------------------------------
1 | import { IPublicTypeNodeSchema, IPublicTypePropsMap } from '@alilc/lowcode-types';
2 | import {
3 | SECTION,
4 | CELL,
5 | COL,
6 | P,
7 | GRID,
8 | BLOCK,
9 | PAGE_NAV,
10 | PAGE_ASIDE,
11 | PAGE_HEADER,
12 | PAGE_FOOTER,
13 | FIXED_CONTAINER,
14 | } from './names';
15 |
16 | export const createHeaderSnippet = (): IPublicTypeNodeSchema => {
17 | return {
18 | componentName: PAGE_HEADER,
19 | title: '页面头部',
20 | props: {},
21 | children: [createCellSnippet()],
22 | };
23 | };
24 | export const createFooterSnippet = (): IPublicTypeNodeSchema => {
25 | return {
26 | componentName: PAGE_FOOTER,
27 | title: '页面尾部',
28 | props: {},
29 | children: [createCellSnippet()],
30 | };
31 | };
32 |
33 | export const createNavSnippet = (): IPublicTypeNodeSchema => {
34 | return {
35 | componentName: PAGE_NAV,
36 | title: '左侧区域',
37 | props: {
38 | width: 200,
39 | },
40 | children: [createBlockSnippet()],
41 | };
42 | };
43 | export const createAsideSnippet = (): IPublicTypeNodeSchema => {
44 | return {
45 | componentName: PAGE_ASIDE,
46 | title: '右侧区域',
47 | props: {
48 | width: 200,
49 | },
50 | children: [createBlockSnippet()],
51 | };
52 | };
53 |
54 | export const createSectionSnippet = ({
55 | blockProps,
56 | }: { blockProps?: IPublicTypePropsMap } = {}): IPublicTypeNodeSchema => {
57 | return {
58 | componentName: SECTION,
59 | title: '区域',
60 | props: {},
61 | children: [createBlockSnippet({ blockProps })],
62 | };
63 | };
64 |
65 | export const createBlockSnippet = ({
66 | blockProps,
67 | }: { blockProps?: IPublicTypePropsMap } = {}): IPublicTypeNodeSchema => {
68 | return {
69 | componentName: BLOCK,
70 | title: '区块',
71 | props: {
72 | ...blockProps,
73 | },
74 | children: [createCellSnippet()],
75 | };
76 | };
77 |
78 | export const createRowColSnippet = (componentName = COL): IPublicTypeNodeSchema => {
79 | return {
80 | componentName,
81 | title: componentName === COL ? '列容器' : '行容器',
82 | props: {},
83 | children: [],
84 | };
85 | };
86 |
87 | export const createGridlSnippet = (): IPublicTypeNodeSchema => {
88 | return {
89 | componentName: GRID,
90 | title: '网格容器',
91 | props: {
92 | rows: 2,
93 | cols: 2,
94 | },
95 | children: [],
96 | };
97 | };
98 |
99 | export const createCellSnippet = (): IPublicTypeNodeSchema => {
100 | return {
101 | componentName: CELL,
102 | title: '容器',
103 | props: {},
104 | children: [],
105 | };
106 | };
107 |
108 | export const createFixedContainerSnippet = (): IPublicTypeNodeSchema => {
109 | return {
110 | componentName: FIXED_CONTAINER,
111 | title: '自由容器',
112 | props: {
113 | style: {
114 | minHeight: 60,
115 | },
116 | },
117 | children: [],
118 | };
119 | };
120 |
121 | /**
122 | * 返回包裹了P标签的schema,会根据dragged的类型设置不同的属性
123 | * @param {*} dragged 被拖入的组件,是个引擎 node 类型
124 | * @returns {} 返回值是个对象
125 | */
126 | export const createPSnippet = (): IPublicTypeNodeSchema => {
127 | return {
128 | componentName: P,
129 | title: '段落',
130 | props: {},
131 | children: [],
132 | };
133 | };
134 |
--------------------------------------------------------------------------------
/lowcode/meta.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-require-imports */
2 | import section from './metas/section';
3 | import block from './metas/block';
4 | import grid from './metas/grid';
5 | import row from './metas/row';
6 | import col from './metas/col';
7 | import cell from './metas/cell';
8 | import page from './metas/page';
9 | import pageHeader from './metas/page-header';
10 | import pageFooter from './metas/page-footer';
11 | import pageAside from './metas/page-aside';
12 | import pageNav from './metas/page-nav';
13 | import pageContent from './metas/page-content';
14 | import p from './metas/p';
15 | import fixedPoint from './metas/fixed-point';
16 | import fixedContainer from './metas/fixed-container';
17 |
18 | // img 修改
19 | // window.parent?.AliLowCodeEngine?.designerCabin?.registerMetadataTransducer?.((metadata) => {
20 | // if (metadata.componentName === 'Image') {
21 | // return {
22 | // ...metadata,
23 | // ...img
24 | // };
25 | // }
26 | // return metadata;
27 | // });
28 |
29 | export default [
30 | { ...page },
31 | { ...pageHeader },
32 | { ...pageFooter },
33 | { ...pageContent },
34 | { ...pageAside },
35 | { ...pageNav },
36 | { ...section },
37 | { ...block },
38 | { ...cell },
39 | { ...row },
40 | { ...col },
41 | { ...grid },
42 | { ...p },
43 | { ...fixedPoint },
44 | { ...fixedContainer },
45 | ];
46 |
--------------------------------------------------------------------------------
/lowcode/metas/col.ts:
--------------------------------------------------------------------------------
1 | import { IPublicModelNode } from '@alilc/lowcode-types';
2 | import { CELL, COL } from '../names';
3 | import { createCellSnippet, createPSnippet } from '../default-schema';
4 | import { getResizingHandlers } from './enhance/experimentals';
5 | import {
6 | onNodeRemoveSelfWhileNoChildren,
7 | onNodeReplaceSelfWithChildrenCell,
8 | onDrageResize,
9 | } from './enhance/callbacks';
10 | import widthSetter from './setter/width';
11 |
12 | export default {
13 | componentName: COL,
14 | title: '列容器',
15 | category: '容器',
16 | npm: {
17 | package: '@alifd/layout',
18 | version: '^0.1.0',
19 | exportName: 'Col',
20 | main: 'lib/index.js',
21 | destructuring: true,
22 | subName: '',
23 | },
24 | props: [
25 | {
26 | name: 'style',
27 | propType: 'object',
28 | },
29 | {
30 | name: 'width',
31 | title: '固定宽度',
32 | display: 'inline',
33 | setter: {
34 | componentName: 'NumberSetter',
35 | props: {
36 | min: 0,
37 | units: ['px'],
38 | },
39 | },
40 | },
41 | {
42 | name: 'height',
43 | title: '固定高度',
44 | display: 'inline',
45 | setter: {
46 | componentName: 'NumberSetter',
47 | props: {
48 | min: 0,
49 | units: ['px'],
50 | },
51 | },
52 | },
53 | ],
54 | configure: {
55 | component: {
56 | isContainer: true,
57 | nestingRule: {
58 | childWhitelist: (node: IPublicModelNode) => {
59 | // 不能 COL 套 COL
60 | // 做了自动包裹 CELL 的处理,所以不需要强制 CELL
61 | return node.componentName !== COL;
62 | },
63 | },
64 | },
65 | supports: { style: true },
66 | props: [
67 | ...widthSetter,
68 | {
69 | name: 'gap',
70 | title: '间距',
71 | defaultValue: 4,
72 | initialValue: 4,
73 | setter: {
74 | componentName: 'NumberSetter',
75 | props: {
76 | step: 4,
77 | min: 0,
78 | },
79 | },
80 | },
81 | ],
82 | advanced: {
83 | getResizingHandlers,
84 | callbacks: {
85 | onNodeRemove: (removedNode: IPublicModelNode, currentNode: IPublicModelNode) => {
86 | onNodeRemoveSelfWhileNoChildren(removedNode, currentNode);
87 | },
88 | onSubtreeModified: (currentNode: IPublicModelNode, e: MouseEvent) => {
89 | onNodeReplaceSelfWithChildrenCell(currentNode, e);
90 | },
91 | /**
92 | * 组件拖入回调逐层向上触发,需要做好判断。
93 | * 组件拖入间隙的的时候包裹 CELL+P, 每一层父节点都会触发
94 | * @param {*} draggedNode 被拖入的组件
95 | * @param {*} currentNode 被拖入到 CELL
96 | */
97 | onNodeAdd: (draggedNode: IPublicModelNode, currentNode: IPublicModelNode) => {
98 | if (!draggedNode || draggedNode.componentName !== CELL) {
99 | const dropLocation = draggedNode.document?.dropLocation;
100 | if (!dropLocation) {
101 | // 没有 dropLocation 一般是 slot, slot 元素不用特殊处理 不做任何包裹
102 | return;
103 | }
104 | const dropTarget = dropLocation.target;
105 |
106 | // 自动包裹 CELL + P
107 | if (dropTarget === currentNode) {
108 | const cellNode = currentNode.document?.createNode(createCellSnippet());
109 | const pNode = currentNode.document?.createNode(createPSnippet());
110 | pNode && cellNode?.insertAfter(pNode);
111 |
112 | cellNode && currentNode.insertAfter(cellNode, draggedNode, false);
113 | pNode?.insertAfter(draggedNode, pNode, false);
114 | }
115 | }
116 | },
117 | ...onDrageResize,
118 | },
119 | initialChildren: [],
120 | },
121 | },
122 |
123 | icon: 'https://img.alicdn.com/imgextra/i1/O1CN01AQZw941ZgdfVtjsDO_!!6000000003224-55-tps-128-128.svg',
124 | };
125 |
--------------------------------------------------------------------------------
/lowcode/metas/enhance/callbacks.ts:
--------------------------------------------------------------------------------
1 | import { IPublicModelNode } from '@alilc/lowcode-types';
2 | import { CELL, ROW, COL } from '../../names';
3 |
4 | /**
5 | * onSubtreeModified 是从叶子节点逐级冒泡, 注意做好判断
6 | * options.isSubDeleting 是否因为父节点删除导致被连带删除。树根节点为 false,其他分支节点、叶子节点则为 true
7 | */
8 | export const onNodeReplaceSelfWithChildrenCell = (currentNode: IPublicModelNode, options: any) => {
9 | const { removeNode, type, isSubDeleting } = options;
10 |
11 | // console.log(currentNode, currentNode.children.length, e)
12 |
13 | // 必须到了触发节点,并且触发节点是子节点
14 | if (
15 | !removeNode ||
16 | !currentNode ||
17 | type !== 'remove' ||
18 | isSubDeleting ||
19 | removeNode.parent !== currentNode
20 | ) {
21 | return;
22 | }
23 |
24 | const { children } = currentNode;
25 | const componentName = children?.get(0)?.componentName;
26 | // 只有一个子元素 Cell/Row/Col,则让子元素替代自己
27 | if (children?.size === 1 && componentName && [CELL, ROW, COL].indexOf(componentName) > -1) {
28 | const child = children.get(0);
29 | const { parent } = currentNode;
30 |
31 | child?.setPropValue('style', currentNode.getPropValue('style'));
32 | child?.setPropValue('width', currentNode.getPropValue('width'));
33 |
34 | child && parent?.insertAfter(child, currentNode, false);
35 |
36 | currentNode.remove();
37 | } else if (children?.size && children?.size > 1) {
38 | // 同名节点提升
39 | const sameNameChild = children.find((n) => n.componentName === currentNode.componentName);
40 | if (sameNameChild?.children) {
41 | sameNameChild.children.reverse().forEach((n) => {
42 | currentNode.insertAfter(n, sameNameChild, false);
43 | });
44 | sameNameChild.remove();
45 | }
46 | }
47 | };
48 |
49 | /**
50 | * 容器元素不允许空着,无子节点的时候删除自己
51 | * @param {*} removeNode
52 | * @param {*} currentNode
53 | * @returns boolean
54 | */
55 | export const onNodeRemoveSelfWhileNoChildren = (
56 | removeNode: IPublicModelNode,
57 | currentNode: IPublicModelNode,
58 | ) => {
59 | if (!removeNode || !currentNode) {
60 | return false;
61 | }
62 |
63 | const { children } = currentNode;
64 | // 若无 children, 删除组件本身
65 | if (children && children.size === 0) {
66 | currentNode.remove();
67 | return true;
68 | }
69 |
70 | return false;
71 | };
72 |
73 | function disableDivider() {
74 | const iframe = window.AliLowCodeEngine.project.simulatorHost;
75 | iframe && iframe.contentWindow?.dispatchEvent(new Event('dividerDisable'));
76 | }
77 | function enableDivider() {
78 | const iframe = window.AliLowCodeEngine.project.simulatorHost;
79 | iframe && iframe.contentWindow?.dispatchEvent(new Event('dividerEnable'));
80 | }
81 |
82 | export const onDrageResize = {
83 | onResizeStart(
84 | e: MouseEvent & {
85 | trigger: string;
86 | deltaX?: number;
87 | deltaY?: number;
88 | },
89 | currentNode: IPublicModelNode,
90 | ) {
91 | disableDivider();
92 |
93 | currentNode.startRect = currentNode.getRect();
94 | currentNode.siblingNode =
95 | e.trigger === 'n' || e.trigger === 'w' ? currentNode.prevSibling : currentNode.nextSibling;
96 | currentNode.siblingRect = currentNode.siblingNode ? currentNode.siblingNode.getRect() : null;
97 | },
98 | onResize(
99 | e: MouseEvent & {
100 | trigger: string;
101 | deltaX?: number;
102 | deltaY?: number;
103 | },
104 | currentNode: IPublicModelNode,
105 | ) {
106 | const { deltaY, deltaX } = e;
107 | const { height: startHeight, width: startWidth } = currentNode.startRect;
108 |
109 | if (e.trigger === 'e' || e.trigger === 'w') {
110 | const newWidth = e.trigger === 'w' ? startWidth - deltaX : startWidth + deltaX;
111 |
112 | // currentNode.setPropValue('style.width', `${newWidth}px`);
113 | currentNode.setPropValue('width', newWidth);
114 | currentNode.getDOMNode().style.flex = `0 0 ${Math.round(newWidth)}px`;
115 |
116 | // 去除兄弟节点的宽度
117 | if (currentNode.siblingRect) {
118 | currentNode.siblingNode.setPropValue('width', undefined);
119 | currentNode.siblingNode.getDOMNode().style.flex = '1 1';
120 |
121 | // const siblingStyle = currentNode.siblingNode.getPropValue('style');
122 | // siblingStyle && (delete siblingStyle.width);
123 | // currentNode.siblingNode.setPropValue('style', siblingStyle);
124 | }
125 | } else if (e.trigger === 's' || e.trigger === 'n') {
126 | const newHeight = e.trigger === 'n' ? startHeight - deltaY : startHeight + deltaY;
127 |
128 | currentNode.setPropValue('style.minHeight', newHeight);
129 | currentNode.getDOMNode().style.flex = '0 0 auto';
130 |
131 | // 去除兄弟节点的高度
132 | if (currentNode.siblingRect) {
133 | currentNode.siblingNode.setPropValue('style.minHeight', undefined);
134 | currentNode.siblingNode.getDOMNode().style.flex = '1 1';
135 |
136 | // const siblingStyle = currentNode.siblingNode.getPropValue('style');
137 | // siblingStyle && (delete siblingStyle.height);
138 | // currentNode.siblingNode.setPropValue('style', siblingStyle);
139 | }
140 | }
141 | },
142 | onResizeEnd() {
143 | enableDivider();
144 | },
145 | };
146 |
--------------------------------------------------------------------------------
/lowcode/metas/enhance/experimentals.ts:
--------------------------------------------------------------------------------
1 | import { IPublicModelNode } from '@alilc/lowcode-types';
2 | import { ROW, COL } from '../../names';
3 |
4 | export const getResizingHandlers = (node: IPublicModelNode) => {
5 | const directionList: string[] = [];
6 | const parentNode = node.parent;
7 | const parentChildrenLength = parentNode?.children?.size;
8 | const parentCN = parentNode?.componentName;
9 | // debugger;
10 | if ((parentCN === ROW || parentCN === COL) && parentChildrenLength && parentChildrenLength > 1) {
11 | if (parentCN === COL) {
12 | // 列容器,只能上下拖动
13 | if (node.index > 0) {
14 | directionList.push('n');
15 | }
16 | if (node.index < parentChildrenLength - 1) {
17 | directionList.push('s');
18 | }
19 | } else {
20 | // 行容器,只能左右拖动
21 | if (node.index > 0) {
22 | directionList.push('w');
23 | }
24 | if (node.index < parentChildrenLength - 1) {
25 | directionList.push('e');
26 | }
27 | }
28 | }
29 |
30 | return directionList;
31 | };
32 |
--------------------------------------------------------------------------------
/lowcode/metas/fixed-container.ts:
--------------------------------------------------------------------------------
1 | import minHeight from './setter/min-height';
2 | import { FIXED_CONTAINER } from '../names';
3 | import { IPublicTypeComponentMetadata } from '@alilc/lowcode-types';
4 |
5 | const config: IPublicTypeComponentMetadata = {
6 | componentName: FIXED_CONTAINER,
7 | title: '自由容器',
8 | category: '布局容器类',
9 | group: '精选组件',
10 | npm: {
11 | package: '@alifd/layout',
12 | version: '^0.1.0',
13 | exportName: 'FixedContainer',
14 | main: 'lib/index.js',
15 | destructuring: true,
16 | subName: '',
17 | },
18 | configure: {
19 | component: {
20 | isContainer: true,
21 | },
22 | props: [
23 | ...minHeight,
24 | {
25 | name: 'items',
26 | title: '配置',
27 | setter: {
28 | componentName: 'ArraySetter',
29 | props: {
30 | itemSetter: {
31 | componentName: 'ObjectSetter',
32 | props: {
33 | config: {
34 | items: [
35 | {
36 | name: 'zIndex',
37 | title: '层级 (zIndex)',
38 | important: true,
39 | setter: 'NumberSetter',
40 | },
41 | {
42 | name: 'left',
43 | title: '右偏移',
44 | setter: 'NumberSetter',
45 | },
46 | {
47 | name: 'top',
48 | title: '下偏移',
49 | setter: 'NumberSetter',
50 | },
51 | {
52 | name: 'primaryKey',
53 | title: '项目编号',
54 | condition: () => false,
55 | setter: 'StringSetter',
56 | },
57 | ],
58 | },
59 | },
60 | },
61 | },
62 | },
63 | extraProps: {
64 | disableAdd: true,
65 | },
66 | },
67 | ],
68 | supports: {
69 | style: true,
70 | loop: false,
71 | },
72 | advanced: {}
73 | },
74 | icon: 'https://alifd.oss-cn-hangzhou.aliyuncs.com/fusion-cool/icons/icon-light/ic_light_table.png',
75 | snippets: [],
76 | };
77 |
78 | export default config;
79 |
--------------------------------------------------------------------------------
/lowcode/metas/fixed-point.ts:
--------------------------------------------------------------------------------
1 | import { IPublicTypeComponentMetadata } from '@alilc/lowcode-types';
2 | import { FIXED_POINT } from '../names';
3 |
4 | const config: IPublicTypeComponentMetadata = {
5 | componentName: FIXED_POINT,
6 | title: '自由节点',
7 | category: '布局容器类',
8 | group: '精选组件',
9 | npm: {
10 | package: '@alifd/layout',
11 | version: '^0.1.0',
12 | exportName: 'FixedPoint',
13 | main: 'lib/index.js',
14 | destructuring: true,
15 | subName: '',
16 | },
17 | configure: {
18 | component: {
19 | isContainer: true,
20 | },
21 | props: [
22 | {
23 | name: 'zIndex',
24 | title: '层级',
25 | setter: {
26 | componentName: 'NumberSetter',
27 | props: {
28 | min: 0,
29 | },
30 | },
31 | },
32 | ],
33 | supports: {
34 | style: true,
35 | loop: false,
36 | },
37 | advanced: {}
38 | },
39 | experimental: {
40 | callbacks: {},
41 | },
42 | icon: 'https://img.alicdn.com/imgextra/i1/O1CN0144G9Iw22y9fiO73NG_!!6000000007188-55-tps-56-56.svg',
43 | snippets: [
44 | {
45 | title: '自由节点',
46 | screenshot:
47 | 'https://img.alicdn.com/imgextra/i1/O1CN0144G9Iw22y9fiO73NG_!!6000000007188-55-tps-56-56.svg',
48 | schema: {
49 | componentName: FIXED_POINT,
50 | title: '自由节点',
51 | props: {},
52 | children: [],
53 | },
54 | },
55 | ],
56 | };
57 |
58 | export default config;
59 |
--------------------------------------------------------------------------------
/lowcode/metas/grid.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IPublicModelNode,
3 | IPublicModelSettingPropEntry,
4 | IPublicTypeComponentMetadata,
5 | } from '@alilc/lowcode-types';
6 | import { CELL, GRID, BLOCK, ROW, COL } from '../names';
7 | import { createCellSnippet, createPSnippet } from '../default-schema';
8 | import widthSetter from './setter/width';
9 | import heightSetter from './setter/min-height';
10 | import {
11 | onNodeRemoveSelfWhileNoChildren,
12 | onNodeReplaceSelfWithChildrenCell,
13 | } from './enhance/callbacks';
14 |
15 | const config: IPublicTypeComponentMetadata = {
16 | componentName: GRID,
17 | title: '网格容器',
18 | category: '容器',
19 | npm: {
20 | package: '@alifd/layout',
21 | version: '^0.1.0',
22 | exportName: 'Grid',
23 | main: 'lib/index.js',
24 | destructuring: true,
25 | subName: '',
26 | },
27 | props: [
28 | {
29 | name: 'style',
30 | propType: 'object',
31 | },
32 | ],
33 | configure: {
34 | component: {
35 | isContainer: true,
36 | nestingRule: {
37 | childWhitelist: (node: IPublicModelNode) => {
38 | return node.componentName !== GRID;
39 | },
40 | },
41 | },
42 | supports: { style: true },
43 | props: [
44 | {
45 | name: 'cols',
46 | title: '列数',
47 | defaultValue: 2,
48 | setter: {
49 | componentName: 'NumberSetter',
50 | props: {
51 | min: 2,
52 | },
53 | },
54 | },
55 | {
56 | title: '宽度控制',
57 | type: 'group',
58 | display: 'block',
59 | condition: (target: IPublicModelSettingPropEntry) => {
60 | return (
61 | target.node?.parent?.componentName &&
62 | [BLOCK, ROW].indexOf(target.node?.parent?.componentName) !== -1
63 | );
64 | },
65 | items: [...widthSetter],
66 | },
67 | {
68 | title: '高度控制',
69 | type: 'group',
70 | display: 'block',
71 | condition: (target: IPublicModelSettingPropEntry) => {
72 | return (
73 | target.node?.parent?.componentName &&
74 | [BLOCK, COL].indexOf(target.node?.parent?.componentName) !== -1
75 | );
76 | },
77 | items: [...heightSetter],
78 | },
79 | // {
80 | // name: 'minWidth',
81 | // title: '单元格最小宽度',
82 | // setter: {
83 | // componentName: 'NumberSetter',
84 | // props: {
85 | // min: 0,
86 | // units: ['px'],
87 | // },
88 | // },
89 | // },
90 | {
91 | name: 'rowGap',
92 | title: '水平间隙',
93 | defaultValue: 4,
94 | setter: {
95 | componentName: 'NumberSetter',
96 | props: {
97 | min: 0,
98 | step: 4,
99 | units: ['px'],
100 | },
101 | },
102 | },
103 | {
104 | name: 'colGap',
105 | title: '垂直间隙',
106 | defaultValue: 4,
107 | initialValue: 4,
108 | setter: {
109 | componentName: 'NumberSetter',
110 | props: {
111 | min: 0,
112 | step: 4,
113 | units: ['px'],
114 | },
115 | },
116 | },
117 | ],
118 | advanced: {
119 | callbacks: {
120 | onNodeRemove: (removedNode: IPublicModelNode, currentNode: IPublicModelNode) => {
121 | onNodeRemoveSelfWhileNoChildren(removedNode, currentNode);
122 | },
123 | onSubtreeModified: (currentNode: IPublicModelNode, options: any) => {
124 | onNodeReplaceSelfWithChildrenCell(currentNode, options);
125 | },
126 | /**
127 | * 组件拖入回调逐层向上触发,需要做好判断。
128 | * 组件拖入间隙的的时候包裹 CELL+P, 每一层父节点都会触发
129 | * @param {*} draggedNode 被拖入的组件
130 | * @param {*} currentNode 被拖入到 CELL
131 | */
132 | onNodeAdd: (draggedNode: IPublicModelNode, currentNode: IPublicModelNode) => {
133 | if (!draggedNode || draggedNode.componentName !== CELL) {
134 | const dropLocation = draggedNode.document?.dropLocation;
135 | if (!dropLocation) {
136 | // 没有 dropLocation 一般是 slot, slot 元素不用特殊处理 不做任何包裹
137 | return;
138 | }
139 | const dropTarget = dropLocation.target;
140 | // 自动包裹 CELL + P
141 | if (dropTarget === currentNode) {
142 | const cellNode = currentNode.document?.createNode(createCellSnippet());
143 | const pNode = currentNode.document?.createNode(createPSnippet());
144 | pNode && cellNode?.insertAfter(pNode);
145 | cellNode && currentNode.insertAfter(cellNode, draggedNode, false);
146 | pNode?.insertAfter(draggedNode, pNode, false);
147 | }
148 | }
149 | },
150 | },
151 | initialChildren: [],
152 | },
153 | },
154 | icon: 'https://alifd.oss-cn-hangzhou.aliyuncs.com/fusion-cool/icons/icon-light/ic_light_table.png',
155 | };
156 |
157 | export default config;
158 |
--------------------------------------------------------------------------------
/lowcode/metas/nav-aside.ts:
--------------------------------------------------------------------------------
1 | import { IPublicModelSettingPropEntry, IPublicTypeFieldConfig } from '@alilc/lowcode-types';
2 |
3 | import { PAGE_NAV, PAGE_ASIDE } from '../names';
4 | import { createNavSnippet, createAsideSnippet } from '../default-schema';
5 |
6 | const navAside: IPublicTypeFieldConfig[] = [
7 | {
8 | name: '!nav',
9 | title: {
10 | label: '左侧区域',
11 | },
12 | setter: 'BoolSetter',
13 | extraProps: {
14 | getValue: (target: IPublicModelSettingPropEntry) => {
15 | const pageNode = target.node;
16 | return !!pageNode?.children?.find((n) => n.componentName === PAGE_NAV);
17 | },
18 | setValue: (target: IPublicModelSettingPropEntry, value: boolean) => {
19 | const pageNode = target.node;
20 | const navNode = pageNode?.children?.find((n) => n.componentName === PAGE_NAV);
21 | if (value && !navNode) {
22 | const navSnippets = createNavSnippet();
23 | const navNode2 = pageNode?.document?.createNode(navSnippets);
24 | navNode2 && pageNode?.insertAfter(navNode2);
25 | } else if (!value && navNode) {
26 | navNode.remove();
27 | }
28 | },
29 | },
30 | },
31 | {
32 | name: '!aside',
33 | title: {
34 | label: '右侧区域',
35 | },
36 | setter: 'BoolSetter',
37 | extraProps: {
38 | getValue: (target: IPublicModelSettingPropEntry) => {
39 | const pageNode = target.node;
40 | return !!pageNode?.children?.find((n) => n.componentName === PAGE_ASIDE);
41 | },
42 | setValue: (target: IPublicModelSettingPropEntry, value: boolean) => {
43 | const pageNode = target.node;
44 | const asideNode = pageNode?.children?.find((n) => n.componentName === PAGE_ASIDE);
45 | if (value && !asideNode) {
46 | const navSnippets = createAsideSnippet();
47 | const asideNode2 = pageNode?.document?.createNode(navSnippets);
48 | asideNode2 && pageNode?.insertAfter(asideNode2);
49 | } else if (!value && asideNode) {
50 | asideNode.remove();
51 | }
52 | },
53 | },
54 | },
55 | ];
56 |
57 | export default navAside;
58 |
--------------------------------------------------------------------------------
/lowcode/metas/p.ts:
--------------------------------------------------------------------------------
1 | import { IPublicTypeComponentMetadata } from '@alilc/lowcode-types';
2 | import { CELL, P } from '../names';
3 | import { onNodeRemoveSelfWhileNoChildren } from './enhance/callbacks';
4 |
5 | const config: IPublicTypeComponentMetadata = {
6 | componentName: P,
7 | title: '段落',
8 | category: '布局容器类',
9 | icon: 'https://img.alicdn.com/imgextra/i4/O1CN01U9QxOy25owYQwUfWV_!!6000000007574-55-tps-128-128.svg',
10 | npm: {
11 | package: '@alifd/layout',
12 | version: '^0.1.0',
13 | exportName: 'P',
14 | main: 'lib/index.js',
15 | destructuring: true,
16 | subName: '',
17 | },
18 | props: [
19 | // {
20 | // name: 'style',
21 | // propType: 'object',
22 | // },
23 | // {
24 | // name: 'prefix',
25 | // propType: 'string',
26 | // defaultValue: 'next-',
27 | // },
28 | // {
29 | // // textSpacing 强行为所有Text类型的孩子之间加间距
30 | // // name: "为文本组件之间添加间距",
31 | // name: 'spacing',
32 | // title: {
33 | // label: '文字间距',
34 | // tip: '开启后,同一“段落”下,多个Text之间会产生间距',
35 | // },
36 | // propType: 'bool',
37 | // condition: () => false,
38 | // defaultValue: true,
39 | // },
40 | // {
41 | // // full 强行开启撑满模式,P的每一个children都是100%的
42 | // // name: "占满一行",
43 | // name: 'full',
44 | // title: {
45 | // label: '占满一行',
46 | // tip: '开启后,子元素将占满一行',
47 | // },
48 | // propType: 'bool',
49 | // condition: () => false,
50 | // defaultValue: false,
51 | // },
52 | // {
53 | // // flex 强行开启flex模式,作为块状布局的容器
54 | // name: 'flex',
55 | // title: {
56 | // label: '块状布局',
57 | // tip: '相对于“行内布局模式”,子元素是不可分割的块',
58 | // },
59 | // propType: 'bool',
60 | // condition: () => false,
61 | // defaultValue: true,
62 | // },
63 | // {
64 | // name: 'wrap',
65 | // title: {
66 | // label: '超长换行',
67 | // tip: '只在块状布局下生效,而行内布局下默认就是换行的',
68 | // },
69 | // propType: 'bool',
70 | // condition: () => false,
71 | // // condition(target) {
72 | // // return target.getProps().getPropValue("flex") || false;
73 | // // },
74 | // defaultValue: true,
75 | // },
76 | // {
77 | // name: 'type',
78 | // title: '字体大小',
79 | // extraProps: {
80 | // defaultValue: 'body2',
81 | // },
82 | // defaultValue: 'body2',
83 | // initialValue: 'body2',
84 | // setter: {
85 | // componentName: 'SelectSetter',
86 | // initialValue: 'body2',
87 | // props: {
88 | // defaultValue: 'body2',
89 | // options: [
90 | // {
91 | // title: 'h1',
92 | // value: 'h1',
93 | // },
94 | // {
95 | // title: 'h2',
96 | // value: 'h2',
97 | // },
98 | // {
99 | // title: 'h3',
100 | // value: 'h3',
101 | // },
102 | // {
103 | // title: 'h4',
104 | // value: 'h4',
105 | // },
106 | // {
107 | // title: 'h5',
108 | // value: 'h5',
109 | // },
110 | // {
111 | // title: 'h6',
112 | // value: 'h6',
113 | // },
114 | // {
115 | // title: 'body1',
116 | // value: 'body1',
117 | // },
118 | // {
119 | // title: 'body2',
120 | // value: 'body2',
121 | // },
122 | // {
123 | // title: 'caption',
124 | // value: 'caption',
125 | // },
126 | // {
127 | // title: 'overline',
128 | // value: 'overline',
129 | // },
130 | // ],
131 | // },
132 | // },
133 | // },
134 | // {
135 | // name: 'verAlign',
136 | // title: '垂直对齐',
137 | // extraProps: {
138 | // defaultValue: 'baseline',
139 | // },
140 | // defaultValue: 'baseline',
141 | // initialValue: 'baseline',
142 | // setter: {
143 | // componentName: 'RadioGroupSetter',
144 | // initialValue: 'baseline',
145 | // props: {
146 | // defaultValue: 'baseline',
147 | // options: [
148 | // {
149 | // title: 'top',
150 | // value: 'top',
151 | // },
152 | // {
153 | // title: 'baseline',
154 | // value: 'baseline',
155 | // },
156 | // {
157 | // title: 'middle',
158 | // value: 'middle',
159 | // },
160 | // {
161 | // title: 'bottom',
162 | // value: 'bottom',
163 | // },
164 | // ],
165 | // },
166 | // },
167 | // },
168 | // {
169 | // name: 'align',
170 | // title: '水平对齐',
171 | // extraProps: {
172 | // defaultValue: 'space-between',
173 | // },
174 | // defaultValue: 'space-between',
175 | // initialValue: 'space-between',
176 | // setter: {
177 | // componentName: 'RadioGroupSetter',
178 | // initialValue: 'space-between',
179 | // defaultValue: 'space-between',
180 | // props: {
181 | // defaultValue: 'space-between',
182 | // options: [
183 | // {
184 | // title: 'space-between',
185 | // value: 'space-between',
186 | // },
187 | // {
188 | // title: 'space-around',
189 | // value: 'space-around',
190 | // },
191 | // {
192 | // title: 'space-evenly',
193 | // value: 'space-evenly',
194 | // },
195 | // {
196 | // title: 'left',
197 | // value: 'left',
198 | // },
199 | // {
200 | // title: 'center',
201 | // value: 'center',
202 | // },
203 | // {
204 | // title: 'right',
205 | // value: 'right',
206 | // },
207 | // ],
208 | // },
209 | // },
210 | // },
211 | // {
212 | // name: 'beforeMargin',
213 | // title: '段前间隙',
214 | // defaultValue: 0,
215 | // initialValue: 0,
216 | // setter: {
217 | // componentName: 'NumberSetter',
218 | // props: {
219 | // units: 'px',
220 | // },
221 | // },
222 | // },
223 | // {
224 | // name: 'afterMargin',
225 | // title: '段后间隙',
226 | // defaultValue: 0,
227 | // initialValue: 0,
228 | // setter: {
229 | // componentName: 'NumberSetter',
230 | // props: {
231 | // units: 'px',
232 | // },
233 | // },
234 | // },
235 | ],
236 | configure: {
237 | component: {
238 | isContainer: true,
239 | nestingRule: {
240 | parentWhitelist: [CELL],
241 | },
242 | },
243 | props: [
244 | {
245 | name: 'spacing',
246 | title: '内容间距',
247 | defaultValue: 'medium',
248 | setter: {
249 | componentName: 'RadioGroupSetter',
250 | props: {
251 | options: [
252 | { title: '小', value: 'small' },
253 | { title: '中', value: 'medium' },
254 | { title: '大', value: 'large' },
255 | ],
256 | },
257 | },
258 | },
259 | ],
260 | supports: {
261 | style: true,
262 | },
263 | advanced: {
264 | callbacks: {
265 | onNodeRemove: onNodeRemoveSelfWhileNoChildren,
266 | onHoverHook: () => false,
267 | onMouseDownHook: () => false,
268 | onClickHook: () => false,
269 | },
270 | },
271 | },
272 | };
273 |
274 | export default config;
275 |
--------------------------------------------------------------------------------
/lowcode/metas/page-aside.ts:
--------------------------------------------------------------------------------
1 | import { IPublicTypeComponentMetadata } from '@alilc/lowcode-types';
2 | import { PAGE_ASIDE } from '../names';
3 |
4 | const config: IPublicTypeComponentMetadata = {
5 | componentName: PAGE_ASIDE,
6 | title: '页面右侧',
7 | npm: {
8 | package: '@alifd/layout',
9 | version: '^0.1.0',
10 | exportName: 'Page',
11 | main: 'lib/index.js',
12 | destructuring: true,
13 | subName: 'Aside',
14 | },
15 | props: [],
16 | configure: {
17 | component: {
18 | isContainer: true,
19 | nestingRule: {},
20 | },
21 | props: [
22 | {
23 | name: 'width',
24 | title: {
25 | label: '宽度',
26 | },
27 | setter: {
28 | componentName: 'NumberSetter',
29 | // props: {
30 | // min: 0,
31 | // units: [{ type: 'px' }],
32 | // },
33 | },
34 | },
35 | ],
36 | advanced: {
37 | callbacks: {
38 | onMoveHook() {
39 | return false;
40 | },
41 | },
42 | },
43 | },
44 | icon: 'https://alifd.oss-cn-hangzhou.aliyuncs.com/fusion-cool/icons/icon-light/ic_light_table.png',
45 | };
46 |
47 | export default config;
48 |
--------------------------------------------------------------------------------
/lowcode/metas/page-content.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IPublicModelNode,
3 | IPublicTypeFieldConfig,
4 | IPublicTypeComponentMetadata,
5 | } from '@alilc/lowcode-types';
6 | import { PAGE_CONTENT } from '../names';
7 | import navAside from './nav-aside';
8 |
9 | const newNavAside = navAside.map((item: IPublicTypeFieldConfig) => {
10 | return { ...item };
11 | });
12 |
13 | const meta: IPublicTypeComponentMetadata = {
14 | componentName: PAGE_CONTENT,
15 | title: '页面主体',
16 | npm: {
17 | package: '@alifd/layout',
18 | version: '^0.1.0',
19 | exportName: 'Page',
20 | main: 'lib/index.js',
21 | destructuring: true,
22 | subName: 'Content',
23 | },
24 | props: [],
25 | configure: {
26 | component: {
27 | isContainer: true,
28 | },
29 | props: [
30 | {
31 | name: 'title',
32 | title: {
33 | label: '子页面标题',
34 | },
35 | defaultValue: '子页面',
36 | setter: {
37 | componentName: 'StringSetter',
38 | initialValue: '子页面',
39 | defaultValue: '子页面',
40 | },
41 | },
42 | {
43 | type: 'group',
44 | title: {
45 | label: '拓展区域',
46 | },
47 | extraProps: {
48 | display: 'block',
49 | },
50 | items: [...newNavAside],
51 | },
52 | {
53 | type: 'group',
54 | title: {
55 | label: '样式',
56 | },
57 | extraProps: {
58 | display: 'block',
59 | },
60 | items: [
61 | {
62 | name: 'style.background',
63 | title: {
64 | label: '主体背景色',
65 | },
66 | defaultValue: 'transparent',
67 | setter: 'ColorSetter',
68 | },
69 | ],
70 | },
71 | ],
72 | advanced: {
73 | callbacks: {
74 | // onNodeAdd,
75 | onNodeRemove: (removedNode: IPublicModelNode, currentNode: IPublicModelNode) => {
76 | // 如果删除的是slot 那么焦点聚焦到PageContent上
77 | if (
78 | removedNode.componentName === 'Slot' &&
79 | ['header', 'footer', 'aside', 'nav'].indexOf(String(removedNode?.slotFor?.key)) > -1
80 | ) {
81 | currentNode.select();
82 | }
83 | },
84 | onMoveHook() {
85 | return false;
86 | },
87 | },
88 | },
89 | },
90 | icon: 'https://alifd.oss-cn-hangzhou.aliyuncs.com/fusion-cool/icons/icon-light/ic_light_table.png',
91 | };
92 |
93 | export default meta;
94 |
--------------------------------------------------------------------------------
/lowcode/metas/page-footer.ts:
--------------------------------------------------------------------------------
1 | import { PAGE_FOOTER } from '../names';
2 | import minHeight from './setter/min-height';
3 | import background from './setter/background';
4 | import { IPublicTypeComponentMetadata } from '@alilc/lowcode-types';
5 |
6 | const meta: IPublicTypeComponentMetadata = {
7 | componentName: PAGE_FOOTER,
8 | title: '页面尾部',
9 | npm: {
10 | package: '@alifd/layout',
11 | version: '^0.1.0',
12 | exportName: 'Page',
13 | main: 'lib/index.js',
14 | destructuring: true,
15 | subName: 'Footer',
16 | },
17 | props: [],
18 | configure: {
19 | component: {
20 | isContainer: true,
21 | nestingRule: {},
22 | },
23 | props: [
24 | {
25 | name: 'fixed',
26 | title: {
27 | label: '是否吸底',
28 | tip: '可以用来放置吸底的表单提交按钮等',
29 | },
30 | defaultValue: false,
31 | setter: {
32 | componentName: 'BoolSetter',
33 | initialValue: false,
34 | defaultValue: false,
35 | },
36 | },
37 | ...minHeight,
38 | ...background,
39 | ],
40 | supports: {
41 | style: true,
42 | loop: false,
43 | },
44 | advanced: {
45 | callbacks: {
46 | onMoveHook() {
47 | return false;
48 | },
49 | },
50 | },
51 | },
52 | icon: 'https://alifd.oss-cn-hangzhou.aliyuncs.com/fusion-cool/icons/icon-light/ic_light_table.png',
53 | };
54 |
55 | export default meta;
56 |
--------------------------------------------------------------------------------
/lowcode/metas/page-header.ts:
--------------------------------------------------------------------------------
1 | import { PAGE_HEADER, CELL } from '../names';
2 | import minHeight from './setter/min-height';
3 | import background from './setter/background';
4 | import { IPublicTypeComponentMetadata } from '@alilc/lowcode-types';
5 |
6 | const config: IPublicTypeComponentMetadata = {
7 | componentName: PAGE_HEADER,
8 | title: '页面头部',
9 | npm: {
10 | package: '@alifd/layout',
11 | version: '^0.1.0',
12 | exportName: 'Page',
13 | main: 'lib/index.js',
14 | destructuring: true,
15 | subName: 'Header',
16 | },
17 | props: [],
18 | configure: {
19 | component: {
20 | isContainer: true,
21 | disableBehaviors: '*',
22 | nestingRule: {
23 | childWhitelist: [CELL],
24 | },
25 | },
26 | props: [
27 | {
28 | name: 'style.padding',
29 | title: {
30 | label: '内边距',
31 | },
32 | setter: {
33 | componentName: 'RadioGroupSetter',
34 | initialValue: '',
35 | props: {
36 | options: [
37 | {
38 | title: '无',
39 | value: '0',
40 | },
41 | {
42 | title: '有',
43 | value: '',
44 | },
45 | ],
46 | },
47 | },
48 | },
49 | {
50 | name: 'fullWidth',
51 | title: '全宽',
52 | setter: 'BoolSetter',
53 | },
54 | {
55 | title: '高度控制',
56 | type: 'group',
57 | display: 'block',
58 | items: [...minHeight],
59 | },
60 | {
61 | title: '样式',
62 | type: 'group',
63 | display: 'block',
64 | items: [...background],
65 | },
66 | ],
67 | advanced: {
68 | callbacks: {
69 | onMoveHook() {
70 | return false;
71 | },
72 | },
73 | },
74 | },
75 | icon: 'https://alifd.oss-cn-hangzhou.aliyuncs.com/fusion-cool/icons/icon-light/ic_light_table.png',
76 | };
77 |
78 | export default config;
79 |
--------------------------------------------------------------------------------
/lowcode/metas/page-nav.ts:
--------------------------------------------------------------------------------
1 | import { IPublicTypeComponentMetadata } from '@alilc/lowcode-types';
2 | import { PAGE_NAV, BLOCK } from '../names';
3 |
4 | const config: IPublicTypeComponentMetadata = {
5 | componentName: PAGE_NAV,
6 | title: '页面左侧',
7 | npm: {
8 | package: '@alifd/layout',
9 | version: '^0.1.0',
10 | exportName: 'Page',
11 | main: 'lib/index.js',
12 | destructuring: true,
13 | subName: 'Nav',
14 | },
15 | props: [],
16 | configure: {
17 | component: {
18 | isContainer: true,
19 | nestingRule: {
20 | childWhitelist: [BLOCK],
21 | },
22 | },
23 | props: [
24 | {
25 | name: 'width',
26 | title: {
27 | label: '宽度',
28 | },
29 | setter: {
30 | componentName: 'NumberSetter',
31 | // props: {
32 | // min: 0,
33 | // units: [{ type: 'px' }],
34 | // },
35 | },
36 | },
37 | ],
38 | supports: {
39 | style: true,
40 | loop: false,
41 | },
42 | advanced: {
43 | callbacks: {
44 | onMoveHook() {
45 | return false;
46 | },
47 | },
48 | },
49 | },
50 | icon: 'https://alifd.oss-cn-hangzhou.aliyuncs.com/fusion-cool/icons/icon-light/ic_light_table.png',
51 | };
52 |
53 | export default config;
54 |
--------------------------------------------------------------------------------
/lowcode/metas/row.ts:
--------------------------------------------------------------------------------
1 | import { IPublicModelNode, IPublicTypeComponentMetadata } from '@alilc/lowcode-types';
2 | import { CELL, ROW } from '../names';
3 | import { createCellSnippet, createPSnippet } from '../default-schema';
4 | import {
5 | onNodeRemoveSelfWhileNoChildren,
6 | onNodeReplaceSelfWithChildrenCell,
7 | onDrageResize,
8 | } from './enhance/callbacks';
9 | import { getResizingHandlers } from './enhance/experimentals';
10 | import minHeight from './setter/min-height';
11 |
12 | const config: IPublicTypeComponentMetadata = {
13 | componentName: ROW,
14 | title: '行容器',
15 | category: '容器',
16 | npm: {
17 | package: '@alifd/layout',
18 | version: '^0.1.0',
19 | exportName: 'Row',
20 | main: 'lib/index.js',
21 | destructuring: true,
22 | subName: '',
23 | },
24 | props: [
25 | {
26 | name: 'style',
27 | propType: 'object',
28 | },
29 | {
30 | name: 'minHeight',
31 | propType: 'number',
32 | },
33 | ],
34 | configure: {
35 | component: {
36 | isContainer: true,
37 | nestingRule: {
38 | childWhitelist: (node: IPublicModelNode) => {
39 | // 不能 ROW 套 ROW
40 | // 做了自动包裹 CELL 的处理,所以不需要强制 CELL
41 | return node.componentName !== ROW;
42 | },
43 | },
44 | },
45 | supports: { style: true },
46 | props: [
47 | ...minHeight,
48 | {
49 | name: 'gap',
50 | title: '间距',
51 | defaultValue: 4,
52 | setter: {
53 | componentName: 'NumberSetter',
54 | props: {
55 | step: 4,
56 | min: 0,
57 | },
58 | },
59 | },
60 | ],
61 | advanced: {
62 | getResizingHandlers,
63 | callbacks: {
64 | onNodeRemove: (removedNode: IPublicModelNode, currentNode: IPublicModelNode) => {
65 | onNodeRemoveSelfWhileNoChildren(removedNode, currentNode);
66 | },
67 | onSubtreeModified: (currentNode: IPublicModelNode, e: MouseEvent) => {
68 | onNodeReplaceSelfWithChildrenCell(currentNode, e);
69 | },
70 | /**
71 | * 组件拖入回调逐层向上触发,需要做好判断。
72 | * 组件拖入的时候包裹 CELL+P, 每一层父节点都会触发
73 | * @param {*} draggedNode 被拖入的组件
74 | * @param {*} currentNode 被拖入到 CELL
75 | */
76 | onNodeAdd: (draggedNode: IPublicModelNode, currentNode: IPublicModelNode) => {
77 | if (!draggedNode || draggedNode.componentName !== CELL) {
78 | const dropLocation = draggedNode?.document?.dropLocation;
79 | if (!dropLocation) {
80 | // 没有 dropLocation 一般是 slot, slot 元素不用特殊处理 不做任何包裹
81 | return;
82 | }
83 |
84 | const dropTarget = dropLocation.target;
85 |
86 | // 自动包裹 CELL + P
87 | if (dropTarget === currentNode) {
88 | const cellNode = currentNode?.document?.createNode(createCellSnippet());
89 | const pNode = currentNode?.document?.createNode(createPSnippet());
90 | pNode && cellNode?.insertAfter(pNode);
91 |
92 | cellNode && currentNode.insertAfter(cellNode, draggedNode, false);
93 | pNode?.insertAfter(draggedNode, pNode, false);
94 | }
95 | }
96 | },
97 | ...onDrageResize,
98 | },
99 | },
100 | },
101 | icon: 'https://img.alicdn.com/imgextra/i1/O1CN01AQZw941ZgdfVtjsDO_!!6000000003224-55-tps-128-128.svg',
102 | };
103 |
104 | export default config;
105 |
--------------------------------------------------------------------------------
/lowcode/metas/section.ts:
--------------------------------------------------------------------------------
1 | import { IPublicModelNode, IPublicTypeComponentMetadata } from '@alilc/lowcode-types';
2 |
3 | import { updateSpan } from '../common/split/auto-block';
4 | import { PAGE, SECTION, BLOCK, CELL } from '../names';
5 | import minHeight from './setter/min-height';
6 | import background from './setter/background';
7 |
8 | const config: IPublicTypeComponentMetadata = {
9 | componentName: SECTION,
10 | title: '区域',
11 | category: '布局容器类',
12 | npm: {
13 | package: '@alifd/layout',
14 | version: '^0.1.0',
15 | exportName: 'Section',
16 | main: 'lib/index.js',
17 | destructuring: true,
18 | subName: '',
19 | },
20 | props: [
21 | {
22 | name: 'style',
23 | propType: 'object',
24 | },
25 | {
26 | name: 'childTotalColumns',
27 | propType: 'number',
28 | },
29 | {
30 | name: 'background',
31 | propType: {
32 | type: 'oneOf',
33 | value: ['lining', 'surface', 'transparent'],
34 | },
35 | defaultValue: 'transparent',
36 | },
37 | ],
38 | configure: {
39 | component: {
40 | isContainer: true,
41 | nestingRule: {
42 | childWhitelist: [BLOCK],
43 | parentWhitelist: (testNode: IPublicModelNode) => {
44 | // 允许拖入LayoutPage aside slot中
45 | if (testNode.componentName === PAGE) {
46 | return true;
47 | }
48 |
49 | if (
50 | testNode.componentName === 'Slot' &&
51 | ['aside'].indexOf(testNode.title as string) > -1
52 | ) {
53 | return true;
54 | }
55 | return false;
56 | },
57 | },
58 | },
59 | props: [
60 | {
61 | type: 'group',
62 | title: {
63 | label: '高度',
64 | },
65 | extraProps: {
66 | display: 'block',
67 | },
68 | items: [...minHeight],
69 | },
70 | {
71 | type: 'group',
72 | title: {
73 | label: '布局',
74 | },
75 | extraProps: {
76 | display: 'block',
77 | },
78 | items: [
79 | {
80 | name: 'gap',
81 | title: '间距',
82 | setter: {
83 | componentName: 'NumberSetter',
84 | props: {
85 | step: 4,
86 | min: 0,
87 | },
88 | },
89 | },
90 | ],
91 | },
92 | {
93 | type: 'group',
94 | title: {
95 | label: '区域功能',
96 | },
97 | extraProps: {
98 | display: 'block',
99 | },
100 | items: [
101 | {
102 | name: 'title',
103 | title: '区域标题',
104 | setter: () => {
105 | if (
106 | window.AliLowCodeEngine &&
107 | window.AliLowCodeEngine.setters.getSetter('TitleSetter')
108 | ) {
109 | return {
110 | componentName: 'TitleSetter',
111 | defaultValue: '区域标题',
112 | props: {
113 | // defaultChecked: true,
114 | },
115 | };
116 | }
117 | return {
118 | componentName: 'StringSetter',
119 | defaultValue: '区域标题',
120 | };
121 | },
122 | },
123 | ],
124 | },
125 | {
126 | type: 'group',
127 | title: {
128 | label: '样式',
129 | },
130 | extraProps: {
131 | display: 'block',
132 | },
133 | items: [...background],
134 | },
135 | ],
136 | supports: {
137 | className: true,
138 | style: true,
139 | loop: false,
140 | },
141 | advanced: {
142 | callbacks: {
143 | onNodeRemove: (removedNode: IPublicModelNode) => {
144 | updateSpan({
145 | parent: removedNode.parent,
146 | child: removedNode,
147 | type: 'delete',
148 | });
149 | },
150 | onLocateHook: ({ dragObject, target, detail }) => {
151 | if (dragObject.nodes?.length === 1) {
152 | const dragNode = dragObject.nodes[0];
153 | const currentDragIndex = dragNode.index;
154 | if (!target.lastFlatenMap) {
155 | updateSpan({
156 | parent: target,
157 | type: 'refresh',
158 | });
159 | }
160 | const flattenMap = target.lastFlatenMap || [];
161 | let distDragIndex = detail.index;
162 | if (distDragIndex > currentDragIndex) {
163 | distDragIndex -= 1;
164 | }
165 | // 只有同行可以换
166 | if (
167 | flattenMap[currentDragIndex]?.groupIndex === flattenMap[distDragIndex]?.groupIndex
168 | ) {
169 | return true;
170 | }
171 | }
172 | // 拖拽多个先不处理
173 | return false;
174 | },
175 | // onHoverHook: () => false,
176 | // onMouseDownHook: () => false,
177 | // onClickHook: () => false,
178 | },
179 | initialChildren: [
180 | {
181 | componentName: BLOCK,
182 | props: {},
183 | children: [
184 | {
185 | componentName: CELL,
186 | props: {},
187 | },
188 | ],
189 | },
190 | ],
191 | },
192 | },
193 | icon: 'https://img.alicdn.com/imgextra/i3/O1CN018CwRJM1ZkIpmeEfRD_!!6000000003232-55-tps-128-128.svg',
194 | };
195 |
196 | export default config;
197 |
--------------------------------------------------------------------------------
/lowcode/metas/setter/background.ts:
--------------------------------------------------------------------------------
1 | // const TooltipLabel = require('./tooltip-label');
2 |
3 | import { IPublicModelSettingPropEntry, IPublicTypeFieldConfig } from '@alilc/lowcode-types';
4 |
5 | const items: IPublicTypeFieldConfig[] = [
6 | // {
7 | // name: 'style.background',
8 | // title: '背景类型',
9 | // initialValue: '',
10 | // defaultValue: '',
11 | // setter: {
12 | // componentName: 'RadioGroupSetter',
13 | // props: {
14 | // options: [
15 | // {
16 | // title:
17 | //
24 | //
25 | //
26 | // ,
27 | // value: 'color',
28 | // },
29 | // {
30 | // title:
31 | //
38 | //
39 | //
40 | // ,
41 | // value: 'img',
42 | // },
43 | // ],
44 | // },
45 | // },
46 | // },
47 | {
48 | name: 'style.backgroundColor',
49 | title: '背景色',
50 | setter: {
51 | componentName: 'ColorSetter',
52 | initialValue: 'rgba(255,255,255,1)',
53 | },
54 | },
55 | {
56 | name: 'style.backgroundImage',
57 | title: '背景图',
58 | setter: {
59 | componentName: 'StringSetter',
60 | initialValue: '',
61 | props: {
62 | placeholder: '输入图片 url',
63 | },
64 | },
65 | extraProps: {
66 | getValue: (target: IPublicModelSettingPropEntry) => {
67 | const bgImg = target.node?.getPropValue('style.backgroundImage');
68 | return bgImg?.match(/^url\((.*)\)$/)?.[1];
69 | },
70 | setValue: (target: IPublicModelSettingPropEntry, value: string) => {
71 | if (value) {
72 | target.node?.setPropValue('style.backgroundImage', `url(${value})`);
73 | } else {
74 | const style = target.node?.getPropValue('style');
75 | style && delete style.backgroundImage;
76 | target.node?.setPropValue('style', style);
77 | }
78 | },
79 | },
80 | },
81 | ];
82 |
83 | export default items;
84 |
--------------------------------------------------------------------------------
/lowcode/metas/setter/gap.ts:
--------------------------------------------------------------------------------
1 | import { IPublicTypeFieldConfig } from '@alilc/lowcode-types';
2 |
3 | const item: IPublicTypeFieldConfig[] = [
4 | {
5 | name: 'gap',
6 | title: '间距',
7 | defaultValue: 0,
8 | setter: {
9 | componentName: 'NumberSetter',
10 | props: {
11 | step: 4,
12 | },
13 | },
14 | },
15 | ];
16 |
17 | export default item;
18 |
--------------------------------------------------------------------------------
/lowcode/metas/setter/height.ts:
--------------------------------------------------------------------------------
1 | import { IPublicModelSettingPropEntry, IPublicTypeFieldConfig } from '@alilc/lowcode-types';
2 |
3 | const items: IPublicTypeFieldConfig[] = [
4 | {
5 | name: '!heightType',
6 | title: '高度类型',
7 | defaultValue: '',
8 | setter: {
9 | componentName: 'RadioGroupSetter',
10 | props: {
11 | options: [
12 | // {
13 | // title: '适应内容',
14 | // value: 'autoFit',
15 | // },
16 | {
17 | title: '自动',
18 | value: 'auto',
19 | },
20 | {
21 | title: '固定',
22 | value: 'fixed',
23 | },
24 | ],
25 | },
26 | },
27 | extraProps: {
28 | getValue: (target: IPublicModelSettingPropEntry) => {
29 | if (target.node?.getPropValue('height')) {
30 | return 'fixed';
31 | }
32 | return 'auto';
33 | // if (target.getNode().getPropValue('autoFit')) {
34 | // return 'autoFit';
35 | // } else {
36 | // return 'auto';
37 | // }
38 | },
39 | setValue: (target: IPublicModelSettingPropEntry, value: string) => {
40 | if (value === 'fixed') {
41 | target.node?.setPropValue('height', parseInt(String(target.node?.getRect()?.height)));
42 | } else if (value === 'auto') {
43 | target.node?.setPropValue('height', undefined);
44 | // target.getNode().setPropValue('autoFit', false);
45 | } else if (value === 'autoFit') {
46 | // target.getNode().setPropValue('autoFit', true);
47 | }
48 | },
49 | },
50 | },
51 | {
52 | name: 'height',
53 | title: '高度值',
54 | defaultValue: '',
55 | setter: {
56 | componentName: 'NumberSetter',
57 | props: {
58 | units: 'px',
59 | },
60 | },
61 | condition: (target: IPublicModelSettingPropEntry) => {
62 | return (
63 | target.node?.getPropValue('!heightType') === 'fixed' ||
64 | !!target.node?.getPropValue('height')
65 | );
66 | },
67 | },
68 | ];
69 |
70 | export default items;
71 |
--------------------------------------------------------------------------------
/lowcode/metas/setter/min-height.ts:
--------------------------------------------------------------------------------
1 | import { IPublicModelSettingPropEntry, IPublicTypeFieldConfig } from '@alilc/lowcode-types';
2 |
3 | const items: IPublicTypeFieldConfig[] = [
4 | {
5 | name: '!heightType',
6 | title: '高度类型',
7 | setter: {
8 | componentName: 'RadioGroupSetter',
9 | initialValue: '',
10 | props: {
11 | options: [
12 | {
13 | title: '适应',
14 | value: 'auto',
15 | },
16 | {
17 | title: '至少',
18 | value: 'min',
19 | },
20 | ],
21 | },
22 | },
23 | extraProps: {
24 | getValue: (target: IPublicModelSettingPropEntry) => {
25 | if (target.node?.getPropValue('style.minHeight')) {
26 | return 'min';
27 | } else {
28 | return 'auto';
29 | }
30 | },
31 | setValue: (target: IPublicModelSettingPropEntry, value: string) => {
32 | if (value === 'min') {
33 | target.node?.setPropValue(
34 | 'style.minHeight',
35 | parseInt(String(target.node?.getRect()?.height)),
36 | );
37 | } else if (value === 'auto') {
38 | target.node?.setPropValue('style.minHeight', null);
39 | }
40 | },
41 | },
42 | },
43 | {
44 | name: 'style.minHeight',
45 | title: '最小高度',
46 | defaultValue: '',
47 | setter: {
48 | componentName: 'NumberSetter',
49 | props: {
50 | units: 'px',
51 | step: 2,
52 | },
53 | },
54 | condition: (target: IPublicModelSettingPropEntry) => {
55 | return (
56 | target.node?.getPropValue('!heightType') === 'min' ||
57 | !!target.node?.getPropValue('style.minHeight')
58 | );
59 | },
60 | },
61 | ];
62 |
63 | export default items;
64 |
--------------------------------------------------------------------------------
/lowcode/metas/setter/padding.ts:
--------------------------------------------------------------------------------
1 | import { IPublicModelSettingPropEntry, IPublicTypeFieldConfig } from '@alilc/lowcode-types';
2 |
3 | const items: IPublicTypeFieldConfig[] = [
4 | {
5 | name: 'style.width',
6 | title: '宽度类型',
7 | defaultValue: '',
8 | setter: {
9 | componentName: 'RadioGroupSetter',
10 | props: {
11 | options: [
12 | {
13 | title: '自适应',
14 | value: '',
15 | },
16 | {
17 | title: '铺满',
18 | value: '100%',
19 | },
20 | {
21 | title: '固定',
22 | value: 'fixed',
23 | },
24 | ],
25 | },
26 | },
27 | extraProps: {
28 | getValue: (target: IPublicModelSettingPropEntry) => {
29 | return typeof target.node?.getPropValue('style.width') === 'number' ? 'fixed' : '';
30 | },
31 | setValue: (target: IPublicModelSettingPropEntry, value: string) => {
32 | if (value === 'fixed') {
33 | target.node?.setPropValue('style.width', parseInt(String(target.node?.getRect()?.width)));
34 | } else if (value === '') {
35 | target.node?.setPropValue('style.width', '');
36 | }
37 | },
38 | },
39 | },
40 | {
41 | name: 'style.width',
42 | title: '宽度值',
43 | defaultValue: '',
44 | initialValue: '',
45 | setter: {
46 | componentName: 'NumberSetter',
47 | props: {
48 | units: 'px',
49 | },
50 | },
51 | condition: (target: IPublicModelSettingPropEntry) =>
52 | typeof target.node?.getPropValue('style.width') === 'number',
53 | },
54 | ];
55 |
56 | export default items;
57 |
--------------------------------------------------------------------------------
/lowcode/metas/setter/tooltip-label.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Balloon from '@alifd/next/lib/balloon';
3 | import Box from '@alifd/next/lib/box';
4 |
5 | export default (props: any) => {
6 | const { overlay, children, ...others } = props;
7 | return (
8 |
15 | {children}
16 |
17 | }
18 | >
19 | {overlay}
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/lowcode/metas/setter/width.ts:
--------------------------------------------------------------------------------
1 | import { IPublicModelSettingPropEntry, IPublicTypeFieldConfig } from '@alilc/lowcode-types';
2 |
3 | const items: IPublicTypeFieldConfig[] = [
4 | {
5 | name: '!widthType',
6 | title: '宽度类型',
7 | defaultValue: '',
8 | setter: {
9 | componentName: 'RadioGroupSetter',
10 | props: {
11 | options: [
12 | // {
13 | // title: '适应内容',
14 | // value: 'autoFit',
15 | // },
16 | {
17 | title: '拉伸',
18 | value: 'auto',
19 | },
20 | {
21 | title: '固定',
22 | value: 'fixed',
23 | },
24 | ],
25 | },
26 | },
27 | extraProps: {
28 | getValue: (target: IPublicModelSettingPropEntry) => {
29 | if (target.node?.getPropValue('width')) {
30 | return 'fixed';
31 | }
32 | return 'auto';
33 | // if (target.getNode().getPropValue('autoFit')) {
34 | // return 'autoFit';
35 | // } else {
36 | // return 'auto';
37 | // }
38 | },
39 | setValue: (target: IPublicModelSettingPropEntry, value: string) => {
40 | if (value === 'fixed') {
41 | target.node?.setPropValue('width', parseInt(String(target.node?.getRect()?.width)));
42 | } else if (value === 'auto') {
43 | target.node?.setPropValue('width', undefined);
44 | // target.getNode().setPropValue('autoFit', false);
45 | } else if (value === 'autoFit') {
46 | // target.getNode().setPropValue('autoFit', true);
47 | }
48 | },
49 | },
50 | },
51 | {
52 | name: 'width',
53 | title: '宽度值',
54 | defaultValue: '',
55 | initialValue: '',
56 | setter: {
57 | componentName: 'NumberSetter',
58 | props: {
59 | units: 'px',
60 | },
61 | },
62 | condition: (target: IPublicModelSettingPropEntry) => {
63 | return (
64 | target.node?.getPropValue('!widthType') === 'fixed' || !!target.node?.getPropValue('width')
65 | );
66 | },
67 | },
68 | ];
69 |
70 | export default items;
71 |
--------------------------------------------------------------------------------
/lowcode/names.ts:
--------------------------------------------------------------------------------
1 | export const PAGE = 'FDPage';
2 | export const PAGE_HEADER = 'FDPageHeader';
3 | export const PAGE_FOOTER = 'FDPageFooter';
4 | export const PAGE_NAV = 'FDPageNav';
5 | export const PAGE_ASIDE = 'FDPageAside';
6 | export const PAGE_CONTENT = 'FDPageContent';
7 | export const SECTION = 'FDSection';
8 | export const BLOCK = 'FDBlock';
9 | export const CELL = 'FDCell';
10 | export const ROW = 'FDRow';
11 | export const COL = 'FDCol';
12 | export const GRID = 'FDGrid';
13 | export const P = 'FDP';
14 | export const FIXED_POINT = 'FDFixedPoint';
15 | export const FIXED_CONTAINER = 'FDFixedContainer';
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@alifd/layout",
3 | "version": "2.4.1",
4 | "description": "基于 Fusion 设计系统的自然布局体系",
5 | "files": [
6 | "demo/",
7 | "es/",
8 | "lib/",
9 | "build/",
10 | "dist/",
11 | "lowcode_lib/",
12 | "lowcode_es/"
13 | ],
14 | "main": "lib/index.js",
15 | "module": "es/index.js",
16 | "lowcodeEditMain": "build/lowcode/view.js",
17 | "author": {
18 | "name": "lianmin",
19 | "email": "406400939@qq.com"
20 | },
21 | "contributors": [],
22 | "scripts": {
23 | "start": "build-scripts start",
24 | "build": "build-scripts build",
25 | "prepublishOnly": "npm run build && npm run lowcode:build",
26 | "lowcode:dev": "build-scripts start --config ./build.lowcode.js",
27 | "lowcode:build": "build-scripts build --config ./build.lowcode.js",
28 | "test": "build-scripts test",
29 | "eslint": "eslint --cache --ext .js,.jsx ./",
30 | "eslint:fix": "npm run eslint -- --fix",
31 | "stylelint": "stylelint \"**/*.{css,scss,less}\"",
32 | "lint": "npm run eslint && npm run stylelint",
33 | "f2elint-scan": "f2elint scan",
34 | "f2elint-fix": "f2elint fix",
35 | "release": "standard-version",
36 | "release:beta": "standard-version --release-as minor --prerelease beta"
37 | },
38 | "lint-staged": {
39 | "@src/**/*.(ts|tsx)": [
40 | "eslint --fix",
41 | "prettier --write"
42 | ],
43 | "src/**/*.(css|scss|less)": [
44 | "stylelint --fix"
45 | ]
46 | },
47 | "standard-version": {
48 | "skip": {
49 | "commit": true
50 | }
51 | },
52 | "keywords": [
53 | "layout",
54 | "fusion",
55 | "ice",
56 | "react",
57 | "component"
58 | ],
59 | "dependencies": {
60 | "@alifd/next": "^1.x",
61 | "@alilc/lowcode-engine": "^1.1.1",
62 | "@alilc/lowcode-types": "^1.1.3-beta.1",
63 | "classnames": "^2.3.2",
64 | "is-valid-array": "^1.0.1",
65 | "lodash-es": "^4.17.21",
66 | "resize-observer-polyfill": "^1.5.1",
67 | "typescript": "^4.9.5",
68 | "@babel/runtime": "^7.0.0",
69 | "react-draggable": "^4.4.4"
70 | },
71 | "devDependencies": {
72 | "@alib/build-scripts": "^0.1.32",
73 | "@alifd/build-plugin-lowcode": "^0.4.6",
74 | "@alifd/next": "1.x",
75 | "@alifd/theme-3": "^0.5.3",
76 | "@alilc/lowcode-types": "^1.1.3-beta.2",
77 | "@iceworks/spec": "^1.0.0",
78 | "@types/hoist-non-react-statics": "^3.3.1",
79 | "@types/lodash-es": "^4.17.6",
80 | "@types/react": "^16.14.35",
81 | "@types/react-dom": "^16.9.4",
82 | "build-plugin-component": "^1.0.0",
83 | "build-plugin-fusion": "^0.1.0",
84 | "build-plugin-jsx-plus": "^0.1.4",
85 | "build-plugin-moment-locales": "^0.1.0",
86 | "commitlint": "^17.6.5",
87 | "enzyme": "^3.10.0",
88 | "enzyme-adapter-react-16": "^1.14.0",
89 | "f2elint": "^1.2.0",
90 | "iceworks": "^3.4.5",
91 | "moment": "^2.29.4",
92 | "react": "^16.3.0",
93 | "react-dom": "^16.3.0",
94 | "standard-version": "^9.5.0"
95 | },
96 | "peerDependencies": {
97 | "@alifd/next": "1.x",
98 | "react": ">=16"
99 | },
100 | "componentConfig": {
101 | "name": "Layout",
102 | "title": "自然布局",
103 | "category": "Information",
104 | "materialSchema": "https://unpkg.alibaba-inc.com/@alifd/layout@2.2.3/build/lowcode/assets-prod.json"
105 | },
106 | "publishConfig": {
107 | "access": "public"
108 | },
109 | "license": "MIT",
110 | "homepage": "https://unpkg.com/@alifd/layout@2.2.2/build/index.html",
111 | "bugs": "https://gitlab.alibaba-inc.com/fusion-design/layout/issues",
112 | "repository": "https://github.com/alibaba-fusion/layout.git",
113 | "resolutions": {
114 | "sass": "1.35.x"
115 | },
116 | "exports": {
117 | "./prototype": {
118 | "require": "./lowcode_lib/meta_entry.js",
119 | "import": "./lowcode_es/meta_entry.js"
120 | },
121 | "./prototypeView": {
122 | "require": "./lowcode_lib/view_entry.js",
123 | "import": "./lowcode_es/view_entry.js"
124 | },
125 | "./*": "./*",
126 | ".": {
127 | "import": "./es/index.js",
128 | "require": "./lib/index.js"
129 | }
130 | },
131 | "lcMeta": {
132 | "type": "component"
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/block.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | useContext,
3 | forwardRef,
4 | ForwardedRef,
5 | ForwardRefExoticComponent,
6 | ForwardRefRenderFunction,
7 | } from 'react';
8 | import classNames from 'classnames';
9 | import { isString } from 'lodash-es';
10 |
11 | import Context from '@/common/context';
12 | import Row from '@/row';
13 | import Cell from '@/cell';
14 | import P from '@/p';
15 | import Text from '@/text';
16 | import { BlockProps, LayoutContextProps, TypeMark } from './types';
17 |
18 | type IBlock = ForwardRefExoticComponent & TypeMark;
19 |
20 | /**
21 | * 区块,默认 flex 布局
22 | * @param props
23 | * @param ref
24 | * @constructor
25 | */
26 | const Block: ForwardRefRenderFunction = (props, ref: ForwardedRef) => {
27 | const {
28 | className,
29 | title,
30 | titleAlign,
31 | extra,
32 | noPadding: noPaddingProp,
33 | mode,
34 | bordered,
35 | width,
36 | contentClassName,
37 | contentStyle = {},
38 | span: spanProp,
39 | divider,
40 | children,
41 | align,
42 | verAlign,
43 | ...others
44 | } = props;
45 | const { prefix, maxNumberOfColumns } = useContext(Context);
46 | const isTransparent = mode === 'transparent';
47 | const noPadding = !isTransparent ? noPaddingProp : true;
48 | const hasHead = !isTransparent && (title || extra);
49 |
50 | let span = spanProp;
51 |
52 | if (!span || span > maxNumberOfColumns || span <= 0) {
53 | span = maxNumberOfColumns;
54 | }
55 |
56 | const clsPrefix = `${prefix}block`;
57 |
58 | const blockCls = classNames(className, clsPrefix, {
59 | [`${prefix}bg--${mode}`]: mode,
60 | [`${clsPrefix}--no-padding`]: noPadding,
61 | [`${clsPrefix}--headless`]: !hasHead,
62 | [`${clsPrefix}--span-${span}`]: span > 0,
63 | [`${clsPrefix}--bordered`]: !isTransparent && bordered,
64 | [`${clsPrefix}--divided`]: divider,
65 | });
66 |
67 | const headCls = classNames({
68 | [`${clsPrefix}-head`]: true,
69 | [`${clsPrefix}-head--no-padding`]: noPadding,
70 | });
71 |
72 | const blockContentCls = classNames(contentClassName, {
73 | [`${clsPrefix}-content`]: true,
74 | [`${clsPrefix}-content--no-padding`]: noPadding,
75 | });
76 |
77 | // 当有 title 或 extra 节点时
78 | if (hasHead) {
79 | return (
80 |
81 |
82 |
83 | {isString(title) ? (
84 |
85 | {title}
86 |
87 | ) : (
88 | title
89 | )}
90 | |
91 |
92 | {extra ? (
93 |
94 | {isString(extra) ? (
95 |
96 | {extra}
97 |
98 | ) : (
99 | extra
100 | )}
101 | |
102 | ) : null}
103 |
104 |
105 |
112 | {children}
113 | |
114 |
115 | );
116 | }
117 |
118 | return (
119 | // @ts-ignore
120 |
121 | {children}
122 | |
123 | );
124 | };
125 |
126 | const RefBlock: IBlock = forwardRef(Block);
127 |
128 | RefBlock.displayName = 'Block';
129 | RefBlock.defaultProps = {
130 | mode: 'surface',
131 | noPadding: false,
132 | };
133 | RefBlock.typeMark = 'Block';
134 |
135 | export default RefBlock;
136 |
--------------------------------------------------------------------------------
/src/cell.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | ForwardRefExoticComponent,
3 | forwardRef,
4 | useContext,
5 | CSSProperties,
6 | ForwardRefRenderFunction,
7 | useMemo,
8 | } from 'react';
9 | import classNames from 'classnames';
10 |
11 | import Context from '@/common/context';
12 | import { VER_ALIGN_ALIAS_MAP } from '@/common/constant';
13 | import { isValidGap, wrapUnit } from '@/utils';
14 | import useFlexClassNames from '@/hooks/use-flex-class-names';
15 | import { CellProps, LayoutContextProps, TypeMark } from './types';
16 |
17 | type ICell = ForwardRefExoticComponent & TypeMark;
18 |
19 | /**
20 | * 单元格容器(默认垂直方向的 flex 布局容器)
21 | */
22 | const Cell: ForwardRefRenderFunction = (props, ref) => {
23 | const {
24 | className,
25 | children,
26 | verAlign,
27 | width,
28 | height,
29 | block,
30 | direction,
31 | align,
32 | style,
33 | // 父级元素处理 autoFit 的相关布局
34 | autoFit,
35 | gap,
36 | __designMode,
37 | componentId,
38 | _componentName,
39 | ...others
40 | } = props;
41 | const { prefix } = useContext(Context);
42 | const clsPrefix = `${prefix}cell`;
43 |
44 | const validWidth = width || style?.width;
45 | const newStyle: CSSProperties = useMemo(
46 | () => ({
47 | ...(!block
48 | ? { display: 'flex', flexDirection: direction === 'ver' ? 'column' : 'row' }
49 | : null),
50 | ...(verAlign
51 | ? {
52 | // @ts-ignore
53 | justifyContent: VER_ALIGN_ALIAS_MAP[verAlign] || verAlign,
54 | }
55 | : null),
56 | ...(width ? { width: wrapUnit(width) } : null),
57 | ...(height ? { height: wrapUnit(height) } : null),
58 | ...(isValidGap(gap) ? { gap: wrapUnit(gap) } : null),
59 | // 有 width 或者 style.width 的时候,设置 flexBasis 宽度
60 | ...(validWidth ? { flexBasis: wrapUnit(validWidth) } : null),
61 | ...style,
62 | }),
63 | [block, direction, verAlign, width, height, gap, style, validWidth],
64 | );
65 |
66 | const flexClassNames = useFlexClassNames(props);
67 |
68 | return (
69 |
77 | {children}
78 |
79 | );
80 | };
81 |
82 | const RefCell: ICell = forwardRef(Cell);
83 |
84 | RefCell.displayName = 'Cell';
85 | RefCell.defaultProps = {
86 | block: false,
87 | direction: 'ver',
88 | };
89 | RefCell.typeMark = 'Cell';
90 |
91 | export default RefCell;
92 |
--------------------------------------------------------------------------------
/src/col.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | useContext,
3 | forwardRef,
4 | ForwardRefExoticComponent,
5 | ForwardRefRenderFunction,
6 | useMemo,
7 | } from 'react';
8 | import classNames from 'classnames';
9 |
10 | import { ALIGN_ALIAS_MAP } from '@/common/constant';
11 | import Context from '@/common/context';
12 | import { wrapUnit, getGapVal } from '@/utils';
13 | import useFlexClassNames from '@/hooks/use-flex-class-names';
14 | import { ColProps, LayoutContextProps, TypeMark } from './types';
15 |
16 | type ICol = ForwardRefExoticComponent & TypeMark;
17 |
18 | /**
19 | * 列拆分布局(子元素如果不是 Row、Col 或 Cell, 则默认用 Cell 包裹)
20 | */
21 | const Col: ForwardRefRenderFunction = (props: ColProps, ref) => {
22 | const {
23 | children,
24 | width,
25 | height,
26 | className,
27 | align,
28 | gap: gapProp,
29 | autoFit,
30 | style,
31 | ...others
32 | } = props;
33 | const { prefix, gridGap } = useContext(Context);
34 | const clsPrefix = `${prefix}col-flex`;
35 | const gap = getGapVal(gridGap, gapProp);
36 |
37 | const validWidth = width || style?.width;
38 | const newStyle = useMemo(
39 | () => ({
40 | // @ts-ignore
41 | alignItems: ALIGN_ALIAS_MAP[align] || align,
42 | justifyContent: 'stretch',
43 | ...(width ? { width: wrapUnit(width) } : null),
44 | ...(height ? { height: wrapUnit(height), flex: '0 0 auto' } : null),
45 | ...(gap ? { gap: wrapUnit(gap) } : null),
46 | // 有 width 或者 style.width 的时候,设置 flexBasis 宽度
47 | ...(validWidth ? { flexBasis: wrapUnit(validWidth) } : null),
48 | ...style,
49 | }),
50 | [align, width, height, gap, style, validWidth],
51 | );
52 | const flexClassNames = useFlexClassNames(props);
53 |
54 | return (
55 |
61 | {children}
62 |
63 | );
64 | };
65 |
66 | const RefCol: ICol = forwardRef(Col);
67 |
68 | RefCol.displayName = 'Col';
69 | RefCol.typeMark = 'Col';
70 |
71 | export default RefCol;
72 |
--------------------------------------------------------------------------------
/src/common/constant.ts:
--------------------------------------------------------------------------------
1 | import { BreakPoints } from '@/types';
2 |
3 | // 默认断点列信息
4 | export const DEFAULT_BREAK_POINTS: BreakPoints = [
5 | {
6 | width: 750,
7 | maxContentWidth: 750,
8 | numberOfColumns: 4,
9 | },
10 | {
11 | width: 960,
12 | maxContentWidth: 960,
13 | numberOfColumns: 8,
14 | },
15 | {
16 | width: 1200,
17 | maxContentWidth: 1200,
18 | numberOfColumns: 12,
19 | },
20 | {
21 | width: Infinity,
22 | maxContentWidth: Infinity,
23 | numberOfColumns: 12,
24 | },
25 | ];
26 |
27 | // css 别名处理
28 | export const ALIGN_ALIAS_MAP = {
29 | left: 'start',
30 | right: 'end',
31 | middle: 'center',
32 | };
33 |
34 | export const VER_ALIGN_ALIAS_MAP = {
35 | top: 'start',
36 | middle: 'center',
37 | bottom: 'end',
38 | };
39 |
40 | export const TEXT_TYPE_MAP = {
41 | 'body-1': 'body2',
42 | 'body-2': 'body1',
43 | subhead: 'h6',
44 | title: 'h5',
45 | headline: 'h4',
46 | 'display-1': 'h3',
47 | 'display-2': 'h2',
48 | 'display-3': 'h1',
49 | };
50 |
--------------------------------------------------------------------------------
/src/common/context.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 | import { last } from 'lodash-es';
3 | import { DEFAULT_BREAK_POINTS } from '@/common/constant';
4 | import { BreakPoint, LayoutContextProps } from '@/types';
5 |
6 | const lastBreakPoint = last(DEFAULT_BREAK_POINTS) as BreakPoint;
7 |
8 | // 默认的布局上下文参数
9 | export const defaultContext: LayoutContextProps = {
10 | prefix: 'fd-layout-',
11 | noPadding: false,
12 | sectionGap: undefined,
13 | blockGap: undefined,
14 | gridGap: undefined,
15 | maxNumberOfColumns: lastBreakPoint.numberOfColumns || 12,
16 | breakPoint: lastBreakPoint,
17 | };
18 |
19 | export default createContext(defaultContext);
20 |
--------------------------------------------------------------------------------
/src/fixed-container.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef, Children, ForwardRefRenderFunction, useContext } from 'react';
2 | import classNames from 'classnames';
3 | import useFlexClassNames from '@/hooks/use-flex-class-names';
4 | import Context from '@/common/context';
5 | import { LayoutContextProps } from './types';
6 |
7 | /**
8 | * 自由容器
9 | */
10 | const FixedContainer: ForwardRefRenderFunction = (props: any, ref) => {
11 | const { children = [], style, items = [] } = props;
12 | const { prefix } = useContext(Context);
13 | const clsPrefix = `${prefix}fixed-container`;
14 | const flexClassNames = useFlexClassNames(props);
15 |
16 | return (
17 |
22 | {Children.map(children, (child, idx) => {
23 | return
{child}
;
24 | })}
25 |
26 | );
27 | };
28 |
29 | FixedContainer.displayName = 'FixedContainer';
30 |
31 | export default forwardRef(FixedContainer);
32 |
--------------------------------------------------------------------------------
/src/fixed-point.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, forwardRef, ForwardRefRenderFunction } from 'react';
2 | import classNames from 'classnames';
3 |
4 | import Context from '@/common/context';
5 | import { LayoutContextProps } from './types';
6 |
7 | /**
8 | * 自由节点
9 | */
10 | const FixedPoint: ForwardRefRenderFunction = (props: any, ref) => {
11 | const { children, left = 0, top = 0, className, zIndex } = props;
12 | const { prefix } = useContext(Context);
13 |
14 | return (
15 |
22 | );
23 | };
24 |
25 | FixedPoint.displayName = 'FixedPoint';
26 |
27 | export default forwardRef(FixedPoint);
28 |
--------------------------------------------------------------------------------
/src/grid.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | forwardRef,
3 | ForwardRefExoticComponent,
4 | ForwardRefRenderFunction,
5 | useContext,
6 | useMemo,
7 | } from 'react';
8 | import classNames from 'classnames';
9 |
10 | import Context from '@/common/context';
11 | import { getGapVal, wrapUnit } from '@/utils';
12 | import useFlexClassNames from '@/hooks/use-flex-class-names';
13 | import { GridProps, LayoutContextProps, TypeMark } from './types';
14 |
15 | type IGrid = ForwardRefExoticComponent & TypeMark;
16 |
17 | /**
18 | * 网格布局
19 | * @param props
20 | * @param ref
21 | */
22 | const Grid: ForwardRefRenderFunction = (props, ref) => {
23 | const {
24 | className,
25 | children,
26 | style,
27 | align,
28 | verAlign,
29 | rowGap: rowGapProp,
30 | colGap: colGapProp,
31 | renderItem,
32 | rows,
33 | cols,
34 | width,
35 | minWidth,
36 | maxWidth,
37 | ...others
38 | } = props;
39 |
40 | const { prefix, gridGap } = useContext(Context);
41 | const clsPrefix = `${prefix}grid`;
42 | const rowGap = getGapVal(gridGap, rowGapProp);
43 | const colGap = getGapVal(gridGap, colGapProp);
44 |
45 | const validWidth = width || style?.width || minWidth;
46 | const memorizedNewStyle = useMemo(() => {
47 | let gtc = `repeat(${cols}, 1fr)`;
48 |
49 | if (cols && cols > 1) {
50 | gtc = `repeat(${cols}, calc( (100% - ${colGap || `var(--page-grid-gap)`} * ${
51 | cols - 1
52 | })/${cols}))`;
53 | } else if (minWidth && maxWidth) {
54 | gtc = `repeat(auto-fill, minmax(${wrapUnit(minWidth)}, ${wrapUnit(maxWidth)}))`;
55 | } else if (minWidth && !maxWidth) {
56 | gtc = `repeat(auto-fit, minmax(${wrapUnit(minWidth)}, auto))`;
57 | } else if (!minWidth && maxWidth) {
58 | gtc = `repeat(auto-fill, minmax(auto, ${wrapUnit(maxWidth)}))`;
59 | }
60 |
61 | return {
62 | display: 'grid',
63 | gridTemplateColumns: gtc,
64 | gridTemplateRows: `repeat(${rows}, 1fr)`,
65 | ...(rowGap ? { gridRowGap: wrapUnit(rowGap) } : null),
66 | ...(colGap ? { gridColumnGap: wrapUnit(colGap) } : null),
67 | // 有 width 或者 style.width 的时候,设置 flexBasis 宽度
68 | ...(validWidth ? { flexBasis: wrapUnit(validWidth) } : null),
69 | ...style,
70 | };
71 | }, [cols, colGap, minWidth, maxWidth, rows, rowGap, style, validWidth]);
72 |
73 | // 优先行渲染
74 | const renderChildren = () => {
75 | return Array.from(new Array(rows)).map((_, row) => {
76 | return Array.from(new Array(cols)).map((__, col) => {
77 | return renderItem ? renderItem(row, col) : null;
78 | });
79 | });
80 | };
81 |
82 | const flexClassNames = useFlexClassNames(props);
83 | return (
84 |
93 | {renderItem ? renderChildren() : children}
94 |
95 | );
96 | };
97 |
98 | const RefGrid: IGrid = forwardRef(Grid);
99 |
100 | RefGrid.displayName = 'Grid';
101 | RefGrid.typeMark = 'Grid';
102 | RefGrid.defaultProps = {
103 | rows: 1,
104 | cols: 1,
105 | };
106 |
107 | export default RefGrid;
108 |
--------------------------------------------------------------------------------
/src/hooks/use-combine-ref.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 |
3 | // @ts-ignore
4 | export default function useCombinedRefs(...refs) {
5 | const targetRef = useRef();
6 |
7 | useEffect(() => {
8 | refs.forEach((r) => {
9 | if (!r) return;
10 |
11 | if (typeof r === 'function') {
12 | r(targetRef.current);
13 | } else {
14 | r.current = targetRef.current;
15 | }
16 | });
17 | }, [refs]);
18 |
19 | return targetRef;
20 | }
21 |
--------------------------------------------------------------------------------
/src/hooks/use-flex-class-names.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import classNames from 'classnames';
3 | import { isNumber, isString } from 'lodash-es';
4 |
5 | import Context from '@/common/context';
6 | import { LayoutContextProps } from '@/types';
7 |
8 | // 通过判断容器 props 值,给容器设置class,由 class 控制容器 flex 样式
9 | const useFlexClassNames = (props: any) => {
10 | const { autoFit, width, height, style } = props;
11 |
12 | const { prefix } = useContext(Context);
13 | const clsPrefix = `${prefix}flex-item`;
14 |
15 | const validWidth = width || style?.width;
16 | const validHeight = isNumber(height) || (isString(height) && height !== '') ? height : undefined;
17 | const validMinHeight = style?.minHeight;
18 |
19 | let isDefault = false;
20 | let isAutoFit = false;
21 | let isValidWidth = false;
22 | let isValidHeight = false;
23 |
24 | if (autoFit) {
25 | isAutoFit = true;
26 | } else if (validHeight || validMinHeight || validWidth) {
27 | isValidHeight = validHeight || validMinHeight;
28 | isValidWidth = validWidth;
29 | } else {
30 | isDefault = true;
31 | }
32 |
33 | return classNames({
34 | [`${clsPrefix}-default`]: isDefault,
35 | [`${clsPrefix}-auto-fit`]: isAutoFit,
36 | [`${clsPrefix}-valid-width`]: isValidWidth,
37 | [`${clsPrefix}-valid-height`]: isValidHeight,
38 | });
39 | };
40 |
41 | export default useFlexClassNames;
42 |
--------------------------------------------------------------------------------
/src/hooks/use-guid.ts:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react';
2 | import { uniqueId } from 'lodash-es';
3 |
4 | export default function useGuid(prefix = '') {
5 | const id = useRef(uniqueId(prefix));
6 | return id.current;
7 | }
8 |
--------------------------------------------------------------------------------
/src/index.scss:
--------------------------------------------------------------------------------
1 | @import 'scss/variable';
2 | @import 'scss/page';
3 | @import 'scss/header-footer';
4 | @import 'scss/p';
5 | @import 'scss/grid';
6 | @import 'scss/section';
7 | @import 'scss/block';
8 | @import 'scss/row-col-cell';
9 | @import 'scss/text';
10 | @import 'scss/space';
11 |
12 | #{$biz-css-prefix}float {
13 | &,
14 | + #{$biz-css-prefix}float {
15 | margin: 0 !important;
16 | margin-left: -2px !important;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | // 大布局
2 | import RefPage from './page/index';
3 | import Header from './page/page-header';
4 | import Nav from './page/nav';
5 | import Aside from './page/aside';
6 | import Content from './page/content';
7 | import Footer from './page/page-footer';
8 |
9 | import './index.scss';
10 |
11 | type IPage = typeof RefPage & {
12 | Header: typeof Header;
13 | Nav: typeof Nav;
14 | Aside: typeof Aside;
15 | Content: typeof Content;
16 | Footer: typeof Footer;
17 | };
18 |
19 | // @ts-ignore
20 | const ExpandedPage: IPage = RefPage;
21 |
22 | ExpandedPage.Header = Header;
23 | ExpandedPage.Nav = Nav;
24 | ExpandedPage.Aside = Aside;
25 | ExpandedPage.Content = Content;
26 | ExpandedPage.Footer = Footer;
27 |
28 | export { Header, Nav, Aside, Content, Footer };
29 |
30 | export { ExpandedPage as Page };
31 |
32 | export { default as Section } from './section';
33 | export { default as Block } from './block';
34 |
35 | // 小布局
36 | export { default as Grid } from './grid';
37 | export { default as Row } from './row';
38 | export { default as Col } from './col';
39 | export { default as Cell } from './cell';
40 | export { default as P } from './p';
41 | export { default as Text } from './text';
42 |
43 | export { default as FixedPoint } from './fixed-point';
44 | export { default as FixedContainer } from './fixed-container';
45 |
46 | export { default as Space } from './space';
47 |
48 | export type { PageProps } from './page/index';
49 | export type { PageHeaderProps, IPageHeader } from './page/page-header';
50 | export type { PageFooterProps, IPageFooter } from './page/page-footer';
51 | export type { PageNavProps, IPageNav } from './page/nav';
52 | export type { PageAsideProps, IPageAside } from './page/aside';
53 | export type { PageContentProps, IPageContent } from './page/content';
54 |
55 | export type {
56 | BreakPoint,
57 | BreakPoints,
58 | SectionProps,
59 | BlockProps,
60 | GridProps,
61 | RowProps,
62 | ColProps,
63 | CellProps,
64 | ParagraphProps,
65 | TextProps,
66 | SpaceProps,
67 | } from './types';
68 |
--------------------------------------------------------------------------------
/src/p.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * 段落
3 | */
4 | import React, {
5 | useContext,
6 | forwardRef,
7 | Children,
8 | ForwardRefExoticComponent,
9 | ForwardRefRenderFunction,
10 | isValidElement,
11 | cloneElement,
12 | ReactNode,
13 | useMemo,
14 | } from 'react';
15 | import { isNumber, isString } from 'lodash-es';
16 | import classNames from 'classnames';
17 |
18 | import Context from '@/common/context';
19 | import { isPresetSize, wrapUnit } from '@/utils';
20 | import Text from '@/text';
21 | import { LayoutContextProps, ParagraphProps, TypeMark } from './types';
22 |
23 | const getChildren = (children: any, type: ParagraphProps['type'] = 'body2') => {
24 | return Children.map(children, (child: ReactNode) => {
25 | // 文本节点 和 纯文本链接默认使用 Text 节点包裹
26 | if (typeof child === 'string') {
27 | return {child} ;
28 | } else if (isValidElement(child)) {
29 | if (child.type === 'a' && isString(child.props.children)) {
30 | return cloneElement(
31 | child,
32 | { ...child.props },
33 |
34 | {child.props.children}
35 | ,
36 | );
37 | // @ts-ignore
38 | } else if (child.type.typeMark === 'Text' && !child.props.type) {
39 | return cloneElement(child, {
40 | // @ts-ignore
41 | type,
42 | });
43 | }
44 | }
45 |
46 | return child;
47 | });
48 | };
49 |
50 | type IParagraph = ForwardRefExoticComponent & TypeMark;
51 |
52 | /**
53 | * 段落布局,自动为段落内元素增加水平和垂直间隙,并支持多种模式对齐
54 | */
55 | const P: ForwardRefRenderFunction = (props, ref) => {
56 | const {
57 | type,
58 | className,
59 | beforeMargin,
60 | afterMargin,
61 | align,
62 | verAlign,
63 | spacing: spacingProp,
64 | hasVerSpacing,
65 | children,
66 | style,
67 | ...others
68 | } = props;
69 | const { prefix } = useContext(Context);
70 | const clsPrefix = `${prefix}p`;
71 | const isCustomSize =
72 | isNumber(spacingProp) ||
73 | (isString(spacingProp) && spacingProp !== '' && !isPresetSize(spacingProp));
74 | const spacing = isCustomSize ? 'medium' : spacingProp;
75 |
76 | const newStyle = useMemo(
77 | () => ({
78 | marginTop: wrapUnit(beforeMargin) || 0,
79 | marginBottom: wrapUnit(afterMargin) || 0,
80 | // 如果 spacingProp 为数值类型,则默认修改当前段落下的间隙值
81 | ...(isCustomSize
82 | ? {
83 | '--page-p-medium-spacing': wrapUnit(spacingProp),
84 | }
85 | : null),
86 | ...style,
87 | }),
88 | [beforeMargin, afterMargin, isCustomSize, style],
89 | );
90 |
91 | return (
92 |
106 | {getChildren(children, type)}
107 |
108 | );
109 | };
110 |
111 | const RefParagraph: IParagraph = forwardRef(P);
112 |
113 | RefParagraph.displayName = 'P';
114 | RefParagraph.typeMark = 'P';
115 |
116 | RefParagraph.defaultProps = {
117 | spacing: 'medium',
118 | hasVerSpacing: true,
119 | verAlign: 'middle',
120 | };
121 |
122 | export default RefParagraph;
123 |
--------------------------------------------------------------------------------
/src/page/aside.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, ReactNode, useContext, cloneElement, Children, isValidElement } from 'react';
2 | import classNames from 'classnames';
3 |
4 | import Context from '@/common/context';
5 | import Block from '@/block';
6 | import { wrapUnit } from '@/utils';
7 | import { BaseBgMode, BaseProps, LayoutContextProps, TypeMark } from '@/types';
8 |
9 | export interface PageAsideProps extends BaseProps, BaseBgMode {
10 | width: number | string;
11 | children?: ReactNode;
12 | }
13 |
14 | export type IPageAside = FC & TypeMark;
15 |
16 | const PageAside: IPageAside = (props: PageAsideProps) => {
17 | const { className, width, mode, children, style = {}, ...others } = props;
18 | const { prefix } = useContext(Context);
19 |
20 | const asideCls = classNames(className, {
21 | [`${prefix}page-aside`]: true,
22 | [`${prefix}bg--${mode}`]: !!mode,
23 | });
24 | const asideInnerCls = classNames(`${prefix}page-aside-inner`);
25 |
26 | const newChildren = Children.map(children, (child: any) => {
27 | const { style: childStyle, ...otherChildProps } = child.props;
28 |
29 | if (isValidElement(child)) {
30 | return cloneElement(child, {
31 | ...otherChildProps,
32 | span: 1,
33 | style: {
34 | ...childStyle,
35 | },
36 | });
37 | } else {
38 | return (
39 |
40 | {child}
41 |
42 | );
43 | }
44 | });
45 |
46 | const newStyle = {
47 | width: wrapUnit(width),
48 | ...style,
49 | };
50 |
51 | if (!children) {
52 | return null;
53 | }
54 |
55 | return (
56 |
59 | );
60 | };
61 |
62 | PageAside.displayName = 'Aside';
63 | PageAside.typeMark = 'Aside';
64 | PageAside.defaultProps = {
65 | width: 200,
66 | mode: 'transparent',
67 | };
68 |
69 | export default PageAside;
70 |
--------------------------------------------------------------------------------
/src/page/content.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | Children,
3 | forwardRef,
4 | ForwardRefExoticComponent,
5 | ForwardRefRenderFunction,
6 | isValidElement,
7 | ReactNode,
8 | useContext,
9 | useRef,
10 | } from 'react';
11 | import classNames from 'classnames';
12 |
13 | import Context from '@/common/context';
14 | import { wrapUnit } from '@/utils';
15 | import { BaseBgMode, BaseProps, LayoutContextProps, TypeMark } from '@/types';
16 |
17 | export interface PageContentProps extends BaseProps, BaseBgMode {
18 | children?: ReactNode;
19 | // string 指的是 calc(100vh - 52px) 这种,而不是 30px
20 | minHeight?: number | string;
21 | noPadding?: boolean;
22 | }
23 |
24 | export type IPageContent = ForwardRefExoticComponent & TypeMark;
25 |
26 | /**
27 | * Content 的高度默认占据 一个屏幕的剩余空间,即使里面内容不足也应该背景色撑满
28 | * @param props
29 | * @param ref
30 | */
31 | const PageContent: ForwardRefRenderFunction = (
32 | props: PageContentProps,
33 | ref,
34 | ) => {
35 | const { children, mode, noPadding, style, ...others } = props;
36 | const { prefix } = useContext(Context);
37 |
38 | const sectionWrapperRef = useRef(null);
39 | let navNode: any;
40 | let asideNode: any;
41 |
42 | const newChildren = Children.map(children, (child) => {
43 | let tm;
44 |
45 | if (isValidElement(child)) {
46 | // @ts-ignore
47 | tm = child?.type?.typeMark;
48 |
49 | if (tm === 'Nav') {
50 | navNode = child;
51 | } else if (tm === 'Aside') {
52 | asideNode = child;
53 | }
54 | }
55 |
56 | return tm !== 'Nav' && tm !== 'Aside' ? child : null;
57 | });
58 |
59 | const navWidth = navNode?.props?.width || 0;
60 | const asideWidth = asideNode?.props?.width || 0;
61 | const centerMode = !!(asideNode || navNode);
62 |
63 | const mainCls = classNames({
64 | [`${prefix}page-main`]: true,
65 | });
66 |
67 | const contentHelpCls = classNames({
68 | [`${prefix}page-bg-${mode}`]: !!mode,
69 | [`${prefix}page-min-height-helper`]: true,
70 | [`${prefix}page-content--with-aside`]: asideNode,
71 | [`${prefix}page-content--with-nav`]: navNode,
72 | [`${prefix}page-content--center-mode`]: navNode || asideNode,
73 | [`${prefix}page-content--single-col`]: !navNode && !asideNode,
74 | });
75 |
76 | const contentCls = classNames({
77 | [`${prefix}page-content`]: true,
78 | [`${prefix}page-content-no-padding`]: noPadding,
79 | [`${prefix}page-content--with-nav`]: navNode,
80 | });
81 |
82 | return (
83 |
84 |
97 | {navNode}
98 |
99 | {newChildren}
100 |
101 | {asideNode}
102 |
103 |
104 | );
105 | };
106 |
107 | const RefPageContent: IPageContent = forwardRef(PageContent);
108 |
109 | RefPageContent.displayName = 'Content';
110 | RefPageContent.typeMark = 'Content';
111 |
112 | export default RefPageContent;
113 |
--------------------------------------------------------------------------------
/src/page/index.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | Children,
3 | CSSProperties,
4 | forwardRef,
5 | ForwardRefRenderFunction,
6 | isValidElement,
7 | ReactElement,
8 | ReactNode,
9 | useEffect,
10 | useMemo,
11 | useRef,
12 | useState,
13 | } from 'react';
14 | import classNames from 'classnames';
15 | import isValidArray from 'is-valid-array';
16 | import ResizeObserver from 'resize-observer-polyfill';
17 |
18 | import { getCurBreakPoint, getMaxNumberOfColumns, isValidGap, wrapUnit } from '@/utils';
19 | import Context from '@/common/context';
20 | import { DEFAULT_BREAK_POINTS } from '@/common/constant';
21 | import useCombinedRefs from '@/hooks/use-combine-ref';
22 | import useGuid from '@/hooks/use-guid';
23 | import PageContent, { PageContentProps } from './content';
24 | import { BaseBgMode, BaseGap, BaseProps, BreakPoint, BreakPoints } from '@/types';
25 |
26 | interface ContentProps extends BaseBgMode {
27 | style?: CSSProperties;
28 | noPadding?: boolean;
29 | }
30 |
31 | export interface PageProps extends PageContentProps {
32 | /**
33 | * class 前缀
34 | */
35 | prefix?: string;
36 |
37 | header?: ReactElement;
38 | footer?: ReactElement;
39 | nav?: ReactElement;
40 | aside?: ReactElement;
41 |
42 | // string 指的是 calc(100vh - 52px) 这种,而不是 30px
43 | minHeight?: number | string;
44 | /**
45 | * 禁用页面内边距(包含 Header, Content, Footer)
46 | */
47 | noPadding?: boolean;
48 |
49 | contentProps?: ContentProps;
50 |
51 | /**
52 | * 章之间的间距(若未指定单位,默认为 px)
53 | */
54 | sectionGap?: BaseGap;
55 | /**
56 | * 水槽间距(若未指定单位,默认为 px)
57 | */
58 | blockGap?: BaseGap;
59 | /**
60 | * 小布局间距(行、列、网格布局的 单元格-Cell 间距, 若未指定单位,默认为 px)
61 | */
62 | gridGap?: BaseGap;
63 |
64 | /**
65 | * 断点配置
66 | */
67 | breakPoints?: BreakPoints;
68 |
69 | children?: ReactNode;
70 |
71 | /**
72 | * 断点更新时回调
73 | * @param curBreakPoint
74 | * @param prevBreakPoint
75 | * @param breakPoints
76 | */
77 | onBreakPointChange?: (
78 | newBreakPoint: BreakPoint,
79 | prevBreakPoint?: BreakPoint,
80 | breakPoints?: BreakPoints,
81 | ) => void;
82 | }
83 |
84 | const Page: ForwardRefRenderFunction = (props, ref) => {
85 | const {
86 | prefix,
87 | className,
88 | style,
89 | children,
90 | minHeight,
91 | mode,
92 | noPadding,
93 | contentProps,
94 | header,
95 | nav,
96 | aside,
97 | footer,
98 | breakPoints: breakPointsProp,
99 | sectionGap,
100 | blockGap,
101 | gridGap,
102 | onBreakPointChange,
103 | ...others
104 | } = props;
105 |
106 | const pageStyle = useMemo(
107 | () => ({
108 | ...style,
109 | minHeight,
110 | }),
111 | [style, minHeight],
112 | );
113 |
114 | // 保证断点一定是有效数组
115 | const breakPoints = isValidArray(breakPointsProp) ? breakPointsProp : DEFAULT_BREAK_POINTS;
116 |
117 | const pageRef = useRef(null);
118 | const combinedRef = useCombinedRefs(ref, pageRef);
119 | const contentRef = useRef(null);
120 | const bpRef = useRef(getCurBreakPoint(breakPoints));
121 | const [curBreakPoint, setBreakPoint] = useState(getCurBreakPoint(breakPoints));
122 | const guid = useGuid('fd-layout-');
123 |
124 | const pageSizeObsr = new ResizeObserver(() => {
125 | const newBreakPoint = getCurBreakPoint(breakPoints);
126 |
127 | if (bpRef?.current?.width !== newBreakPoint.width && onBreakPointChange) {
128 | onBreakPointChange(newBreakPoint, bpRef?.current, breakPoints);
129 | }
130 |
131 | bpRef.current = newBreakPoint;
132 | setBreakPoint(newBreakPoint);
133 | });
134 |
135 | useEffect(() => {
136 | if (pageRef?.current) {
137 | pageSizeObsr.observe(pageRef.current);
138 | }
139 |
140 | // 默认执行一次回调
141 | if (onBreakPointChange) {
142 | onBreakPointChange(getCurBreakPoint(breakPoints), undefined, breakPoints);
143 | }
144 |
145 | return () => {
146 | if (pageRef.current) {
147 | pageSizeObsr.unobserve(pageRef.current);
148 | }
149 | };
150 | }, []);
151 |
152 | let headerNode = header;
153 | let footerNode = footer;
154 | let navNode = nav;
155 | let asideNode = aside;
156 | const contentsNodes: ReactElement[] = [];
157 |
158 | // 非标准节点 如 Section, P 等
159 | const tmp = Children.map(children, (child) => {
160 | if (isValidElement(child)) {
161 | // @ts-ignore
162 | const tm = child?.type?.typeMark;
163 |
164 | if (tm) {
165 | if (tm === 'Header') {
166 | headerNode = child;
167 | } else if (tm === 'Footer') {
168 | footerNode = child;
169 | } else if (tm === 'Aside') {
170 | asideNode = child;
171 | } else if (tm === 'Nav') {
172 | navNode = child;
173 | } else if (tm === 'Content') {
174 | contentsNodes.push(child);
175 | } else {
176 | return child;
177 | }
178 |
179 | return null;
180 | }
181 | }
182 |
183 | return child;
184 | });
185 |
186 | const nonStdChildren = Array.isArray(tmp) ? tmp.filter((c) => !!c) : null;
187 |
188 | const pageCls = classNames(className, {
189 | [`${prefix}page`]: true,
190 | [`${prefix}page--col-${curBreakPoint.numberOfColumns}`]: true,
191 | [`${prefix}page--not-tab`]: true,
192 | [`${prefix}page--headless`]: !headerNode,
193 | [`${prefix}page--footless`]: !footerNode,
194 | [`${prefix}page--no-padding`]: noPadding,
195 | [`${prefix}bg--${mode}`]: !!mode,
196 | });
197 |
198 | const defaultContent =
199 | contentsNodes.length > 0 ? (
200 | contentsNodes
201 | ) : (
202 |
209 | {navNode}
210 | {asideNode}
211 | {nonStdChildren}
212 |
213 | );
214 |
215 | return (
216 | <>
217 |
234 | {/* @ts-ignore */}
235 |
236 |
247 | {headerNode}
248 | {defaultContent}
249 | {footerNode}
250 |
251 |
252 | >
253 | );
254 | };
255 |
256 | const RefPage = forwardRef(Page);
257 |
258 | RefPage.displayName = 'Page';
259 |
260 | RefPage.defaultProps = {
261 | prefix: 'fd-layout-',
262 | mode: 'lining',
263 | breakPoints: DEFAULT_BREAK_POINTS,
264 | };
265 |
266 | export default RefPage;
267 |
--------------------------------------------------------------------------------
/src/page/nav.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, ReactNode, cloneElement, useContext, isValidElement, Children } from 'react';
2 | import classNames from 'classnames';
3 |
4 | import Context from '@/common/context';
5 | import Block from '@/block';
6 | import { wrapUnit } from '@/utils';
7 | import { BaseBgMode, BaseProps, LayoutContextProps, TypeMark } from '@/types';
8 |
9 | export interface PageNavProps extends BaseProps, BaseBgMode {
10 | width: number | string;
11 | children?: ReactNode;
12 | }
13 |
14 | export type IPageNav = FC & TypeMark;
15 |
16 | const PageNav: IPageNav = (props: PageNavProps) => {
17 | const { children, className, mode, width, style = {}, ...others } = props;
18 | const { prefix } = useContext(Context);
19 |
20 | const navCls = classNames(className, {
21 | [`${prefix}page-nav`]: true,
22 | [`${prefix}bg--${mode}`]: !!mode,
23 | });
24 | const navInnerCls = classNames(`${prefix}page-nav-inner`);
25 |
26 | const newChildren = Children.map(children, (child: any) => {
27 | const { style: childStyle, ...otherChildProps } = child?.props || {};
28 |
29 | if (isValidElement(child)) {
30 | return cloneElement(child, {
31 | ...otherChildProps,
32 | span: 1,
33 | style: {
34 | ...childStyle,
35 | },
36 | });
37 | } else {
38 | return {child} ;
39 | }
40 | });
41 |
42 | const newStyle = {
43 | width: wrapUnit(width),
44 | ...style,
45 | };
46 |
47 | if (!children) {
48 | return null;
49 | }
50 |
51 | return (
52 |
53 | {newChildren}
54 |
55 | );
56 | };
57 |
58 | PageNav.displayName = 'Nav';
59 | PageNav.typeMark = 'Nav';
60 | PageNav.defaultProps = {
61 | width: 200,
62 | mode: 'transparent',
63 | };
64 |
65 | export default PageNav;
66 |
--------------------------------------------------------------------------------
/src/page/page-footer.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, ReactNode, useContext } from 'react';
2 | import classNames from 'classnames';
3 |
4 | import Context from '@/common/context';
5 | import { BaseBgMode, BaseProps, LayoutContextProps, TypeMark } from '@/types';
6 |
7 | export interface PageFooterProps extends BaseProps, BaseBgMode {
8 | divider?: boolean;
9 | fixed?: boolean;
10 | noTopPadding?: boolean;
11 | fullWidth?: boolean;
12 | children?: ReactNode;
13 | }
14 |
15 | export type IPageFooter = FC & TypeMark;
16 |
17 | const PageFooter: IPageFooter = (props: PageFooterProps) => {
18 | const { className, fixed, divider, mode, noTopPadding, children, fullWidth, ...others } = props;
19 | const { prefix } = useContext(Context);
20 | const clsPrefix = `${prefix}page-footer`;
21 |
22 | const footerCls = classNames(className, {
23 | [`${clsPrefix}--dividing`]: divider,
24 | [`${prefix}page-footer`]: true,
25 | [`${prefix}page-footer--fixed`]: fixed,
26 | [`${clsPrefix}--no-top-padding`]: noTopPadding,
27 | [`${clsPrefix}--fullwidth`]: fullWidth,
28 | [`${prefix}bg--${mode}`]: !!mode,
29 | });
30 |
31 | if (!children) {
32 | return null;
33 | }
34 |
35 | return (
36 |
39 | );
40 | };
41 |
42 | PageFooter.displayName = 'Footer';
43 | PageFooter.typeMark = 'Footer';
44 | PageFooter.defaultProps = {
45 | noTopPadding: false,
46 | divider: false,
47 | fullWidth: false,
48 | mode: 'surface',
49 | };
50 |
51 | export default PageFooter;
52 |
--------------------------------------------------------------------------------
/src/page/page-header.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, ReactNode, useContext } from 'react';
2 | import classNames from 'classnames';
3 |
4 | import Context from '@/common/context';
5 | import { BaseBgMode, BaseProps, LayoutContextProps, TypeMark } from '@/types';
6 |
7 | export interface PageHeaderProps extends BaseProps, BaseBgMode {
8 | // 禁用底部 padding
9 | noBottomPadding?: boolean;
10 | // 禁用与内容之间的间隙
11 | noBottomMargin?: boolean;
12 | // 禁用整体 padding
13 | noPadding?: boolean;
14 | divider?: boolean;
15 | fullWidth?: boolean;
16 | children?: ReactNode;
17 | }
18 |
19 | export type IPageHeader = FC & TypeMark;
20 |
21 | const PageHeader: IPageHeader = (props: PageHeaderProps) => {
22 | const {
23 | className,
24 | children,
25 | noPadding,
26 | noBottomMargin,
27 | mode,
28 | noBottomPadding,
29 | divider,
30 | fullWidth,
31 | ...others
32 | } = props;
33 | const { prefix } = useContext(Context);
34 | const clsPrefix = `${prefix}page-header`;
35 |
36 | const headerCls = classNames(className, clsPrefix, {
37 | [`${clsPrefix}--dividing`]: divider,
38 | [`${clsPrefix}--no-bottom-padding`]: noBottomPadding,
39 | [`${clsPrefix}--no-margin`]: noBottomMargin,
40 | [`${clsPrefix}--no-padding`]: noPadding,
41 | [`${clsPrefix}--fullwidth`]: fullWidth,
42 | [`${prefix}bg--${mode}`]: !!mode,
43 | });
44 |
45 | if (!children) {
46 | return null;
47 | }
48 |
49 | return (
50 |
53 | );
54 | };
55 |
56 | PageHeader.displayName = 'Header';
57 | PageHeader.typeMark = 'Header';
58 | PageHeader.defaultProps = {
59 | noBottomPadding: false,
60 | divider: false,
61 | fullWidth: false,
62 | mode: 'surface',
63 | };
64 |
65 | export default PageHeader;
66 |
--------------------------------------------------------------------------------
/src/row.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | useContext,
3 | forwardRef,
4 | ForwardRefExoticComponent,
5 | ForwardRefRenderFunction,
6 | useMemo,
7 | } from 'react';
8 | import classNames from 'classnames';
9 |
10 | import { VER_ALIGN_ALIAS_MAP } from '@/common/constant';
11 | import Context from '@/common/context';
12 | import { getGapVal, wrapUnit } from '@/utils';
13 | import useFlexClassNames from '@/hooks/use-flex-class-names';
14 | import { LayoutContextProps, RowProps, TypeMark } from './types';
15 |
16 | type IRow = ForwardRefExoticComponent & TypeMark;
17 |
18 | /**
19 | * 行拆分布局 (子元素如果不是 Row、Col 或 Cell, 则默认用 Cell 包裹)
20 | */
21 | const Row: ForwardRefRenderFunction = (props: RowProps, ref) => {
22 | const {
23 | width,
24 | height,
25 | children,
26 | className,
27 | verAlign,
28 | style,
29 | autoFit,
30 | gap: gapProp,
31 | ...others
32 | } = props;
33 | const { prefix, gridGap } = useContext(Context);
34 | const clsPrefix = `${prefix}row-flex`;
35 | const gap = getGapVal(gridGap, gapProp);
36 |
37 | const validWidth = width || style?.width;
38 |
39 | const newStyle = useMemo(
40 | () => ({
41 | // @ts-ignore
42 | alignItems: VER_ALIGN_ALIAS_MAP[verAlign] || verAlign,
43 | ...(width ? { width: wrapUnit(width) } : null),
44 | ...(height ? { height: wrapUnit(height) } : null),
45 | ...(gap ? { gap: wrapUnit(gap) } : null),
46 | // 有 width 或者 style.width 的时候,设置 flexBasis 宽度
47 | ...(validWidth ? { flexBasis: wrapUnit(validWidth) } : null),
48 | ...style,
49 | }),
50 | [verAlign, width, height, gap, style, validWidth],
51 | );
52 | const flexClassNames = useFlexClassNames(props);
53 |
54 | return (
55 |
61 | {children}
62 |
63 | );
64 | };
65 |
66 | const RefRow: IRow = forwardRef(Row);
67 |
68 | RefRow.displayName = 'Row';
69 | RefRow.typeMark = 'Row';
70 |
71 | export default RefRow;
72 |
--------------------------------------------------------------------------------
/src/scss/block.scss:
--------------------------------------------------------------------------------
1 | @import 'variable';
2 |
3 | #{$biz-css-prefix}block {
4 | border-radius: var(--page-block-corner);
5 | overflow: hidden;
6 |
7 | &-head {
8 | padding: var(--page-block-padding-tb) var(--page-block-padding-lr);
9 |
10 | &--no-padding {
11 | padding: 0;
12 | }
13 | }
14 |
15 |
16 | &--headless {
17 | padding: var(--page-block-padding-tb) var(--page-block-padding-lr);
18 | }
19 |
20 | &--bordered {
21 | border: var(--page-block-border-width) solid var(--page-block-border-color);
22 | }
23 |
24 | &-content {
25 | padding-left: var(--page-block-padding-lr);
26 | padding-right: var(--page-block-padding-lr);
27 | padding-bottom: var(--page-block-padding-tb);
28 | }
29 |
30 | &--no-padding,
31 | &-content--no-padding {
32 | padding: 0;
33 | }
34 |
35 | &-title,
36 | &-title #{$biz-css-prefix}p {
37 | color: var(--page-block-title-font-color);
38 | }
39 |
40 | &-extra,
41 | &-extra #{$biz-css-prefix}p {
42 | color: var(--page-block-extra-font-color);
43 | }
44 |
45 | &--divided {
46 | #{$biz-css-prefix}block-head {
47 | border-bottom: var(--line-1) solid var(--color-line1-1);
48 | }
49 |
50 | #{$biz-css-prefix}block-content {
51 | padding-top: var(--page-block-padding-tb);
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/scss/grid.scss:
--------------------------------------------------------------------------------
1 | @import 'variable';
2 |
3 | #{$biz-css-prefix}grid {
4 | display: grid;
5 | gap: var(--page-grid-gap);
6 |
7 | &-align--left {
8 | justify-items: start;
9 | }
10 | &-valign--middle,
11 | &-align--center {
12 | justify-items: center;
13 | }
14 | &-align--right {
15 | justify-items: end;
16 | }
17 | &-align--stretch {
18 | justify-items: stretch;
19 | }
20 |
21 | &-valign--top {
22 | align-items: start;
23 | }
24 | &-valign--center,
25 | &-valign--middle {
26 | align-items: center;
27 | }
28 | &-valign--bottom {
29 | align-items: end;
30 | }
31 | &-valign--stretch {
32 | align-items: stretch;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/scss/header-footer.scss:
--------------------------------------------------------------------------------
1 | @import 'variable.scss';
2 |
3 | #{$biz-css-prefix}page {
4 | &-header {
5 | margin-bottom: var(--page-section-gap);
6 | padding: var(--page-padding-tb) var(--page-header-padding-lr) var(--page-block-padding-tb);
7 |
8 | &--no-bottom-padding {
9 | padding-bottom: 0;
10 | }
11 |
12 | &--no-padding {
13 | padding: 0;
14 | }
15 |
16 | &--dividing {
17 | border-bottom: var(--line-1) solid var(--page-header-divider-color);
18 | }
19 |
20 | &--no-margin {
21 | margin-bottom: 0;
22 | }
23 |
24 | &--fixed {
25 | position: fixed;
26 | width: 100%;
27 | left: 0;
28 | top: 0;
29 | z-index: var(--elevation-4);
30 | }
31 | }
32 |
33 | &-footer {
34 | margin-top: var(--page-section-gap);
35 | flex: 0 0 auto;
36 | padding: var(--page-block-padding-tb) var(--page-footer-padding-lr) var(--page-padding-tb);
37 |
38 | &--dividing {
39 | border-top: var(--line-1) solid var(--page-header-divider-color);
40 | }
41 |
42 | &--fixed {
43 | position: fixed;
44 | width: 100%;
45 | left: 0;
46 | bottom: 0;
47 | z-index: var(--elevation-4);
48 | }
49 | }
50 |
51 | &-header,
52 | &-footer {
53 | box-sizing: border-box;
54 |
55 | &--no-top-padding {
56 | padding-top: 0;
57 | }
58 |
59 | &-inner {
60 | box-sizing: border-box;
61 | margin-left: auto;
62 | margin-right: auto;
63 | max-width: var(--page-max-content-width);
64 | }
65 | }
66 |
67 | &-header--fullwidth &-header-inner,
68 | &-footer--fullwidth &-footer-inner {
69 | max-width: none;
70 | }
71 | }
72 |
73 | #{$biz-css-prefix}page--no-padding #{$biz-css-prefix}page-header {
74 | padding-top: 0;
75 | padding-left: 0;
76 | padding-right: 0;
77 | }
78 |
79 | #{$biz-css-prefix}page--no-padding #{$biz-css-prefix}page-footer {
80 | padding-bottom: 0;
81 | padding-left: 0;
82 | padding-right: 0;
83 | }
84 |
--------------------------------------------------------------------------------
/src/scss/p.scss:
--------------------------------------------------------------------------------
1 | @import 'variable';
2 |
3 | #{$biz-css-prefix} {
4 | &cell {
5 | &-align {
6 | &--left #{$biz-css-prefix}p {
7 | justify-content: flex-start;
8 | }
9 | &--center #{$biz-css-prefix}p {
10 | justify-content: center;
11 | }
12 | &--right #{$biz-css-prefix}p {
13 | justify-content: flex-end;
14 | }
15 | &--space-evenly #{$biz-css-prefix}p {
16 | justify-content: space-evenly;
17 | }
18 | &--space-between #{$biz-css-prefix}p {
19 | justify-content: space-between;
20 | }
21 | &--space-around #{$biz-css-prefix}p {
22 | justify-content: space-around;
23 | }
24 |
25 | &--middle,
26 | &--center,
27 | &--left {
28 | @each $size in $sizes {
29 | #{$biz-css-prefix}p-spacing--#{$size} {
30 | & > * {
31 | margin-right: var(--page-p-#{$size}-spacing);
32 | }
33 | }
34 | }
35 | }
36 |
37 | &--space-evenly,
38 | &--space-around,
39 | &--space-between,
40 | &--stretch {
41 | #{$biz-css-prefix}p-spacing {
42 | & > *,
43 | & > text {
44 | margin-left: 0;
45 | margin-right: 0;
46 | }
47 | }
48 | }
49 |
50 | &--left,
51 | &--center,
52 | &--middle {
53 | #{$biz-css-prefix}p-spacing {
54 | & > * {
55 | &:last-child {
56 | margin-right: 0;
57 | }
58 | }
59 | }
60 | }
61 |
62 | &--right {
63 | @each $size in $sizes {
64 | #{$biz-css-prefix}p-spacing--#{$size} {
65 | & > * {
66 | margin-left: var(--page-p-#{$size}-spacing);
67 | }
68 | }
69 | }
70 |
71 | #{$biz-css-prefix}p-spacing {
72 | & > * {
73 | margin-right: 0;
74 |
75 | &:first-child {
76 | margin-left: 0;
77 | }
78 | }
79 | }
80 | }
81 | }
82 | }
83 |
84 | &p {
85 | display: flex;
86 | box-sizing: border-box;
87 | flex-direction: row;
88 | flex-wrap: wrap;
89 | color: var(--page-p-font-color);
90 | width: 100%;
91 |
92 | // 段落下的纯文本
93 | &-text {
94 | font-size: var(--p-body-1-font-size) !important;
95 | font-weight: var(--p-body-1-font-weight);
96 | line-height: var(--p-body-1-line-height);
97 | }
98 |
99 | @each $type in $fontTypes {
100 | &--#{$type} {
101 | font-size: var(--p-#{$type}-font-size);
102 | font-weight: var(--p-#{$type}-font-weight);
103 | line-height: var(--p-#{$type}-line-height);
104 | }
105 | }
106 |
107 | @each $size in $sizes {
108 | &-before-padding {
109 | &--#{$size} {
110 | margin-top: var(--page-p-padding-#{$size}-size);
111 |
112 | &:first-child {
113 | margin-top: 0;
114 | }
115 | }
116 | }
117 |
118 | &-after-padding {
119 | &--#{$size} {
120 | margin-bottom: var(--page-p-padding-#{$size}-size);
121 |
122 | &:last-child {
123 | margin-bottom: 0;
124 | }
125 | }
126 | }
127 | }
128 |
129 | &-margin > * {
130 | margin-top: var(--page-p-el-margin);
131 | margin-bottom: var(--page-p-el-margin);
132 | }
133 |
134 | // notice: 对于已经认定的纯文本节点,不加上下间距(依赖行高处理)
135 | &-margin > #{$biz-css-prefix}text,
136 | &-margin > text {
137 | margin-top: 0;
138 | margin-bottom: 0;
139 | }
140 |
141 | @each $size in $sizes {
142 | &-spacing--#{$size} {
143 | & > * {
144 | margin-right: var(--page-p-#{$size}-spacing);
145 | }
146 | {$biz-css-prefix}p-spacing--middle,
147 | {$biz-css-prefix}p-spacing--center,
148 | {$biz-css-prefix}p-spacing--left {
149 | & > * {
150 | margin-right: var(--page-p-#{$size}-spacing);
151 | }
152 | }
153 | {$biz-css-prefix}p-spacing--right {
154 | & > * {
155 | margin-left: var(--page-p-#{$size}-spacing);
156 | }
157 | }
158 | }
159 | }
160 |
161 | &-spacing {
162 | & > * {
163 | &:first-child {
164 | margin-left: 0;
165 | }
166 |
167 | &:last-child {
168 | margin-right: 0;
169 | }
170 | }
171 |
172 | &--left,
173 | &--center,
174 | &--middle {
175 | & > * {
176 | &:last-child {
177 | margin-right: 0;
178 | }
179 | }
180 | }
181 |
182 | &--right {
183 | & > * {
184 | margin-right: 0;
185 |
186 | &:first-child {
187 | margin-left: 0;
188 | }
189 | }
190 | }
191 |
192 | &--space-between,
193 | &--space-around,
194 | &--space-evenly {
195 | & > * {
196 | margin-left: 0;
197 | margin-right: 0;
198 | }
199 | }
200 | }
201 |
202 | &-valign--top {
203 | align-items: flex-start;
204 | vertical-align: top;
205 | }
206 |
207 | &-valign--stretch {
208 | align-items: stretch;
209 | vertical-align: middle;
210 | }
211 |
212 | &-valign--center,
213 | &-valign--middle {
214 | align-items: center;
215 | vertical-align: middle;
216 | }
217 |
218 | &-valign--bottom {
219 | align-items: flex-end;
220 | vertical-align: bottom;
221 | }
222 |
223 | &-valign--baseline {
224 | align-items: baseline;
225 | vertical-align: baseline;
226 | }
227 | }
228 |
229 | // notice: 为了处理 cell 中的 align, p 的 align 模式,优先级提升
230 | &page #{$biz-css-prefix}p-align {
231 | &--right {
232 | text-align: right;
233 | justify-content: flex-end;
234 | }
235 |
236 | &--left {
237 | text-align: left;
238 | justify-content: flex-start;
239 | }
240 |
241 | &--center {
242 | text-align: center;
243 | justify-content: center;
244 | }
245 |
246 | &--space-between {
247 | justify-content: space-between;
248 | }
249 |
250 | &--space-evenly {
251 | justify-content: space-evenly;
252 | }
253 |
254 | &--space-around {
255 | justify-content: space-around;
256 | }
257 | }
258 | }
259 |
--------------------------------------------------------------------------------
/src/scss/page.scss:
--------------------------------------------------------------------------------
1 | @import 'variable.scss';
2 |
3 | #{$biz-css-prefix}bg {
4 | @each $bgType in $backgroundTypes {
5 | &--#{$bgType} {
6 | background: var(--color-#{$bgType});
7 | }
8 | }
9 | }
10 |
11 | #{$biz-css-prefix}page {
12 | display: flex;
13 | flex-direction: column;
14 | box-sizing: border-box;
15 | min-height: 100vh;
16 | margin: 0 auto;
17 |
18 | &-min-height-helper {
19 | box-sizing: border-box;
20 | margin: 0 auto;
21 | width: 100%;
22 | }
23 |
24 | &-content--center-mode {
25 | padding-left: var(--page-padding-lr);
26 | padding-right: var(--page-padding-lr);
27 |
28 | #{$biz-css-prefix}page-content {
29 | max-width: var(--page-max-content-width);
30 | }
31 | }
32 |
33 | &-content {
34 | display: grid;
35 | margin: 0 auto;
36 | gap: var(--page-section-gap);
37 | width: 100%;
38 |
39 | {$biz-css-prefix}page-content-no-padding {
40 | padding: 0;
41 | }
42 |
43 | #{$biz-css-prefix}page-main {
44 | display: flex;
45 | flex-direction: column;
46 | justify-content: stretch;
47 | align-items: flex-start;
48 | gap: var(--page-section-gap);
49 | flex: 1;
50 |
51 | & > * {
52 | width: 100%;
53 | }
54 | }
55 |
56 | #{$biz-css-prefix}page-nav {
57 | &-inner {
58 | display: flex;
59 | box-sizing: border-box;
60 | flex-direction: column;
61 | justify-content: flex-start;
62 | flex-wrap: wrap;
63 | gap: var(--page-block-gap);
64 |
65 | & > * {
66 | max-width: 100%;
67 | }
68 | }
69 | }
70 |
71 | #{$biz-css-prefix}page-aside {
72 | &-inner {
73 | display: flex;
74 | box-sizing: border-box;
75 | flex-direction: column;
76 | justify-content: flex-start;
77 | flex-wrap: wrap;
78 | flex: 0 0 auto;
79 | gap: var(--page-block-gap);
80 |
81 | & > * {
82 | max-width: 100%;
83 | }
84 | }
85 | }
86 | }
87 | }
88 |
89 | #{$biz-css-prefix}page--not-tab#{$biz-css-prefix}page--headless {
90 | #{$biz-css-prefix}page-content {
91 | padding-top: 0;
92 | }
93 | }
94 |
95 | #{$biz-css-prefix}page--not-tab#{$biz-css-prefix}page--footless {
96 | #{$biz-css-prefix}page-content {
97 | padding-bottom: 0;
98 | }
99 |
100 | #{$biz-css-prefix}page-main > *:last-child {
101 | padding-bottom: var(--page-padding-tb);
102 | }
103 | }
104 |
105 | #{$biz-css-prefix}page-content--single-col {
106 | padding-left: 0;
107 | padding-right: 0;
108 | }
109 |
110 | #{$biz-css-prefix}page--not-tab {
111 | #{$biz-css-prefix}page-min-height-helper {
112 | flex: 1;
113 |
114 | &:first-child {
115 | #{$biz-css-prefix}page-main > *:first-child {
116 | padding-top: var(--page-padding-tb);
117 | }
118 | #{$biz-css-prefix}page-main > *:last-child {
119 | padding-bottom: var(-page-padding-tb);
120 | }
121 | }
122 |
123 | #{$biz-css-prefix}page-aside,
124 | #{$biz-css-prefix}page-nav {
125 | padding-top: 0;
126 | padding-bottom: 0;
127 | }
128 | }
129 | }
130 |
131 | // nav, first-section, aside 顶部采用页面内边距
132 | #{$biz-css-prefix}page--headless#{$biz-css-prefix}page--not-tab {
133 | #{$biz-css-prefix}page-content {
134 | #{$biz-css-prefix}page-nav,
135 | #{$biz-css-prefix}page-main > *:first-child,
136 | #{$biz-css-prefix}page-aside {
137 | padding-top: var(--page-padding-tb);
138 | }
139 | }
140 | }
141 |
142 | // nav, first-section, aside 底部采用页面内边距
143 | #{$biz-css-prefix}page--footless {
144 | #{$biz-css-prefix}page-content {
145 | #{$biz-css-prefix}page-nav,
146 | #{$biz-css-prefix}page-main > *:last-child,
147 | #{$biz-css-prefix}page-aside {
148 | padding-bottom: var(--page-padding-tb);
149 | }
150 | }
151 | }
152 |
153 | #{$biz-css-prefix}page--no-padding {
154 | #{$biz-css-prefix}header,
155 | #{$biz-css-prefix}page-aside,
156 | #{$biz-css-prefix}page-nav {
157 | padding-top: 0 !important;
158 | }
159 |
160 | #{$biz-css-prefix}footer,
161 | #{$biz-css-prefix}page-aside,
162 | #{$biz-css-prefix}page-nav {
163 | padding-bottom: 0 !important;
164 | }
165 | }
166 |
167 | #{$biz-css-prefix}page--no-padding#{$biz-css-prefix}page--headless {
168 | #{$biz-css-prefix}page-main > *:first-child {
169 | padding-top: 0 !important;
170 | }
171 | }
172 |
173 | #{$biz-css-prefix}page--no-padding#{$biz-css-prefix}page--footless {
174 | #{$biz-css-prefix}page-main > *:last-child {
175 | padding-bottom: 0 !important;
176 | }
177 | }
178 |
179 | #{$biz-css-prefix}page--no-padding #{$biz-css-prefix}page-content--center-mode {
180 | padding-left: 0;
181 | padding-right: 0;
182 | }
183 |
--------------------------------------------------------------------------------
/src/scss/row-col-cell.scss:
--------------------------------------------------------------------------------
1 | @import 'variable';
2 |
3 | #{$biz-css-prefix} {
4 | @each $bgType in $backgroundTypes {
5 | &-bg--#{$bgType} {
6 | background: var(--color-#{$bgType});
7 | }
8 | }
9 |
10 | &cell {
11 | box-sizing: border-box;
12 | // 1.去掉 flex 场景: cell 单独定高,如果有 flex 属性会影响高度
13 | // 2. 保留 flex 场景:在 block 容器中,放入希望 cell 容器可以撑满父级容器的高度
14 | // flex: 1 1 auto;
15 |
16 | &-align {
17 | &--left {
18 | align-items: flex-start;
19 | }
20 | &--center {
21 | align-items: center;
22 | }
23 | &--right {
24 | align-items: flex-end;
25 | }
26 |
27 | &-stretch {
28 | & > *,
29 | & > text {
30 | margin-left: 0;
31 | margin-right: 0;
32 | }
33 | }
34 | }
35 | }
36 |
37 | &row-flex {
38 | display: flex;
39 | flex-direction: row;
40 | flex-wrap: nowrap;
41 | flex: 1 1 auto;
42 | gap: var(--page-grid-gap);
43 | // 行容器下的子容器 flex 属性,只在 validWidth 时生效
44 | // 并且只有前两个参数 0 0,第三个参数需要根据 validWidth 设置,所以单独在子容器里面设置
45 | > #{$biz-css-prefix}flex-item-valid-width {
46 | flex: 0 0;
47 | }
48 |
49 | > #{$biz-css-prefix}fixed-container#{$biz-css-prefix}flex-item-valid-height {
50 | flex: 1 1 0;
51 | }
52 | }
53 |
54 | &col-flex {
55 | display: flex;
56 | flex-direction: column;
57 | flex-wrap: nowrap;
58 | flex: 1 1;
59 | gap: var(--page-grid-gap);
60 | // 列容器下的子容器 flex 属性,只在 validHeight 时生效
61 | > #{$biz-css-prefix}flex-item-valid-height {
62 | flex: 0 0 auto;
63 | }
64 | }
65 |
66 | &row-flex,
67 | &col-flex {
68 | > #{$biz-css-prefix}flex-item {
69 | &-default {
70 | flex: 1 1 0 !important;
71 | }
72 | &-auto-fit {
73 | flex: 0 0 auto;
74 | }
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/scss/section.scss:
--------------------------------------------------------------------------------
1 | @import 'variable';
2 |
3 | #{$biz-css-prefix} {
4 | §ion {
5 | display: grid;
6 | box-sizing: border-box;
7 | padding-left: var(--page-padding-lr);
8 | padding-right: var(--page-padding-lr);
9 |
10 | &-head {
11 | width: 100%;
12 | margin-left: auto;
13 | margin-right: auto;
14 | }
15 |
16 | &-block-wrapper {
17 | display: grid;
18 | width: 100%;
19 | margin-left: auto;
20 | margin-right: auto;
21 | height: 100%;
22 | gap: var(--page-block-gap);
23 | }
24 |
25 | &-inner--with-head {
26 | display: flex;
27 | flex: 1 1 auto;
28 | width: 100%;
29 | max-width: var(--page-max-content-width);
30 | margin: 0 auto;
31 | gap: var(--page-block-gap);
32 | }
33 |
34 | &-inner-without-head {
35 | max-width: var(--page-max-content-width);
36 | margin: 0 auto;
37 | }
38 |
39 | &--no-gap {
40 | margin-bottom: 0;
41 | }
42 |
43 | &-title #{$biz-css-prefix}p,
44 | &-title {
45 | color: var(--page-section-title-font-color);
46 | }
47 |
48 | &-extra #{$biz-css-prefix}p,
49 | &-extra {
50 | color: var(--page-section-extra-font-color);
51 | }
52 |
53 | &-no-padding {
54 | padding: 0;
55 | }
56 | }
57 |
58 | &page-content--with-nav §ion,
59 | &page-content--with-aside §ion {
60 | padding-left: 0;
61 | padding-right: 0;
62 | }
63 |
64 | &page-content--with-nav §ion-inner,
65 | &page-content--with-aside §ion-inner {
66 | max-width: none;
67 | }
68 | }
69 |
70 | @each $colNum in $columnNumEnum {
71 | #{$biz-css-prefix}page--col-#{$colNum} {
72 | #{$biz-css-prefix}section-block-wrapper,
73 | #{$biz-css-prefix}section-inner {
74 | grid-template-columns: repeat($colNum, 1fr);
75 | }
76 |
77 | @for $i from ($colNum + 1) through $maxNumberOfColumns {
78 | #{$biz-css-prefix}block--span-#{$i} {
79 | grid-column: span $colNum;
80 | }
81 | }
82 |
83 | @for $i from 1 through $colNum {
84 | #{$biz-css-prefix}block--span-#{$i} {
85 | grid-column: span $i;
86 | }
87 | }
88 | }
89 | }
90 |
91 | #{$biz-css-prefix}page--no-padding #{$biz-css-prefix}section {
92 | padding-left: 0;
93 | padding-right: 0;
94 | }
95 |
--------------------------------------------------------------------------------
/src/scss/space.scss:
--------------------------------------------------------------------------------
1 | @import 'variable';
2 |
3 | #{$biz-css-prefix}space {
4 | $space: &;
5 | $hoz: #{$space}--hoz;
6 | $ver: #{$space}--ver;
7 |
8 | &--hoz {
9 | display: block;
10 | }
11 | &--ver {
12 | display: inline-block;
13 | }
14 |
15 | @each $size in $sizes {
16 | &--#{$size} {
17 | @at-root {
18 | {$hoz} {
19 | height: var(--page-space-#{$size}-size);
20 | }
21 | {$ver} {
22 | width: var(--page-space-#{$size}-size);
23 | }
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/scss/text.scss:
--------------------------------------------------------------------------------
1 | @import 'variable';
2 |
3 | #{$biz-css-prefix}text {
4 | &-inherit {
5 | color: var(--page-body1-font-color);
6 | font-size: inherit;
7 | font-weight: inherit;
8 | font-family: inherit;
9 | line-height: inherit;
10 | }
11 |
12 | @each $type in $fontTypes {
13 | #{$biz-css-prefix}p--#{$type} > a,
14 | &-#{$type} {
15 | color: var(--page-#{$type}-font-color);
16 | font-size: var(--p-#{$type}-font-size);
17 | font-weight: var(--p-#{$type}-font-weight);
18 | line-height: var(--p-#{$type}-line-height);
19 | font-family: var(--font-family-base);
20 | }
21 | }
22 | }
23 |
24 | #{$biz-css-prefix}text {
25 | text-align: justify;
26 |
27 | &-title {
28 | font-weight: bold;
29 | margin-bottom: 0.5em;
30 | }
31 |
32 | & + &-title {
33 | margin-top: 1.2em;
34 | }
35 |
36 | &-paragraph {
37 | color: #1f2633;
38 | margin-bottom: 1em;
39 | font-size: 14px;
40 | line-height: 1.5;
41 | }
42 |
43 | mark {
44 | padding: 0;
45 | background: #fffbc7;
46 | color: #1f2633;
47 | }
48 |
49 | strong {
50 | font-weight: bold;
51 | }
52 |
53 | code {
54 | background-color: #f4f6f9;
55 | color: #1f2633;
56 | border: 1px solid #e4e8ee;
57 | margin: 0 0.2em;
58 | padding: 0.2em 0.4em 0.1em;
59 | font-size: 85%;
60 | border-radius: 4px;
61 | }
62 |
63 | ul,
64 | ol {
65 | margin: 0 0 1em 0;
66 | padding: 0;
67 | }
68 |
69 | li {
70 | list-style-type: circle;
71 | margin: 0 0 0 20px;
72 | padding: 0 0 0 4px;
73 | }
74 |
75 | a {
76 | text-decoration: none;
77 | &:link {
78 | color: rgba(3, 193, 253, 1);
79 | }
80 | &:visited {
81 | color: rgba(0, 123, 176, 1);
82 | }
83 | &:hover {
84 | color: rgba(0, 157, 214, 1);
85 | }
86 | &:active {
87 | text-decoration: underline;
88 | color: rgba(0, 157, 214, 1);
89 | }
90 | }
91 | }
92 |
93 | h1#{$biz-css-prefix}text-title {
94 | font-size: 24px;
95 | }
96 |
97 | h2#{$biz-css-prefix}text-title {
98 | font-size: 20px;
99 | }
100 |
101 | h3#{$biz-css-prefix}text-title {
102 | font-size: 16px;
103 | }
104 |
105 | h4#{$biz-css-prefix}text-title {
106 | font-size: 16px;
107 | }
108 |
109 | h5#{$biz-css-prefix}text-title {
110 | font-size: 14px;
111 | }
112 |
113 | h6#{$biz-css-prefix}text-title {
114 | font-size: 14px;
115 | }
116 |
--------------------------------------------------------------------------------
/src/section.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | useContext,
3 | Children,
4 | forwardRef,
5 | cloneElement,
6 | ReactNode,
7 | ReactElement,
8 | ForwardRefRenderFunction,
9 | ForwardRefExoticComponent,
10 | useMemo,
11 | } from 'react';
12 | import { isString, isNil } from 'lodash-es';
13 | import classNames from 'classnames';
14 |
15 | import Context from '@/common/context';
16 | import { wrapUnit, getGapVal, isValidGap } from '@/utils';
17 | import Block from '@/block';
18 | import Row from '@/row';
19 | import Col from '@/col';
20 | import Cell from '@/cell';
21 | import P from '@/p';
22 | import { SectionProps, LayoutContextProps, TypeMark } from './types';
23 |
24 | type ISection = ForwardRefExoticComponent & TypeMark;
25 |
26 | /**
27 | * 获取计算后的子节点
28 | * @param children
29 | * @param numberOfColumns
30 | * @param maxNumberOfColumns
31 | */
32 | function getValidChildren(
33 | children: ReactNode,
34 | numberOfColumns: number,
35 | maxNumberOfColumns: number,
36 | ) {
37 | const newChildren = wrapBlock(children, maxNumberOfColumns);
38 |
39 | return calBlockSpan(newChildren as ReactElement[], numberOfColumns);
40 | }
41 |
42 | /**
43 | * 为非 Block 节点,包裹 Block 子元素
44 | * @param children
45 | * @param maxNumberOfColumns
46 | * @return ReactNode[]
47 | */
48 | function wrapBlock(children: ReactNode, maxNumberOfColumns: number) {
49 | let tmp: ReactNode[] = [];
50 | const ret: ReactNode[] = [];
51 | const validChildList = Children.toArray(children).filter((child) => !isNil(child));
52 |
53 | validChildList.forEach((child: any, index) => {
54 | if (child?.type === Block || child?.type?.typeMark === 'Block') {
55 | if (tmp.length > 0) {
56 | ret.push(
57 |
58 | {tmp}
59 | ,
60 | );
61 | tmp = [];
62 | }
63 |
64 | ret.push(child);
65 |
66 | if (tmp.length > 0) {
67 | ret.push(
68 |
69 | {[...tmp]}
70 | ,
71 | );
72 | tmp = [];
73 | }
74 | } else {
75 | tmp.push(child);
76 | }
77 |
78 | if (index === validChildList.length - 1 && tmp.length > 0) {
79 | ret.push(
80 |
81 | {[...tmp]}
82 | ,
83 | );
84 | tmp = [];
85 | }
86 | });
87 |
88 | return ret;
89 | }
90 |
91 | /**
92 | * 为 Block 自动调整列宽
93 | * @param children
94 | * @param numberOfColumns
95 | */
96 | function calBlockSpan(children: ReactElement[], numberOfColumns: number) {
97 | let ret: ReactElement[] = [];
98 | let stack: ReactElement[] = [];
99 | let counter = 0;
100 | const len = children.length;
101 |
102 | for (let i = 0; i < len; i++) {
103 | const child = children[i];
104 | const span: number = +child.props.span;
105 |
106 | if (span < numberOfColumns) {
107 | if (span + counter <= numberOfColumns) {
108 | stack.push(child);
109 | counter += span;
110 | } else {
111 | ret = [...ret, ...adjustColWidth(stack, counter, numberOfColumns)];
112 | stack = [child];
113 | counter = span;
114 | }
115 | } else {
116 | if (stack.length > 0) {
117 | ret = [...ret, ...adjustColWidth(stack, counter, numberOfColumns)];
118 | stack = [];
119 | counter = 0;
120 | }
121 |
122 | ret = [...ret, ...adjustColWidth([child], numberOfColumns, numberOfColumns)];
123 | }
124 |
125 | if (i === len - 1 && stack.length > 0) {
126 | ret = [...ret, ...adjustColWidth(stack, counter, numberOfColumns)];
127 | }
128 | }
129 | return ret;
130 | }
131 |
132 | /**
133 | * 重算列宽
134 | * @param blockNodes
135 | * @param totalSpan
136 | * @param maxColNum
137 | */
138 | function adjustColWidth(blockNodes: ReactElement[], totalSpan: number, maxColNum: number) {
139 | return blockNodes.map((item) => {
140 | const { span } = item.props;
141 |
142 | return cloneElement(item, {
143 | ...item.props,
144 | span: Math.round((span / totalSpan) * maxColNum),
145 | });
146 | });
147 | }
148 |
149 | /**
150 | * 章节
151 | * @param props
152 | * @param ref
153 | */
154 | const Section: ForwardRefRenderFunction = (props, ref) => {
155 | const {
156 | className,
157 | children,
158 | title,
159 | titleAlign,
160 | extra,
161 | gap: blockGapProp,
162 | noPadding,
163 | ...others
164 | } = props;
165 |
166 | const {
167 | prefix,
168 | blockGap: blockGapContext,
169 | breakPoint: { numberOfColumns },
170 | maxNumberOfColumns,
171 | } = useContext(Context);
172 |
173 | const clsPrefix = `${prefix}section`;
174 | const hasHead = title || extra;
175 | // classNames
176 | const sectionCls = classNames(clsPrefix, className);
177 | const blockWrapperCls = `${clsPrefix}-block-wrapper`;
178 | const innerContainerWithHead = classNames(`${clsPrefix}-inner--with-head`, {
179 | [`${clsPrefix}-no-padding`]: noPadding,
180 | });
181 | const innerContainerWithoutHead = classNames(`${clsPrefix}-inner-without-head`);
182 |
183 | // 此处定义的是 blockGap
184 | const gap = getGapVal(blockGapContext, blockGapProp);
185 |
186 | const blockWrapperStyle = useMemo(() => {
187 | return {
188 | ...(isValidGap(gap)
189 | ? {
190 | gridColumnGap: wrapUnit(gap),
191 | gridRowGap: wrapUnit(gap),
192 | }
193 | : null),
194 | };
195 | }, [gap]);
196 |
197 | const newChildren = useMemo(
198 | () => getValidChildren(children, numberOfColumns, maxNumberOfColumns),
199 | [children, numberOfColumns, maxNumberOfColumns],
200 | );
201 |
202 | // 带有 title&extra
203 | if (hasHead) {
204 | return (
205 |
206 |
207 |
208 |
209 |
210 | {isString(title) ? {title} : title}
211 | |
212 | {extra ? (
213 |
214 | {isString(extra) ? (
215 |
216 | {extra}
217 |
218 | ) : (
219 | extra
220 | )}
221 | |
222 | ) : null}
223 |
224 |
225 |
226 | {newChildren}
227 |
228 | |
229 |
230 |
231 |
232 | );
233 | }
234 |
235 | return (
236 |
237 |
241 | {newChildren}
242 |
243 |
244 | );
245 | };
246 |
247 | const RefSection: ISection = forwardRef(Section);
248 | RefSection.displayName = 'Section';
249 | RefSection.typeMark = 'Section';
250 |
251 | export default RefSection;
252 |
--------------------------------------------------------------------------------
/src/space.tsx:
--------------------------------------------------------------------------------
1 | import { createElement, useContext, FC, useMemo } from 'react';
2 | import classNames from 'classnames';
3 | import { isNumber, isString } from 'lodash-es';
4 |
5 | import Context from '@/common/context';
6 | import { isPresetSize, wrapUnit } from '@/utils';
7 | import { LayoutContextProps, SpaceProps } from './types';
8 |
9 | /**
10 | * 间距
11 | */
12 | const Space: FC = (props) => {
13 | const { className, size, direction, children, style, ...others } = props;
14 | const { prefix } = useContext(Context);
15 | const clsPrefix = `${prefix}space`;
16 | const isVertical = direction === 'ver';
17 |
18 | const spaceCls = classNames(clsPrefix, {
19 | [`${clsPrefix}--${size}`]: isPresetSize(size as string),
20 | [`${clsPrefix}--${direction}`]: true,
21 | });
22 |
23 | const newStyle = useMemo(() => {
24 | const isCustomSize = isNumber(size) || (isString(size) && size !== '' && !isPresetSize(size));
25 |
26 | return {
27 | ...style,
28 | ...(isCustomSize && direction === 'hoz' ? { height: wrapUnit(size) } : null),
29 | ...(isCustomSize && direction === 'ver' ? { width: wrapUnit(size) } : null),
30 | };
31 | }, [size, style, direction]);
32 |
33 | return createElement(
34 | isVertical ? 'span' : 'div',
35 | {
36 | ...others,
37 | className: spaceCls,
38 | style: newStyle,
39 | },
40 | children,
41 | );
42 | };
43 |
44 | Space.displayName = 'Space';
45 | Space.defaultProps = {
46 | size: 'medium',
47 | direction: 'hoz',
48 | };
49 |
50 | export default Space;
51 |
--------------------------------------------------------------------------------
/src/text.tsx:
--------------------------------------------------------------------------------
1 | import React, { createElement, useMemo, FC, useContext } from 'react';
2 | import classNames from 'classnames';
3 |
4 | import Context from '@/common/context';
5 | import { TEXT_TYPE_MAP } from '@/common/constant';
6 | import { TextProps, LayoutContextProps, TypeMark } from './types';
7 |
8 | export type ITextComponent = FC & TypeMark;
9 | /**
10 | * 文字 包含:字号、字重、行高
11 | * @param props
12 | */
13 | const Text: ITextComponent = (props) => {
14 | const {
15 | className,
16 | type,
17 | style,
18 | component = 'span',
19 | strong,
20 | underline,
21 | delete: deleteProp,
22 | code,
23 | mark,
24 | color,
25 | align,
26 | backgroundColor,
27 | children,
28 | ...others
29 | } = props;
30 | const { prefix } = useContext(Context);
31 |
32 | // @ts-ignore
33 | const newType = TEXT_TYPE_MAP[type] || type;
34 |
35 | const cls = classNames(className, {
36 | [`${prefix}text`]: true,
37 | [`${prefix}text-${newType}`]: newType,
38 | });
39 |
40 | const memorizedChildren = useMemo(() => {
41 | let _children = children;
42 |
43 | if (typeof _children === 'string' && _children.indexOf('\n') !== -1) {
44 | const childrenList = _children.split('\n');
45 | const newChildren: any = [];
46 | childrenList.forEach((child) => {
47 | newChildren.push(child);
48 | newChildren.push( );
49 | });
50 | newChildren.pop();
51 |
52 | _children = newChildren;
53 | }
54 |
55 | if (strong) {
56 | _children = {_children} ;
57 | }
58 |
59 | if (underline) {
60 | _children = {_children} ;
61 | }
62 |
63 | if (deleteProp) {
64 | _children = {_children};
65 | }
66 |
67 | if (code) {
68 | _children = {_children}
;
69 | }
70 |
71 | if (mark) {
72 | _children = {_children} ;
73 | }
74 | return _children;
75 | }, [children, mark, code, deleteProp, underline, strong]);
76 |
77 | const newStyle = useMemo(
78 | () => ({
79 | ...(color ? { color } : null),
80 | ...(backgroundColor ? { backgroundColor } : null),
81 | textAlign: align,
82 | ...style,
83 | }),
84 | [color, backgroundColor, align, style],
85 | );
86 |
87 | return createElement(
88 | component,
89 | {
90 | ...others,
91 | style: newStyle,
92 | className: cls,
93 | },
94 | memorizedChildren,
95 | );
96 | };
97 |
98 | Text.displayName = 'Text';
99 | Text.typeMark = 'Text';
100 | Text.defaultProps = {
101 | align: 'left',
102 | type: 'body2',
103 | component: 'span',
104 | };
105 |
106 | export default Text;
107 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | import { isNumber, isString } from 'lodash-es';
2 | import { DEFAULT_BREAK_POINTS } from '@/common/constant';
3 | import { BreakPoint, BreakPoints } from '@/types';
4 |
5 | /**
6 | * 获取当前屏幕断点
7 | * @param breakPoints
8 | */
9 | export function getCurBreakPoint(breakPoints: BreakPoints): BreakPoint {
10 | let screenWidths: number[] = [];
11 | const tmpMap: any = {};
12 | const availWidth = document.body.clientWidth;
13 |
14 | breakPoints.forEach((bp) => {
15 | const { width } = bp;
16 | screenWidths.push(width);
17 | tmpMap[width] = bp;
18 | });
19 |
20 | screenWidths = screenWidths.sort((a, b) => a - b);
21 |
22 | const len = screenWidths.length;
23 |
24 | let ret = tmpMap[screenWidths[0]];
25 |
26 | if (availWidth > screenWidths[len - 1]) {
27 | ret = tmpMap[screenWidths[len - 1]];
28 | } else {
29 | for (let i = 1; i < len; i++) {
30 | const breakPoint = tmpMap[screenWidths[i]];
31 |
32 | if (screenWidths[i - 1] <= availWidth && availWidth < screenWidths[i]) {
33 | ret = breakPoint;
34 | }
35 | }
36 | }
37 |
38 | return ret;
39 | }
40 |
41 | /**
42 | * 获取最大断点列数
43 | */
44 | export function getMaxNumberOfColumns(breakPoints: BreakPoints = DEFAULT_BREAK_POINTS): number {
45 | const screenWidths = breakPoints.map((bp) => bp.width);
46 | const maxWidth = Math.max(...screenWidths);
47 | const bp = breakPoints.find((breakPoint) => breakPoint?.width === maxWidth);
48 |
49 | return bp?.numberOfColumns || 12;
50 | }
51 |
52 | /**
53 | * 是否为标准的预设尺寸
54 | * @param val
55 | */
56 | export function isPresetSize(val: any): boolean {
57 | return ['small', 'medium', 'large'].includes(val);
58 | }
59 |
60 | /**
61 | * 为数值包裹自动单位
62 | * eg:
63 | * wrapUnit(2) -> 2px
64 | * wrapUnit('2em') -> 2em
65 | * @param value
66 | * @param unit
67 | */
68 | export function wrapUnit(value: number | string | undefined, unit = 'px') {
69 | return isNumber(value) ? `${value}${unit}` : value;
70 | }
71 |
72 | /**
73 | * 是否为一个有效的 gap 值:
74 | * eg: 0.9 / -9px / auto
75 | * @param val
76 | */
77 | export function isValidGap(val: any): boolean {
78 | const gapValReg = /^-?[0-9]*\.?[0-9]+([a-z|A-z]*)$/;
79 | return isNumber(val) || (isString(val) && (gapValReg.test(val) || val === 'auto'));
80 | }
81 |
82 | /**
83 | * 获取最终的 gap 值
84 | * @param contextGap 上下文中的 gap 值
85 | * @param propGap 属性中定义的 gap 值
86 | */
87 | export function getGapVal(contextGap: any, propGap: any) {
88 | if (isValidGap(propGap)) {
89 | return wrapUnit(propGap);
90 | } else if (isValidGap(contextGap)) {
91 | return wrapUnit(contextGap);
92 | }
93 | return undefined;
94 | }
95 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "buildOnSave": false,
4 | "compilerOptions": {
5 | "baseUrl": ".",
6 | "outDir": "build",
7 | "module": "esnext",
8 | "target": "es6",
9 | "jsx": "react",
10 | "moduleResolution": "node",
11 | "lib": ["es6", "dom"],
12 | "sourceMap": true,
13 | "allowJs": true,
14 | "noUnusedLocals": true,
15 | "allowSyntheticDefaultImports": true,
16 | "noImplicitReturns": true,
17 | "noImplicitThis": true,
18 | "noImplicitAny": true,
19 | "skipLibCheck": true,
20 | "strict": true,
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["src/**/*.ts", "src/**/*.tsx", "global.d.ts", "lowcode/**/*.ts", "lowcode/**/*.tsx"],
26 | "exclude": ["node_modules", "build", "public"]
27 | }
28 |
--------------------------------------------------------------------------------