├── .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 | 57 |
58 |
59 | 60 |
61 | 62 | Header 63 | 64 |
65 | 66 | 73 | 80 |
81 | 82 | 83 | 84 |
85 |
86 | 87 | 88 | Block 89 | 90 | 91 |
92 |
93 | 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 |
27 |

28 | 29 | 自然布局 30 | 区块 31 | 32 |

33 |
34 |

段落布局

35 |
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 | 134 | 137 | 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 |
168 | 169 |
, 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 |
25 |

26 | 27 | 自然布局 28 | 小布局 29 | 30 |

31 |
32 |

字体

33 |
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 |
93 | 94 |
, 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 |
28 | 29 |
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 |
115 | 116 |
, 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 | 35 | 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: () => , 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 |
160 | 161 | 列表页 162 | 查询表格 163 | 164 |

165 | 组件间距 166 | Component Spacing 167 |

168 |

描述组件之间的间距关系

169 |
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 |
236 | 237 |
, 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 |
242 |

243 | Alibaba Fusion Design 244 |
245 | Copyright © 2022 Fusion Team 246 |

247 |
248 |
249 | ); 250 | }; 251 | 252 | ReactDOM.render( 253 |
254 | 255 |
, 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 |
142 | 143 |
, 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 |
40 |

41 | 42 | 自然布局 43 | 区块 44 | 45 |

46 |

区块

47 |
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 |
152 | 153 |
, 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 |
20 |
{children}
21 |
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 |