Compare commits
207 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a3ab3c093 | |||
| 55602fc961 | |||
| a3c0e463ca | |||
| 79f9a8c179 | |||
| ea96872261 | |||
| a86e0e049c | |||
| c4e1ed67f5 | |||
| 179de0cc52 | |||
| 6de54884b5 | |||
| 55bf417ddf | |||
| ca56edd9ae | |||
| 65149f1326 | |||
| 939466648d | |||
| 8fe57333a0 | |||
| 269e304d68 | |||
| 8c80255f81 | |||
| 359aa0b148 | |||
| 51ca4a72ce | |||
| b83d965955 | |||
| a1eea02f94 | |||
| 0d0479e3e5 | |||
| 67d602a3ec | |||
| 8dd289cd7d | |||
| 82bc5db4d8 | |||
| 85c053b3db | |||
| 32f5667cae | |||
| 1d0fb4f274 | |||
| 0011cdca0f | |||
| 0f3f0afdfa | |||
| b53a9f7eef | |||
| 5336c2ec28 | |||
| cdbd2351a2 | |||
| 1f03977f7d | |||
| a2cc418a73 | |||
| 239c0b0a28 | |||
| ae04850bb2 | |||
| ca740c83ae | |||
| 00bf52a411 | |||
| c1306e24e0 | |||
| 28040fc006 | |||
| 3f3981bb9d | |||
| 253ceca969 | |||
| a0ed57ff4f | |||
| f83c6bc0f6 | |||
| bb92210594 | |||
| 163ecedff3 | |||
| 22a2e9bdd1 | |||
| 696f82bd26 | |||
| a0484e8f64 | |||
| cf31ef19f4 | |||
| fe31246483 | |||
| 9daf3872e7 | |||
| f17ccbb7f6 | |||
| f854f75b8a | |||
| df3021f75e | |||
| 8bf53a76f8 | |||
| 791fbb162a | |||
| 704277b8f0 | |||
| 1fc22a4535 | |||
| ffaa276b5c | |||
| 56d0877507 | |||
| 897fe0a5a1 | |||
| b52bbad547 | |||
| f7c837a8a9 | |||
| f64d178ab8 | |||
| f2dcc942ed | |||
| ac48a8880e | |||
| 0f1711868d | |||
| 06cd3b3a18 | |||
| 2664613ab2 | |||
| e39a913a87 | |||
| 52864b5400 | |||
| 27166ec949 | |||
| 1a8d15c9da | |||
| 43b99de01c | |||
| 1c1ad9e5eb | |||
| 776837079a | |||
| 14375284e4 | |||
| cb6aabf6ce | |||
| 3f42516816 | |||
| 93608d1037 | |||
| 41a61cce4e | |||
| 47364e4219 | |||
| 9ed6a7d1a4 | |||
| 6f2c8f239a | |||
| 6215ed96f2 | |||
| 190aa1d8ee | |||
| bceb7a5c9c | |||
| d64042922b | |||
| 9ada3fecb3 | |||
| 07febb385f | |||
| ddbda0e6fb | |||
| 9f7441c134 | |||
| 9fd9673bf4 | |||
| 489118b88a | |||
| 23c2e2e9b6 | |||
| 6c65362a5d | |||
| 7470190e0b | |||
| b2293ba9f4 | |||
| 63dca2a9b7 | |||
| eaff3604f3 | |||
| 5b121939e6 | |||
| 62b39daf7e | |||
| 32962d47fb | |||
| d5e42c93bb | |||
| fc93f4424a | |||
| d6a08eecd2 | |||
| 181bfee2d2 | |||
| 8fd085d4fd | |||
| 35e258b32a | |||
| 62d403f80a | |||
| 73aded3672 | |||
| 8c07821d93 | |||
| 765ea07cd5 | |||
| a27c241d9c | |||
| 4d6b67eecd | |||
| 6fc889a161 | |||
| c07b554360 | |||
| 149002dddd | |||
| 45b0d856c1 | |||
| 6fc0498901 | |||
| f2919c1aa0 | |||
| 5dd5ab62a8 | |||
| 5e7e285452 | |||
| b3a458a914 | |||
| 1acfa6864b | |||
| baf071f130 | |||
| 6b1d55a11f | |||
| a8357f45b8 | |||
| 9db6358588 | |||
| 5bc2fb2d5a | |||
| 05a0f8f490 | |||
| c884712331 | |||
| e35ff792a0 | |||
| 138cbe9b20 | |||
| 979aa34259 | |||
| 76a5a2c659 | |||
| 53ebd89c9a | |||
| 3980810502 | |||
| accd826376 | |||
| 5ca1118f88 | |||
| 14cd4c54a1 | |||
| 7f46137c56 | |||
| 1db52948db | |||
| 2550411c1c | |||
| 640859d36e | |||
| d91763379c | |||
| 588861f649 | |||
| 91b53da38e | |||
| 9b0ada94a0 | |||
| f604996e56 | |||
| fb26540f27 | |||
| 558f2cd0a6 | |||
| 8a1234e565 | |||
| 630c486eb8 | |||
| 324f450851 | |||
| d6ffb50e85 | |||
| 2906efc8fc | |||
| 2259fe77fc | |||
| dbbd1dddc7 | |||
| 14e17ebbed | |||
| 4a06eb20cb | |||
| 089a482f75 | |||
| afb94d6a30 | |||
| b9403cfec6 | |||
| 6cf673e7f6 | |||
| 653386dcc0 | |||
| 208e275bb1 | |||
| 77f4145adf | |||
| 11f166a2a6 | |||
| 0452fdb7f1 | |||
| 0068d3426e | |||
| c5840f29b7 | |||
| 963bfe1ab8 | |||
| ae806aee35 | |||
| b29e23599b | |||
| f53ce94c6e | |||
| d775a5003a | |||
| 46d5f59b4c | |||
| 0c0d95fe58 | |||
| 9dd11f7a6d | |||
| fd2fa943b0 | |||
| 70fc6f8030 | |||
| 4f7233bd6c | |||
| 564d50b168 | |||
| 0b3e632745 | |||
| 3f073eeb43 | |||
| 6bbbe634ad | |||
| 8fefa6344f | |||
| 5ebbb428fb | |||
| c240c1824b | |||
| d63517ecb9 | |||
| 1a17ee1e94 | |||
| e1bab66819 | |||
| 89bcd45728 | |||
| e86a6d7cd9 | |||
| 46b54459ce | |||
| 85898dee19 | |||
| e429610606 | |||
| b6814d6484 | |||
| 3d7b58f19a | |||
| b6a47acdde | |||
| 226d71c704 | |||
| 1c72ee6ac2 | |||
| 09a1e2135a | |||
| 7999f604fd | |||
| a7df29c3de |
@@ -2,11 +2,12 @@
|
||||
"extends": ["eslint-config-ali/typescript/react", "prettier"],
|
||||
"ignorePatterns": ["**/dist/**/*", "**/lib/**/*", "**/node_modules/**/*", "scripts/**/*"],
|
||||
"rules": {
|
||||
"@typescript-eslint/consistent-type-definitions": "off",
|
||||
"@typescript-eslint/dot-notation": "off",
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
"import/no-cycle": "off",
|
||||
"no-nested-ternary": "off",
|
||||
"no-useless-return":"off",
|
||||
"no-useless-return": "off",
|
||||
"no-param-reassign": "off",
|
||||
"prefer-destructuring": "off"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
name: CI
|
||||
# Event设置为main分支的pull request事件,
|
||||
# 这里的main分支相当于master分支,github项目新建是把main设置为默认分支,我懒得改了所以就保持这样吧
|
||||
on:
|
||||
pull_request:
|
||||
branches: main
|
||||
jobs:
|
||||
# 只需要定义一个job并命名为CI
|
||||
CI:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# 拉取项目代码
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
# 给当前环境下载node
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18.x'
|
||||
# 检查缓存
|
||||
# 如果key命中缓存则直接将缓存的文件还原到 path 目录,从而减少流水线运行时间
|
||||
# 若 key 没命中缓存时,在当前Job成功完成时将自动创建一个新缓存
|
||||
- name: Cache
|
||||
# 缓存命中结果会存储在steps.[id].outputs.cache-hit里,该变量在继后的step中可读
|
||||
id: cache-dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
# 缓存文件目录的路径
|
||||
path: |
|
||||
**/node_modules
|
||||
# key中定义缓存标志位的生成方式。runner.OS指当前环境的系统。外加对yarn.lock内容生成哈希码作为key值,如果yarn.lock改变则代表依赖有变化。
|
||||
# 这里用yarn.lock而不是package.json是因为package.json中还有version和description之类的描述项目但和依赖无关的属性
|
||||
key: ${{runner.OS}}-${{hashFiles('**/yarn.lock')}}
|
||||
# 安装依赖
|
||||
- name: Installing Dependencies
|
||||
# 如果缓存标志位没命中,则执行该step。否则就跳过该step
|
||||
if: steps.cache-dependencies.outputs.cache-hit != 'true'
|
||||
run: yarn install
|
||||
# 预构建
|
||||
- name: Prebuild
|
||||
run: yarn build
|
||||
# 运行代码扫描
|
||||
- name: Running Lint
|
||||
# 通过前面章节定义的命令行执行代码扫描
|
||||
run: yarn eslint
|
||||
# 运行自动化测试
|
||||
- name: Running Test
|
||||
# 通过前面章节定义的命令行执行自动化测试
|
||||
run: yarn test
|
||||
@@ -0,0 +1,53 @@
|
||||
# Simple workflow for deploying static content to GitHub Pages
|
||||
name: Deploy Github Pages
|
||||
|
||||
on:
|
||||
# Runs on pushes targeting the default branch
|
||||
push:
|
||||
branches: ['main']
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow one concurrent deployment
|
||||
concurrency:
|
||||
group: 'pages'
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Single deploy job since we're just deploying
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
- name: Build
|
||||
run: yarn build
|
||||
- name: Build docs
|
||||
run: yarn typedoc
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v3
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v2
|
||||
with:
|
||||
# Upload dist repository
|
||||
path: './docs'
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v2
|
||||
@@ -9,3 +9,4 @@ coverage/
|
||||
.umi-production/
|
||||
.log
|
||||
.eslintcache
|
||||
*.pem
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn commitlint --edit "$1"
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn lint-staged --allow-empty
|
||||
@@ -5,7 +5,7 @@
|
||||
<h1 align="center">Tango LowCode Builder</h1>
|
||||
<div align="center">
|
||||
|
||||
A source code based low-code builder, developed by NetEase Cloud Music Develop Team.
|
||||
A source code based low-code builder.
|
||||
|
||||
[](https://github.com/NetEase/tango/blob/main/LICENSE)
|
||||
[](http://npmjs.org/package/@music163/tango-designer)
|
||||
@@ -20,7 +20,7 @@ English | [简体中文](/README.zh-CN.md)
|
||||
|
||||
You can view the detailed usage guide through the following links:
|
||||
|
||||
- Document site: <https://netease.github.io/tango/>
|
||||
- Document site: <https://netease.github.io/tango-site/>
|
||||
- Playground application: <https://tango-demo.musicfe.com/designer/>
|
||||
|
||||
## ✨ Features
|
||||
@@ -31,32 +31,28 @@ You can view the detailed usage guide through the following links:
|
||||
- Out-of-the-box front-end low-code designer, providing flexible and easy-to-use designer React components
|
||||
- Developed using TypeScript, providing complete type definition files
|
||||
|
||||
## 💡 Examples
|
||||
|
||||
You can use tango to build lowcode builders, for example:
|
||||
|
||||
| Preview | Description |
|
||||
| ---------------------------------------------------------------------------------------------- | ----------------------- |
|
||||
|  | Internal App Builder |
|
||||
|  | Dashboard App Builder |
|
||||
|  | Mobile App Builder |
|
||||
|  | ReactNative App Builder |
|
||||
|  | Email Builder |
|
||||
|
||||
## 🌐 Compatibility
|
||||
|
||||
- Modern browsers(Chrome >= 80, Edge >= 80, last 2 safari versions, last 2 firefox versions)
|
||||
|
||||
## 🎯 Milestone
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
dateFormat YYYY-MM-DD
|
||||
title Tango Release Milestone
|
||||
excludes weekends
|
||||
%% (`excludes` accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays".)
|
||||
|
||||
section Builder Release
|
||||
Alpha version :active, des1, 2023-08-10, 2023-08-30
|
||||
Beta version : des2, 2023-09-01, 2023-09-30
|
||||
1.0 RC : des3, after des2, 40d
|
||||
1.0 version : des4, after des3, 21d
|
||||
```
|
||||
|
||||
## 💻 Development
|
||||
|
||||
### Recommended Development Environment
|
||||
### Environment
|
||||
|
||||
- Node.js >= 16.0.0
|
||||
- Yarn >= 1.22.0
|
||||
- Node `>= 18`
|
||||
- Yarn `>= 1.22 && < 2`
|
||||
|
||||
### Development Quick Start
|
||||
|
||||
@@ -74,11 +70,29 @@ yarn
|
||||
yarn start
|
||||
```
|
||||
|
||||
### Local Https certificate
|
||||
|
||||
If you need to use https in the local development environment, you can use the following command to generate a certificate:
|
||||
|
||||
```bash
|
||||
brew install mkcert
|
||||
|
||||
# install the local certificate
|
||||
mkcert -install
|
||||
|
||||
# enter the playground app directory
|
||||
cd apps/playground
|
||||
|
||||
# generate a certificate
|
||||
mkcert local.netease.com # or change to your own domain name
|
||||
```
|
||||
|
||||
## 💬 Community
|
||||
|
||||
Join NetEase Tango Community to share your ideas, suggestions, or questions and connect with other users and contributors.
|
||||
|
||||
- Discord: <https://discord.gg/B6hkGTe4Rz>
|
||||
- [Usage Trends](https://npm-compare.com/@music163/tango-helpers,@music163/tango-context,@music163/tango-core,@music163/tango-setting-form,@music163/tango-sandbox,@music163/tango-ui,@music163/tango-designer)
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
@@ -89,16 +103,23 @@ Please read the [github contribution guide](https://docs.github.com/en/get-start
|
||||
- Commit and push your code
|
||||
- Open a Pull Request
|
||||
|
||||
## 📈 Usage Trends
|
||||
|
||||
[Usage Trends of Tango Packages](https://npm-compare.com/@music163/tango-helpers,@music163/tango-context,@music163/tango-core,@music163/tango-setting-form,@music163/tango-sandbox,@music163/tango-ui,@music163/tango-designer)
|
||||
|
||||
## 💗 Acknowledgments
|
||||
|
||||
Thanks to the NetEase Cloud Music Front-end team, Public Technology team, Live Broadcasting Technology team, and all the colleagues who participated in the Tango project.
|
||||
Thanks to the NetEase Cloud Music Front-end team, Public Technology team, Live Broadcasting Technology team, and all the developers who participated in the Tango project.
|
||||
|
||||
Thank you to CodeSandbox for providing the [Sandpack](https://sandpack.codesandbox.io/) project, which provides powerful online code execution capabilities for Tango.
|
||||
|
||||
## 📣 Product Promotion
|
||||
|
||||

|
||||
|
||||
Don't waste your time restoring the UI, try the NetEase Cloud Music "Seal D2C" development tool! Easily turn your design into code, support React, RN, Vue, WeChat apps and other multi-end scenarios, what you see is what you get!
|
||||
|
||||
Experience "Seal D2C" now:
|
||||
|
||||
- I'm a Figma user: <https://www.figma.com/community/plugin/1174548852019950797/seal-figma-to-code-d2c/>
|
||||
- I'm a MasterGo user: <https://mastergo.com/community/plugin/98956774428196/>
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the terms of the [MIT license](./LICENSE)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<h1 align="center">Tango 低代码设计器</h1>
|
||||
<div align="center">
|
||||
|
||||
一个源码驱动的低代码设计器,来自网易云音乐技术团队
|
||||
一个源码驱动的低代码设计器框架
|
||||
|
||||
[](https://github.com/NetEase/tango/blob/main/LICENSE)
|
||||
[](http://npmjs.org/package/@music163/tango-designer)
|
||||
@@ -20,43 +20,39 @@
|
||||
|
||||
可以通过下面的链接查看详细的使用指南:
|
||||
|
||||
- 官方文档站点: <https://netease.github.io/tango/>
|
||||
- 官方文档站点: <https://netease.github.io/tango-site/>
|
||||
- 演示应用: <https://tango-demo.musicfe.com/designer/>
|
||||
|
||||
## ✨ 特性
|
||||
|
||||
- 经历网易云音乐内网生产环境的实际检验,可灵活集成应用于低代码平台,本地开发工具等
|
||||
- 基于源码 AST 驱动,无私有 DSL 和协议
|
||||
- 提供实时出码能力,支持源码进,源码出
|
||||
- 开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件
|
||||
- 使用 TypeScript 开发,提供完整的类型定义文件
|
||||
- 提供基于源码 AST 驱动的低代码引擎,无私有 DSL 和协议
|
||||
- 提供实时出码能力,支持源码进,源码出,适配多种源码开发场景
|
||||
- 提供开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件
|
||||
- 使用 TypeScript 开发,提供完整的类型定义文件和完善的文档支持
|
||||
|
||||
## 💡 例子
|
||||
|
||||
你可以使用 Tango 快速构建多种类型的基于源码的低代码搭建工具,例如:
|
||||
|
||||
| 预览图 | 说明 |
|
||||
| ---------------------------------------------------------------------------------------------- | -------------------- |
|
||||
|  | 中后台系统搭建 |
|
||||
|  | 仪表盘应用搭建 |
|
||||
|  | H5活动页面搭建 |
|
||||
|  | ReactNative 应用搭建 |
|
||||
|  | 营销类邮件搭建 |
|
||||
|
||||
## 🌐 兼容环境
|
||||
|
||||
- 现代浏览器(Chrome >= 80, Edge >= 80, last 2 safari versions, last 2 firefox versions)
|
||||
|
||||
## 🎯 里程碑
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
dateFormat YYYY-MM-DD
|
||||
title Tango Release Milestone
|
||||
excludes weekends
|
||||
%% (`excludes` accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays".)
|
||||
|
||||
section Builder Release
|
||||
Alpha version :active, des1, 2023-08-10, 2023-08-30
|
||||
Beta version : des2, 2023-09-01, 2023-09-30
|
||||
1.0 RC : des3, after des2, 40d
|
||||
1.0 version : des4, after des3, 21d
|
||||
```
|
||||
|
||||
## 💻 开发
|
||||
|
||||
### 推荐开发环境
|
||||
|
||||
- Node.js >= 16.0.0
|
||||
- Yarn >= 1.22.0
|
||||
- Node `>= 18`
|
||||
- Yarn `>= 1.22 && < 2`
|
||||
|
||||
### 本地开发调试方法
|
||||
|
||||
@@ -74,11 +70,29 @@ yarn
|
||||
yarn start
|
||||
```
|
||||
|
||||
### 本地 https 证书
|
||||
|
||||
如果需要在本地开发环境中使用 https,可以使用以下命令生成证书:
|
||||
|
||||
```bash
|
||||
brew install mkcert
|
||||
|
||||
# 将 mkcert 添加到本地根 CA,仅在本地生效
|
||||
mkcert -install
|
||||
|
||||
# 进入 playground 应用目录
|
||||
cd apps/playground
|
||||
|
||||
# 为网站生成一个由 mkcert 签名的证书
|
||||
mkcert local.netease.com
|
||||
```
|
||||
|
||||
## 💬 社区讨论
|
||||
|
||||
参与 NetEase Tango 的社区,以分享您的想法、建议或问题,并与其他用户和贡献者建立联系。
|
||||
|
||||
- Discord: <https://discord.gg/B6hkGTe4Rz>
|
||||
- [使用趋势](https://npm-compare.com/@music163/tango-helpers,@music163/tango-context,@music163/tango-core,@music163/tango-setting-form,@music163/tango-sandbox,@music163/tango-ui,@music163/tango-designer)
|
||||
|
||||
## 🤝 参与共建
|
||||
|
||||
@@ -90,16 +104,23 @@ yarn start
|
||||
- 合并修改 git rebase master
|
||||
- 发起 Pull Request
|
||||
|
||||
## 📈 使用量趋势
|
||||
|
||||
[Tango 使用量趋势](https://npm-compare.com/@music163/tango-helpers,@music163/tango-context,@music163/tango-core,@music163/tango-setting-form,@music163/tango-sandbox,@music163/tango-ui,@music163/tango-designer)
|
||||
|
||||
## 💗 致谢
|
||||
|
||||
感谢网易云音乐公共技术团队,大前端团队,直播技术团队,以及所有参与过 Tango 项目的同学们。
|
||||
感谢网易云音乐公共技术团队,大前端团队,直播技术团队,以及所有参与过 Tango 项目的开发者。
|
||||
|
||||
感谢 CodeSandbox 提供的 [Sandpack](https://sandpack.codesandbox.io/) 项目,为 Tango 提供了强大的基于浏览器的代码构建与执行能力。
|
||||
|
||||
## 📣 产品推广
|
||||
|
||||

|
||||
|
||||
不要再浪费时间还原 UI 啦,快来试试网易云音乐「海豹 D2C」研发工具吧!轻松将设计稿转为代码,支持 React、RN、Vue、微信小程序等多端场景,所见即所得!
|
||||
|
||||
立即体验「海豹 D2C」:
|
||||
|
||||
- 我是 Figma 用户:<https://www.figma.com/community/plugin/1174548852019950797/seal-figma-to-code-d2c/>
|
||||
- 我是 MasterGo 用户:<https://mastergo.com/community/plugin/98956774428196/>
|
||||
|
||||
## 📄 开源协议
|
||||
|
||||
此项目遵循 [MIT 开源协议](./LICENSE)
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
PORT=6006
|
||||
HOST=local.netease.com
|
||||
@@ -0,0 +1,9 @@
|
||||
/node_modules
|
||||
/.env.local
|
||||
/.umirc.local.ts
|
||||
/config/config.local.ts
|
||||
/src/.umi
|
||||
/src/.umi-production
|
||||
/src/.umi-test
|
||||
/dist
|
||||
.swc
|
||||
@@ -1,33 +1,18 @@
|
||||
import path from 'path';
|
||||
import { defineConfig } from 'umi';
|
||||
|
||||
const resolvePackageIndex = (relativeEntry: string) =>
|
||||
path.resolve(__dirname, '../../../packages/', relativeEntry);
|
||||
path.resolve(__dirname, '../../packages/', relativeEntry);
|
||||
|
||||
export default {
|
||||
export default defineConfig({
|
||||
routes: [
|
||||
{
|
||||
exact: true,
|
||||
path: '/',
|
||||
component: 'index',
|
||||
name: '首页',
|
||||
},
|
||||
{ path: '/', component: 'index' },
|
||||
{ path: '/mail', component: 'mail' },
|
||||
{ path: '/docs', component: 'docs' },
|
||||
],
|
||||
devServer: {
|
||||
host: 'local.netease.com',
|
||||
port: 7007,
|
||||
https: {
|
||||
key: path.resolve(__dirname, 'local.netease.com-key.pem'),
|
||||
cert: path.resolve(__dirname, 'local.netease.com.pem'),
|
||||
},
|
||||
headers: { 'Origin-Agent-Cluster': '?0' },
|
||||
},
|
||||
targets: {
|
||||
chrome: 79,
|
||||
firefox: false,
|
||||
safari: false,
|
||||
edge: false,
|
||||
ios: false,
|
||||
},
|
||||
npmClient: 'yarn',
|
||||
mfsu: false,
|
||||
codeSplitting: false,
|
||||
alias: {
|
||||
'@music163/tango-helpers': resolvePackageIndex('helpers/src/index.ts'),
|
||||
'@music163/tango-core': resolvePackageIndex('core/src/index.ts'),
|
||||
@@ -44,6 +29,21 @@ export default {
|
||||
moment: 'moment',
|
||||
antd: 'antd',
|
||||
},
|
||||
headScripts: [
|
||||
'https://unpkg.com/react@18.2.0/umd/react.development.js',
|
||||
'https://unpkg.com/react-dom@18.2.0/umd/react-dom.development.js',
|
||||
'https://unpkg.com/react-is@18.2.0/umd/react-is.production.min.js',
|
||||
'https://unpkg.com/moment/min/moment-with-locales.js',
|
||||
'https://unpkg.com/styled-components@5.3.11/dist/styled-components.js',
|
||||
'https://unpkg.com/antd@4.24.13/dist/antd-with-locales.min.js',
|
||||
'https://unpkg.com/prettier@2.6.0/standalone.js',
|
||||
'https://unpkg.com/prettier@2.6.0/parser-babel.js',
|
||||
],
|
||||
https: {
|
||||
key: path.resolve(__dirname, 'local.netease.com-key.pem'),
|
||||
cert: path.resolve(__dirname, 'local.netease.com.pem'),
|
||||
http2: false,
|
||||
},
|
||||
chainWebpack: (config: any) => {
|
||||
// @see https://github.com/graphql/graphql-js/issues/1272#issuecomment-393903706
|
||||
config.module
|
||||
@@ -61,4 +61,5 @@ export default {
|
||||
config.module.rule('js').include.add(resolvePackageIndex('ui/src'));
|
||||
return config;
|
||||
},
|
||||
};
|
||||
monorepoRedirect: {},
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
# `playground`
|
||||
|
||||
搭建平台演示示例
|
||||
|
||||
## 说明
|
||||
@@ -1,28 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC4sAqDE3nhtSWD
|
||||
YqV4mlglnzIs2AioZr2uxfj/OKrpE1ZML/dflJyc5NGUpOZWV+W1vRc70wWVNFxB
|
||||
Si8nrYrrseoID7Ehxxe6+J1m/JsEbhZWHwV3M1iGW7JD7VhujaDpSLg67wMTMpSW
|
||||
3BUS+0NT+MNfCisp+u/ls4fXPCTgXiWcjMULm95oLlTeYwurPHfxwn9f9hh85I5I
|
||||
K3dI7w8MsQN7EFNrJz3UfrNLpmARMtSUBMwBYqrCitoUxRT0q7KYCru7kHxGPB6A
|
||||
SQrFpSpOWT79vp8GBSp9g3axhRsK+aEgzQ0QGrFPHBwBP/kWgmdB1URc/K7STSC9
|
||||
xyMNZOM7AgMBAAECggEATbyDYDKVbaRymr+tyHqmuYctdmSkGEXOdz8LFFoEzj/+
|
||||
ZekWpVuHJB7H/FoAPb2XiYyCuAKVafz1C+IuwPOx8o0bZbM9KmuDmIZGsm+GzDGO
|
||||
I0fBZC/vhfYYrbC8NSRV43thWCT8VVtH4cvW7vtllnWxvlIoYu4lhbiwZ68AEpO1
|
||||
8PlJsOhnagfkFpG8LehSDs9if5L+Ig0geQEcUY3pGUM9LKYsRoxFv6Vh8wQku1XD
|
||||
YOxLvLWDPqJ75m816t50RARe3e6eDJ+ukzDCoDjmLP8uhf1sk1FcL5aOgUNU3VOi
|
||||
P6sIH5uzuD0MA3FXx2mItYLOc44r4uaFJZiRUMEXGQKBgQDCjUYblE4rh0nKFkqx
|
||||
AhgfcRgpgLbP42OwFtD0u20RXdZAEJDRDQ43NdOgjFp2B50k75LBPIoFXYUZdzKj
|
||||
l7S2FH5O9Cc6hEgZGI4hCiYd+nQVI1mxv820YnzZJN4oooxyL+B5RlnW5cDT7LJN
|
||||
11Q5PRewkM+XpfbE3lKUjTNcDwKBgQDzBS+j8T9GJSzV2xgA+mpGHe1RjDq6yQtk
|
||||
Rl5GWpflW4Nr3/58BbmCWNA8WP0NOWutJhrkltwLCqJC8OZX/WHnmcmq2k+UAfQQ
|
||||
z98p+xcMWOTcl5XIbveuhYoZb2udt9WVJLcpRVxVhEWcBrtHsrpScZXsKYf/TaZ1
|
||||
JtCjPU5KFQKBgGXj/UqmYkYzxXZ8NEP998pHvcLGsXew/F2VpXv2yQlmXrYQtvd1
|
||||
YfOSUjJsL1hPZoKYogBoB1UGBJYwsimxYyhVFU2eWwkvjF7wWEd/fDUJHVCQwgHw
|
||||
lPga+I303BDmCK4o2uRI7vY4P8P92+gelPKhR03mmYIvnky+rvsz9CkLAoGAVerl
|
||||
du6Z0jDecxUsnVvrKrL17jjHorXlYyRFvFXXEe2SvxbIIAzeEylXJZ7IiF5epS6t
|
||||
n1d+oCC4UTZeGYmpjXofhFn+fHNcWH1FhtAQy/q5nwuwltY0luz7cBamU3Jk/n+m
|
||||
id7N0CYdfhYbLDQSD4e822ureyV2zkBUzwGjpikCgYEAoCVbhkxVrQHRmxLC2jDx
|
||||
pM94W1ij9TqblfGg3OMpPXjpTdlknatWjBw7JkoyxYiYJq5Gi2p4QV6HHUvS1osj
|
||||
BB+4A99/PD9A0ICoJglz8SXlGe/895zwHwpKT9f0zjizQrEd2KCPf75URa+t2FGY
|
||||
B2GkXsbChRWVrey5+uMzUD8=
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -1,25 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEQjCCAqqgAwIBAgIRAIeLJ2jwt4MSKycHFmFXL6YwDQYJKoZIhvcNAQELBQAw
|
||||
fTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSkwJwYDVQQLDCB3d3N1
|
||||
bkBzdW4tbWJwLmxvY2FsIChTVU4gV2Vpd2VpKTEwMC4GA1UEAwwnbWtjZXJ0IHd3
|
||||
c3VuQHN1bi1tYnAubG9jYWwgKFNVTiBXZWl3ZWkpMB4XDTIyMDcyODAxNTIxMFoX
|
||||
DTI0MTAyODAxNTIxMFowVDEnMCUGA1UEChMebWtjZXJ0IGRldmVsb3BtZW50IGNl
|
||||
cnRpZmljYXRlMSkwJwYDVQQLDCB3d3N1bkBzdW4tbWJwLmxvY2FsIChTVU4gV2Vp
|
||||
d2VpKTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALiwCoMTeeG1JYNi
|
||||
pXiaWCWfMizYCKhmva7F+P84qukTVkwv91+UnJzk0ZSk5lZX5bW9FzvTBZU0XEFK
|
||||
Lyetiuux6ggPsSHHF7r4nWb8mwRuFlYfBXczWIZbskPtWG6NoOlIuDrvAxMylJbc
|
||||
FRL7Q1P4w18KKyn67+Wzh9c8JOBeJZyMxQub3mguVN5jC6s8d/HCf1/2GHzkjkgr
|
||||
d0jvDwyxA3sQU2snPdR+s0umYBEy1JQEzAFiqsKK2hTFFPSrspgKu7uQfEY8HoBJ
|
||||
CsWlKk5ZPv2+nwYFKn2DdrGFGwr5oSDNDRAasU8cHAE/+RaCZ0HVRFz8rtJNIL3H
|
||||
Iw1k4zsCAwEAAaNmMGQwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUF
|
||||
BwMBMB8GA1UdIwQYMBaAFDILZR+SW32Ug0HkAKrH1KT2a1ttMBwGA1UdEQQVMBOC
|
||||
EWxvY2FsLm5ldGVhc2UuY29tMA0GCSqGSIb3DQEBCwUAA4IBgQCEdeV2Likl4MUt
|
||||
wWeN/X3tAR3L7mkl7e4f0CQ4v2lOs8cekOP7p3ZFk5nSyVbxda3usPB0OtbVTibw
|
||||
qZ1n/TMRb5epHroXIMleqngP82zFZNg+WVyHm/ZAml8gVD0VZjYM1Tb+U9+SrgGw
|
||||
lvZndbuUXa3h/d0irPH90KsdEeAR1SqwzAzvLO2JgvBuimHRceQWcTiiGWDZOyii
|
||||
tux4U+pEzh0Dia40uFozFAoYZPDUpZ+gsiQ3MR3HOrAu2OR0a+CfelDXZNLR3C4z
|
||||
cBKr53ajAbHstnpxKJh9IfeKph44PrbDo6HMe1XPprl7idoEesX3Ma3SBgm4lmId
|
||||
cfEOUWNIYOgtOT5bflXNNyzXOgYVjpLnjwg4EOASO/XIjhvqdjIyCu7HJMIcZ9oo
|
||||
Aob2exeluv5LVwic1UJQDl2McDBIgJ8R32oEN4skCKIBSbWlxWyjnxL5I5WkJF4y
|
||||
vlSx8bKAjVr8map0C3rWISuZdgheJkF96qjwhVSxOv++p6srq6M=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -2,24 +2,19 @@
|
||||
"name": "playground",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "playground of tango-apps",
|
||||
"license": "MIT",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"author": "Wells <ww.sun@outlook.com>",
|
||||
"scripts": {
|
||||
"build": "COMPRESS=none umi build",
|
||||
"start": "HTTPS=1 umi dev"
|
||||
"dev": "cross-env HOST=local.netease.com PORT=8001 umi dev",
|
||||
"build": "umi build",
|
||||
"postinstall": "umi setup",
|
||||
"setup": "umi setup",
|
||||
"start": "npm run dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/tango-designer": "*",
|
||||
"@music163/tango-helpers": "*",
|
||||
"@music163/antd": "^0.2.4",
|
||||
"antd": "^4.24.2",
|
||||
"coral-system": "^1.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"umi": "^3.5.24"
|
||||
"coral-system": "^1.0.5",
|
||||
"umi": "^4.2.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { IApi } from 'umi';
|
||||
|
||||
export default (api: IApi) => {
|
||||
api.addMiddlewares(() => {
|
||||
return (req, res, next) => {
|
||||
res.setHeader('Origin-Agent-Cluster', '?0');
|
||||
next();
|
||||
};
|
||||
});
|
||||
};
|
||||
|
After Width: | Height: | Size: 177 KiB |
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Row, Switch } from 'antd';
|
||||
import { observer } from '@music163/tango-designer';
|
||||
import { observer } from '@music163/tango-context';
|
||||
|
||||
const OtherPanel = observer(({ autoRemove, setAutoRemove }: any) => {
|
||||
const onChange = (checked: boolean) => {
|
||||
|
||||
@@ -1,117 +1,42 @@
|
||||
import React from 'react';
|
||||
import { Box, Group } from 'coral-system';
|
||||
import { Box, Group, Text } from 'coral-system';
|
||||
import { Avatar, Space, Switch } from 'antd';
|
||||
import { BranchesOutlined, MenuOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { registerSetter } from '@music163/tango-designer';
|
||||
import type { ComponentPrototypeType } from '@music163/tango-helpers';
|
||||
import type { IVariableTreeNode } from '@music163/tango-helpers';
|
||||
import { FooSetter } from '../components';
|
||||
import { ToggleButton } from '@music163/tango-ui';
|
||||
|
||||
// folder-name
|
||||
// 物料列表定义
|
||||
const bizToggleButtonPrototype: ComponentPrototypeType = {
|
||||
name: 'CtPcToggleButton',
|
||||
exportType: 'defaultExport',
|
||||
title: '示例业务组件',
|
||||
icon: 'icon-tupian',
|
||||
type: 'element',
|
||||
docs: 'https://redstone.fn.netease.com/mt/fe-comp/w8bq8px7n5/toggle-button',
|
||||
hasChildren: false,
|
||||
props: [
|
||||
{
|
||||
name: 'checked',
|
||||
title: '是否选中',
|
||||
setter: 'boolSetter',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
name: 'children',
|
||||
title: '文本',
|
||||
setter: 'textSetter',
|
||||
initValue: '按钮',
|
||||
},
|
||||
],
|
||||
package: '@music/ct-pc-toggle-button',
|
||||
};
|
||||
export * from './mock-files';
|
||||
|
||||
const sampleBlockCode = `
|
||||
<Section>
|
||||
<Result
|
||||
status="success"
|
||||
title="Successfully Purchased Cloud Server ECS!"
|
||||
subTitle="Order number: 2017182818828182881 Cloud server configuration takes 1-5 minutes, please wait."
|
||||
extra={[
|
||||
<Button type="primary" key="console">
|
||||
Go Console
|
||||
</Button>,
|
||||
<Button key="buy">Buy Again</Button>,
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
`;
|
||||
|
||||
const SnippetSuccessResult: ComponentPrototypeType = {
|
||||
name: 'SnippetSuccessResult',
|
||||
title: '成功结果',
|
||||
icon: 'icon-tupian',
|
||||
type: 'snippet',
|
||||
package: '@music163/antd',
|
||||
initChildren: sampleBlockCode,
|
||||
relatedImports: ['Section', 'Result', 'Button'],
|
||||
};
|
||||
|
||||
const Snippet2ColumnLayout: ComponentPrototypeType = {
|
||||
name: 'Snippet2ColumnLayout',
|
||||
title: '两列布局',
|
||||
icon: 'icon-columns',
|
||||
type: 'snippet',
|
||||
package: '@music163/antd',
|
||||
initChildren: `
|
||||
<Columns columns={12}>
|
||||
<Column colSpan={6}></Column>
|
||||
<Column colSpan={6}></Column>
|
||||
</Columns>
|
||||
`,
|
||||
relatedImports: ['Columns', 'Column'],
|
||||
};
|
||||
|
||||
const Snippet3ColumnLayout: ComponentPrototypeType = {
|
||||
name: 'Snippet3ColumnLayout',
|
||||
title: '三列布局',
|
||||
icon: 'icon-column3',
|
||||
type: 'snippet',
|
||||
package: '@music163/antd',
|
||||
initChildren: `
|
||||
<Columns columns={12}>
|
||||
<Column colSpan={4}></Column>
|
||||
<Column colSpan={4}></Column>
|
||||
<Column colSpan={4}></Column>
|
||||
</Columns>
|
||||
`,
|
||||
relatedImports: ['Columns', 'Column'],
|
||||
};
|
||||
|
||||
const SnippetButtonGroup: ComponentPrototypeType = {
|
||||
name: 'SnippetButtonGroup',
|
||||
title: '按钮组',
|
||||
icon: 'icon-anniuzu',
|
||||
type: 'snippet',
|
||||
package: '@music163/antd',
|
||||
initChildren: `
|
||||
<Space>
|
||||
<Button type="primary">按钮1</Button>
|
||||
<Button>按钮2</Button>
|
||||
</Space>
|
||||
`,
|
||||
relatedImports: ['Space', 'Button'],
|
||||
};
|
||||
|
||||
export const prototypes = {
|
||||
CtPcToggleButton: bizToggleButtonPrototype,
|
||||
SnippetSuccessResult,
|
||||
Snippet2ColumnLayout,
|
||||
Snippet3ColumnLayout,
|
||||
SnippetButtonGroup,
|
||||
};
|
||||
export const bootHelperVariables: IVariableTreeNode[] = [
|
||||
{
|
||||
key: '$helpers',
|
||||
title: '工具函数',
|
||||
children: [
|
||||
{
|
||||
title: '设置变量值',
|
||||
key: '() => tango.setStoreValue("variableName", "variableValue")',
|
||||
type: 'function',
|
||||
help: '设置状态值,tango.setStoreValue("variableName", "variableValue")',
|
||||
},
|
||||
{
|
||||
title: '获取变量值',
|
||||
key: '() => tango.getStoreValue("variableName")',
|
||||
type: 'function',
|
||||
help: '获取状态值',
|
||||
},
|
||||
{ title: '打开弹窗', key: '() => tango.openModal("")', type: 'function' },
|
||||
{ title: '关闭弹窗', key: '() => tango.closeModal("")', type: 'function' },
|
||||
{ title: '切换路由', key: '() => tango.navigateTo("/")', type: 'function' },
|
||||
{
|
||||
title: '拷贝到剪贴板',
|
||||
key: '() => tango.copyToClipboard("hello")',
|
||||
type: 'function',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// 注册自定义 setter
|
||||
registerSetter({
|
||||
@@ -122,8 +47,17 @@ registerSetter({
|
||||
// 平台 Logo
|
||||
export function Logo() {
|
||||
return (
|
||||
<Box width="50px" display="flex" alignItems="center" justifyContent="center" fontSize="20px">
|
||||
<MenuOutlined />
|
||||
<Box width="50px" display="flex" alignItems="center" justifyContent="center" fontSize="14px">
|
||||
<ToggleButton
|
||||
shape="text"
|
||||
items={[
|
||||
{ label: '回到首页', key: 'home' },
|
||||
{ label: '文档', key: 'doc' },
|
||||
{ label: '设置', key: 'setting' },
|
||||
]}
|
||||
>
|
||||
<MenuOutlined />
|
||||
</ToggleButton>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -132,12 +66,12 @@ export function Logo() {
|
||||
export function ProjectDetail() {
|
||||
return (
|
||||
<Box display="flex" alignItems="center" columnGap="l">
|
||||
<Box className="ProjectName" fontSize="18px" fontWeight="bold">
|
||||
lc-online-test
|
||||
</Box>
|
||||
<Box className="BranchName" as="code" fontSize="14px">
|
||||
<Text className="ProjectName" fontSize="18px" fontWeight="bold" truncated>
|
||||
community-test
|
||||
</Text>
|
||||
<Text className="BranchName" as="code" fontSize="14px" truncated>
|
||||
<BranchesOutlined /> feature/list
|
||||
</Box>
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
const packageJson = {
|
||||
name: 'demo',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@music163/tango-mail': '0.2.4',
|
||||
'@music163/tango-boot': '0.2.5',
|
||||
react: '17.0.2',
|
||||
'react-dom': '17.0.2',
|
||||
'prop-types': '15.7.2',
|
||||
tslib: '2.5.0',
|
||||
},
|
||||
};
|
||||
|
||||
const tangoConfigJson = {
|
||||
designerConfig: {
|
||||
autoGenerateComponentId: true,
|
||||
},
|
||||
packages: {
|
||||
react: {
|
||||
version: '17.0.2',
|
||||
library: 'React',
|
||||
type: 'dependency',
|
||||
resources: ['https://unpkg.com/react@{{version}}/umd/react.development.js'],
|
||||
},
|
||||
'react-dom': {
|
||||
version: '17.0.2',
|
||||
library: 'ReactDOM',
|
||||
type: 'dependency',
|
||||
resources: ['https://unpkg.com/react-dom@{{version}}/umd/react-dom.development.js'],
|
||||
},
|
||||
'react-is': {
|
||||
version: '16.13.1',
|
||||
library: 'ReactIs',
|
||||
type: 'dependency',
|
||||
resources: ['https://unpkg.com/react-is@{{version}}/umd/react-is.production.min.js'],
|
||||
},
|
||||
'@music163/tango-boot': {
|
||||
description: '云音乐低代码运行时框架',
|
||||
version: '0.2.5',
|
||||
library: 'TangoBoot',
|
||||
type: 'baseDependency',
|
||||
resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'],
|
||||
// resources: ['http://localhost:9001/boot.js'],
|
||||
},
|
||||
'@music163/tango-mail': {
|
||||
description: 'TangoMail 基础物料',
|
||||
version: '0.2.4',
|
||||
library: 'TangoMail',
|
||||
type: 'baseDependency',
|
||||
resources: ['https://unpkg.com/@music163/tango-mail@{{version}}/dist/index.js'],
|
||||
designerResources: ['https://unpkg.com/@music163/tango-mail@{{version}}/dist/designer.js'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const routesCode = `
|
||||
import Mail from "./pages/mail";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
exact: true,
|
||||
component: Mail,
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
`;
|
||||
|
||||
const entryCode = `
|
||||
import { runApp } from '@music163/tango-boot';
|
||||
import routes from './routes';
|
||||
|
||||
runApp({
|
||||
boot: {
|
||||
mountElement: document.querySelector('#root'),
|
||||
qiankun: false,
|
||||
},
|
||||
|
||||
router: {
|
||||
type: 'browser',
|
||||
config: routes,
|
||||
},
|
||||
});
|
||||
`;
|
||||
|
||||
const viewHomeCode = `
|
||||
import React from 'react';
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Container,
|
||||
Head,
|
||||
Hr,
|
||||
Html,
|
||||
Preview,
|
||||
Section,
|
||||
Text,
|
||||
} from '@music163/tango-mail';
|
||||
|
||||
const WelcomeEmail = () => (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>The sales intelligence platform that helps you uncover qualified leads.</Preview>
|
||||
<Body style={main}>
|
||||
<Container style={container}>
|
||||
<Text style={paragraph}>Hi wells,</Text>
|
||||
<Text style={paragraph}>
|
||||
Welcome to Koala, the sales intelligence platform that helps you uncover qualified leads
|
||||
and close deals faster.
|
||||
</Text>
|
||||
<Section style={btnContainer}>
|
||||
<Button style={button} href="https://getkoala.com">
|
||||
Get started
|
||||
</Button>
|
||||
</Section>
|
||||
<Text style={paragraph}>
|
||||
Best,
|
||||
The Koala team
|
||||
</Text>
|
||||
<Hr style={hr} />
|
||||
<Text style={footer}>470 Noor Ave STE B #1148, South San Francisco, CA 94080</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
</Html>
|
||||
);
|
||||
|
||||
const main = {
|
||||
backgroundColor: '#ffffff',
|
||||
fontFamily:
|
||||
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif',
|
||||
};
|
||||
|
||||
const container = {
|
||||
margin: '0 auto',
|
||||
padding: '20px 0 48px',
|
||||
};
|
||||
|
||||
const paragraph = {
|
||||
fontSize: '16px',
|
||||
lineHeight: '26px',
|
||||
};
|
||||
|
||||
const btnContainer = {
|
||||
textAlign: 'center',
|
||||
};
|
||||
|
||||
const button = {
|
||||
backgroundColor: '#5F51E8',
|
||||
borderRadius: '3px',
|
||||
color: '#fff',
|
||||
fontSize: '16px',
|
||||
textDecoration: 'none',
|
||||
textAlign: 'center',
|
||||
display: 'block',
|
||||
padding: '12px',
|
||||
};
|
||||
|
||||
const hr = {
|
||||
borderColor: '#cccccc',
|
||||
margin: '20px 0',
|
||||
};
|
||||
|
||||
const footer = {
|
||||
color: '#8898aa',
|
||||
fontSize: '12px',
|
||||
};
|
||||
|
||||
export default WelcomeEmail;
|
||||
`;
|
||||
|
||||
export const mailFiles = [
|
||||
{ filename: '/package.json', code: JSON.stringify(packageJson) },
|
||||
{ filename: '/tango.config.json', code: JSON.stringify(tangoConfigJson) },
|
||||
{ filename: '/README.md', code: '# readme' },
|
||||
{ filename: '/src/index.js', code: entryCode },
|
||||
{ filename: '/src/pages/mail.js', code: viewHomeCode },
|
||||
{ filename: '/src/routes.js', code: routesCode },
|
||||
];
|
||||
@@ -0,0 +1,462 @@
|
||||
const packageJson = {
|
||||
name: 'demo',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@music163/antd': '0.2.5',
|
||||
'@music163/tango-boot': '0.3.5',
|
||||
react: '17.0.2',
|
||||
'react-dom': '17.0.2',
|
||||
'prop-types': '15.7.2',
|
||||
tslib: '2.5.0',
|
||||
},
|
||||
};
|
||||
|
||||
const tangoConfigJson = {
|
||||
designerConfig: {
|
||||
autoGenerateComponentId: true,
|
||||
},
|
||||
packages: {
|
||||
react: {
|
||||
version: '17.0.2',
|
||||
library: 'React',
|
||||
type: 'dependency',
|
||||
resources: ['https://unpkg.com/react@{{version}}/umd/react.development.js'],
|
||||
},
|
||||
'react-dom': {
|
||||
version: '17.0.2',
|
||||
library: 'ReactDOM',
|
||||
type: 'dependency',
|
||||
resources: ['https://unpkg.com/react-dom@{{version}}/umd/react-dom.development.js'],
|
||||
},
|
||||
'react-is': {
|
||||
version: '16.13.1',
|
||||
library: 'ReactIs',
|
||||
type: 'dependency',
|
||||
resources: ['https://unpkg.com/react-is@{{version}}/umd/react-is.production.min.js'],
|
||||
},
|
||||
'styled-components': {
|
||||
version: '5.3.5',
|
||||
library: 'styled',
|
||||
type: 'dependency',
|
||||
resources: ['https://unpkg.com/styled-components@{{version}}/dist/styled-components.min.js'],
|
||||
},
|
||||
moment: {
|
||||
version: '2.29.4',
|
||||
library: 'moment',
|
||||
type: 'dependency',
|
||||
resources: ['https://unpkg.com/moment@{{version}}/moment.js'],
|
||||
},
|
||||
'@music163/tango-boot': {
|
||||
description: '云音乐低代码运行时框架',
|
||||
version: '0.3.5',
|
||||
library: 'TangoBoot',
|
||||
type: 'baseDependency',
|
||||
resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'],
|
||||
// resources: ['http://localhost:9001/boot.js'],
|
||||
},
|
||||
'@music163/antd': {
|
||||
description: '云音乐低代码中后台应用基础物料',
|
||||
version: '0.2.6',
|
||||
library: 'TangoAntd',
|
||||
type: 'baseDependency',
|
||||
resources: [
|
||||
'https://unpkg.com/@music163/antd@{{version}}/dist/index.js',
|
||||
// 'http://localhost:9002/designer.js',
|
||||
'https://unpkg.com/antd@4.24.13/dist/antd.css',
|
||||
],
|
||||
designerResources: [
|
||||
'https://unpkg.com/@music163/antd@{{version}}/dist/designer.js',
|
||||
'https://unpkg.com/antd@4.24.13/dist/antd.css',
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const helperCode = `
|
||||
export function registerComponentPrototype(proto) {
|
||||
if (!proto) return;
|
||||
if (!window.localTangoComponentPrototypes) {
|
||||
window.localTangoComponentPrototypes = {};
|
||||
}
|
||||
if (proto.name) {
|
||||
window.localTangoComponentPrototypes[proto.name] = proto;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const routesCode = `
|
||||
import Index from "./pages/list";
|
||||
import Detail from "./pages/detail";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
exact: true,
|
||||
component: Index
|
||||
},
|
||||
{
|
||||
path: '/detail',
|
||||
exact: true,
|
||||
component: Detail
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
`;
|
||||
|
||||
const storeIndexCode = `
|
||||
export { default as app } from './app';
|
||||
export { default as counter } from './counter';
|
||||
`;
|
||||
|
||||
const entryCode = `
|
||||
import { runApp } from '@music163/tango-boot';
|
||||
import routes from './routes';
|
||||
import './services';
|
||||
import './stores';
|
||||
import './index.less';
|
||||
|
||||
runApp({
|
||||
boot: {
|
||||
mountElement: document.querySelector('#root'),
|
||||
qiankun: false,
|
||||
},
|
||||
|
||||
router: {
|
||||
type: 'browser',
|
||||
config: routes,
|
||||
},
|
||||
});
|
||||
`;
|
||||
|
||||
const storeCounter = `
|
||||
import { defineStore } from '@music163/tango-boot';
|
||||
|
||||
const counter = defineStore({
|
||||
// state
|
||||
num: 0,
|
||||
|
||||
// action
|
||||
increment: () => counter.num++,
|
||||
|
||||
decrement: () => {
|
||||
counter.num--;
|
||||
},
|
||||
}, 'counter');
|
||||
|
||||
export default counter;
|
||||
`;
|
||||
|
||||
const viewHomeCode = `
|
||||
import React from "react";
|
||||
import { definePage } from "@music163/tango-boot";
|
||||
import {
|
||||
Page,
|
||||
Section,
|
||||
Box,
|
||||
Button,
|
||||
Input,
|
||||
FormilyForm,
|
||||
FormilyFormItem,
|
||||
Table,
|
||||
} from "@music163/antd";
|
||||
import { Space } from "@music163/antd";
|
||||
import { LocalButton } from "../components";
|
||||
import { OutButton } from "../components";
|
||||
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Page title={tango.stores.app.title} subTitle={<><Button>hello</Button></>}>
|
||||
<Section tid="section0">
|
||||
<Box></Box>
|
||||
</Section>
|
||||
<Section tid="section1" title="Section Title">
|
||||
your input: <Input tid="input1" defaultValue="hello" />
|
||||
copy input: <Input value={tango.page.input1?.value} />
|
||||
<Table
|
||||
columns={[
|
||||
{ title: "姓名", dataIndex: "name", key: "name" },
|
||||
{ title: "年龄", dataIndex: "age", key: "age" },
|
||||
{ title: "住址", dataIndex: "address", key: "address" },
|
||||
]}
|
||||
tid="table1"
|
||||
/>
|
||||
</Section>
|
||||
<Section tid="section2">
|
||||
<Space tid="space1">
|
||||
<LocalButton />
|
||||
<OutButton />
|
||||
<Button tid="button1">button</Button>
|
||||
</Space>
|
||||
</Section>
|
||||
<Section title="区块标题" tid="section3">
|
||||
<FormilyForm tid="formilyForm1">
|
||||
<FormilyFormItem name="input1" component="Input" label="表单项" />
|
||||
<FormilyFormItem name="select1" component="Select" label="表单项" />
|
||||
</FormilyForm>
|
||||
</Section>
|
||||
<Section title="原生 DOM" tid="section4">
|
||||
<h1 style={{ ...{ color: "red" }, fontSize: 64 }}>
|
||||
hello world
|
||||
</h1>
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid #ccc",
|
||||
borderRadius: "5px",
|
||||
backgroundColor: "#f4f4f4",
|
||||
}}
|
||||
>
|
||||
<form onSubmit={tango.stores.app.submitForm} style={{
|
||||
padding: "10px",
|
||||
}}>
|
||||
<div>
|
||||
<label>Username:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
style={{ margin: "5px" }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label>Password:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
style={{ margin: "5px" }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label>Role:</label>
|
||||
<select id="role" name="role" style={{ margin: "5px" }}>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="user">User</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
marginTop: '5px',
|
||||
padding: "3px 10px",
|
||||
backgroundColor: "#007bff",
|
||||
color: "#fff",
|
||||
border: "none",
|
||||
borderRadius: "5px",
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Section>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default definePage(App);
|
||||
`;
|
||||
|
||||
export const emptyPageCode = `
|
||||
import React from "react";
|
||||
import { definePage } from "@music163/tango-boot";
|
||||
import {
|
||||
Page,
|
||||
Section,
|
||||
} from "@music163/antd";
|
||||
|
||||
function App() {
|
||||
return (<Page title="Detail Page">
|
||||
<Section></Section>
|
||||
</Page>)
|
||||
}
|
||||
|
||||
export default definePage(App);
|
||||
`;
|
||||
|
||||
const componentsButtonCode = `
|
||||
import React from 'react';
|
||||
import { registerComponentPrototype } from '../utils';
|
||||
|
||||
export default function MyButton(props) {
|
||||
return <button {...props}>my button</button>
|
||||
}
|
||||
|
||||
registerComponentPrototype({
|
||||
name: 'LocalButton',
|
||||
title: 'Local Button',
|
||||
exportType: 'namedExport',
|
||||
package: '/src/components',
|
||||
props: [
|
||||
{ name: 'background', title: '背景色', setter: 'colorSetter' },
|
||||
],
|
||||
});
|
||||
`;
|
||||
|
||||
const outButtonCode = `
|
||||
import React from 'react';
|
||||
|
||||
export default function OutButton(props) {
|
||||
return <button {...props}>Out button (from other file)</button>
|
||||
}
|
||||
`;
|
||||
|
||||
const componentsInputCode = `
|
||||
import React from 'react';
|
||||
import { registerComponentPrototype } from '../utils';
|
||||
|
||||
export default function MyInput(props) {
|
||||
return <input {...props} />;
|
||||
}
|
||||
|
||||
registerComponentPrototype({
|
||||
name: 'LocalInput',
|
||||
title: 'Local Input',
|
||||
exportType: 'namedExport',
|
||||
package: '/src/components',
|
||||
props: [
|
||||
{ name: 'color', title: '文本色', setter: 'colorSetter' },
|
||||
],
|
||||
});
|
||||
`;
|
||||
|
||||
const componentsEntryCode = `
|
||||
export { default as LocalButton } from './button';
|
||||
export { default as LocalInput } from './input';
|
||||
export { default as OutButton } from './out-button';
|
||||
`;
|
||||
|
||||
const storeApp = `
|
||||
import { defineStore } from '@music163/tango-boot';
|
||||
|
||||
export default defineStore({
|
||||
title: 'Page Title',
|
||||
|
||||
array: [1, 2, 3],
|
||||
|
||||
test: function() {
|
||||
console.log('test');
|
||||
},
|
||||
|
||||
submitForm: function(event) {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(event.target);
|
||||
const username = formData.get("username");
|
||||
const password = formData.get("password");
|
||||
const role = formData.get("role");
|
||||
console.log("Submitted Data:", { username, password, role });
|
||||
}
|
||||
}, 'app');
|
||||
`;
|
||||
|
||||
const serviceCode = `
|
||||
import { defineServices } from '@music163/tango-boot';
|
||||
import './sub';
|
||||
|
||||
export default defineServices({
|
||||
longLongLongLongLongLongLongLongGet: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/cc974ffbaa7a85c77f30e4ce67deb67f/api/getUserProfile',
|
||||
formatter: res => res.data,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
get: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/cc974ffbaa7a85c77f30e4ce67deb67f/api/getUserProfile',
|
||||
formatter: res => res.data,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
list: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/anchor-list-normal',
|
||||
},
|
||||
add: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
|
||||
method: 'post',
|
||||
description: '新增用户'
|
||||
},
|
||||
update: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
|
||||
method: 'post',
|
||||
description: '更新用户'
|
||||
},
|
||||
delete: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users?id=1',
|
||||
description: '删除用户'
|
||||
},
|
||||
});
|
||||
`;
|
||||
|
||||
const subServiceCode = `
|
||||
import { defineServices } from '@music163/tango-boot';
|
||||
|
||||
export default defineServices({
|
||||
list: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/anchor-list-normal',
|
||||
},
|
||||
}, {
|
||||
namespace: 'sub',
|
||||
});
|
||||
`;
|
||||
|
||||
const lessCode = `
|
||||
body {
|
||||
font-size: 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
const cssCode = `
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
color: red;
|
||||
}
|
||||
`;
|
||||
|
||||
export const sampleFiles = [
|
||||
{ filename: '/package.json', code: JSON.stringify(packageJson) },
|
||||
{ filename: '/tango.config.json', code: JSON.stringify(tangoConfigJson) },
|
||||
{ filename: '/README.md', code: '# readme' },
|
||||
{ filename: '/src/index.less', code: lessCode },
|
||||
{ filename: '/src/style.css', code: cssCode },
|
||||
{ filename: '/src/index.js', code: entryCode },
|
||||
{ filename: '/src/pages/list.js', code: viewHomeCode },
|
||||
{ filename: '/src/pages/detail.js', code: emptyPageCode },
|
||||
{ filename: '/src/components/button.js', code: componentsButtonCode },
|
||||
{ filename: '/src/components/out-button.js', code: outButtonCode },
|
||||
{ filename: '/src/components/input.js', code: componentsInputCode },
|
||||
{ filename: '/src/components/index.js', code: componentsEntryCode },
|
||||
{ filename: '/src/routes.js', code: routesCode },
|
||||
{ filename: '/src/stores/index.js', code: storeIndexCode },
|
||||
{ filename: '/src/stores/app.js', code: storeApp },
|
||||
{ filename: '/src/stores/counter.js', code: storeCounter },
|
||||
{ filename: '/src/services/index.js', code: serviceCode },
|
||||
{ filename: '/src/services/sub.js', code: subServiceCode },
|
||||
{ filename: '/src/utils/index.js', code: helperCode },
|
||||
];
|
||||
|
||||
export const genDefaultPage = (index: number) => ({
|
||||
name: 'new-page',
|
||||
code: `
|
||||
import React from 'react';
|
||||
import tango, { definePage } from '@music163/tango-boot';
|
||||
import { Page, Section } from '@music163/antd';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Page title="空白模板${index}">
|
||||
<Section></Section>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export default definePage(App);
|
||||
`,
|
||||
});
|
||||
@@ -0,0 +1,293 @@
|
||||
import * as basePrototypes from '@music163/antd/prototypes';
|
||||
import type { IComponentPrototype, Dict } from '@music163/tango-helpers';
|
||||
|
||||
const sampleBlockCode = `
|
||||
<Section>
|
||||
<Result
|
||||
status="success"
|
||||
title="Successfully Purchased Cloud Server ECS!"
|
||||
subTitle="Order number: 2017182818828182881 Cloud server configuration takes 1-5 minutes, please wait."
|
||||
extra={[
|
||||
<Button type="primary" key="console">
|
||||
Go Console
|
||||
</Button>,
|
||||
<Button key="buy">Buy Again</Button>,
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
`;
|
||||
|
||||
const SnippetSuccessResult: IComponentPrototype = {
|
||||
name: 'SnippetSuccessResult',
|
||||
title: '成功结果',
|
||||
icon: 'icon-tupian',
|
||||
type: 'snippet',
|
||||
package: '@music163/antd',
|
||||
initChildren: sampleBlockCode,
|
||||
relatedImports: ['Section', 'Result', 'Button'],
|
||||
};
|
||||
|
||||
const Snippet2ColumnLayout: IComponentPrototype = {
|
||||
name: 'Snippet2ColumnLayout',
|
||||
title: '两列布局',
|
||||
icon: 'icon-columns',
|
||||
type: 'snippet',
|
||||
package: '@music163/antd',
|
||||
initChildren: `
|
||||
<Columns columns={12}>
|
||||
<Column colSpan={6}></Column>
|
||||
<Column colSpan={6}></Column>
|
||||
</Columns>
|
||||
`,
|
||||
relatedImports: ['Columns', 'Column'],
|
||||
};
|
||||
|
||||
const Snippet3ColumnLayout: IComponentPrototype = {
|
||||
name: 'Snippet3ColumnLayout',
|
||||
title: '三列布局',
|
||||
icon: 'icon-column-3',
|
||||
type: 'snippet',
|
||||
package: '@music163/antd',
|
||||
initChildren: `
|
||||
<Columns columns={12}>
|
||||
<Column colSpan={4}>
|
||||
</Column>
|
||||
<Column colSpan={4}>
|
||||
</Column>
|
||||
<Column colSpan={4}>
|
||||
</Column>
|
||||
</Columns>
|
||||
`,
|
||||
relatedImports: ['Columns', 'Column'],
|
||||
};
|
||||
|
||||
const SnippetButtonGroup: IComponentPrototype = {
|
||||
name: 'SnippetButtonGroup',
|
||||
title: '按钮组',
|
||||
icon: 'icon-anniuzu',
|
||||
type: 'snippet',
|
||||
package: '@music163/antd',
|
||||
initChildren: `
|
||||
<Space>
|
||||
<Button type="primary">按钮1</Button>
|
||||
<Button>按钮2</Button>
|
||||
</Space>
|
||||
`,
|
||||
relatedImports: ['Space', 'Button'],
|
||||
};
|
||||
|
||||
// hack some prototypes
|
||||
basePrototypes['Section'].siblingNames = [
|
||||
'SnippetButtonGroup',
|
||||
'Snippet2ColumnLayout',
|
||||
'Snippet3ColumnLayout',
|
||||
'SnippetSuccessResult',
|
||||
];
|
||||
|
||||
export const nativeDomPrototypes = () => {
|
||||
const doms = [
|
||||
'div',
|
||||
'span',
|
||||
'h1',
|
||||
'h2',
|
||||
'p',
|
||||
'a',
|
||||
'img',
|
||||
'ul',
|
||||
'ol',
|
||||
'li',
|
||||
'input',
|
||||
'button',
|
||||
'form',
|
||||
'table',
|
||||
'tr',
|
||||
'td',
|
||||
'header',
|
||||
'footer',
|
||||
'nav',
|
||||
'section',
|
||||
'article',
|
||||
'aside',
|
||||
'main',
|
||||
'video',
|
||||
'audio',
|
||||
'label',
|
||||
'select',
|
||||
'option',
|
||||
'textarea',
|
||||
'iframe',
|
||||
];
|
||||
const componentPrototypes: Dict<IComponentPrototype> = doms.reduce(
|
||||
(acc: Dict<IComponentPrototype>, tag) => {
|
||||
acc[tag] = {
|
||||
name: tag,
|
||||
title: tag,
|
||||
hasChildren: true,
|
||||
package: '',
|
||||
type: 'element',
|
||||
props: [
|
||||
{
|
||||
name: 'style',
|
||||
title: '样式',
|
||||
group: 'style',
|
||||
setter: 'expressionSetter',
|
||||
setterProps: {
|
||||
expressionType: 'cssObject',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
title: '类名',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'id',
|
||||
title: 'ID',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'onClick',
|
||||
title: '点击事件',
|
||||
setter: 'actionSetter',
|
||||
group: 'event',
|
||||
},
|
||||
],
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
return componentPrototypes;
|
||||
};
|
||||
|
||||
// iconfont: https://www.iconfont.cn/manage/index?manage_type=myprojects&projectId=2891794
|
||||
const prototypes: Dict<IComponentPrototype> = {
|
||||
...nativeDomPrototypes(),
|
||||
...(basePrototypes as any),
|
||||
SnippetSuccessResult,
|
||||
Snippet2ColumnLayout,
|
||||
Snippet3ColumnLayout,
|
||||
SnippetButtonGroup,
|
||||
Section: {
|
||||
...basePrototypes['Section'],
|
||||
props: [
|
||||
// ...(basePrototypes['Section'].props as any),
|
||||
{
|
||||
name: 'title',
|
||||
title: '标题',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'extra',
|
||||
title: '额外内容',
|
||||
setter: 'renderSetter',
|
||||
options: [
|
||||
{
|
||||
label: '按钮组',
|
||||
value: 'ButtonGroup',
|
||||
renderBody:
|
||||
'<ButtonGroup><Button>button1</Button><Button>button2</Button></ButtonGroup>',
|
||||
relatedImports: ['ButtonGroup', 'Button'],
|
||||
},
|
||||
],
|
||||
template: '(v) => {\n return {{content}};\n}',
|
||||
},
|
||||
],
|
||||
},
|
||||
Box: {
|
||||
name: 'Box',
|
||||
title: '盒子',
|
||||
icon: 'icon-mianban',
|
||||
type: 'container',
|
||||
package: '@music163/antd',
|
||||
hasChildren: true,
|
||||
siblingNames: ['Box'],
|
||||
props: [
|
||||
{
|
||||
name: 'aaa',
|
||||
title: 'aaa',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'bbb',
|
||||
title: 'bbb',
|
||||
setter: 'textSetter',
|
||||
deprecated: true,
|
||||
},
|
||||
{
|
||||
name: 'ccc',
|
||||
title: 'ccc',
|
||||
setter: 'textSetter',
|
||||
deprecated: true,
|
||||
},
|
||||
{
|
||||
name: 'd',
|
||||
title: 'd',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'onClick',
|
||||
title: '点击事件',
|
||||
setter: 'eventSetter',
|
||||
template: '(e) => {\n {{content}}\n}',
|
||||
tip: '回调参数说明:e 为事件对象',
|
||||
},
|
||||
{
|
||||
name: 'renderFoo',
|
||||
title: 'foo自定义渲染',
|
||||
setter: 'renderSetter',
|
||||
options: [
|
||||
{
|
||||
label: '占位空间',
|
||||
value: 'Box',
|
||||
render: '() => <Box>test</Box>',
|
||||
relatedImports: ['Box'],
|
||||
},
|
||||
{
|
||||
label: '占位文本',
|
||||
value: 'Text',
|
||||
render: '() => <Text>text</Text>',
|
||||
relatedImports: ['Text'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
Columns: {
|
||||
name: 'Columns',
|
||||
type: 'container',
|
||||
icon: 'icon-column-4',
|
||||
package: '@music163/antd',
|
||||
hasChildren: true,
|
||||
childrenNames: ['Column'],
|
||||
},
|
||||
Column: {
|
||||
name: 'Column',
|
||||
type: 'container',
|
||||
icon: 'icon-juxing',
|
||||
package: '@music163/antd',
|
||||
hasChildren: true,
|
||||
siblingNames: ['Column'],
|
||||
},
|
||||
Text: {
|
||||
name: 'Text',
|
||||
type: 'element',
|
||||
icon: 'icon-wenzi',
|
||||
package: '@music163/antd',
|
||||
initChildren: '文本内容',
|
||||
},
|
||||
Placeholder: {
|
||||
name: 'Placeholder',
|
||||
type: 'element',
|
||||
package: '@music163/antd',
|
||||
},
|
||||
ButtonGroup: {
|
||||
name: 'ButtonGroup',
|
||||
type: 'element',
|
||||
package: '@music163/antd',
|
||||
hasChildren: true,
|
||||
childrenNames: ['Button'],
|
||||
},
|
||||
};
|
||||
|
||||
export default prototypes;
|
||||
@@ -3,3 +3,4 @@
|
||||
|
||||
@primary-color: #2f54eb;
|
||||
@border-radius-base: 2px;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Outlet } from 'umi';
|
||||
import './index.less';
|
||||
|
||||
export default function Layout() {
|
||||
return <Outlet />;
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
const packageJson = {
|
||||
name: 'demo',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@music163/antd': '0.1.6',
|
||||
'@music163/tango-boot': '0.1.3',
|
||||
react: '17.0.2',
|
||||
'react-dom': '17.0.2',
|
||||
'prop-types': '15.7.2',
|
||||
tslib: '2.5.0',
|
||||
},
|
||||
};
|
||||
|
||||
const tangoConfigJson = {
|
||||
packages: {
|
||||
react: {
|
||||
version: '17.0.2',
|
||||
library: 'React',
|
||||
type: 'dependency',
|
||||
resources: ['https://unpkg.com/react@{{version}}/umd/react.development.js'],
|
||||
},
|
||||
'react-dom': {
|
||||
version: '17.0.2',
|
||||
library: 'ReactDOM',
|
||||
type: 'dependency',
|
||||
resources: ['https://unpkg.com/react-dom@{{version}}/umd/react-dom.development.js'],
|
||||
},
|
||||
'react-is': {
|
||||
version: '16.13.1',
|
||||
library: 'ReactIs',
|
||||
type: 'dependency',
|
||||
resources: ['https://unpkg.com/react-is@{{version}}/umd/react-is.production.min.js'],
|
||||
},
|
||||
'styled-components': {
|
||||
version: '5.3.5',
|
||||
library: 'styled',
|
||||
type: 'dependency',
|
||||
resources: ['https://unpkg.com/styled-components@{{version}}/dist/styled-components.min.js'],
|
||||
},
|
||||
moment: {
|
||||
version: '2.29.4',
|
||||
library: 'moment',
|
||||
type: 'dependency',
|
||||
resources: ['https://unpkg.com/moment@{{version}}/moment.js'],
|
||||
},
|
||||
'@music163/tango-boot': {
|
||||
version: '0.1.3',
|
||||
library: 'TangoBoot',
|
||||
type: 'baseDependency',
|
||||
resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'],
|
||||
// resources: ['http://localhost:9001/boot.js'],
|
||||
description: '云音乐低代码运行时框架',
|
||||
},
|
||||
'@music163/antd': {
|
||||
version: '0.1.7',
|
||||
library: 'TangoAntd',
|
||||
type: 'baseDependency',
|
||||
resources: [
|
||||
'https://unpkg.com/@music163/antd@{{version}}/dist/index.js',
|
||||
'https://unpkg.com/antd@4.24.13/dist/antd.css',
|
||||
],
|
||||
description: '云音乐低代码中后台应用基础物料',
|
||||
designerResources: [
|
||||
'https://unpkg.com/@music163/antd@{{version}}/dist/designer.js',
|
||||
// 'http://localhost:9002/designer.js',
|
||||
'https://unpkg.com/antd@4.24.13/dist/antd.css',
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const appJson: any = {
|
||||
pages: [
|
||||
{
|
||||
path: '/',
|
||||
name: '首页',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const routesCode = `
|
||||
import Index from "./pages/list";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
exact: true,
|
||||
component: Index
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
`;
|
||||
|
||||
const storeIndexCode = `
|
||||
export { default as app } from './app';
|
||||
export { default as counter } from './counter';
|
||||
`;
|
||||
|
||||
const entryCode = `
|
||||
import { runApp } from '@music163/tango-boot';
|
||||
import routes from './routes';
|
||||
import './services';
|
||||
import './stores';
|
||||
import './index.less';
|
||||
|
||||
runApp({
|
||||
boot: {
|
||||
mountElement: document.querySelector('#root'),
|
||||
qiankun: false,
|
||||
},
|
||||
|
||||
router: {
|
||||
type: 'browser',
|
||||
config: routes,
|
||||
},
|
||||
});
|
||||
`;
|
||||
|
||||
const storeCounter = `
|
||||
import { defineStore } from '@music163/tango-boot';
|
||||
|
||||
const counter = defineStore({
|
||||
// state
|
||||
num: 0,
|
||||
|
||||
// action
|
||||
increment: () => counter.num++,
|
||||
|
||||
decrement: () => {
|
||||
counter.num--;
|
||||
},
|
||||
}, 'counter');
|
||||
|
||||
export default counter;
|
||||
`;
|
||||
|
||||
const viewHomeCode = `
|
||||
import React from "react";
|
||||
import { definePage } from "@music163/tango-boot";
|
||||
import {
|
||||
Page,
|
||||
Section,
|
||||
Button,
|
||||
Input,
|
||||
FormilyForm,
|
||||
} from "@music163/antd";
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Page title={tango.stores.app.title}>
|
||||
<Section>
|
||||
</Section>
|
||||
<Section>
|
||||
<Button>button</Button>
|
||||
<Input />
|
||||
</Section>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default definePage(App);
|
||||
`;
|
||||
|
||||
const storeApp = `
|
||||
import { defineStore } from '@music163/tango-boot';
|
||||
|
||||
export default defineStore({
|
||||
|
||||
title: 'Page Title',
|
||||
|
||||
array: [1, 2, 3],
|
||||
}, 'app');
|
||||
`;
|
||||
|
||||
const serviceCode = `
|
||||
import { defineServices } from '@music163/tango-boot';
|
||||
|
||||
export default defineServices({
|
||||
get: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/cc974ffbaa7a85c77f30e4ce67deb67f/api/getUserProfile',
|
||||
formatter: res => res.data,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
list: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/anchor-list-normal',
|
||||
},
|
||||
add: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
|
||||
method: 'post',
|
||||
},
|
||||
update: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
|
||||
method: 'post',
|
||||
},
|
||||
delete: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users?id=1',
|
||||
},
|
||||
});
|
||||
`;
|
||||
|
||||
const lessCode = `
|
||||
body {
|
||||
font-size: 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
const cssCode = `
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
color: red;
|
||||
}
|
||||
`;
|
||||
|
||||
export const sampleFiles = [
|
||||
{ filename: '/package.json', code: JSON.stringify(packageJson) },
|
||||
{ filename: '/appJson.json', code: JSON.stringify(appJson) },
|
||||
{ filename: '/tango.config.json', code: JSON.stringify(tangoConfigJson) },
|
||||
{ filename: '/README.md', code: '# readme' },
|
||||
{ filename: '/src/index.less', code: lessCode },
|
||||
{ filename: '/src/style.css', code: cssCode },
|
||||
{ filename: '/src/index.js', code: entryCode },
|
||||
{ filename: '/src/pages/list.js', code: viewHomeCode },
|
||||
{ filename: '/src/routes.js', code: routesCode },
|
||||
{ filename: '/src/stores/index.js', code: storeIndexCode },
|
||||
{ filename: '/src/stores/app.js', code: storeApp },
|
||||
{ filename: '/src/stores/counter.js', code: storeCounter },
|
||||
{ filename: '/src/services/index.js', code: serviceCode },
|
||||
{ filename: '/src/utils/index.js', code: `export function foo() {}` },
|
||||
];
|
||||
|
||||
export const genDefaultPage = (index: number) => ({
|
||||
name: 'new-page',
|
||||
code: `
|
||||
import React from 'react';
|
||||
import tango, { definePage } from '@music163/tango-boot';
|
||||
import { Page, Section } from '@music163/antd';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Page title="空白模板${index}">
|
||||
<Section></Section>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export default definePage(App);
|
||||
`,
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
const DocsPage = () => {
|
||||
return (
|
||||
<div>
|
||||
<p>This is umi docs.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocsPage;
|
||||
@@ -1,21 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Tango Playground</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="https://unpkg.com/react@16.14.0/umd/react.development.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@16.14.0/umd/react-dom.development.js"></script>
|
||||
<script src="https://unpkg.com/react-is@16.13.1/umd/react-is.production.min.js"></script>
|
||||
<script src="https://unpkg.com/moment/min/moment-with-locales.js"></script>
|
||||
<script src="https://unpkg.com/styled-components@5.3.11/dist/styled-components.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone@7.15.6/babel.js"></script>
|
||||
<script src="https://unpkg.com/prettier@2.6.0/standalone.js"></script>
|
||||
<script src="https://unpkg.com/prettier@2.6.0/parser-babel.js"></script>
|
||||
<script src="https://unpkg.com/antd@4.24.13/dist/antd-with-locales.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { Button, Space } from 'antd';
|
||||
import { Button, Form, Input, Modal, Space } from 'antd';
|
||||
import {
|
||||
Designer,
|
||||
DesignerPanel,
|
||||
@@ -9,92 +8,209 @@ import {
|
||||
Toolbar,
|
||||
WorkspacePanel,
|
||||
WorkspaceView,
|
||||
ComponentsPanel,
|
||||
CodeEditor,
|
||||
Sandbox,
|
||||
DndQuery,
|
||||
themeLight,
|
||||
} from '@music163/tango-designer';
|
||||
import { createEngine, Workspace } from '@music163/tango-core';
|
||||
import { Logo, ProjectDetail } from './share';
|
||||
import { sampleFiles } from '../mock/project';
|
||||
import './index.less';
|
||||
import prototypes from '../helpers/prototypes';
|
||||
import { Logo, ProjectDetail, bootHelperVariables, emptyPageCode, sampleFiles } from '../helpers';
|
||||
import {
|
||||
ApiOutlined,
|
||||
AppstoreAddOutlined,
|
||||
BuildOutlined,
|
||||
FunctionOutlined,
|
||||
PlusOutlined,
|
||||
createFromIconfontCN,
|
||||
} from '@ant-design/icons';
|
||||
import { Action, PackageOutlined } from '@music163/tango-ui';
|
||||
import { useState } from 'react';
|
||||
|
||||
const menuData = {
|
||||
common: [
|
||||
{
|
||||
title: '常用',
|
||||
items: [
|
||||
'Button',
|
||||
'Section',
|
||||
'Columns',
|
||||
'Column',
|
||||
'Box',
|
||||
'Text',
|
||||
'Space',
|
||||
'Typography',
|
||||
'Title',
|
||||
'Paragraph',
|
||||
'Table',
|
||||
'Each',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '输入',
|
||||
items: ['Input', 'InputNumber', 'Select'],
|
||||
},
|
||||
{
|
||||
title: 'Formily表单',
|
||||
items: ['FormilyForm', 'FormilyFormItem', 'FormilySubmit', 'FormilyReset'],
|
||||
},
|
||||
{
|
||||
title: '数据展示',
|
||||
items: ['Comment'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// 1. 实例化工作区
|
||||
const workspace = new Workspace({
|
||||
entry: '/src/index.js',
|
||||
files: sampleFiles,
|
||||
prototypes,
|
||||
});
|
||||
|
||||
// inject workspace to window for debug
|
||||
(window as any).__workspace__ = workspace;
|
||||
|
||||
// 2. 引擎初始化
|
||||
const engine = createEngine({
|
||||
workspace,
|
||||
menuData,
|
||||
defaultActiveView: 'design', // dual code design
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
window.__workspace__ = workspace;
|
||||
|
||||
// 3. 沙箱初始化
|
||||
const sandboxQuery = new DndQuery({
|
||||
context: 'iframe',
|
||||
});
|
||||
|
||||
// 4. 图标库初始化(物料面板和组件树使用了 iconfont 里的图标)
|
||||
createFromIconfontCN({
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_2891794_151xsllxqd7.js',
|
||||
});
|
||||
|
||||
/**
|
||||
* 3. 平台初始化,访问 https://local.netease.com:6006/
|
||||
* 5. 平台初始化,访问 https://local.netease.com:6006/
|
||||
*/
|
||||
export default function App() {
|
||||
const [menuLoading, setMenuLoading] = useState(true);
|
||||
const [menuData, setMenuData] = useState(false);
|
||||
const [showNewPageModal, setShowNewPageModal] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
return (
|
||||
<Designer engine={engine} sandboxQuery={sandboxQuery}>
|
||||
<Designer
|
||||
theme={themeLight}
|
||||
engine={engine}
|
||||
config={{
|
||||
customActionVariables: bootHelperVariables,
|
||||
customExpressionVariables: bootHelperVariables,
|
||||
}}
|
||||
sandboxQuery={sandboxQuery}
|
||||
>
|
||||
<DesignerPanel
|
||||
logo={<Logo />}
|
||||
description={<ProjectDetail />}
|
||||
actions={
|
||||
<Box px="l">
|
||||
<Toolbar>
|
||||
<Toolbar.Item key="routeSwitch" placement="left" />
|
||||
<Toolbar.Item key="routeSwitch" placement="left" activeViews={['design']} />
|
||||
<Toolbar.Item key="addPage" placement="left" activeViews={['design']}>
|
||||
<Action
|
||||
tooltip="添加页面"
|
||||
shape="outline"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setShowNewPageModal(true)}
|
||||
/>
|
||||
</Toolbar.Item>
|
||||
<Toolbar.Item key="history" placement="left" activeViews={['design']} />
|
||||
<Toolbar.Item key="preview" placement="left" activeViews={['design']} />
|
||||
<Toolbar.Item key="togglePanel" placement="right" activeViews={['design']} />
|
||||
<Toolbar.Item key="modeSwitch" placement="right" />
|
||||
<Toolbar.Item key="togglePanel" placement="right" />
|
||||
<Toolbar.Separator />
|
||||
<Toolbar.Item key="extra" placement="right">
|
||||
<Toolbar.Item placement="right">
|
||||
<Space>
|
||||
<Button type="primary">发布</Button>
|
||||
</Space>
|
||||
</Toolbar.Item>
|
||||
</Toolbar>
|
||||
<Modal
|
||||
title="添加新页面"
|
||||
open={showNewPageModal}
|
||||
onCancel={() => setShowNewPageModal(false)}
|
||||
footer={null}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={(values) => {
|
||||
workspace.addViewFile(values.name, emptyPageCode);
|
||||
setShowNewPageModal(false);
|
||||
}}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item label="文件名" name="name" required rules={[{ required: true }]}>
|
||||
<Input placeholder="请输入文件名" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
提交
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Sidebar>
|
||||
<Sidebar.Item key="outline" />
|
||||
<Sidebar.Item key="components">
|
||||
<ComponentsPanel menuData={menuData as any} loading={menuLoading} />
|
||||
</Sidebar.Item>
|
||||
<Sidebar.Item key="variables" isFloat width={800} />
|
||||
<Sidebar.Item key="dataSource" isFloat width={800} />
|
||||
<Sidebar.Item key="dependency" isFloat width={800} />
|
||||
<Sidebar.Item
|
||||
key="components"
|
||||
label="组件"
|
||||
icon={<AppstoreAddOutlined />}
|
||||
widgetProps={{
|
||||
menuData,
|
||||
}}
|
||||
/>
|
||||
<Sidebar.Item key="outline" label="结构" icon={<BuildOutlined />} />
|
||||
<Sidebar.Item
|
||||
key="variables"
|
||||
label="变量"
|
||||
icon={<FunctionOutlined />}
|
||||
isFloat
|
||||
width={800}
|
||||
/>
|
||||
<Sidebar.Item key="dataSource" label="接口" icon={<ApiOutlined />} isFloat width={800} />
|
||||
<Sidebar.Item
|
||||
key="dependency"
|
||||
label="依赖"
|
||||
icon={<PackageOutlined />}
|
||||
isFloat
|
||||
width={800}
|
||||
/>
|
||||
</Sidebar>
|
||||
<WorkspacePanel>
|
||||
<WorkspaceView mode="code">
|
||||
<CodeEditor />
|
||||
</WorkspaceView>
|
||||
<WorkspaceView mode="design">
|
||||
<Sandbox
|
||||
onMessage={(e) => {
|
||||
if (e.type === 'done') {
|
||||
const sandboxWindow: any = sandboxQuery.window;
|
||||
if (sandboxWindow.TangoAntd) {
|
||||
if (sandboxWindow.TangoAntd.menuData) {
|
||||
setMenuData(sandboxWindow.TangoAntd.menuData);
|
||||
}
|
||||
if (sandboxWindow.TangoAntd.prototypes) {
|
||||
workspace.setComponentPrototypes(sandboxWindow.TangoAntd.prototypes);
|
||||
}
|
||||
// if (sandboxWindow.TangoAntd) {
|
||||
// if (sandboxWindow.TangoAntd.menuData) {
|
||||
// setMenuData(sandboxWindow.TangoAntd.menuData);
|
||||
// }
|
||||
// if (sandboxWindow.TangoAntd.prototypes) {
|
||||
// workspace.setComponentPrototypes(sandboxWindow.TangoAntd.prototypes);
|
||||
// }
|
||||
// }
|
||||
if (sandboxWindow.localTangoComponentPrototypes) {
|
||||
workspace.setComponentPrototypes(sandboxWindow.localTangoComponentPrototypes);
|
||||
}
|
||||
setMenuLoading(false);
|
||||
}
|
||||
}}
|
||||
navigatorExtra={<Button size="small">hello world</Button>}
|
||||
/>
|
||||
</WorkspaceView>
|
||||
<WorkspaceView mode="code">
|
||||
<CodeEditor />
|
||||
</WorkspaceView>
|
||||
</WorkspacePanel>
|
||||
<SettingPanel />
|
||||
</DesignerPanel>
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { Button, Space } from 'antd';
|
||||
import {
|
||||
Designer,
|
||||
DesignerPanel,
|
||||
SettingPanel,
|
||||
Sidebar,
|
||||
Toolbar,
|
||||
WorkspacePanel,
|
||||
WorkspaceView,
|
||||
CodeEditor,
|
||||
Sandbox,
|
||||
DndQuery,
|
||||
themeLight,
|
||||
} from '@music163/tango-designer';
|
||||
import { createEngine, Workspace } from '@music163/tango-core';
|
||||
import { Logo, ProjectDetail, bootHelperVariables } from '@/helpers';
|
||||
import {
|
||||
AppstoreAddOutlined,
|
||||
BuildOutlined,
|
||||
ClusterOutlined,
|
||||
createFromIconfontCN,
|
||||
} from '@ant-design/icons';
|
||||
import { mailFiles } from '@/helpers/mail-files';
|
||||
|
||||
// 1. 实例化工作区
|
||||
const workspace = new Workspace({
|
||||
entry: '/src/index.js',
|
||||
files: mailFiles,
|
||||
});
|
||||
|
||||
// 2. 引擎初始化
|
||||
const engine = createEngine({
|
||||
workspace,
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
window.__mailWorkspace__ = workspace;
|
||||
|
||||
// 3. 沙箱初始化
|
||||
const sandboxQuery = new DndQuery({
|
||||
context: 'iframe',
|
||||
});
|
||||
|
||||
// 4. 图标库初始化(物料面板和组件树使用了 iconfont 里的图标)
|
||||
createFromIconfontCN({
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_2891794_151xsllxqd7.js',
|
||||
});
|
||||
|
||||
/**
|
||||
* 5. 平台初始化,访问 https://local.netease.com:6006/
|
||||
*/
|
||||
export default function App() {
|
||||
const [menuLoading, setMenuLoading] = useState(true);
|
||||
const [menuData, setMenuData] = useState(false);
|
||||
return (
|
||||
<Designer
|
||||
theme={themeLight}
|
||||
engine={engine}
|
||||
config={{
|
||||
customActionVariables: bootHelperVariables,
|
||||
customExpressionVariables: bootHelperVariables,
|
||||
}}
|
||||
sandboxQuery={sandboxQuery}
|
||||
>
|
||||
<DesignerPanel
|
||||
logo={<Logo />}
|
||||
description={<ProjectDetail />}
|
||||
actions={
|
||||
<Box px="l">
|
||||
<Toolbar>
|
||||
<Toolbar.Item key="history" placement="left" />
|
||||
<Toolbar.Item key="preview" placement="left" />
|
||||
<Toolbar.Item key="modeSwitch" placement="right" />
|
||||
<Toolbar.Item key="togglePanel" placement="right" />
|
||||
<Toolbar.Separator />
|
||||
<Toolbar.Item placement="right">
|
||||
<Space>
|
||||
<Button type="primary">发布</Button>
|
||||
</Space>
|
||||
</Toolbar.Item>
|
||||
</Toolbar>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Sidebar>
|
||||
<Sidebar.Item
|
||||
key="components"
|
||||
label="组件"
|
||||
icon={<AppstoreAddOutlined />}
|
||||
widgetProps={{
|
||||
menuData: menuData as any,
|
||||
loading: menuLoading,
|
||||
}}
|
||||
/>
|
||||
<Sidebar.Item key="outline" label="结构" icon={<BuildOutlined />} />
|
||||
<Sidebar.Item
|
||||
key="dependency"
|
||||
label="依赖"
|
||||
icon={<ClusterOutlined />}
|
||||
isFloat
|
||||
width={800}
|
||||
/>
|
||||
</Sidebar>
|
||||
<WorkspacePanel>
|
||||
<WorkspaceView mode="design">
|
||||
<Sandbox
|
||||
onMessage={(e) => {
|
||||
if (e.type === 'done') {
|
||||
const sandboxWindow: any = sandboxQuery.window;
|
||||
if (sandboxWindow.TangoMail) {
|
||||
if (sandboxWindow.TangoMail.menuData) {
|
||||
setMenuData(sandboxWindow.TangoMail.menuData);
|
||||
engine.designer.setMenuData(sandboxWindow.TangoMail.menuData);
|
||||
}
|
||||
if (sandboxWindow.TangoMail.prototypes) {
|
||||
workspace.setComponentPrototypes(sandboxWindow.TangoMail.prototypes);
|
||||
}
|
||||
}
|
||||
setMenuLoading(false);
|
||||
}
|
||||
}}
|
||||
navigatorExtra={<Button size="small">hello world</Button>}
|
||||
/>
|
||||
</WorkspaceView>
|
||||
<WorkspaceView mode="code">
|
||||
<CodeEditor />
|
||||
</WorkspaceView>
|
||||
</WorkspacePanel>
|
||||
<SettingPanel />
|
||||
</DesignerPanel>
|
||||
</Designer>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json"
|
||||
"extends": "./src/.umi/tsconfig.json"
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
import 'umi/typings';
|
||||
@@ -3,7 +3,7 @@ import { SystemProvider } from 'coral-system';
|
||||
import 'antd/dist/antd.css';
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
actions: { argTypesRegex: '^on.*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
|
||||
@@ -16,14 +16,16 @@
|
||||
"dependencies": {
|
||||
"@music163/tango-setting-form": "*",
|
||||
"@music163/tango-ui": "*",
|
||||
"mobx": "6.9.0",
|
||||
"mobx-react-lite": "4.0.2"
|
||||
"mobx": "6.12.3",
|
||||
"mobx-react-lite": "4.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@storybook/addon-actions": "^6.5.14",
|
||||
"@storybook/addon-essentials": "^6.5.14",
|
||||
"@storybook/addon-links": "^6.5.14",
|
||||
"@storybook/react": "^6.5.14",
|
||||
"antd": "^4.24.2",
|
||||
"babel-plugin-styled-components": "^2.0.7",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.2"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { CodeSandbox } from '@music163/tango-sandbox';
|
||||
import { JsonView } from '@music163/tango-ui';
|
||||
|
||||
export default {
|
||||
title: 'CodeSandbox',
|
||||
};
|
||||
|
||||
const entryFilename = '/index.js';
|
||||
|
||||
const packageJson = {
|
||||
name: 'demo',
|
||||
private: true,
|
||||
main: entryFilename,
|
||||
dependencies: {
|
||||
react: '^17.0.1',
|
||||
'react-dom': '^17.0.1',
|
||||
},
|
||||
};
|
||||
|
||||
const entryFile = `
|
||||
import React from 'react';
|
||||
import ReactDOM from "react-dom";
|
||||
import App from './src';
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
|
||||
ReactDOM.render(
|
||||
<App />,
|
||||
rootElement
|
||||
);
|
||||
`;
|
||||
|
||||
const srcAppFile = `
|
||||
import React from 'react';
|
||||
|
||||
export default function App() {
|
||||
return <div style={{ background: 'yellow', color: 'red' }}>hello world</div>;
|
||||
}
|
||||
`;
|
||||
|
||||
const files = {
|
||||
'/package.json': {
|
||||
code: JSON.stringify(packageJson),
|
||||
},
|
||||
[entryFilename]: {
|
||||
code: entryFile,
|
||||
},
|
||||
'/src/index.js': { code: srcAppFile },
|
||||
};
|
||||
|
||||
export function Basic() {
|
||||
return (
|
||||
<Box display="flex" height="100vh">
|
||||
<CodeSandbox
|
||||
template="create-react-app"
|
||||
bundlerURL="https://codesandbox.fn.netease.com"
|
||||
files={files}
|
||||
entry={entryFilename}
|
||||
style={{
|
||||
flex: 1,
|
||||
}}
|
||||
/>
|
||||
<Box width="30%">
|
||||
<JsonView src={files} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,190 +1,419 @@
|
||||
import React, { useState } from 'react';
|
||||
import { EditableVariableTree, FormModel, SettingForm } from '@music163/tango-setting-form';
|
||||
import { ComponentPrototypeType, getValue } from '@music163/tango-helpers';
|
||||
import { createServices } from '@music163/request';
|
||||
import React from 'react';
|
||||
import { FormModel, SettingForm, register } from '@music163/tango-setting-form';
|
||||
import { IComponentPrototype } from '@music163/tango-helpers';
|
||||
import { BUILT_IN_SETTERS } from '@music163/tango-designer/src/setters';
|
||||
import { Box } from 'coral-system';
|
||||
import { JsonView } from '@music163/tango-ui';
|
||||
import { toJS } from 'mobx';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Card } from 'antd';
|
||||
import { createFromIconfontCN } from '@ant-design/icons';
|
||||
|
||||
const BLACK_LIST = ['codeSetter', 'eventSetter', 'modelSetter', 'routerSetter'];
|
||||
|
||||
BUILT_IN_SETTERS.filter((setter) => !BLACK_LIST.includes(setter.name)).forEach(register);
|
||||
|
||||
createFromIconfontCN({
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_2891794_151xsllxqd7.js',
|
||||
});
|
||||
|
||||
export default {
|
||||
title: 'SettingForm/ Setters',
|
||||
title: 'SettingForm',
|
||||
};
|
||||
|
||||
const modelVariables = [
|
||||
{
|
||||
title: 'stores',
|
||||
key: 'stores',
|
||||
selectable: false,
|
||||
children: [
|
||||
{
|
||||
title: 'app',
|
||||
key: 'stores.app',
|
||||
selectable: false,
|
||||
children: [
|
||||
{ title: 'app.title', key: 'stores.app.title', raw: '"hello"' },
|
||||
{ title: 'app.age', key: 'stores.app.age', raw: '20' },
|
||||
{ title: 'app.detail', key: 'stores.app.detail', raw: '{ foo: "foo" }' },
|
||||
{ title: 'app.list', key: 'stores.app.list', type: 'function', raw: '() => {}' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'user',
|
||||
key: 'stores.user',
|
||||
selectable: false,
|
||||
children: [
|
||||
{ title: 'name', key: 'stores.user.name' },
|
||||
{ title: 'age', key: 'stores.user.age' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'services',
|
||||
key: 'services',
|
||||
selectable: false,
|
||||
children: [
|
||||
{
|
||||
title: 'listUsers',
|
||||
key: 'services.listUsers',
|
||||
type: 'function',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
/**
|
||||
* 表单值预览
|
||||
*/
|
||||
const FormValuePreview = observer(({ model }: { model: FormModel }) => {
|
||||
const data = toJS(model.values);
|
||||
return <JsonView src={data} />;
|
||||
});
|
||||
|
||||
const prototype: ComponentPrototypeType = {
|
||||
name: 'Test',
|
||||
exportType: 'namedExport',
|
||||
title: '测试',
|
||||
icon: 'icon-test',
|
||||
interface SettingFormDemoProps {
|
||||
initValues?: object;
|
||||
prototype?: IComponentPrototype;
|
||||
}
|
||||
|
||||
function SettingFormDemo({ initValues, prototype }: SettingFormDemoProps) {
|
||||
const model = new FormModel(initValues, { onChange: console.log });
|
||||
return (
|
||||
<Box display="flex">
|
||||
<Box flex="0 0 320px" overflow="hidden">
|
||||
<SettingForm
|
||||
model={model}
|
||||
prototype={prototype}
|
||||
showIdentifier={{
|
||||
identifierKey: 'tid',
|
||||
getIdentifier: () => `time${Date.now()}`,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box position="relative">
|
||||
<Card title="表单状态预览" style={{ position: 'sticky', top: 0 }}>
|
||||
<FormValuePreview model={model} />
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const prototypeHasBasicProps: IComponentPrototype = {
|
||||
name: 'Sample',
|
||||
title: '演示组件',
|
||||
package: 'sample-pkg',
|
||||
type: 'element',
|
||||
category: 'basic',
|
||||
package: '@music163/antd',
|
||||
hasChildren: false,
|
||||
docs: 'https://4x-ant-design.antgroup.com/components/slider-cn',
|
||||
props: [
|
||||
{
|
||||
name: 'code',
|
||||
title: 'codeSetter',
|
||||
setter: 'codeSetter',
|
||||
},
|
||||
{
|
||||
name: 'style',
|
||||
title: 'codeSetter(cssObject)',
|
||||
setter: 'codeSetter',
|
||||
setterProps: {
|
||||
expressionType: 'cssObject',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'text',
|
||||
title: 'textSetter',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'router',
|
||||
title: 'routerSetter',
|
||||
setter: 'routerSetter',
|
||||
name: 'text2',
|
||||
title: 'textAreaSetter',
|
||||
setter: 'textAreaSetter',
|
||||
},
|
||||
{
|
||||
name: 'object',
|
||||
title: '对象属性',
|
||||
tip: '一个嵌套的对象',
|
||||
props: [
|
||||
{
|
||||
name: 'name',
|
||||
title: 'Name',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'age',
|
||||
title: 'Age',
|
||||
setter: 'numberSetter',
|
||||
},
|
||||
{
|
||||
name: 'address',
|
||||
title: 'Address',
|
||||
props: [
|
||||
{
|
||||
name: 'city',
|
||||
title: 'City',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
getVisible: (form) => {
|
||||
return form.getValue('text') !== 'test';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'expression',
|
||||
title: 'expressionSetter',
|
||||
setter: 'expressionSetter',
|
||||
},
|
||||
{
|
||||
name: 'model',
|
||||
title: 'modelSetter',
|
||||
setter: 'modelSetter',
|
||||
},
|
||||
{
|
||||
name: 'image',
|
||||
name: 'src',
|
||||
title: 'imageSetter',
|
||||
setter: 'imageSetter',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
title: 'numberSetter',
|
||||
setter: 'numberSetter',
|
||||
},
|
||||
{
|
||||
name: 'number2',
|
||||
title: 'sliderSetter',
|
||||
setter: 'sliderSetter',
|
||||
},
|
||||
{
|
||||
name: 'bool',
|
||||
title: 'boolSetter',
|
||||
setter: 'boolSetter',
|
||||
},
|
||||
{
|
||||
name: 'enum',
|
||||
title: 'enumSetter',
|
||||
setter: 'enumSetter',
|
||||
},
|
||||
{
|
||||
name: 'list',
|
||||
title: 'listSetter',
|
||||
setter: 'listSetter',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export function Basic() {
|
||||
return (
|
||||
<SettingFormDemo
|
||||
initValues={{
|
||||
bool: true,
|
||||
enum: {
|
||||
aaa: 'aaa',
|
||||
bbb: 'bbb',
|
||||
ccc: 'ccc',
|
||||
},
|
||||
list: [{ key: 1 }, { key: 2 }],
|
||||
}}
|
||||
prototype={prototypeHasBasicProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function DeprecatedProp() {
|
||||
return (
|
||||
<SettingFormDemo
|
||||
prototype={{
|
||||
name: 'Deprecated',
|
||||
package: 'sample-pkg',
|
||||
type: 'element',
|
||||
props: [
|
||||
{
|
||||
name: 'number',
|
||||
title: 'numberSetter',
|
||||
setter: 'numberSetter',
|
||||
tip: '这是一个文本属性',
|
||||
docs: 'https://4x-ant-design.antgroup.com/components/slider-cn',
|
||||
deprecated: '使用 text2 替代',
|
||||
},
|
||||
{
|
||||
name: 'number1',
|
||||
title: 'sliderSetter',
|
||||
setter: 'sliderSetter',
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Validate() {
|
||||
return (
|
||||
<SettingFormDemo
|
||||
prototype={{
|
||||
name: 'Validate',
|
||||
package: 'sample-pkg',
|
||||
type: 'element',
|
||||
props: [
|
||||
{
|
||||
name: 'number',
|
||||
title: 'numberSetter',
|
||||
setter: 'numberSetter',
|
||||
validate: (value) => {
|
||||
if (!value && value !== 0) {
|
||||
return '必填';
|
||||
}
|
||||
if (value < 0) {
|
||||
return '必须大于 0';
|
||||
}
|
||||
if (value > 10) {
|
||||
return '必须小于等于 10';
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ObjectSetter() {
|
||||
return (
|
||||
<SettingFormDemo
|
||||
prototype={{
|
||||
name: 'Object',
|
||||
package: 'sample-pkg',
|
||||
type: 'element',
|
||||
props: [
|
||||
{
|
||||
name: 'text',
|
||||
title: 'textSetter',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'object',
|
||||
title: 'objectSetter',
|
||||
setter: 'objectSetter',
|
||||
props: [
|
||||
{
|
||||
name: 'text',
|
||||
title: 'textSetter',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
title: 'numberSetter',
|
||||
setter: 'numberSetter',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function InitValues() {
|
||||
return (
|
||||
<SettingFormDemo
|
||||
initValues={{
|
||||
bool: true,
|
||||
bool1: '{{true}}',
|
||||
style: {
|
||||
background: 'red',
|
||||
},
|
||||
object: {
|
||||
text: 'text',
|
||||
number: 10,
|
||||
},
|
||||
object1: {
|
||||
text: 'text',
|
||||
number: '{{tango.stores.user?.age}}',
|
||||
},
|
||||
// 只会有一种情况传下来的是字符串,就是用户代码里存在 rest operator,这时候不需要额外处理,提示用户就使用代码模式
|
||||
object2: '{{{ text: "text22", number: 22, ...{ extra: "some" } }}}',
|
||||
list: [{ key: 'aaa' }, { key: 'bbb' }], // list object
|
||||
list1: "{{[{ key: 'aaa' }, { key: 'bbb' }]}}", // raw code
|
||||
}}
|
||||
prototype={{
|
||||
name: 'InitValues',
|
||||
package: 'sample-pkg',
|
||||
type: 'element',
|
||||
props: [
|
||||
{
|
||||
name: 'bool',
|
||||
title: 'value初始化',
|
||||
setter: 'boolSetter',
|
||||
},
|
||||
{
|
||||
name: 'bool1',
|
||||
title: 'value初始化',
|
||||
setter: 'boolSetter',
|
||||
},
|
||||
{
|
||||
name: 'bool2',
|
||||
title: '无初值',
|
||||
setter: 'boolSetter',
|
||||
},
|
||||
{
|
||||
name: 'style',
|
||||
title: 'codeSetter',
|
||||
setter: 'codeSetter',
|
||||
setterProps: {
|
||||
expressionType: 'cssObject',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'object',
|
||||
props: [
|
||||
{
|
||||
name: 'text',
|
||||
title: 'text',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
title: 'number',
|
||||
setter: 'numberSetter',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'object1',
|
||||
props: [
|
||||
{
|
||||
name: 'text',
|
||||
title: 'text',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
title: 'number',
|
||||
setter: 'numberSetter',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'object2',
|
||||
props: [
|
||||
{
|
||||
name: 'text',
|
||||
title: 'text',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
title: 'number',
|
||||
setter: 'numberSetter',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'list',
|
||||
title: 'listSetter',
|
||||
setter: 'listSetter',
|
||||
},
|
||||
{
|
||||
name: 'list1',
|
||||
title: 'listSetter',
|
||||
setter: 'listSetter',
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Lite() {
|
||||
return (
|
||||
<Box width={320} border="solid" borderColor="line2">
|
||||
<SettingForm
|
||||
showSearch={false}
|
||||
showGroups={false}
|
||||
showItemSubtitle={false}
|
||||
prototype={prototypeHasBasicProps}
|
||||
disableSwitchExpressionSetter
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function HideToggleCode() {
|
||||
const model = new FormModel({});
|
||||
return (
|
||||
<Box width={320} border="solid" borderColor="line2">
|
||||
<SettingForm model={model} prototype={prototypeHasBasicProps} disableSwitchExpressionSetter />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const prototypeHasExtraProps: IComponentPrototype = {
|
||||
name: 'ExtraProps',
|
||||
type: 'element',
|
||||
package: '@music163/antd',
|
||||
props: [
|
||||
{
|
||||
name: 'choice',
|
||||
title: 'choiceSetter',
|
||||
setter: 'choiceSetter',
|
||||
options: [
|
||||
{ label: '选项1', value: '1' },
|
||||
{ label: '选项2', value: '2' },
|
||||
{ label: '选项3', value: '3' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'picker',
|
||||
title: 'pickerSetter',
|
||||
setter: 'pickerSetter',
|
||||
options: [
|
||||
{ label: '选项1', value: '1' },
|
||||
{ label: '选项2', value: '2' },
|
||||
{ label: '选项3', value: '3' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'actionList',
|
||||
title: 'actionListSetter',
|
||||
setter: 'actionListSetter',
|
||||
},
|
||||
{
|
||||
name: 'list',
|
||||
title: 'listSetter',
|
||||
setter: 'listSetter',
|
||||
},
|
||||
{
|
||||
name: 'options',
|
||||
title: 'optionSetter',
|
||||
setter: 'optionSetter',
|
||||
},
|
||||
{
|
||||
name: 'columns',
|
||||
title: 'tableColumnsSetter',
|
||||
setter: 'tableColumnsSetter',
|
||||
},
|
||||
{
|
||||
name: 'css',
|
||||
title: 'cssSetter',
|
||||
setter: 'cssSetter',
|
||||
},
|
||||
{
|
||||
name: 'extra',
|
||||
title: 'jsxSetter',
|
||||
setter: 'jsxSetter',
|
||||
},
|
||||
{
|
||||
name: 'icon',
|
||||
title: 'iconSetter',
|
||||
setter: 'iconSetter',
|
||||
},
|
||||
{
|
||||
name: 'iconType',
|
||||
title: 'iconTypeSetter',
|
||||
setter: 'iconTypeSetter',
|
||||
},
|
||||
{
|
||||
name: 'onClick',
|
||||
title: 'eventSetter',
|
||||
tip: '当点击按钮时',
|
||||
setter: 'eventSetter',
|
||||
group: 'event',
|
||||
},
|
||||
{
|
||||
name: 'onClick2',
|
||||
title: 'actionSetter',
|
||||
tip: '当点击按钮时',
|
||||
setter: 'actionSetter',
|
||||
group: 'event',
|
||||
},
|
||||
{
|
||||
name: 'disabled',
|
||||
title: 'boolSetter',
|
||||
tip: 'disabled 是否禁用',
|
||||
defaultValue: false,
|
||||
setter: 'boolSetter',
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
title: 'choiceSetter',
|
||||
tip: 'size 按钮的尺寸',
|
||||
defaultValue: 'medium',
|
||||
setter: 'choiceSetter',
|
||||
setterProps: {
|
||||
options: [
|
||||
{ label: '小', value: 'small' },
|
||||
{ label: '中', value: 'medium' },
|
||||
{ label: '大', value: 'large' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'color',
|
||||
title: 'colorSetter',
|
||||
setter: 'colorSetter',
|
||||
},
|
||||
{
|
||||
name: 'columns',
|
||||
title: 'columnSetter',
|
||||
setter: 'columnSetter',
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
title: 'dateSetter',
|
||||
@@ -195,189 +424,137 @@ const prototype: ComponentPrototypeType = {
|
||||
title: 'dateRangeSetter',
|
||||
setter: 'dateRangeSetter',
|
||||
},
|
||||
{
|
||||
name: 'enum',
|
||||
title: 'enumSetter',
|
||||
setter: 'enumSetter',
|
||||
setterProps: {},
|
||||
},
|
||||
{
|
||||
name: 'time',
|
||||
title: 'timeSetter',
|
||||
setter: 'timeSetter',
|
||||
},
|
||||
{
|
||||
name: 'timeRange',
|
||||
name: 'time',
|
||||
title: 'timeRangeSetter',
|
||||
setter: 'timeRangeSetter',
|
||||
},
|
||||
{
|
||||
name: 'dataSource',
|
||||
title: 'jsonSetter 不推荐',
|
||||
name: 'enum',
|
||||
title: 'enumSetter',
|
||||
setter: 'enumSetter',
|
||||
},
|
||||
{
|
||||
name: 'event',
|
||||
title: 'eventSetter',
|
||||
setter: 'eventSetter',
|
||||
},
|
||||
{
|
||||
name: 'json',
|
||||
title: 'jsonSetter',
|
||||
setter: 'jsonSetter',
|
||||
},
|
||||
{
|
||||
name: 'listSetter',
|
||||
title: 'listSetter',
|
||||
setter: 'listSetter',
|
||||
name: 'jsx',
|
||||
title: 'jsxSetter',
|
||||
setter: 'jsxSetter',
|
||||
},
|
||||
{
|
||||
name: 'count',
|
||||
title: 'numberSetter',
|
||||
setter: 'numberSetter',
|
||||
name: 'render',
|
||||
title: 'renderPropsSetter',
|
||||
setter: 'renderPropsSetter',
|
||||
},
|
||||
{
|
||||
name: 'options',
|
||||
title: 'optionSetter',
|
||||
setter: 'optionSetter',
|
||||
name: 'cell',
|
||||
title: 'tableCellSetter',
|
||||
setter: 'tableCellSetter',
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
title: 'pickerSetter',
|
||||
defaultValue: 'solid',
|
||||
setter: 'pickerSetter',
|
||||
setterProps: {
|
||||
options: [
|
||||
{ label: '文本型', value: 'text' },
|
||||
{ label: '实体型', value: 'solid' },
|
||||
{ label: '幽灵', value: 'ghost' },
|
||||
],
|
||||
},
|
||||
name: 'expandable',
|
||||
title: 'tableExpandableSetter',
|
||||
setter: 'tableExpandableSetter',
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
title: 'textSetter',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'invalid',
|
||||
title: 'invalidSetter',
|
||||
setter: 'invalidSetter',
|
||||
name: 'router',
|
||||
title: 'routerSetter',
|
||||
setter: 'routerSetter',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const deerService = createServices(
|
||||
{
|
||||
listMy: {
|
||||
url: '/my/upload/list',
|
||||
},
|
||||
listFav: {
|
||||
url: '/my/star/list',
|
||||
},
|
||||
listPub: {
|
||||
url: '/list',
|
||||
},
|
||||
},
|
||||
{
|
||||
baseURL: 'https://febase-openapi.fn.netease.com/deer/api/deer/pic',
|
||||
withCredentials: false, // 解决跨域时必须非*问题
|
||||
headers: {
|
||||
'Febase-Auth': 'dskPVkIRnQ2dEn1DbyxURmUj4rl4BsCh53xFAsJnvVs=',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const context = {
|
||||
stores: {
|
||||
app: {
|
||||
title: 'Sample App',
|
||||
age: 20,
|
||||
detail: {
|
||||
foo: 'foo',
|
||||
bar: 'bar',
|
||||
biz: {
|
||||
x: 'xxx',
|
||||
export function ExtraSetters() {
|
||||
return (
|
||||
<SettingFormDemo
|
||||
initValues={{
|
||||
router: 'www.163.com',
|
||||
expression: `{ foo: 'foo' }`,
|
||||
object: {
|
||||
name: 'Alice',
|
||||
},
|
||||
},
|
||||
newKey: 'xxx',
|
||||
},
|
||||
foo: {
|
||||
test: 'test string',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 表单值预览
|
||||
*/
|
||||
const FormValuePreview = observer(({ model }: { model: FormModel }) => {
|
||||
const data = toJS(model.values);
|
||||
return <JsonView src={data} />;
|
||||
});
|
||||
|
||||
export function Basic() {
|
||||
const model = new FormModel(
|
||||
{
|
||||
router: 'www.163.com',
|
||||
expression: {
|
||||
foo: 'foo',
|
||||
},
|
||||
object: {
|
||||
name: 'Alice',
|
||||
},
|
||||
image:
|
||||
'https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13270238619/2cc5/0782/1d6e/009b96bf90c557b9bbde09b1687a2c80.png',
|
||||
},
|
||||
{ onChange: console.log },
|
||||
);
|
||||
|
||||
return (
|
||||
<Box display="flex">
|
||||
<SettingForm
|
||||
model={model}
|
||||
remoteServices={{
|
||||
ImageService: deerService as any,
|
||||
}}
|
||||
prototype={prototype}
|
||||
evaluateContext={{
|
||||
tango: context,
|
||||
__UNSAFE_TANGO_CURRENT_PAGE_RENDER_RUNTIME__: { routeData: { params: {}, query: {} } },
|
||||
}}
|
||||
modelVariables={modelVariables}
|
||||
expressionVariables={modelVariables}
|
||||
/>
|
||||
<Box position="relative">
|
||||
<Card title="表单状态预览" style={{ position: 'sticky', top: 0 }}>
|
||||
<FormValuePreview model={model} />
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
image:
|
||||
'https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13270238619/2cc5/0782/1d6e/009b96bf90c557b9bbde09b1687a2c80.png',
|
||||
}}
|
||||
prototype={prototypeHasExtraProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function NoExpressionSwitch() {
|
||||
const model = new FormModel({});
|
||||
export function StyleProps() {
|
||||
return (
|
||||
<Box>
|
||||
<SettingForm model={model} prototype={prototype} disableSwitchExpressionSetter />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function Binding() {
|
||||
const [data, setData] = useState<any>({});
|
||||
return (
|
||||
<Box>
|
||||
<Box as="code" bg="highlight" color="white" fontSize="24px">
|
||||
selected: {data?.key}
|
||||
</Box>
|
||||
<EditableVariableTree
|
||||
dataSource={modelVariables as any}
|
||||
getPreviewValue={(node) => {
|
||||
if (!node || !node.key) {
|
||||
return;
|
||||
}
|
||||
if (node.type === 'function') {
|
||||
return node.raw;
|
||||
}
|
||||
const keyPath = node.key.replaceAll('?', '');
|
||||
return getValue(context, keyPath);
|
||||
}}
|
||||
onSelect={setData}
|
||||
onAddVariable={console.log}
|
||||
onSave={console.log}
|
||||
/>
|
||||
</Box>
|
||||
<SettingFormDemo
|
||||
prototype={{
|
||||
name: 'Box',
|
||||
title: 'Box',
|
||||
type: 'element',
|
||||
package: '@music163/antd',
|
||||
props: [
|
||||
{
|
||||
name: 'style',
|
||||
title: 'styleSetter',
|
||||
setter: 'styleSetter',
|
||||
},
|
||||
{
|
||||
name: 'display',
|
||||
title: 'displaySetter',
|
||||
setter: 'displaySetter',
|
||||
},
|
||||
{
|
||||
name: 'flexDirection',
|
||||
title: 'flexDirectionSetter',
|
||||
setter: 'flexDirectionSetter',
|
||||
},
|
||||
{
|
||||
name: 'flexGap',
|
||||
title: 'flexGapSetter',
|
||||
setter: 'flexGapSetter',
|
||||
},
|
||||
{
|
||||
name: 'flexJustifyContent',
|
||||
title: 'flexJustifyContentSetter',
|
||||
setter: 'flexJustifyContentSetter',
|
||||
},
|
||||
{
|
||||
name: 'flexAlignItems',
|
||||
title: 'flexAlignItemsSetter',
|
||||
setter: 'flexAlignItemsSetter',
|
||||
},
|
||||
{
|
||||
name: 'spacing',
|
||||
title: 'spacingSetter',
|
||||
setter: 'spacingSetter',
|
||||
},
|
||||
{
|
||||
name: 'color',
|
||||
title: 'colorSetter',
|
||||
setter: 'colorSetter',
|
||||
},
|
||||
{
|
||||
name: 'bg',
|
||||
title: 'bgSetter',
|
||||
setter: 'bgSetter',
|
||||
},
|
||||
{
|
||||
name: 'border',
|
||||
title: 'borderSetter',
|
||||
setter: 'borderSetter',
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { ActionSelect } from '@music163/tango-ui';
|
||||
|
||||
export default {
|
||||
title: 'UI/ActionSelect',
|
||||
component: ActionSelect,
|
||||
};
|
||||
|
||||
const options = [
|
||||
{ label: 'action1', value: 'action1' },
|
||||
{ label: 'action2', value: 'action2' },
|
||||
{ label: 'action3', value: 'action3' },
|
||||
];
|
||||
|
||||
export const Basic = {
|
||||
args: {
|
||||
defaultText: '选择动作',
|
||||
options,
|
||||
onSelect: console.log,
|
||||
},
|
||||
};
|
||||
|
||||
export const showInput = {
|
||||
args: {
|
||||
text: '选择动作',
|
||||
options,
|
||||
showInput: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import { Action } from '@music163/tango-ui';
|
||||
import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { Space } from 'antd';
|
||||
|
||||
export default {
|
||||
title: 'UI/Action',
|
||||
component: Action,
|
||||
};
|
||||
|
||||
export const Basic = {
|
||||
args: {
|
||||
icon: <PlusOutlined />,
|
||||
tooltip: '添加服务函数',
|
||||
},
|
||||
};
|
||||
|
||||
export const Link = {
|
||||
args: {
|
||||
icon: <QuestionCircleOutlined />,
|
||||
tooltip: '查看帮助文档',
|
||||
href: 'https://music.163.com',
|
||||
},
|
||||
};
|
||||
|
||||
export const Outline = {
|
||||
args: {
|
||||
icon: <PlusOutlined />,
|
||||
tooltip: '添加服务函数',
|
||||
shape: 'outline',
|
||||
},
|
||||
};
|
||||
|
||||
export const Small = {
|
||||
args: {
|
||||
size: 'small',
|
||||
icon: <PlusOutlined />,
|
||||
tooltip: '添加服务函数',
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled = () => {
|
||||
return (
|
||||
<Space>
|
||||
<Action icon={<PlusOutlined />} tooltip="添加服务函数" disabled />
|
||||
<Action icon={<PlusOutlined />} tooltip="添加服务函数" disabled shape="outline" />
|
||||
<Action
|
||||
icon={<QuestionCircleOutlined />}
|
||||
tooltip="查看帮助文档"
|
||||
href="https://music.163.com"
|
||||
disabled
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
export const List = () => {
|
||||
return (
|
||||
<Space>
|
||||
<Action icon={<PlusOutlined />} tooltip="添加服务函数" />
|
||||
<Action
|
||||
icon={<QuestionCircleOutlined />}
|
||||
tooltip="查看帮助文档"
|
||||
href="https://music.163.com"
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ClassNameInput } from '@music163/tango-ui';
|
||||
|
||||
export default {
|
||||
title: 'UI/ClassNameInput',
|
||||
};
|
||||
|
||||
export function Basic() {
|
||||
return <ClassNameInput defaultValue="hello world" onChange={console.log} />;
|
||||
}
|
||||
|
||||
export function Controlled() {
|
||||
const [value, setValue] = useState('');
|
||||
return <ClassNameInput value={value} onChange={setValue} />;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import { DragPanel } from '@music163/tango-ui';
|
||||
import { Box, Text } from 'coral-system';
|
||||
import { Button } from 'antd';
|
||||
|
||||
export default {
|
||||
title: 'UI/DragPanel',
|
||||
};
|
||||
|
||||
export function Basic() {
|
||||
return (
|
||||
<>
|
||||
<DragPanel
|
||||
title="弹层标题"
|
||||
extra="右上角内容"
|
||||
width={350}
|
||||
body={<Box p="m">内容</Box>}
|
||||
footer={<Text>底部信息</Text>}
|
||||
>
|
||||
<Button>点击唤起浮层</Button>
|
||||
</DragPanel>
|
||||
<DragPanel
|
||||
title="弹层标题"
|
||||
extra="右上角内容"
|
||||
width={350}
|
||||
body={<Box p="m">内容</Box>}
|
||||
footer={<Text>底部信息</Text>}
|
||||
>
|
||||
<Button
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
right: 20,
|
||||
}}
|
||||
>
|
||||
底部浮层自动修正弹出区域
|
||||
</Button>
|
||||
</DragPanel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function FooterCustom() {
|
||||
return (
|
||||
<DragPanel
|
||||
title="弹层标题"
|
||||
extra="右上角内容"
|
||||
width={350}
|
||||
body={<Box p="m">内容</Box>}
|
||||
footer={(close) => (
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Text>底部信息</Text>
|
||||
<Button size="small" onClick={() => close()}>
|
||||
点此关闭
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
>
|
||||
<Button>点击唤起浮层</Button>
|
||||
</DragPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export function ResizeablePanel() {
|
||||
return (
|
||||
<DragPanel
|
||||
title="弹层标题"
|
||||
resizeable
|
||||
extra="右上角内容"
|
||||
width={350}
|
||||
height={400}
|
||||
body={
|
||||
<Box p="m" textAlign="center" background="#fa8484">
|
||||
内容
|
||||
</Box>
|
||||
}
|
||||
footer={<Text>底部信息</Text>}
|
||||
>
|
||||
<Button>点击唤起浮层</Button>
|
||||
</DragPanel>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { IconButton, InputCode } from '@music163/tango-ui';
|
||||
import { Action, InputCode, InputStyleCode } from '@music163/tango-ui';
|
||||
import { BlockOutlined } from '@ant-design/icons';
|
||||
|
||||
export default {
|
||||
@@ -22,12 +22,51 @@ const context = {
|
||||
export function Basic() {
|
||||
return (
|
||||
<InputCode
|
||||
suffix={<IconButton icon={<BlockOutlined />} />}
|
||||
suffix={<Action icon={<BlockOutlined />} size="small" />}
|
||||
autoCompleteContext={{ tango: context }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CSS() {
|
||||
return <InputStyleCode enableESLint suffix={<Action icon={<BlockOutlined />} size="small" />} />;
|
||||
}
|
||||
|
||||
const code = `
|
||||
function foo() {
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
}
|
||||
`;
|
||||
|
||||
export function Readonly() {
|
||||
return <InputCode shape="inset" readOnly value={code} />;
|
||||
}
|
||||
|
||||
export function Inset() {
|
||||
return <InputCode shape="inset" />;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { ToggleButton, IconFont } from '@music163/tango-ui';
|
||||
import { Box, Flex } from 'coral-system';
|
||||
import { ToggleButton, UndoOutlined, RedoOutlined } from '@music163/tango-ui';
|
||||
import { Box, Group, Flex } from 'coral-system';
|
||||
|
||||
export default {
|
||||
title: 'UI/ToggleButton',
|
||||
@@ -9,28 +9,73 @@ export default {
|
||||
export function Basic() {
|
||||
return (
|
||||
<Box>
|
||||
<Flex gap="l" bg="#efefef" p="l">
|
||||
<Box display="flex" columnGap="l" p="l">
|
||||
<ToggleButton>
|
||||
<IconFont type="icon-undo" />
|
||||
<UndoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton selected>
|
||||
<IconFont type="icon-redo" />
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton disabled>
|
||||
<IconFont type="icon-redo" />
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
</Box>
|
||||
<Box display="flex" columnGap="l" p="l">
|
||||
<ToggleButton type="primary">
|
||||
<UndoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton type="primary" selected>
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton type="primary" disabled>
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function DarkMode() {
|
||||
return (
|
||||
<Box>
|
||||
<Flex gap="l" bg="#222" p="l">
|
||||
<ToggleButton shape="ghost">
|
||||
<UndoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton shape="ghost" selected>
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton shape="ghost" disabled>
|
||||
<UndoOutlined />
|
||||
</ToggleButton>
|
||||
</Flex>
|
||||
<Flex gap="l" bg="#222" p="l">
|
||||
<ToggleButton shape="ghost">
|
||||
<IconFont type="icon-undo" />
|
||||
<ToggleButton shape="text">
|
||||
<UndoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton shape="ghost" selected>
|
||||
<IconFont type="icon-redo" />
|
||||
<ToggleButton shape="text" selected>
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton shape="ghost" disabled>
|
||||
<IconFont type="icon-undo" />
|
||||
<ToggleButton shape="text" disabled>
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function ButtonGroup() {
|
||||
return (
|
||||
<Group attached>
|
||||
<ToggleButton>
|
||||
<UndoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton selected>
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton disabled>
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { VariableTree } from '@music163/tango-designer/src/components';
|
||||
import { Box } from 'coral-system';
|
||||
|
||||
export default {
|
||||
title: 'Designer/VariableTree',
|
||||
};
|
||||
|
||||
const data = [
|
||||
{
|
||||
title: 'Stores',
|
||||
key: 'stores',
|
||||
selectable: false,
|
||||
children: [
|
||||
{
|
||||
title: 'user',
|
||||
key: 'stores.user',
|
||||
children: [
|
||||
{
|
||||
title: 'name',
|
||||
key: 'stores.user.name',
|
||||
raw: '"Tom"',
|
||||
},
|
||||
{
|
||||
title: 'age',
|
||||
key: 'stores.user.age',
|
||||
raw: '18',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'book',
|
||||
key: 'stores.book',
|
||||
children: [
|
||||
{
|
||||
title: 'name',
|
||||
key: 'stores.book.name',
|
||||
raw: '"JavaScript 高级程序设计"',
|
||||
},
|
||||
{
|
||||
title: 'price',
|
||||
key: 'stores.book.price',
|
||||
raw: '99.99',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Services',
|
||||
key: 'services',
|
||||
selectable: false,
|
||||
children: [
|
||||
{
|
||||
title: 'add',
|
||||
key: 'services.add',
|
||||
},
|
||||
{
|
||||
title: 'multiply',
|
||||
key: 'services.multiply',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function Basic() {
|
||||
return (
|
||||
<Box width={600} border="solid">
|
||||
<VariableTree dataSource={data} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
# Dependencies
|
||||
/node_modules
|
||||
|
||||
# Production
|
||||
/build
|
||||
|
||||
# Generated files
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
@@ -1,41 +0,0 @@
|
||||
# Website
|
||||
|
||||
This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
|
||||
|
||||
### Installation
|
||||
|
||||
```
|
||||
$ yarn
|
||||
```
|
||||
|
||||
### Local Development
|
||||
|
||||
```
|
||||
$ yarn start
|
||||
```
|
||||
|
||||
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
||||
|
||||
### Build
|
||||
|
||||
```
|
||||
$ yarn build
|
||||
```
|
||||
|
||||
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
||||
|
||||
### Deployment
|
||||
|
||||
Using SSH:
|
||||
|
||||
```
|
||||
$ USE_SSH=true yarn deploy
|
||||
```
|
||||
|
||||
Not using SSH:
|
||||
|
||||
```
|
||||
$ GIT_USER=<Your GitHub username> yarn deploy
|
||||
```
|
||||
|
||||
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||
};
|
||||
|
Before Width: | Height: | Size: 297 KiB |
@@ -1,76 +0,0 @@
|
||||
---
|
||||
title: Tango LowCode Builder Released!
|
||||
authors: [wwsun]
|
||||
tags: [netease, tango, open-source]
|
||||
---
|
||||
|
||||
## 📝 Tango 简介
|
||||
|
||||
Tango 是一个用于快速构建低代码平台的低代码设计器框架,借助 Tango 只需要数行代码就可以完成一个基本的低代码平台前端系统的搭建。Tango 低代码设计器直接读取前端项目的源代码,并以源代码为中心,执行和渲染前端视图,并为用户提供低代码可视化搭建能力,用户的搭建操作会转为对源代码的修改。借助于 Tango 构建的低代码工具或平台,可以实现 源码进,源码出的效果,无缝与企业内部现有的研发体系进行集成。
|
||||
|
||||
### ✨ 核心特性
|
||||
|
||||
- 经历网易云音乐内网生产环境的实际检验,可灵活集成应用于低代码平台,本地开发工具等
|
||||
- 基于源码 AST 驱动,无私有 DSL 和协议
|
||||
- 提供实时出码能力,支持源码进,源码出
|
||||
- 开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件
|
||||
- 使用 TypeScript 开发,提供完整的类型定义文件
|
||||
|
||||
### 🏗️ 基于源码的低代码搭建方案
|
||||
|
||||
Tango 低代码引擎不依赖私有搭建协议和 DSL,而是直接使用源代码驱动,引擎内部将源码转为 AST,用户的所有的搭建操作转为对 AST 的遍历和修改,进而将 AST 重新生成为代码,将代码同步给在线沙箱执行。与传统的 [基于 Schema 驱动的低代码方案](https://mp.weixin.qq.com/s/yqYey76qLGYPfDtpGkVFfA) 相比,不受私有 DSL 和协议的限制,能够完美的实现低代码搭建与源码开发的无缝集成。
|
||||
|
||||
<img src="https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13140534982/ee2e/f42c/cc9a/184e2918a011b57d46e6c64a2722fa44.png" />
|
||||
|
||||
### 📄 源码进,源码出
|
||||
|
||||
由于引擎内核完全基于源代码驱动实现,Tango 低代码引擎能够实现源代码进,源代码出的可视化搭建能力,不提供任何私有的中间产物。如果公司内部已经有了一套完善的研发体系(代码托管、构建、部署、CDN),那么可以直接使用 Tango 低代码引擎与现有的服务集成构建低代码开发平台。
|
||||
|
||||

|
||||
|
||||
### 🏆 产品优势
|
||||
|
||||
与基于私有 Schema 的低代码搭建方案相比,Tango 低代码引擎具有如下优势:
|
||||
|
||||
| 对比项 | 基于 Schema 的低代码搭建方案 | Tango(基于源码 AST 转换) |
|
||||
| ------------ | -------------------------------------------------- | --------------------------------------------------------------------- |
|
||||
| 适用场景 | 面向特定的垂直搭建场景,例如表单,营销页面等 | 🔥 面面向以源码为中心的应用搭建场景 |
|
||||
| 语言能力 | 依赖私有协议扩展,不灵活,且难以与编程语言能力对齐 | 🔥 直接基于 JavaScript 语言,可以使用所有的语言特性,不存在扩展性问题 |
|
||||
| 开发能力 | LowCode | 🔥 LowCode + ProCode |
|
||||
| 源码导出 | 以 Schema 为中心,单向出码,不可逆 | 🔥 以源码为中心,双向转码 |
|
||||
| 自定义依赖 | 需要根据私有协议扩展封装,定制成本高 | 🔥 原有组件可以无缝低成本接入 |
|
||||
| 集成研发设施 | 定制成本高,需要额外定制 | 🔥 低成本接入,可以直接复用原有的部署发布能力 |
|
||||
|
||||
## 📐 技术架构
|
||||
|
||||
Tango 低代码引擎在实现上进行了分层解藕,使得上层的低代码平台与底层的低代码引擎可以独立开发和维护,快速集成部署。此外,Tango 低代码引擎定义了一套开放的物料生态体系,开发者可以自由的贡献扩展组件配置能力的属性设置器,以及扩展低代码物料的二方三方业务组件。
|
||||
|
||||
具体的技术架构如下图所示:
|
||||
|
||||

|
||||
|
||||
## ⏰ 开源里程碑
|
||||
|
||||
Tango 低代码引擎是网易云音乐内部低代码平台的核心构件,开源涉及到大量的核心逻辑解藕的工作,这将给我们正常的工作带来大量的额外工作,因此我们计划分阶段推进 Tango 低代码引擎的开源事项。
|
||||
|
||||
1. 今天我们正式发布 Tango 低代码引擎的第一个社区版本,该版本将会包括 Tango 低代码引擎的核心代码库,TangoBoot 应用框架,以及基于 antd v4 适配的低代码组件库。
|
||||
2. 我们计划在今年的 **9 月 30 日** 发布低代码引擎的 **1.0 Beta** 版本,该版本将会对核心的实现面向社区场景重构,移除掉我们在云音乐内部的一些兼容代码,并将核心的实现进行重构和优化。
|
||||
3. 我们计划在今年的 **10 月 30 日** 发布低代码引擎的 **1.0 RC** 版本,该版本将会保证核心 API 基本稳定,不再发生 BREAKING CHANGE,同时我们将会提供完善翔实的开发指南、部署文档、和演示应用。
|
||||
4. **正式版**本我们将在 **2023 年 Q4 结束前** 发布,届时我们会进一步完善我们的开源社区运营机制。
|
||||
|
||||

|
||||
|
||||
## 🤝 社区建设
|
||||
|
||||
我们的开源工作正在积极推进中,可以通过如下的信息了解到我们的最新进展:
|
||||
|
||||
- Github 仓库:<https://github.com/NetEase/tango>
|
||||
- 文档站点:<https://netease.github.io/tango/>
|
||||
|
||||
欢迎大家加入到我们的社区中来,一起参与到 Tango 低代码引擎的开源建设中来。有任何问题都可以通过 [Github Issues](https://github.com/NetEase/tango/issues) 反馈给我们,我们会及时跟进处理。
|
||||
|
||||
## 💗 致谢
|
||||
|
||||
感谢网易云音乐公共技术团队,大前端团队,直播技术团队,以及所有参与过 Tango 项目的同学们。
|
||||
|
||||
感谢 CodeSandbox 提供的 [Sandpack](https://sandpack.codesandbox.io/) 项目,为 Tango 提供了强大的基于浏览器的代码构建与执行能力。
|
||||
|
Before Width: | Height: | Size: 259 KiB |
|
Before Width: | Height: | Size: 26 KiB |
@@ -1,5 +0,0 @@
|
||||
wwsun:
|
||||
name: Wells SUN
|
||||
title: Core maintainer
|
||||
url: https://github.com/wwsun
|
||||
image_url: https://github.com/wwsun.png
|
||||
@@ -1,38 +0,0 @@
|
||||
# 目录结构
|
||||
|
||||
## 目录结构规范
|
||||
|
||||
在目录结构上,我们约定了一套通用的目录结构
|
||||
|
||||
```md
|
||||
── src
|
||||
│ +── assets
|
||||
│ +── pages
|
||||
│ +── components
|
||||
│ +── services/index.js
|
||||
│ +── stores
|
||||
│ ├── routes.js // 路由配置
|
||||
│ ├── global.less // 全局样式
|
||||
| |-- index.js 应用启动配置
|
||||
└── package.json
|
||||
|---tango.config.json 设计器和 external 配置
|
||||
```
|
||||
|
||||
## `src/components` 自定义组件
|
||||
|
||||
该目录放置本地开发的组件,例如
|
||||
|
||||
```jsx
|
||||
// src/components/sample.jsx
|
||||
import React from 'react';
|
||||
import { Box } from '@music163/antd';
|
||||
|
||||
// 如果需要被搭建,使用 Box 代替 div,并且支持透出属性
|
||||
export function Sample(props) {
|
||||
return <Box {...props}>sample component</Box>;
|
||||
}
|
||||
```
|
||||
|
||||
:::info
|
||||
注意,如果需要 Sample 组件能够被设计器解析和搭建,则一定要使用 MusicOne 的 `Box` 组件代替普遍的 `div` 元素,你也可以使用 `<Box as="span" />` 来改变渲染的 html 元素。
|
||||
:::
|
||||
@@ -1,134 +0,0 @@
|
||||
# 应用入口
|
||||
|
||||
框架通过 `runApp` 创建渲染整个应用,在创建应用时可以传入应用的全局配置。
|
||||
|
||||
## 基本配置
|
||||
|
||||
通过 `src/index.js` 对应用进行全局配置,设置路由、运行时环境、服务函数、状态模型等。
|
||||
|
||||
```js
|
||||
import { runApp, tangoBootConfig } from '@music163/tango-boot';
|
||||
import { message } from '@music163/antd';
|
||||
|
||||
// 配置请求错误发生时消息默认的展现方式,此处使用 message 组件进行消息浮层的展示
|
||||
tangoBootConfig.toast = message;
|
||||
|
||||
runApp({
|
||||
// 启动项配置
|
||||
boot: {},
|
||||
|
||||
// 应用容器注入
|
||||
providers: [],
|
||||
|
||||
// 状态模型
|
||||
stores: {},
|
||||
|
||||
// 服务函数
|
||||
services: {},
|
||||
|
||||
// 路由配置
|
||||
router: {},
|
||||
});
|
||||
```
|
||||
|
||||
## 启动项配置
|
||||
|
||||
`boot` 配置项用于应用的启动项配置:
|
||||
|
||||
```js
|
||||
runApp({
|
||||
boot: {
|
||||
// 应用挂载的 dom 结点
|
||||
mountElement: document.querySelector('#root'),
|
||||
|
||||
// 是否开启 qiankun,如开启则对应为 `qiankun: { appName: string }`
|
||||
qiankun: false,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## 路由配置
|
||||
|
||||
`router` 配置项用于配置应用的前端路由,底层基于 `react-router` 实现:
|
||||
|
||||
```js
|
||||
import routes from './routes';
|
||||
|
||||
runApp({
|
||||
router: {
|
||||
// 路由类型: hash | browser
|
||||
type: 'hash',
|
||||
|
||||
// 路由配置信息
|
||||
config: routes,
|
||||
|
||||
// basename,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
TangoBoot 使用的是 ReactRouter 的[静态路由配置方案](https://github.com/remix-run/react-router/tree/v5/packages/react-router-config)。具体的路由配置文件如下:
|
||||
|
||||
```js
|
||||
// routes.js
|
||||
import Index from './pages/index';
|
||||
import About from './pages/about';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
exact: true,
|
||||
component: Index,
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
component: About,
|
||||
},
|
||||
{
|
||||
path: '/user/:id',
|
||||
component: About,
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
```
|
||||
|
||||
## 应用容器配置
|
||||
|
||||
由于 `runApp` 封装了根组件的渲染逻辑,某些时候,你可能想要为根组件包裹特定的容器类组件,例如多语言配置,状态容器等等,可以借助 `providers` 配置项进行:
|
||||
|
||||
```js
|
||||
runApp({
|
||||
// 传入 providers 组件实例,按照传入顺序进行包裹
|
||||
providers: [<ConfigProvider />, <LocaleProvider />],
|
||||
});
|
||||
```
|
||||
|
||||
## 设置环境变量 {#env}
|
||||
|
||||
某些时候你可能需要自定义一些环境变量,以便在应用运行在不同的环境中的时候进行快速的判断。你可以借助 `tango.env` 来快速的获取到应用的环境信息。需要注意的是,默认情况下不会有任何的环境信息,你可以在需要的时候通过在 `runApp` 中传入 `getEnv` 设置的方式来设置你想要的环境变量。
|
||||
|
||||
例如:
|
||||
|
||||
```js
|
||||
runApp({
|
||||
// 自定义应用的环境信息,你可以返回对象
|
||||
getEnv() {
|
||||
if (location.origin.includes('tango')) {
|
||||
// tango 设计器中
|
||||
return 'development';
|
||||
}
|
||||
if (location.origin.includes('localhost')) {
|
||||
// 本地开发
|
||||
return 'local';
|
||||
}
|
||||
return 'production';
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
在任意地方消费环境变量,例如:
|
||||
|
||||
```jsx
|
||||
<Box isRender={tango.env === 'development'}>一个仅在开发态显示的区域</Box>
|
||||
```
|
||||
@@ -1,87 +0,0 @@
|
||||
# 全局单例
|
||||
|
||||
TangoBoot 默认提供全局单例模式,意味着,您可以在项目中始终使用该全局单例来进行必要的应用运行时信息的访问。
|
||||
|
||||
:::tip
|
||||
单例意味着单一实例,也叫单例模式,是一种比较常见的设计模式。限制类实例化次数只能一次,一个类只有一个实例,并提供一个访问它的全局访问点。TangoBoot 单例意味着您的应用各种始终只有一个 TangoBoot 实例,并且可以借助该单一实例,一致的获取到应用到运行时信息。
|
||||
:::
|
||||
|
||||
## 如何获取全局单例
|
||||
|
||||
```js
|
||||
// 从模块包引入
|
||||
import tango from '@music163/tango-boot';
|
||||
|
||||
// 或直接从 window 获取
|
||||
window.tango;
|
||||
|
||||
// 两者是对等的
|
||||
console.log(tango === window.tango); // true
|
||||
```
|
||||
|
||||
### 可以从 `tango` 单例中获取的信息
|
||||
|
||||
```js
|
||||
// tango
|
||||
{
|
||||
// 自定义配置信息
|
||||
config: {},
|
||||
|
||||
// 自定义辅助函数
|
||||
helpers: {},
|
||||
|
||||
// History
|
||||
history: History;
|
||||
|
||||
// 服务函数集合
|
||||
services: {},
|
||||
|
||||
// 状态模型集合
|
||||
stores: {},
|
||||
|
||||
// builtin helpers
|
||||
closeModal: Function;
|
||||
openModal: Function;
|
||||
setStoreValue: Function;
|
||||
getStoreValue: Function;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## 如何使用
|
||||
|
||||
### 在模型定义中使用
|
||||
|
||||
```jsx
|
||||
import tango, { defineModel } from '@music163/tango-boot';
|
||||
|
||||
export default defineModel(
|
||||
{
|
||||
title: 'hello',
|
||||
|
||||
async listUsers() {
|
||||
const data = await tango.services.listUsers();
|
||||
this.title = data;
|
||||
},
|
||||
},
|
||||
'app'
|
||||
);
|
||||
```
|
||||
|
||||
### 在视图中使用
|
||||
|
||||
```jsx
|
||||
import tango, { definePage } from '@music163/tango-boot';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Box>
|
||||
{tango.stores.app.title}
|
||||
|
||||
<Button onClick={tango.services.list}>request</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default definePage(App);
|
||||
```
|
||||
@@ -1,130 +0,0 @@
|
||||
# 常用的工具函数
|
||||
|
||||
tango-boot 提供了一组便捷的工具函数,用于应对日常开发过程中的典型操作逻辑。
|
||||
|
||||
### `getUser()`
|
||||
|
||||
> 从 v1.15.0 开始提供
|
||||
|
||||
获取当前的登录用户
|
||||
|
||||
```js
|
||||
const user = tango.getUser();
|
||||
```
|
||||
|
||||
### `checkPrivilege({ appCode, privilegeCode })`
|
||||
|
||||
> 从 v1.15.0 开始提供
|
||||
|
||||
检查 PMS 权限
|
||||
|
||||
```js
|
||||
const test = await tango.checkPrivilege({ appCode, privilegeCode });
|
||||
```
|
||||
|
||||
### `copyToClipboard(text: string)`
|
||||
|
||||
复制文本到剪贴板
|
||||
|
||||
```js
|
||||
tango.copyToClipboard('hello world');
|
||||
```
|
||||
|
||||
### `navigateTo(link: string, data?: object)`
|
||||
|
||||
导航到其他前端路由
|
||||
|
||||
```js
|
||||
// 基本用法
|
||||
tango.navigateTo('/about');
|
||||
|
||||
// 带路由参数
|
||||
tango.navigateTo('/about', { the: 'query' }); // /about?the=query
|
||||
```
|
||||
|
||||
### `showToast(text: string, type: string, time: number)`
|
||||
|
||||
- text 消息正文
|
||||
- type 消息类型 `success | error | warn`
|
||||
- time 消息展示的时间,单位秒,默认为 3 秒
|
||||
|
||||
消息提示
|
||||
|
||||
```js
|
||||
tango.showToast('hello world');
|
||||
|
||||
tango.showToast('something is error', 'error', 3);
|
||||
```
|
||||
|
||||
### `openModal(modalId: string)`
|
||||
|
||||
根据弹层 ID 唤起目标弹层
|
||||
|
||||
```js
|
||||
tango.openModal('modal123');
|
||||
```
|
||||
|
||||
### `closeModal(modalId: string)`
|
||||
|
||||
根据弹层 ID 关闭目标弹层
|
||||
|
||||
```js
|
||||
tango.closeModal('modal123');
|
||||
```
|
||||
|
||||
### `formatDate(date, format)`
|
||||
|
||||
- date: `number | string | Moment` 日期值
|
||||
- format: `string` 格式化,取值参考 https://momentjs.com/docs/#/displaying/format/
|
||||
|
||||
格式化日期值
|
||||
|
||||
```js
|
||||
tango.formatDate(1659496225323, 'YYYY-MM-DD HH:mm:ss'); // 2022-08-03 11:10:25
|
||||
|
||||
tango.formatDate('2022-11-11', 'X'); // 1668096000
|
||||
```
|
||||
|
||||
### `formatNumber(number, locale, options)`
|
||||
|
||||
- number: `number`
|
||||
- locale: `string`
|
||||
- options: `Intl.NumberFormatOptions`,参考取值 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat
|
||||
|
||||
格式化数字
|
||||
|
||||
```js
|
||||
tango.formatNumber(99999); // 99,999
|
||||
```
|
||||
|
||||
### `getCurrentLanguage`
|
||||
|
||||
获取当前语种
|
||||
|
||||
```js
|
||||
tango.getCurrentLanguage(); // en-US
|
||||
```
|
||||
|
||||
### `setLanguage(localeCode)`
|
||||
|
||||
设置当前语种
|
||||
|
||||
```js
|
||||
tango.setLanguage('en-US');
|
||||
```
|
||||
|
||||
### `getLanguageList`
|
||||
|
||||
获取当前应用可用语种列表
|
||||
|
||||
```js
|
||||
tango.getLanguageList();
|
||||
```
|
||||
|
||||
### `trans(module:key)`
|
||||
|
||||
根据多语言 key 获取当前语种文案
|
||||
|
||||
```js
|
||||
tango.trans('common:add'); // 新增
|
||||
```
|
||||
@@ -1,24 +0,0 @@
|
||||
# 国际化
|
||||
|
||||
TangoBoot 本身不提供国际化功能,国际化和您的 UI 组件库相关,在云音乐,我们采用的是基于 `@music163/antd` 的 UI 层解决方案。可以借助应用入口的容器配置功能进行国际化配置。
|
||||
|
||||
:::tip
|
||||
如果您还不了解应用入口配置,可以先访问 [应用入口](./app) 一节进行了解。
|
||||
:::
|
||||
|
||||
## 配置示例
|
||||
|
||||
```js
|
||||
import { runApp } from '@music163/tango-boot';
|
||||
import { ConfigProvider } from '@music163/antd';
|
||||
import locale from '@music163/antd/es/locale/zh_CN';
|
||||
import 'moment/dist/locale/zh-cn';
|
||||
|
||||
// 设置 moment 的文案
|
||||
moment.locale('zh-cn');
|
||||
|
||||
runApp({
|
||||
// 设置 UI 组件库的文案配置
|
||||
providers: [<ConfigProvider locale={locale} />],
|
||||
});
|
||||
```
|
||||
@@ -1,139 +0,0 @@
|
||||
# TangoBoot
|
||||
|
||||
TangoBoot 是一个服务于 Tango 低代码应用开发体系的前端应用框架,提供了标准的数据请求,状态管理,路由方案,并提供了通用的运行时工具函数,让开发者通过简单的配置即可生成单页应用。
|
||||
|
||||
## 应用架构
|
||||
|
||||
TangoBoot 的应用架构使用的是 View-Model-Service 三层模型,其中模型层定义了 Observable States,视图层观察 Model 的变化而进行自动更新,服务层用来创建一组服务函数,供视图层和模型层消费。图如下图所示:
|
||||
|
||||
<img
|
||||
alt="image"
|
||||
width="600px"
|
||||
src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13760951704/985c/9706/7f18/be713816a143d3a054f51c9d1fc59b36.png"
|
||||
/>
|
||||
|
||||
## 核心 API
|
||||
|
||||
- `runApp` 创建应用入口
|
||||
- `definePage` 定义 Reactive 视图
|
||||
- `defineStore` 定义可观察的状态模型
|
||||
- `defineServices` 定义服务函数
|
||||
|
||||
### 创建应用入口
|
||||
|
||||
其中`index.js` 为应用的入口文件,一个基本的实现为
|
||||
|
||||
```jsx
|
||||
import { runApp } from '@music163/tango-boot';
|
||||
import routes from './routes';
|
||||
import services from './services';
|
||||
import home from './stores/home';
|
||||
import counter from './stores/counter';
|
||||
// 全局样式
|
||||
import './global.less';
|
||||
|
||||
const { mount, unmount, bootstrap } = runApp({
|
||||
boot: {
|
||||
mountElement: document.querySelector('#root'),
|
||||
qiankun: false,
|
||||
},
|
||||
|
||||
stores: {
|
||||
home,
|
||||
counter,
|
||||
},
|
||||
|
||||
services,
|
||||
|
||||
router: {
|
||||
type: 'browser',
|
||||
config: routes,
|
||||
},
|
||||
});
|
||||
|
||||
export { mount, unmount, bootstrap };
|
||||
```
|
||||
|
||||
### 创建视图模型
|
||||
|
||||
通过 defineStore 来进行视图模型的定义非常简单,只需简单的声明状态和动作即可
|
||||
|
||||
```jsx
|
||||
import { defineStore } from '@music163/tango-boot';
|
||||
|
||||
const counter = defineStore({
|
||||
num: 0,
|
||||
|
||||
get() {},
|
||||
|
||||
decrement: function () {
|
||||
counter.num--;
|
||||
},
|
||||
|
||||
increment: () => counter.num++,
|
||||
});
|
||||
|
||||
export default counter;
|
||||
```
|
||||
|
||||
### 创建 Reactive 视图
|
||||
|
||||
视图层如果要监听状态的变化,只需要 `definePage` 对视图组件进行一层包裹即可。
|
||||
|
||||
```jsx
|
||||
import React from 'react';
|
||||
import tango, { definePage } from '@music163/tango-boot';
|
||||
|
||||
class App extends React.Component {
|
||||
increment = () => {
|
||||
tango.stores.counter.increment();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Counter: {tango.stores.counter.num}</h1>
|
||||
<button type="primary" onClick={this.increment}>
|
||||
+1
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default definePage(App);
|
||||
```
|
||||
|
||||
### 创建数据服务函数
|
||||
|
||||
`defineServices` 提供了低成本声明异步服务函数的能力。
|
||||
|
||||
```jsx
|
||||
import { defineServices } from '@music163/tango-boot';
|
||||
|
||||
export default defineServices({
|
||||
list: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
|
||||
formatter: (res) => {
|
||||
const { data, message } = res;
|
||||
return {
|
||||
code: 200,
|
||||
list: data,
|
||||
total: data.length,
|
||||
message,
|
||||
};
|
||||
},
|
||||
},
|
||||
add: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
|
||||
method: 'post',
|
||||
},
|
||||
update: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
|
||||
method: 'post',
|
||||
},
|
||||
delete: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users?id=1',
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -1,66 +0,0 @@
|
||||
# 双向绑定
|
||||
|
||||
TangoBoot 支持快捷实现组件与视图双向绑定。
|
||||
|
||||
## 借助 `withModel` 实现双向绑定
|
||||
|
||||
`withModel` 是 TangoBoot 内置的模型绑定 HOC,支持将组件的内部状态同步到某个 store 定义的状态中。基本的用法如下:
|
||||
|
||||
```jsx
|
||||
class Input extends React.Component {
|
||||
foo() {}
|
||||
|
||||
render() {
|
||||
return <input {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
const ModelInput = withModel({
|
||||
// 设置从组件同步到 store 中的状态值
|
||||
getValueFromEvent(e: any) {
|
||||
return e.target.value;
|
||||
},
|
||||
})(Input);
|
||||
```
|
||||
|
||||
现在 `ModelInput` 获取了双向绑定的能力,其 `value` 变化后的值将会通过 `onChange` 事件同步给绑定的模型变量
|
||||
|
||||
```jsx
|
||||
const Store = defineStore(
|
||||
{
|
||||
name: 'alice',
|
||||
},
|
||||
'user'
|
||||
);
|
||||
|
||||
const ModelApp = defineView((props) => {
|
||||
return (
|
||||
<div>
|
||||
<ModelInput model="user.name" />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
## 配置选项
|
||||
|
||||
`withModel` 的定义如下:
|
||||
|
||||
```jsx
|
||||
withModel(options)(Component);
|
||||
```
|
||||
|
||||
其中 options 的配置如下:
|
||||
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
| ----------------- | ------------------ | ---------- | -------------------------- |
|
||||
| name | string | | 组件的 displayName |
|
||||
| valuePropName | string | "value" | 组件的受控值 |
|
||||
| trigger | string | "onChange" | 组件值变化时的回调函数 |
|
||||
| getValueFromEvent | `(...args) => any` | | 从回调函数参数中的取值方法 |
|
||||
|
||||
借助 `withModel` 增强后,Component 组件将会获得如下属性:
|
||||
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
| ------ | ------ | ------ | ------------------------ |
|
||||
| model | string | | 绑定的模型变量的变量路径 |
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# 创建页面
|
||||
|
||||
TangoBoot 内置提供了一个创建 reactive 视图的模型,用户无需关注复杂的依赖关系,只需要将视图组件使用 `definePage` 包裹即可。
|
||||
|
||||
## definePage
|
||||
|
||||
基于传递进来的组件创建一个 reactive 视图。reactive 视图会根据 store 数据的变化来自动触发视图重新渲染。例如
|
||||
|
||||
```jsx
|
||||
import React from 'react';
|
||||
import { definePage, defineStore } from '@music163/tango-boot';
|
||||
|
||||
const user = defineStore({ name: 'Bob' });
|
||||
|
||||
export default definePage(() => <div>Hello {user.name}!</div>);
|
||||
```
|
||||
|
||||
`definePage` 内部为被包裹的组件实现了一个最佳的 `shouldComponentUpdate` 或 `memo`。
|
||||
|
||||
使用 `definePage` 包裹后的组件还会默认接收一些额外的属性,以便于开发者便捷的获取部分状态信息。具体包括:
|
||||
|
||||
- `routerData` 路由数据:包括 `params` 路由参数,`query` 查询参数,`pathname` 路径名
|
||||
- `stores` 模型索引
|
||||
- `services` 服务索引
|
||||
|
||||
## defineView
|
||||
|
||||
`defineView` 是一个更加轻量的实现 reactive 视图的包裹器,推荐所有在视图中使用的本地组件都使用它来进行包裹。与 `definePage` 不同的是,他没有额外的属性透传进来。
|
||||
|
||||
```jsx
|
||||
export default defineView(() => (
|
||||
<div>
|
||||
<div>Hello {user.name}!</div>
|
||||
<div>Your first post is: {timeline.posts[0]}</div>
|
||||
</div>
|
||||
));
|
||||
```
|
||||
|
||||
:::info
|
||||
`defineView` 的实现依赖了 [react-easy-state](https://github.com/RisingStack/react-easy-state#creating-reactive-views),感兴趣的同学可以访问对应的链接了解更详细的内容。
|
||||
:::
|
||||
@@ -1,33 +0,0 @@
|
||||
# 组件实例获取
|
||||
|
||||
TangoBoot 支持快捷设置和获取组件的实例。
|
||||
|
||||
## 借助 `withRef` 注册组件实例
|
||||
|
||||
`withRef` 是 TangoBoot 内置的实例注册 HOC,可以快捷的进行组件实例的注册和获取。基本的用法如下:
|
||||
|
||||
```tsx
|
||||
class Input extends React.Component {
|
||||
foo() {}
|
||||
|
||||
render() {
|
||||
return <input {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
const ClassInput = withRef()(Input);
|
||||
```
|
||||
|
||||
借助 `withRef` 包裹组件后,当新的组件实例被设置 `id` 属性时,会自动将对应的实例注册到 `tango.refs` 中。例如:
|
||||
|
||||
```tsx
|
||||
export function Basic() {
|
||||
const inputRef = useRef();
|
||||
useEffect(() => {
|
||||
console.log(tango.refs.classInput, inputRef.current);
|
||||
}, []);
|
||||
|
||||
// 设置 id 属性后,会自动组件实例到 `tango.refs` 中
|
||||
return <ClassInput id="classInput" ref={inputRef} />;
|
||||
}
|
||||
```
|
||||
@@ -1,90 +0,0 @@
|
||||
# 消费异步数据服务
|
||||
|
||||
TangoBoot 提供一个配置式的创建数据服务函数的方法,通过 `defineServices` 可以非常轻松的声明一组异步服务函数,供视图层或模型层消费。
|
||||
|
||||
## 简单用法
|
||||
|
||||
```js
|
||||
import { defineServices } from '@music163/tango-boot';
|
||||
|
||||
const services = defineServices({
|
||||
list: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
|
||||
},
|
||||
add: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
|
||||
method: 'post',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
调用异步服务函数
|
||||
|
||||
```js
|
||||
async () => {
|
||||
// 最简单的调用方法
|
||||
await services.list();
|
||||
|
||||
// 用可以快捷传递参数,参数1为数据载荷,参数2为请求配置
|
||||
await services.add(payload, config);
|
||||
};
|
||||
```
|
||||
|
||||
## 请求配置
|
||||
|
||||
默认情况下,`defineServices` 底层采用 [axios](https://github.com/axios/axios) 发起请求。
|
||||
|
||||
```js
|
||||
import { defineServices } from '@music163/tango-boot';
|
||||
|
||||
export default defineServices({
|
||||
list: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
|
||||
formatter: (res) => {
|
||||
const { data, message } = res;
|
||||
return {
|
||||
code: 200,
|
||||
list: data,
|
||||
total: data.length,
|
||||
message,
|
||||
};
|
||||
},
|
||||
// 这里你也可以传入 axios 的其他配置
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
常用的配置如下:
|
||||
|
||||
- url 请求地址
|
||||
- method 请求方法,默认为 `GET`
|
||||
- `GET`
|
||||
- `POST`
|
||||
- `PUT`
|
||||
- `DELETE`
|
||||
- formatter 格式化响应数据
|
||||
- requestType 请求体编码类型(content-type),默认值为 `json`
|
||||
- `json` 默认将 JavaScript 对象序列化为 JSON
|
||||
- `x-www-form-urlencoded` 对应为 `application/x-www-form-urlencoded`
|
||||
- headers 自定义请求头
|
||||
- timeout 超时的毫秒数
|
||||
|
||||
更多的配置选项可以参考 [axios 请求配置文档](https://axios-http.com/zh/docs/req_config)。
|
||||
|
||||
:::tip 请求体编码
|
||||
默认情况下 TangoBoot 将 JavaScript 对象序列化为 JSON。如果要以 `application/x-www-form-urlencoded` 格式发送数据,可以将 `requestType` 设置为 `x-www-form-urlencoded`。
|
||||
|
||||
`application/x-www-form-urlencoded` 数据被编码成以 '&' 分隔的键 - 值对,同时以 '=' 分隔键和值。非字母或数字的字符会被 [percent-encoding](https://developer.mozilla.org/zh-CN/docs/Glossary/percent-encoding): 这也就是为什么这种类型不支持二进制数据 (应使用 multipart/form-data 代替).
|
||||
:::
|
||||
|
||||
|
||||
## 配置错误消息展示
|
||||
|
||||
配置请求错误发生时消息默认的展现方式,此处使用 `message` 组件进行消息浮层的展示。推荐在应用的入口文件进行配置:
|
||||
|
||||
```js
|
||||
import { tangoBootConfig } from '@music163/tango-boot';
|
||||
import { message } from '@music163/antd';
|
||||
|
||||
tangoBootConfig.toast = message;
|
||||
```
|
||||
@@ -1,214 +0,0 @@
|
||||
# 管理前端应用状态
|
||||
|
||||
TangoBoot 采用了一个轻量级的使用 Reactive 的状态管理方案,开发者可以非常轻松的进行页面状态的管理。一个最基本的示例如下所示:
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
import { defineStore, definePage } from '@music163/tango-boot';
|
||||
|
||||
const counter = defineStore({
|
||||
num: 0,
|
||||
increment: () => counter.num++,
|
||||
});
|
||||
|
||||
// 当状态变化的时候,视图会自动刷新
|
||||
export default definePage(() => <button onClick={counter.increment}>{counter.num}</button>);
|
||||
```
|
||||
|
||||
值得注意的是,开发者必须借助 `defineStore` 来定义状态,它会帮助你创建一个 observable 的状态对象。借助 `definePage` 来定义视图,它会帮你订一个 reactive 的视图组件,并且在自动监听状态的变化,按需进行 UI 更新。
|
||||
|
||||
## 创建状态模型 Stores
|
||||
|
||||
基本语法为 `defineStore(storeObject: object, namespace?: string)`
|
||||
|
||||
- `storeObject` 为原始的状态对象
|
||||
- `namespace` 为该 Store 对应的命名空间,如果你定义了,可以借助 `tango.stores[namespace]` 来获取该引用
|
||||
|
||||
一个基本的例子如下:
|
||||
|
||||
```js
|
||||
import { defineStore } from '@music163/tango-boot';
|
||||
|
||||
const user = defineStore({ name: 'Rick' });
|
||||
// stores behave like normal JS objects
|
||||
user.name = 'Bob';
|
||||
```
|
||||
|
||||
### 复杂的数据结构
|
||||
|
||||
```js
|
||||
// stores can include any valid JS structure
|
||||
// including nested data, arrays, Maps, Sets, getters, setters, inheritance, ...
|
||||
const user = defineStore({
|
||||
profile: {
|
||||
firstName: 'Bob',
|
||||
lastName: 'Smith',
|
||||
get name() {
|
||||
return `${user.profile.firstName} ${user.profile.lastName}`;
|
||||
},
|
||||
},
|
||||
hobbies: ['programming', 'sports'],
|
||||
friends: new Map(),
|
||||
});
|
||||
|
||||
// stores may be mutated in any syntactically valid way
|
||||
user.profile.firstName = 'Bob';
|
||||
delete user.profile.lastName;
|
||||
user.hobbies.push('reading');
|
||||
user.friends.set('id', otherUser);
|
||||
```
|
||||
|
||||
### 异步方法
|
||||
|
||||
```js
|
||||
const userStore = defineStore({
|
||||
user: {},
|
||||
async fetchUser() {
|
||||
userStore.user = await fetch('/user');
|
||||
|
||||
// or use this
|
||||
// this.user = await fetch('/user');
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 多个 Store
|
||||
|
||||
useStore.js
|
||||
|
||||
```js
|
||||
import { store } from '@music163/tango-boot';
|
||||
|
||||
const userStore = defineStore(
|
||||
{
|
||||
user: {},
|
||||
async fetchUser() {
|
||||
userStore.user = await fetch('/user');
|
||||
},
|
||||
},
|
||||
'userStore',
|
||||
);
|
||||
|
||||
export default userStore;
|
||||
```
|
||||
|
||||
recipesStore.js
|
||||
|
||||
```js
|
||||
import tango, { store } from '@music163/tango-boot';
|
||||
import userStore from './userStore';
|
||||
|
||||
const recipesStore = defineStore(
|
||||
{
|
||||
recipes: [],
|
||||
|
||||
// 方式1:直接用调用
|
||||
async fetchRecipes() {
|
||||
recipesStore.recipes = await fetch(`/recipes?user=${userStore.user.id}`);
|
||||
},
|
||||
|
||||
// 方法2:使用 this 调用,仅支持 method property,不支持箭头函数
|
||||
async fetchRecipes2() {
|
||||
this.recipes = await fetch(`/recipes?user=${userStore.user.id}`);
|
||||
},
|
||||
|
||||
// 方法3:使用 tango 全局变量调用,你也可以使用这种方式引入其他的 store
|
||||
async fetchRecipes3() {
|
||||
tango.stores.recipesStore.recipes = await fetch(
|
||||
`/recipes?user=${tango.stores.userStore.user.id}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
'recipesStore',
|
||||
);
|
||||
|
||||
export default recipesStore;
|
||||
```
|
||||
|
||||
## 创建 Reactive 视图
|
||||
|
||||
你可以借助 `definePage` 创建视图,也可以直接包裹您的原始组件。基本用法为 `definePage(Component)`,借助 `definePage` 可以帮你自动监听状态的变化,并在变化时自动触发视图的重新渲染。
|
||||
|
||||
一个简单的例子如下:
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
import { view, store } from '@music163/tango-boot';
|
||||
|
||||
// this is a global state store
|
||||
const user = defineStore({ name: 'Bob' });
|
||||
|
||||
// this is re-rendered whenever user.name changes
|
||||
export default definePage(() => (
|
||||
<div>
|
||||
<input value={user.name} onChange={(ev) => (user.name = ev.target.value)} />
|
||||
<div>Hello {user.name}!</div>
|
||||
</div>
|
||||
));
|
||||
```
|
||||
|
||||
## 实现视图与状态双向绑定
|
||||
|
||||
TangoBoot 提供了一个名为 `withModel` 的 [HOC](https://zh-hans.reactjs.org/docs/higher-order-components.html),可以借助它来实现视图与状态的双向绑定。
|
||||
|
||||
使用方法如下:
|
||||
|
||||
```js
|
||||
withModel(options)(BaseComponent);
|
||||
```
|
||||
|
||||
### 参数配置
|
||||
|
||||
其中可配置的 options 参数包括:
|
||||
|
||||
- `name` 用于配置包裹后组件的 displayName,可选
|
||||
- `valuePropName` 绑定的 value 属性名,用于实现双向绑定的受控逻辑,默认为 `value`
|
||||
- `trigger` 设置收集字段值变更的时机,默认为 `onChange`
|
||||
- `getValueFromEvent` 设置从事件回调中获取 value 值的方法,默认为 `val => val`,直接返回 trigger 的第一个参数
|
||||
|
||||
嵌套 `whitModel` 后,组件将会获得两个新增的属性:
|
||||
|
||||
- `model` 用于绑定 Store 中的状态
|
||||
- `innerRef` 用于获取内部组件的 [ref](https://zh-hans.reactjs.org/docs/refs-and-the-dom.html) 引用
|
||||
|
||||
### 双向绑定示例
|
||||
|
||||
基本用法如下:
|
||||
|
||||
```jsx
|
||||
import tango, { withModel, defineView, defineStore } from '@music163/tango-boot';
|
||||
|
||||
// 定义一个基本的 Input 组件
|
||||
class Input extends React.Component {
|
||||
foo() {}
|
||||
|
||||
render() {
|
||||
return <input {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
// 实现双向绑定
|
||||
const ModelInput = withModel({
|
||||
getValueFromEvent(e) {
|
||||
return e.target.value;
|
||||
},
|
||||
})(Input);
|
||||
|
||||
// 定义一个 Store
|
||||
defineStore(
|
||||
{
|
||||
name: 'alice',
|
||||
},
|
||||
'user',
|
||||
);
|
||||
|
||||
// 双向绑定验证
|
||||
const ModelApp = defineView((props) => {
|
||||
return (
|
||||
<div>
|
||||
<ModelInput model="user.name" />
|
||||
<div>{tango.stores.user.name}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
```
|
||||
@@ -1,87 +0,0 @@
|
||||
# tango.config.json
|
||||
|
||||
`tango.config.json` 是 Tango 项目的核心配置文件,当您的项目使用 Tango 设计器在线开发时会读取该文件进行线上开发环境的初始化。
|
||||
|
||||
## entry
|
||||
|
||||
非必填。`entry` 是项目的入口文件,用于在线设计器的初始化,默认取值为 `/src/index.js`。
|
||||
|
||||
## packages
|
||||
|
||||
项目使用的 npm 包依赖信息,用于定义依赖的类型以及外置依赖资源。该配置项为一个 map,key 为对应的 npm 包名,value 为该依赖的配置信息。
|
||||
|
||||
依赖的类型 `type` 可以是 `baseDependency` 即项目的基础依赖,用于获取项目基础物料列表的信息;也可以是 `bizDependency` 即业务组件依赖,用于获取合并到物料面板业务组件菜单中的业务组件列表;也可以是 `depenency` 即项目的其他依赖,只用于提供 external 信息与版本信息等,不会和物料面板产生关系。
|
||||
|
||||
当配置了外置依赖资源 `resources` 与其对应的全局变量 `library` 时,当这些依赖被 import 时,相关资源不会被打包到 bundle 中,而是在运行时再去外部获取这些扩展依赖,其等效于之前的 `sandbox.externalResources` 与 `sandbox.externals` 配置项。此外,可以将 `resource` 中的版本号替换为 `{{version}}`,其会在平台上构建时替换为版本号。特殊的,可以定义 `designerResources` 用于适配平台的设计器视图。
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"react": {
|
||||
"version": "17.0.2",
|
||||
"library": "React",
|
||||
"type": "dependency",
|
||||
"resources": ["https://unpkg.com/react@{{version}}/umd/react.development.js"]
|
||||
},
|
||||
"react-dom": {
|
||||
"version": "17.0.2",
|
||||
"library": "ReactDOM",
|
||||
"type": "dependency",
|
||||
"resources": ["https://unpkg.com/react-dom@{{version}}/umd/react-dom.development.js"]
|
||||
},
|
||||
"@music163/tango-boot": {
|
||||
"version": "0.1.3",
|
||||
"library": "TangoBoot",
|
||||
"type": "baseDependency",
|
||||
"resources": ["https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js"],
|
||||
"description": "云音乐低代码运行时框架"
|
||||
},
|
||||
"@music163/antd": {
|
||||
"version": "0.1.2",
|
||||
"library": "TangoAntd",
|
||||
"type": "baseDependency",
|
||||
"resources": [
|
||||
"https://unpkg.com/@music163/antd/{{version}}/dist/index.js",
|
||||
"https://unpkg.com/@music163/antd/{{version}}/dist/index.css"
|
||||
],
|
||||
"description": "云音乐低代码中后台应用基础物料",
|
||||
"designerResources": [
|
||||
"https://unpkg.com/@music163/antd/{{version}}/dist/designer.js",
|
||||
"https://unpkg.com/@music163/antd/{{version}}/dist/index.css"
|
||||
]
|
||||
},
|
||||
"@music163/biz-comp": {
|
||||
"type": "bizDependency",
|
||||
"version": "0.0.2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::tip 注意依赖的先后顺序
|
||||
由于依赖是按照定义的先后顺序注入至沙箱内,因此需要确保 `packages` 内依赖的先后顺序,否则可能会导致应用加载依赖失败(例如将 `react` 放置在组件包之后,会导致组件包无法正常加载)。
|
||||
:::
|
||||
|
||||
## externalResources
|
||||
|
||||
除了上述 `packages` 定义的外置依赖资源外,在这里可以定义沙箱启动时引入的非 npm 包的外置依赖资源列表,可以是 `js` 或 `css` 等静态文件。例如:
|
||||
|
||||
```json
|
||||
{
|
||||
"externalResources": ["https://tango.netease.io/script.js"]
|
||||
}
|
||||
```
|
||||
|
||||
## proxy
|
||||
|
||||
非必填。项目的代理规则。
|
||||
|
||||
```json
|
||||
{
|
||||
"proxy": {
|
||||
"/api/pms": {
|
||||
"target": "http://cms.qa.igame.163.com",
|
||||
"changeOrigin": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,58 +0,0 @@
|
||||
# 设计器扩展概览
|
||||
|
||||
主要介绍如何扩展 tango 设计器。tango 设计器提供了三个部位的自定义扩展能力,分别是标题栏,工具栏,和侧边栏。可以通过下面这张图进行简要的了解:
|
||||
|
||||
<img src="https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13359732115/c5ca/c5ae/bbd4/852daf849ac39162baf3bc3a985de6de.png" />
|
||||
|
||||
## 标题栏扩展
|
||||
|
||||
`DesignerPanel` 提供了 logo, description, actions 三个扩展点,分别对应于 平台标识、应用描述、行动点区域,开发者可以按照需求进行扩展。例如:
|
||||
|
||||
```jsx
|
||||
<DesignerPanel logo={<Logo />} description={<ProjectDetail />} actions={<Box>新版沙箱</Box>}>
|
||||
</SidebarPanel>
|
||||
```
|
||||
|
||||
## 工具栏扩展
|
||||
|
||||
工具栏面板支持任意组合和设置渲染的位置,开发者可以通过 `ToolbarPanel.Item` 添加自定义的工具栏选项,例如下面的代码扩展了一个 newPage 按钮,用于支持在设计器中创建新页面的需求。
|
||||
|
||||
```jsx
|
||||
<ToolbarPanel>
|
||||
<ToolbarPanel.Item key="history" />
|
||||
<ToolbarPanel.Item key="viewportRefresh" />
|
||||
<ToolbarPanel.Item key="preview" />
|
||||
<ToolbarPanel.Item key="routeSwitch" />
|
||||
<ToolbarPanel.Item key="newPage">
|
||||
<Button
|
||||
onClick={() => {
|
||||
const { name, code } = genDefaultPage(index);
|
||||
engine.workspace.addViewPage(`${name}${index++}`, code);
|
||||
message.success('页面新建成功');
|
||||
}}
|
||||
>
|
||||
添加新页面
|
||||
</Button>
|
||||
</ToolbarPanel.Item>
|
||||
<ToolbarPanel.Item key="modeSwitch" placement="center" />
|
||||
<ToolbarPanel.Item key="viewportSwitch" placement="right" />
|
||||
</ToolbarPanel>
|
||||
```
|
||||
|
||||
## 侧边栏扩展
|
||||
|
||||
侧边栏面板支持任意组合和调换顺序,用户可以通过 `SidebarPanel.Item` 组件进行自定义扩展,默认情况下根据一些内置的 key 标识,设计器会自动渲染对应的面板,用户也可以根据自己的需求采用自定义渲染子节点的方案进行扩展。
|
||||
例如,下面的示例代码中扩展了一个 “自定义面板”。
|
||||
|
||||
```jsx
|
||||
<SidebarPanel>
|
||||
<SidebarPanel.Item key="components" />
|
||||
<SidebarPanel.Item key="outline" />
|
||||
<SidebarPanel.Item key="dataSource" />
|
||||
<SidebarPanel.Item key="dependency" />
|
||||
<SidebarPanel.Item key="history" />
|
||||
<SidebarPanel.Item key="custom" title="自定义面板" icon={<BulbOutlined />}>
|
||||
<Box p="m">这里是一个自定义的面板,你可以任意添加</Box>
|
||||
</SidebarPanel.Item>
|
||||
</SidebarPanel>
|
||||
```
|
||||
@@ -1,67 +0,0 @@
|
||||
# 外部数据源
|
||||
|
||||
如果您的扩展部件需要使用外部数据源,可以参考本文的做法。
|
||||
|
||||
## 存在部件共享
|
||||
|
||||
如果数据源需要在多个扩展部件中共享,我们推荐您借助 tango 的 `remoteServices` 规范进行接口的实现。
|
||||
|
||||
### 外部数据源定义
|
||||
|
||||
例如,我们需要调用一组图片素材的接口,此时我们可以定义一个 `ImageService`,如下:
|
||||
|
||||
```tsx
|
||||
import { createServices } from '@music/request';
|
||||
|
||||
export const remoteServices = {
|
||||
ImageService: createServices(
|
||||
{
|
||||
listMy: {
|
||||
url: '/my/upload/list',
|
||||
},
|
||||
listFav: {
|
||||
url: '/my/star/list',
|
||||
},
|
||||
listPub: {
|
||||
url: '/list',
|
||||
},
|
||||
},
|
||||
{
|
||||
baseURL: 'https://febase-openapi.fn.netease.com/deer/api/deer/pic',
|
||||
withCredentials: false, // 解决跨域时必须非*问题
|
||||
}
|
||||
),
|
||||
};
|
||||
```
|
||||
|
||||
### 外部数据源引入
|
||||
|
||||
我们在设计器初始化的时候可以通过 `remoteServices` 属性进行外部数据源的传入。
|
||||
|
||||
```tsx
|
||||
<Designer remoteServices={remoteServices}></Designer>
|
||||
```
|
||||
|
||||
### 在设计器扩展中使用定义的外部数据源
|
||||
|
||||
```tsx
|
||||
import { useRemoteServices } from '@music163/tango-designer';
|
||||
|
||||
export function CustomWidget() {
|
||||
const remoteServices = useRemoteServices();
|
||||
}
|
||||
```
|
||||
|
||||
## 仅在单个组件
|
||||
|
||||
如果您的外部数据服务没有额外的共享需求,您也可以直接在组件内部进行数据服务的发起。
|
||||
|
||||
```tsx
|
||||
import request from '@music/request';
|
||||
|
||||
export function CustomWidget() {
|
||||
useEffect(() => {
|
||||
request('//some.domain/get');
|
||||
}, []);
|
||||
}
|
||||
```
|
||||
@@ -1,141 +0,0 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# 物料接入
|
||||
|
||||
Tango 提供了低成本的物料接入方式,支持直接复用现有的组件体系。只需要在现有组件包基础上简单的提供配置说明问题,即可轻松接入到 Tango 低代码生态体系中。
|
||||
|
||||
import Link from '@docusaurus/Link';
|
||||
|
||||
## 预览视图和设计器视图
|
||||
|
||||
- 预览视图:是组件默认的渲染模式,通常我们只需要关注此视图行为。
|
||||
- 设计器视图:为低代码平台定制的渲染视图,当我们需要自定义组件在 Tango 设计器中的部分渲染行为时,可以通过修改设计器视图的渲染逻辑实现。
|
||||
|
||||
### 设计器视图和 `withDnd`
|
||||
|
||||
通常情况下我们不需要太关注于设计器视图,只需通过 `withDnd` 简单的进行包装即可。例如:
|
||||
|
||||
```jsx
|
||||
// 将 antd Button 包装一层提供设计器视图
|
||||
export Button = withDnd({
|
||||
name: 'Button', // 必须,组件的名字,用来正确设置组件的 displayName
|
||||
isFunctionComponent: true, // 可选,如果包裹的组件为函数组件,可以在此设置
|
||||
overrideProps: {}, // 可选,覆盖掉包裹组件的默认属性值
|
||||
})(AntButton);
|
||||
```
|
||||
|
||||
默认情况下 withDnd 会在组件外层包裹一层 dnd 容器,以便于组件能够在设计器中被拖拽:
|
||||
|
||||
- draggable 属性表示该区域可以被拖拽
|
||||
- data-dnd 用来追踪渲染的 dom 元素
|
||||
|
||||
```jsx
|
||||
<div className="dnd-wrapper" draggable data-dnd="button:123">
|
||||
<button>hello</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 自定义拖拽容器
|
||||
|
||||
由于默认情况下拖拽容器是一个 `div` 节点,为[块级元素](https://www.w3schools.com/cssref/pr_class_display.asp)。部分情况下,你可能期望它渲染为一个[行内容器](https://www.w3schools.com/cssref/pr_class_display.asp),此时可以通过如下方式配置:
|
||||
|
||||
```jsx
|
||||
export Button = withDnd({
|
||||
name: 'Button',
|
||||
display: 'inline-block', // 将 dnd 容器渲染为行内元素
|
||||
wrapperStyle: {}, // 传入 dnd 容器的自定义样式
|
||||
})(AntButton);
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 禁用拖拽容器
|
||||
|
||||
某些情况下,你的组件可能会检查子节点的有效性,此时你可能不期望组件被包装额外的容器节点,此时你可以关闭拖拽容器,但你需要保证你的组件能够接收父级元素传入的属性信息并附加到组件的跟节点上,否则将无法正确的设置组件的 dnd 信息。
|
||||
|
||||
```jsx
|
||||
export Button = withDnd({
|
||||
name: 'Button',
|
||||
hasWrapper: false, // 禁用拖拽容器
|
||||
})(MyButton);
|
||||
|
||||
const MyButton = (props) => {
|
||||
const { children, ...rest } = props;
|
||||
// 此时,需要保证你的组件能够正确的将多余的属性透传到组件的根节点上
|
||||
return <button {...rest}>{children}</button>
|
||||
}
|
||||
```
|
||||
|
||||
## 组件包接入
|
||||
|
||||
组件包一般包含多个组件导出,例如 `@music163/antd` 就是典型的组件包,它面向中后台场景提供统一的物料层解决方案。组件包接入 Tango 体系,无需修改现有的代码实现,只需要在此基础上提供额外的配置文件即可。如下所示:
|
||||
|
||||
```
|
||||
- src
|
||||
- button
|
||||
- designer.tsx # 设计器视图,可以不提供
|
||||
- index.ts # 默认视图出口
|
||||
- prototype.ts # 组件可配置描述协议
|
||||
- index.ts
|
||||
- prototypes.ts
|
||||
- designer.ts
|
||||
```
|
||||
|
||||
参考示例:https://g.hz.netease.com/NeteaseMusicUI/music-one/-/tree/master/packages/components/src/action
|
||||
|
||||
### 设计器视图的实现
|
||||
|
||||
对大部分组件而言,都不需要提供特定的设计器视图,只需要使用 tango 提供的 dnd hoc 进行简单的包裹导出即可。
|
||||
|
||||
```tsx
|
||||
import { Button as Base, ButtonProps } from 'antd';
|
||||
import { withDnd } from '@music/tango-apps-shared';
|
||||
|
||||
export const Button = withDnd<HTMLDivElement, ButtonProps<any>>({
|
||||
name: 'Button',
|
||||
})(Base);
|
||||
```
|
||||
|
||||
### 编写 prototypes 文件
|
||||
|
||||
可以参考 <Link to="/docs/protocol/material-protocol">物料协议</Link> 编写该文档
|
||||
|
||||
### 属性设置器选择
|
||||
|
||||
参考属性设置器文档进行选择。
|
||||
|
||||
### 可配置项的验证
|
||||
|
||||
TangoApps 提供了 SettingFormPlayground 组件用来测试组件的可配置能力,可以直接在组件文档中加入可配置测试示例,例如在 storybook 中进行测试。
|
||||
|
||||
```jsx
|
||||
import React from 'react';
|
||||
import { SettingFormPlayground } from '@music/tango-apps-setting-form';
|
||||
import { Input, prototypes } from '@music163/antd';
|
||||
|
||||
export default {
|
||||
title: 'Prototype/Input',
|
||||
};
|
||||
|
||||
export function Basic() {
|
||||
return (
|
||||
<SettingFormPlayground prototype={prototypes.Input}>
|
||||
<Input />
|
||||
</SettingFormPlayground>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
SettingFormPlayground 组件支持实时预览配置结果,预览效果如下:
|
||||
|
||||
<img src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13140796879/edbb/5dad/a330/013a98940e487956b605e1e533f54371.png" />
|
||||
|
||||
## 单个业务组件接入
|
||||
|
||||
单个组件接入可以参考组件包的接入方案。
|
||||
|
||||
:::info
|
||||
考虑到单组件形态的特点,在云音乐业务场景中,TangoStudio 将会与红石物料中心对接,提供业务组件的自动化接入能力,目前该能力正在开发过程中。
|
||||
:::
|
||||
@@ -1,9 +0,0 @@
|
||||
# DesignerPanel 设计器布局容器
|
||||
|
||||
设计器的主框架提供了一个基本的低代码设计器的布局容器,你可以很轻松的通过此布局容器来实现自己的设计器布局。
|
||||
|
||||
## 属性列表
|
||||
|
||||
import TypesTable from '@site/src/components/TypesTable';
|
||||
|
||||
<TypesTable name="DesignerPanelProps" />
|
||||
@@ -1,56 +0,0 @@
|
||||
# Designer 设计器容器
|
||||
|
||||
设计器根节点,注入全局状态。
|
||||
|
||||
## 属性列表
|
||||
|
||||
import TypesTable from '@site/src/components/TypesTable';
|
||||
|
||||
<TypesTable name="DesignerProps" />
|
||||
|
||||
## 设计器初始化
|
||||
|
||||
```jsx
|
||||
<Designer engine={engine} sandboxQuery={sandboxQuery} remoteServices={remoteServices}></Designer>
|
||||
```
|
||||
|
||||
## engine
|
||||
|
||||
设计器引擎实例,用于管理设计器的核心状态。
|
||||
|
||||
### 基本的初始化方式
|
||||
|
||||
```js
|
||||
const engine = createEngine({
|
||||
entry: '/src/index.js',
|
||||
files: sampleFiles,
|
||||
componentPrototypes: prototypes as any,
|
||||
});
|
||||
```
|
||||
|
||||
### 自定义 workspace 的初始化方式
|
||||
|
||||
默认情况下,引擎采用的是源码解析模式,即将源码解析为 ast 树,后续的搭建逻辑转为对 ast 树的操作。如果你想自定义搭建逻辑,可以通过自定义 workspace 的方式来实现。
|
||||
|
||||
引擎在 2.0 本本中提供了新的 JsonWorkspace 来支持自定义搭建逻辑,JsonWorkspace 采用的是 json 格式的数据结构,你可以通过自定义 json 来实现自定义搭建逻辑。
|
||||
|
||||
```js
|
||||
const engine = createEngine({
|
||||
workspace: new JsonWorkspace({
|
||||
prototypes: prototypes as any,
|
||||
files: schemaFiles,
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
:::tip
|
||||
按照这种方式,你可以自定义自己的 Workspace 实现。具体可以参考 Workspace 的实现标准。
|
||||
:::
|
||||
|
||||
## sandboxQuery
|
||||
|
||||
沙箱的查询实例,用于向沙箱注册 dom 查询能力。
|
||||
|
||||
## remoteServices
|
||||
|
||||
远程服务实例,用于注册全局共享的数据服务实例。
|
||||
@@ -1,27 +0,0 @@
|
||||
# Hooks 钩子方法
|
||||
|
||||
设计器提供了一组 Hooks 用于快速获取设计器的各种状态。
|
||||
|
||||
import TypesTable from '@site/src/components/TypesTable';
|
||||
|
||||
## useWorkspace
|
||||
|
||||
获取工作区状态。
|
||||
|
||||
```js
|
||||
// App 需要放置在 Designer 容器中
|
||||
function App() {
|
||||
const workspace = useWorkspace();
|
||||
// do what you want
|
||||
}
|
||||
```
|
||||
|
||||
## useDesigner
|
||||
|
||||
```js
|
||||
// App 需要放置在 Designer 容器中
|
||||
function App() {
|
||||
const designer = useDesigner();
|
||||
// do what you want
|
||||
}
|
||||
```
|
||||
@@ -1,9 +0,0 @@
|
||||
# Sandbox 沙箱
|
||||
|
||||
设计器的运行时沙箱,用来执行代码,渲染页面。
|
||||
|
||||
import TypesTable from '@site/src/components/TypesTable';
|
||||
|
||||
默认沙箱是一个基于 CodeSandbox 的沙箱实例,可以直接在浏览器端执行代码。
|
||||
|
||||
<TypesTable name="SandboxProps" />
|
||||
@@ -1,9 +0,0 @@
|
||||
# SettingPanel 属性设置面板
|
||||
|
||||
属性设置器用于在配置面板中展示特定配置项的配置逻辑。
|
||||
|
||||
## 属性列表
|
||||
|
||||
import TypesTable from '@site/src/components/TypesTable';
|
||||
|
||||
<TypesTable name="SettingPanelProps" />
|
||||
@@ -1,38 +0,0 @@
|
||||
# SidebarPanel 侧边栏面板
|
||||
|
||||
侧边栏面板提供了一个简单易用的主操作面板,可以将一些高频操作和核心部件放置到侧边栏面板中,方便用户快速操作。
|
||||
|
||||
## 属性列表
|
||||
|
||||
import TypesTable from '@site/src/components/TypesTable';
|
||||
|
||||
<TypesTable name="SidebarPanelProps" />
|
||||
|
||||
## 创建浮动面板
|
||||
|
||||
借助 `isFloat` 和 `width` 属性可以创建浮动自定义宽度面板,脱离框架对侧边栏的宽度限制。
|
||||
|
||||
```jsx
|
||||
<SidebarPanel.Item key="history" isFloat width="40vw" />
|
||||
```
|
||||
|
||||
## 徽标提示
|
||||
|
||||
`showBadge` 属性可以设置工具栏是否显示徽标提示,用于某些面板需要对外展现需要被用户注意的时机。
|
||||
|
||||
<img
|
||||
alt="img"
|
||||
src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/18224398190/2847/b86d/df55/c817e86a0b1b3a6b08f7cc98362caec5.png"
|
||||
width="300px"
|
||||
/>
|
||||
|
||||
```jsx
|
||||
<SidebarPanel.Item
|
||||
key="info"
|
||||
label="消息"
|
||||
title="消息列表"
|
||||
icon={<NotificationOutlined />}
|
||||
showBadge={{ count: 2 }}>
|
||||
<Box>info panel</Box>
|
||||
</SidebarPanel.Item>
|
||||
```
|
||||
@@ -1,10 +0,0 @@
|
||||
# ViewPanel 主视图面板
|
||||
|
||||
设计器的主视图面板,用来放置设计器的主视图,包括:沙箱,编辑器,预览等。主视图可以放置多个,但只有一个会在激活状态。
|
||||
|
||||
## 属性列表
|
||||
|
||||
import TypesTable from '@site/src/components/TypesTable';
|
||||
|
||||
<TypesTable name="ViewPanelProps" />
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# WorkspacePanel 工作区面板
|
||||
|
||||
主工作区的布局容器,用于放置工作区的多重视图。
|
||||
@@ -1,71 +0,0 @@
|
||||
import Link from '@docusaurus/Link';
|
||||
|
||||
# 快速开始
|
||||
|
||||
Tango 是一个用于快速构建低代码工具的前端框架。借助 Tango 只需要少量代码就可以快速的搭建一个基本的低代码设计器。本文将会帮助你借助 Tango 实现一个基本的低代码可视化设计器。
|
||||
|
||||
:::warning 测试版免责声明
|
||||
基于 Tango 低代码引擎的低代码平台已经在网易云音乐内部服务了内部众多业务开发场景。由于开源工作涉及到大量的代码分离工作,当前开源版本为低代码引擎的 alpha 演示版本,API 可能不稳定,不建议在生产环境中使用。我们预计在 2023 年 Q4 发布可以运行在生产环境的正式版。
|
||||
:::
|
||||
|
||||
## 环境要求
|
||||
|
||||
- 安装 node 16.0.0 以上版本: <https://nodejs.org/en/download/>
|
||||
- 安装 npm 8.x 或 yarn 1.x: <https://classic.yarnpkg.com/en/docs/getting-started>
|
||||
|
||||
## 启动 Tango Playground
|
||||
|
||||
1. 克隆仓库至本地
|
||||
```sh
|
||||
git clone https://github.com/NetEase/tango.git
|
||||
```
|
||||
2. 在 hosts 文件下添加本地开发的域名
|
||||
```hosts
|
||||
127.0.0.1 local.netease.com
|
||||
```
|
||||
3. 安装依赖并启动 Tango Playground
|
||||
```sh
|
||||
yarn install
|
||||
yarn start
|
||||
```
|
||||
4. 打开浏览器访问 <https://local.netease.com:6006> 并允许不安全的证书
|
||||
|
||||
## 启动本地沙箱
|
||||
|
||||
本地开发除了需要启动 Tango 的 playground 外,还需要一个可用的沙箱环境来承载设计器。
|
||||
|
||||
如果没有可用的在线沙箱,可以通过本地启动一个沙箱来完成开发;如果已有一个在线沙箱,且沙箱与本地的 playground 共用一级域名,请直接跳至第 7 步修改 `bundlerURL` 为在线沙箱的地址。
|
||||
|
||||
1. 克隆仓库至本地
|
||||
```sh
|
||||
git clone https://github.com/NetEase/codesandbox-client.git
|
||||
```
|
||||
2. 修改 `Caddyfile`,将 `:8080` 修改为 playground 的域名,例如 `local.netease.com`
|
||||
3. 将上述域名添加至 hosts 文件
|
||||
```hosts
|
||||
127.0.0.1 local.netease.com
|
||||
```
|
||||
4. 安装依赖并构建沙箱产物
|
||||
```sh
|
||||
yarn install
|
||||
yarn build:deps
|
||||
yarn build:sandpack
|
||||
```
|
||||
你也可以从 [这里](https://github.com/NetEase/codesandbox-client/releases) 下载预构建的沙箱产物,创建 `www` 子文件夹并将压缩包解压到该文件夹内
|
||||
5. 使用 Caddy 托管构建沙箱后的静态文件,若本地没有 Caddy 可前往 <https://caddyserver.com/download> 下载
|
||||
```sh
|
||||
caddy run
|
||||
```
|
||||
6. 打开浏览器访问 <https://local.netease.com:8443> 并允许不安全的证书
|
||||
7. 修改 `apps/playground/src/pages/index.tsx`,找到 `<Sandbox />` 组件,将 `bundlerURL` 修改为 `https://local.netease.com:8443`
|
||||
```jsx
|
||||
<Sandbox bundlerURL="https://local.netease.com:8443" />
|
||||
```
|
||||
8. 回到 playground 页面并刷新,现在 playground 将使用你指定的沙箱来运行设计器
|
||||
|
||||
若你本地有 Docker 也可将上述的第 4-5 步改为使用 Docker 构建镜像并启动:
|
||||
|
||||
```sh
|
||||
docker build -t tango-codesandbox .
|
||||
docker run -p 8443:8443 tango-codesandbox
|
||||
```
|
||||
@@ -1,81 +0,0 @@
|
||||
# 属性设置器
|
||||
|
||||
属性设置器用于在配置面板中展示特定配置项的配置逻辑。Tango 内置了多种标准的属性设置器,对于一些特殊场景,内置的属性设置器可能无法满足你的需要,此时开发者可以扩展自己的属性设置。
|
||||
|
||||
## 内置属性设置器
|
||||
|
||||
| 设置器名 | 接收值类型 | 设置器说明 | 可配置项 |
|
||||
| ----------------- | ------------------------------------ | -------------------------- | ------------------ |
|
||||
| eventSetter | expression | 动作函数或事件监听器设置器 | |
|
||||
| actionListSetter | | ActionList 的 value 设置器 | |
|
||||
| boolSetter | boolean | 布尔值设置器 | |
|
||||
| choiceSetter | `{ label: string, value: string }[]` | RadioGroup 设置器 | - options 选项列表 |
|
||||
| pickerSetter | `{ label: string, value: string }[]` | 下拉值设置器 | - options 选项列表 |
|
||||
| colorSetter | string | 色彩 | |
|
||||
| dateSetter | string | 日期设置器 | |
|
||||
| dateRangeSetter | `string[]` | 日期范围设置器 | |
|
||||
| timeSetter | `string` | 时间设置器 | |
|
||||
| timeRangeSetter | `string[]` | 时间范围设置器 | |
|
||||
| expressionSetter | expression | 表达式设置器 | |
|
||||
| jsonSetter | json expression | JSON 表达式设置器 | |
|
||||
| jsxSetter | jsx expression | JSX 设置器 | |
|
||||
| iconSetter | string | Icon 组件设置器 | |
|
||||
| iconTypeSetter | string | Icon 组件类型设置器 | |
|
||||
| numberSetter | number | 数字类型设置器 | |
|
||||
| textSetter | string | 文本设置器 | |
|
||||
| textAreaSetter | string | 文本域设置器 | |
|
||||
| modelSetter | string | 变量绑定设置器 | |
|
||||
| enumSetter | enum | 枚举值设置器 | |
|
||||
| sliderSetter | number | 滑块设置器 | |
|
||||
| listSetter | `object[]` | 列表值设置器 | |
|
||||
| renderPropsSetter | Function | render props 设置器 | |
|
||||
| imageSetter | string | 云鹿图片设置器 | |
|
||||
|
||||
## 注册自定义属性设置器
|
||||
|
||||
### 属性设置器的实现标准
|
||||
|
||||
属性设置器组件是一个标准的受控型表单组件,开发者所开发的组件只需要暴露 `value` 和 `onChange(value)` 两个属性让属性面板的表单容器进行控制即可。
|
||||
|
||||
例如,我们可以实现一个简单的字符串设置器 StringSetter,其实现如下:
|
||||
|
||||
```jsx
|
||||
function StringSetter({ value, onChange, ...rest }) {
|
||||
const handleChange = (e) => {
|
||||
onChange(e.target.value);
|
||||
};
|
||||
return <input value={value} onChange={handleChange} />;
|
||||
}
|
||||
```
|
||||
|
||||
### 注册属性设置器
|
||||
|
||||
注册属性设置器非常简单,只需要借助设置器暴露出来的 `registerSetter` 即可。
|
||||
|
||||
```jsx
|
||||
import { registerSetter } from '@music163/tango-designer';
|
||||
|
||||
// 注册自定义 setter
|
||||
registerSetter({
|
||||
name: 'stringSetter',
|
||||
component: StringSetter,
|
||||
});
|
||||
```
|
||||
|
||||
### 使用自定义的属性设置器
|
||||
|
||||
在组件的物料协议中直接声明对应的属性设置器名称即可。例如:
|
||||
|
||||
```jsx
|
||||
const prototype = {
|
||||
name: 'PageHeader',
|
||||
title: 'PageHeader',
|
||||
props: [
|
||||
{
|
||||
name: 'title',
|
||||
title: '标题',
|
||||
setter: 'stringSetter',
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
@@ -1,37 +0,0 @@
|
||||
import Link from '@docusaurus/Link';
|
||||
|
||||
# 简介
|
||||
|
||||
Tango 是一个用于快速构建低代码平台的低代码设计器框架,借助 Tango 只需要数行代码就可以完成一个基本的低代码平台前端系统的搭建。Tango 低代码设计器**直接读取前端项目的源代码,并以源代码为中心,执行和渲染前端视图,并为用户提供低代码可视化搭建能力,用户的搭建操作会转为对代码的修改**。借助 Tango 可以实现 **源码进,源码出**的效果。
|
||||
|
||||
## 引擎架构概览
|
||||
|
||||
Tango 低代码引擎在实现上进行了分层解藕,使得上层的低代码平台与底层的低代码引擎可以独立开发和维护,快速集成部署。另一方面,Tango 低代码引擎定义了一套开放的物料生态体系,开发者可以自由的贡献扩展组件配置能力的属性设置器,以及扩展低代码物料的二方三方业务组件。
|
||||
|
||||
如下图所示:
|
||||
|
||||
<img src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/18236990116/aabf/8a16/28ee/f0fd29a84c7aa40bb26383cdef12b88c.png" />
|
||||
|
||||
## 引擎设计器构成概览
|
||||
|
||||
Tango 低代码引擎设计器用于低成本初始化一个基本的低代码平台,其前端部分主要包括如下几个部分:
|
||||
|
||||
- 设计器主框架:进行外框架初始化,状态管理,拖拽引擎绑定等核心逻辑。
|
||||
- 侧边栏面板:提供可扩展的侧边面板,除了内置通用的面板,用户还可以任意添加自定义的面板。
|
||||
- 属性设置面板:属性设置器面板,支持用户进行属性配置。
|
||||
- 运行时沙箱:用于设计器运行时视图的渲染。
|
||||
- Web IDE:用于进行在线源代码编辑。
|
||||
|
||||
<img src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30108642346/b8cf/e86d/ef5a/514d90b722b5d8dc0e18516ed594a07b.png" />
|
||||
|
||||
## 基于源代码的引擎内核
|
||||
|
||||
Tango 低代码引擎不依赖私有搭建协议和 DSL,而是直接使用源代码驱动,引擎内部将源码转为 AST,用户的所有的搭建操作转为对 AST 的遍历和修改,进而将 AST 重新生成为代码,将代码同步给在线沙箱执行。与传统的借助 [Schema 驱动的低代码方案](https://mp.weixin.qq.com/s/yqYey76qLGYPfDtpGkVFfA) 相比,不受到私有 DSL 和协议的限制,能够完美的实现低代码搭建与源码开发的无缝集成。
|
||||
|
||||
<img src="https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13140534982/ee2e/f42c/cc9a/184e2918a011b57d46e6c64a2722fa44.png" />
|
||||
|
||||
## 源码进,源码出
|
||||
|
||||
由于引擎内核完全基于源代码驱动实现,Tango 低代码引擎能够实现源代码进,源代码出的可视化搭建能力,不提供任何私有的中间产物。从而使得,Tango 构建的线上研发能力可以与团队现有的研发服务(代码托管、构建、部署、CDN)无缝衔接与集成。
|
||||
|
||||
<img src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13208022400/b809/b82e/77b0/5e4fe78a8f11c8ed89c9ec9ced43e845.png" />
|
||||
@@ -1,136 +0,0 @@
|
||||
# 基础组件包规范
|
||||
|
||||
## 目录结构
|
||||
|
||||
```txt
|
||||
+ src
|
||||
+ button
|
||||
- view.tsx // 默认视图文件
|
||||
- index.ts // 渲染视图入口文件
|
||||
- designer.ts // 设计器视图入口文件
|
||||
- prototype.ts // 组件描述文件
|
||||
+ date-picker
|
||||
- index.ts // 组件包默认入口文件
|
||||
- designer.ts // 组件包设计器视图入口文件
|
||||
```
|
||||
|
||||
## 组件包入口
|
||||
|
||||
### 默认入口
|
||||
|
||||
`src/index.ts` 是组件包的默认出口文件。
|
||||
|
||||
### 设计视图入口
|
||||
|
||||
`src/designer.ts` 是组件包的设计视图出口文件。您至少应该导出 `menuData` 和 `prototypes` 两个模块:
|
||||
|
||||
```ts
|
||||
export { Button } from './button';
|
||||
export { Card } from './card';
|
||||
//...
|
||||
|
||||
// 组件的配置描述列表
|
||||
export const prototypes = [
|
||||
{
|
||||
title: '按钮',
|
||||
name: 'Button',
|
||||
props: [
|
||||
{
|
||||
name: 'size',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
//...
|
||||
],
|
||||
},
|
||||
//...
|
||||
];
|
||||
|
||||
export const menuData = {
|
||||
// 常用组件
|
||||
common: [
|
||||
{
|
||||
title: '基本',
|
||||
items: ['Button'],
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
其中 `menuData` 的 key 可选列表如下:
|
||||
|
||||
| key | 分类标题 |
|
||||
| ------- | -------- |
|
||||
| common | 常用组件 |
|
||||
| atom | 原子组件 |
|
||||
| snippet | 组合 |
|
||||
| block | 区块 |
|
||||
|
||||
## 组件入口
|
||||
|
||||
### 组件默认视图文件
|
||||
|
||||
`src/button/view.tsx`
|
||||
|
||||
```tsx
|
||||
export function Button({ children, ...rest }) {
|
||||
return <button {...rest}>{children}</button>;
|
||||
}
|
||||
```
|
||||
|
||||
需要注意的是,你需要将组件多余的属性 `...rest` 透传给跟结点 `<button {...rest}>`,以便于在设计器中时组件能够接收到跟踪信息。
|
||||
|
||||
### 组件默认入口文件
|
||||
|
||||
`src/button/index.ts`
|
||||
|
||||
```ts
|
||||
export * from './view';
|
||||
```
|
||||
|
||||
### 组件设计器视图文件
|
||||
|
||||
设计器视图是您的组件在 tango 设计器中进行展示的视图,大部分情况下,您无需关注此逻辑,特殊情况下,您的组件可能需要在设计器下有不一样的展现和数据透出,此时可以定义该视图文件实现。
|
||||
|
||||
`src/button/designer.tsx`
|
||||
|
||||
```tsx
|
||||
import { withDnd } from '@music/tango-apps-shared';
|
||||
import { Button as ButtonBase } from './view';
|
||||
|
||||
export const Button = withDnd({
|
||||
// 组件名
|
||||
name: 'Button',
|
||||
// 是否在设计器中有包裹层,用于设置 dnd 的追踪信息
|
||||
hasWrapper: false,
|
||||
})(ButtonBase);
|
||||
```
|
||||
|
||||
### 组件描述文件
|
||||
|
||||
`src/button/prototype.ts`
|
||||
|
||||
```ts
|
||||
import { ComponentPrototypeType } from '@music/tango-apps-shared';
|
||||
|
||||
export const Button: ComponentPrototypeType = {
|
||||
title: '按钮',
|
||||
name: 'Button',
|
||||
exportType: 'namedExport',
|
||||
icon: 'icon-anniu',
|
||||
type: 'element',
|
||||
package: '@music163/antd',
|
||||
hasChildren: false,
|
||||
props: [
|
||||
{
|
||||
name: 'children',
|
||||
title: '文案',
|
||||
setter: 'textSetter',
|
||||
initValue: '按钮',
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
## 依赖说明
|
||||
|
||||
如果您的组件会视图组件,推荐您的组件基于 `@music163/antd` 或 `@music163/antd` 的最新版本进行开发,且不要依赖其他 UI 组件包,例如 `antd`, `fusion` 等。
|
||||
@@ -1,148 +0,0 @@
|
||||
# 基础物料协议
|
||||
|
||||
import TypesTable from '@site/src/components/TypesTable';
|
||||
|
||||
物料协议定义了一个组件的基本信息,包括组件的名称、属性、拖拽规则等。物料协议是为组件附加的额外描述文件,不会侵入到你的组件代码中,因此你可以在不改动组件代码的情况下,为组件添加物料协议。
|
||||
|
||||
## 组件原型 prototype
|
||||
|
||||
<TypesTable name="ComponentPrototypeType" />
|
||||
|
||||
## 组件属性 ComponentPropType
|
||||
|
||||
<TypesTable name="ComponentPropType" />
|
||||
|
||||
### 嵌套属性
|
||||
|
||||
当某个组件属性为嵌套属性时,例如 `<Table scroll={{ x: 800, y: 400 }} />`,此时可以描述如下:
|
||||
|
||||
```jsx
|
||||
{
|
||||
name: 'Table',
|
||||
props: [
|
||||
{
|
||||
name: 'scroll',
|
||||
props: [
|
||||
{
|
||||
name: 'x',
|
||||
setter: 'numberSetter',
|
||||
},
|
||||
{
|
||||
name: 'y',
|
||||
setter: 'numberSetter',
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
### 属性的关联展示
|
||||
|
||||
当某个属性依赖某个特定的其他属性值时,可以借助 `getVisible` 实现关联展示控制:
|
||||
|
||||
```js
|
||||
{
|
||||
name: 'Button',
|
||||
props: [
|
||||
{
|
||||
name: 'shape',
|
||||
},
|
||||
{
|
||||
name: 'buttonType',
|
||||
getVisible: (form) => {
|
||||
// 配置项 buttonType 仅在配置项 shape 的值为 button 时才展示
|
||||
return form.getValue('shape') === 'button';
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### 动态属性设置
|
||||
|
||||
当组件需要依据某个属性值进行动态设置其他属性时,可以借助 `getProp` 实现控制:
|
||||
|
||||
```js
|
||||
{
|
||||
name: 'FormItem',
|
||||
props: [
|
||||
{
|
||||
name: 'component',
|
||||
title: '控件类型',
|
||||
},
|
||||
{
|
||||
name: 'componentProps',
|
||||
title: '子组件属性',
|
||||
getProp(form) {
|
||||
const type = form.getValue('component');
|
||||
const proto = { ...componentMap[type] };
|
||||
const props = omitProps(proto.props, [
|
||||
'placeholder',
|
||||
'options',
|
||||
'onChange',
|
||||
'defaultValue',
|
||||
'value',
|
||||
]);
|
||||
return {
|
||||
title: proto.title + '属性',
|
||||
props,
|
||||
};
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
### 属性的输入提示
|
||||
|
||||
当某个属性在输入时需要进行输入提示时,例如某个函数属性需要提示其签名的模版,则可以借助属性输入提示实现:
|
||||
|
||||
```js
|
||||
{
|
||||
name: 'TableColumn',
|
||||
props: [
|
||||
{
|
||||
name: 'render',
|
||||
setter: 'expressionSetter',
|
||||
autoCompleteOptions: ['(value, record, index) => { return null; }'],
|
||||
}
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 组件拖拽规则 ComponentDndRulesType
|
||||
|
||||
<TypesTable name="ComponentDndRulesType" />
|
||||
|
||||
### canDrag/canDrop
|
||||
|
||||
在 onDragStart 的时候执行。
|
||||
|
||||
```js
|
||||
export const Page = {
|
||||
name: 'Page',
|
||||
rules: {
|
||||
canDrag() {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### canMoveIn/canMoveOut
|
||||
|
||||
在 onDragEnter 的时候执行。
|
||||
|
||||
```js
|
||||
export const Modal = {
|
||||
name: 'Modal',
|
||||
rules: {
|
||||
canMoveIn(incomingName) {
|
||||
return !(incomingName === Modal.name);
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
@@ -1,167 +0,0 @@
|
||||
const lightCodeTheme = require('prism-react-renderer/themes/github');
|
||||
const darkCodeTheme = require('prism-react-renderer/themes/dracula');
|
||||
|
||||
/** @type {import('@docusaurus/types').Config} */
|
||||
const config = {
|
||||
title: '使用 Tango 构建你的低代码生产力工具',
|
||||
tagline: '基于源代码 AST 实现可视化搭建操作,支持实时出码,不受私有 DSL 和协议限制',
|
||||
favicon: 'img/favicon.ico',
|
||||
|
||||
// Set the production url of your site here
|
||||
url: 'https://netease.github.io',
|
||||
// Set the /<baseUrl>/ pathname under which your site is served
|
||||
// For GitHub pages deployment, it is often '/<projectName>/'
|
||||
baseUrl: '/tango',
|
||||
|
||||
// GitHub pages deployment config.
|
||||
// If you aren't using GitHub pages, you don't need these.
|
||||
organizationName: 'netease', // Usually your GitHub org/user name.
|
||||
projectName: 'tango', // Usually your repo name.
|
||||
|
||||
onBrokenLinks: 'throw',
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
|
||||
// Even if you don't use internalization, you can use this field to set useful
|
||||
// metadata like html lang. For example, if your site is Chinese, you may want
|
||||
// to replace "en" with "zh-Hans".
|
||||
i18n: {
|
||||
defaultLocale: 'zh-CN',
|
||||
locales: ['zh-CN', 'en'],
|
||||
},
|
||||
|
||||
// scripts: [{ src: 'https://buttons.github.io/buttons.js', async: true, defer: true }],
|
||||
|
||||
presets: [
|
||||
[
|
||||
'classic',
|
||||
/** @type {import('@docusaurus/preset-classic').Options} */
|
||||
({
|
||||
docs: {
|
||||
sidebarPath: require.resolve('./sidebars.js'),
|
||||
// Please change this to your repo.
|
||||
// Remove this to remove the "edit this page" links.
|
||||
editUrl:
|
||||
'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
|
||||
},
|
||||
blog: {
|
||||
showReadingTime: true,
|
||||
// Please change this to your repo.
|
||||
// Remove this to remove the "edit this page" links.
|
||||
editUrl:
|
||||
'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
|
||||
},
|
||||
theme: {
|
||||
customCss: require.resolve('./src/css/custom.css'),
|
||||
},
|
||||
}),
|
||||
],
|
||||
],
|
||||
|
||||
themeConfig:
|
||||
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
|
||||
({
|
||||
// Replace with your project's social card
|
||||
image: 'img/social-card.png',
|
||||
announcementBar: {
|
||||
id: 'notion_alert',
|
||||
content: '🏗 当前版本为测试版,请暂时不要用于生产环境,正式版将于2023年Q4发布!',
|
||||
backgroundColor: 'var(--ifm-color-primary-contrast-background)',
|
||||
textColor: 'var(--ifm-color-primary-contrast-foreground)',
|
||||
isCloseable: false,
|
||||
},
|
||||
navbar: {
|
||||
title: '',
|
||||
logo: {
|
||||
alt: 'Tango Logo',
|
||||
src: 'img/logo.png',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'docSidebar',
|
||||
sidebarId: 'designer',
|
||||
position: 'left',
|
||||
label: '引擎文档',
|
||||
},
|
||||
{
|
||||
type: 'docSidebar',
|
||||
sidebarId: 'boot',
|
||||
position: 'left',
|
||||
label: '应用框架',
|
||||
},
|
||||
{
|
||||
type: 'docSidebar',
|
||||
sidebarId: 'protocol',
|
||||
position: 'left',
|
||||
label: '协议',
|
||||
},
|
||||
{ to: '/blog', label: '博客', position: 'left' },
|
||||
{
|
||||
type: 'html',
|
||||
position: 'right',
|
||||
value: `<a class="navbar__link github-button" href="https://github.com/netease/tango" data-color-scheme="no-preference: light; light: light; dark: dark;" data-size="large" data-show-count="true" aria-label="Star netease/tango on GitHub">Github</a><script async defer src="https://buttons.github.io/buttons.js"></script>`,
|
||||
},
|
||||
{
|
||||
href: 'https://tango-demo.musicfe.com/designer/',
|
||||
label: '演示应用',
|
||||
position: 'right',
|
||||
},
|
||||
{
|
||||
type: 'localeDropdown',
|
||||
position: 'right',
|
||||
},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
style: 'dark',
|
||||
links: [
|
||||
{
|
||||
title: '文档',
|
||||
items: [
|
||||
{
|
||||
label: '快速开始',
|
||||
to: '/docs/designer/quick-start',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '社区',
|
||||
items: [
|
||||
{
|
||||
label: 'Stack Overflow',
|
||||
href: 'https://stackoverflow.com/questions/tagged/netease-tango',
|
||||
},
|
||||
{
|
||||
label: 'Discord',
|
||||
href: 'https://discordapp.com/invite/docusaurus',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '更多',
|
||||
items: [
|
||||
{
|
||||
label: '博客',
|
||||
to: '/blog',
|
||||
},
|
||||
{
|
||||
label: 'GitHub',
|
||||
href: 'https://github.com/netease/tango',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
logo: {
|
||||
alt: 'NetEase Cloud Music',
|
||||
src: 'https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/15400855445/6c8b/10d4/a8a8/452c8518b0c0a660549996d366cdff77.png',
|
||||
width: 320,
|
||||
},
|
||||
copyright: `Copyright © ${new Date().getFullYear()} NetEase Cloud Music, Inc.`,
|
||||
},
|
||||
prism: {
|
||||
theme: lightCodeTheme,
|
||||
darkTheme: darkCodeTheme,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
@@ -1,318 +0,0 @@
|
||||
{
|
||||
"homepage.hero.alert": {
|
||||
"message": "🏗 The current version is 0.x alpha version. Please do not use it in a production environment. The 1.0 version will be released in 2023Q4."
|
||||
},
|
||||
"homepage.hero.title": {
|
||||
"message": "Develop your low-code tools based on Tango"
|
||||
},
|
||||
"homepage.hero.tagline": {
|
||||
"message": "Source code based low-code engine, support realtime code generation, without private schema and DSL"
|
||||
},
|
||||
"homepage.hero.button.document": {
|
||||
"message": "Document"
|
||||
},
|
||||
"homepage.timeline.alpha": {
|
||||
"message": "Core codebase published, documentation site released"
|
||||
},
|
||||
"homepage.timeline.beta": {
|
||||
"message": "Core API refactoring and optimization for community scenarios"
|
||||
},
|
||||
"homepage.timeline.rc": {
|
||||
"message": "The core API is basically stable and no longer Breaking Changes."
|
||||
},
|
||||
"homepage.timeline.stable": {
|
||||
"message": "API is completely stable, provides good community support, and can be used in production environments."
|
||||
},
|
||||
"homepage.features.low-code": {
|
||||
"message": "LowCode"
|
||||
},
|
||||
"homepage.features.low-code-content": {
|
||||
"message": "Provide a general low-code engine that supports quickly creating and modifying applications through drag-and-drop and configuration, making pattern-based application development easy."
|
||||
},
|
||||
"homepage.features.code-driven": {
|
||||
"message": "Code Driven"
|
||||
},
|
||||
"homepage.features.code-driven-content": {
|
||||
"message": "The underlying low-code engine is driven by source code, without proprietary building protocols and DSL, supporting online low-code and source code dual-mode synchronous development."
|
||||
},
|
||||
"homepage.features.outbox": {
|
||||
"message": "Outbox"
|
||||
},
|
||||
"homepage.features.outbox-content": {
|
||||
"message": "Provide out-of-the-box low-code designer components, support easy gateways, and quickly build low-code productivity tools based on low-code engines."
|
||||
},
|
||||
"theme.ErrorPageContent.title": {
|
||||
"message": "This page crashed.",
|
||||
"description": "The title of the fallback page when the page crashed"
|
||||
},
|
||||
"theme.NotFound.title": {
|
||||
"message": "Page Not Found",
|
||||
"description": "The title of the 404 page"
|
||||
},
|
||||
"theme.NotFound.p1": {
|
||||
"message": "We could not find what you were looking for.",
|
||||
"description": "The first paragraph of the 404 page"
|
||||
},
|
||||
"theme.NotFound.p2": {
|
||||
"message": "Please contact the owner of the site that linked you to the original URL and let them know their link is broken.",
|
||||
"description": "The 2nd paragraph of the 404 page"
|
||||
},
|
||||
"theme.admonition.note": {
|
||||
"message": "note",
|
||||
"description": "The default label used for the Note admonition (:::note)"
|
||||
},
|
||||
"theme.admonition.tip": {
|
||||
"message": "tip",
|
||||
"description": "The default label used for the Tip admonition (:::tip)"
|
||||
},
|
||||
"theme.admonition.danger": {
|
||||
"message": "danger",
|
||||
"description": "The default label used for the Danger admonition (:::danger)"
|
||||
},
|
||||
"theme.admonition.info": {
|
||||
"message": "info",
|
||||
"description": "The default label used for the Info admonition (:::info)"
|
||||
},
|
||||
"theme.admonition.caution": {
|
||||
"message": "caution",
|
||||
"description": "The default label used for the Caution admonition (:::caution)"
|
||||
},
|
||||
"theme.blog.archive.title": {
|
||||
"message": "Archive",
|
||||
"description": "The page & hero title of the blog archive page"
|
||||
},
|
||||
"theme.blog.archive.description": {
|
||||
"message": "Archive",
|
||||
"description": "The page & hero description of the blog archive page"
|
||||
},
|
||||
"theme.BackToTopButton.buttonAriaLabel": {
|
||||
"message": "Scroll back to top",
|
||||
"description": "The ARIA label for the back to top button"
|
||||
},
|
||||
"theme.blog.paginator.navAriaLabel": {
|
||||
"message": "Blog list page navigation",
|
||||
"description": "The ARIA label for the blog pagination"
|
||||
},
|
||||
"theme.blog.paginator.newerEntries": {
|
||||
"message": "Newer Entries",
|
||||
"description": "The label used to navigate to the newer blog posts page (previous page)"
|
||||
},
|
||||
"theme.blog.paginator.olderEntries": {
|
||||
"message": "Older Entries",
|
||||
"description": "The label used to navigate to the older blog posts page (next page)"
|
||||
},
|
||||
"theme.blog.post.paginator.navAriaLabel": {
|
||||
"message": "Blog post page navigation",
|
||||
"description": "The ARIA label for the blog posts pagination"
|
||||
},
|
||||
"theme.blog.post.paginator.newerPost": {
|
||||
"message": "Newer Post",
|
||||
"description": "The blog post button label to navigate to the newer/previous post"
|
||||
},
|
||||
"theme.blog.post.paginator.olderPost": {
|
||||
"message": "Older Post",
|
||||
"description": "The blog post button label to navigate to the older/next post"
|
||||
},
|
||||
"theme.blog.post.plurals": {
|
||||
"message": "One post|{count} posts",
|
||||
"description": "Pluralized label for \"{count} posts\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
|
||||
},
|
||||
"theme.blog.tagTitle": {
|
||||
"message": "{nPosts} tagged with \"{tagName}\"",
|
||||
"description": "The title of the page for a blog tag"
|
||||
},
|
||||
"theme.tags.tagsPageLink": {
|
||||
"message": "View All Tags",
|
||||
"description": "The label of the link targeting the tag list page"
|
||||
},
|
||||
"theme.colorToggle.ariaLabel": {
|
||||
"message": "Switch between dark and light mode (currently {mode})",
|
||||
"description": "The ARIA label for the navbar color mode toggle"
|
||||
},
|
||||
"theme.colorToggle.ariaLabel.mode.dark": {
|
||||
"message": "dark mode",
|
||||
"description": "The name for the dark color mode"
|
||||
},
|
||||
"theme.colorToggle.ariaLabel.mode.light": {
|
||||
"message": "light mode",
|
||||
"description": "The name for the light color mode"
|
||||
},
|
||||
"theme.docs.breadcrumbs.navAriaLabel": {
|
||||
"message": "Breadcrumbs",
|
||||
"description": "The ARIA label for the breadcrumbs"
|
||||
},
|
||||
"theme.docs.DocCard.categoryDescription": {
|
||||
"message": "{count} items",
|
||||
"description": "The default description for a category card in the generated index about how many items this category includes"
|
||||
},
|
||||
"theme.docs.paginator.navAriaLabel": {
|
||||
"message": "Docs pages",
|
||||
"description": "The ARIA label for the docs pagination"
|
||||
},
|
||||
"theme.docs.paginator.previous": {
|
||||
"message": "Previous",
|
||||
"description": "The label used to navigate to the previous doc"
|
||||
},
|
||||
"theme.docs.paginator.next": {
|
||||
"message": "Next",
|
||||
"description": "The label used to navigate to the next doc"
|
||||
},
|
||||
"theme.docs.tagDocListPageTitle.nDocsTagged": {
|
||||
"message": "One doc tagged|{count} docs tagged",
|
||||
"description": "Pluralized label for \"{count} docs tagged\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
|
||||
},
|
||||
"theme.docs.tagDocListPageTitle": {
|
||||
"message": "{nDocsTagged} with \"{tagName}\"",
|
||||
"description": "The title of the page for a docs tag"
|
||||
},
|
||||
"theme.docs.versionBadge.label": {
|
||||
"message": "Version: {versionLabel}"
|
||||
},
|
||||
"theme.docs.versions.unreleasedVersionLabel": {
|
||||
"message": "This is unreleased documentation for {siteTitle} {versionLabel} version.",
|
||||
"description": "The label used to tell the user that he's browsing an unreleased doc version"
|
||||
},
|
||||
"theme.docs.versions.unmaintainedVersionLabel": {
|
||||
"message": "This is documentation for {siteTitle} {versionLabel}, which is no longer actively maintained.",
|
||||
"description": "The label used to tell the user that he's browsing an unmaintained doc version"
|
||||
},
|
||||
"theme.docs.versions.latestVersionSuggestionLabel": {
|
||||
"message": "For up-to-date documentation, see the {latestVersionLink} ({versionLabel}).",
|
||||
"description": "The label used to tell the user to check the latest version"
|
||||
},
|
||||
"theme.docs.versions.latestVersionLinkLabel": {
|
||||
"message": "latest version",
|
||||
"description": "The label used for the latest version suggestion link label"
|
||||
},
|
||||
"theme.common.editThisPage": {
|
||||
"message": "Edit this page",
|
||||
"description": "The link label to edit the current page"
|
||||
},
|
||||
"theme.common.headingLinkTitle": {
|
||||
"message": "Direct link to {heading}",
|
||||
"description": "Title for link to heading"
|
||||
},
|
||||
"theme.lastUpdated.atDate": {
|
||||
"message": " on {date}",
|
||||
"description": "The words used to describe on which date a page has been last updated"
|
||||
},
|
||||
"theme.lastUpdated.byUser": {
|
||||
"message": " by {user}",
|
||||
"description": "The words used to describe by who the page has been last updated"
|
||||
},
|
||||
"theme.lastUpdated.lastUpdatedAtBy": {
|
||||
"message": "Last updated{atDate}{byUser}",
|
||||
"description": "The sentence used to display when a page has been last updated, and by who"
|
||||
},
|
||||
"theme.navbar.mobileVersionsDropdown.label": {
|
||||
"message": "Versions",
|
||||
"description": "The label for the navbar versions dropdown on mobile view"
|
||||
},
|
||||
"theme.tags.tagsListLabel": {
|
||||
"message": "Tags:",
|
||||
"description": "The label alongside a tag list"
|
||||
},
|
||||
"theme.AnnouncementBar.closeButtonAriaLabel": {
|
||||
"message": "Close",
|
||||
"description": "The ARIA label for close button of announcement bar"
|
||||
},
|
||||
"theme.blog.sidebar.navAriaLabel": {
|
||||
"message": "Blog recent posts navigation",
|
||||
"description": "The ARIA label for recent posts in the blog sidebar"
|
||||
},
|
||||
"theme.CodeBlock.copied": {
|
||||
"message": "Copied",
|
||||
"description": "The copied button label on code blocks"
|
||||
},
|
||||
"theme.CodeBlock.copyButtonAriaLabel": {
|
||||
"message": "Copy code to clipboard",
|
||||
"description": "The ARIA label for copy code blocks button"
|
||||
},
|
||||
"theme.CodeBlock.copy": {
|
||||
"message": "Copy",
|
||||
"description": "The copy button label on code blocks"
|
||||
},
|
||||
"theme.CodeBlock.wordWrapToggle": {
|
||||
"message": "Toggle word wrap",
|
||||
"description": "The title attribute for toggle word wrapping button of code block lines"
|
||||
},
|
||||
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": {
|
||||
"message": "Toggle the collapsible sidebar category '{label}'",
|
||||
"description": "The ARIA label to toggle the collapsible sidebar category"
|
||||
},
|
||||
"theme.NavBar.navAriaLabel": {
|
||||
"message": "Main",
|
||||
"description": "The ARIA label for the main navigation"
|
||||
},
|
||||
"theme.navbar.mobileLanguageDropdown.label": {
|
||||
"message": "Languages",
|
||||
"description": "The label for the mobile language switcher dropdown"
|
||||
},
|
||||
"theme.TOCCollapsible.toggleButtonLabel": {
|
||||
"message": "On this page",
|
||||
"description": "The label used by the button on the collapsible TOC component"
|
||||
},
|
||||
"theme.blog.post.readMore": {
|
||||
"message": "Read More",
|
||||
"description": "The label used in blog post item excerpts to link to full blog posts"
|
||||
},
|
||||
"theme.blog.post.readMoreLabel": {
|
||||
"message": "Read more about {title}",
|
||||
"description": "The ARIA label for the link to full blog posts from excerpts"
|
||||
},
|
||||
"theme.blog.post.readingTime.plurals": {
|
||||
"message": "One min read|{readingTime} min read",
|
||||
"description": "Pluralized label for \"{readingTime} min read\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
|
||||
},
|
||||
"theme.docs.breadcrumbs.home": {
|
||||
"message": "Home page",
|
||||
"description": "The ARIA label for the home page in the breadcrumbs"
|
||||
},
|
||||
"theme.docs.sidebar.collapseButtonTitle": {
|
||||
"message": "Collapse sidebar",
|
||||
"description": "The title attribute for collapse button of doc sidebar"
|
||||
},
|
||||
"theme.docs.sidebar.collapseButtonAriaLabel": {
|
||||
"message": "Collapse sidebar",
|
||||
"description": "The title attribute for collapse button of doc sidebar"
|
||||
},
|
||||
"theme.docs.sidebar.navAriaLabel": {
|
||||
"message": "Docs sidebar",
|
||||
"description": "The ARIA label for the sidebar navigation"
|
||||
},
|
||||
"theme.docs.sidebar.closeSidebarButtonAriaLabel": {
|
||||
"message": "Close navigation bar",
|
||||
"description": "The ARIA label for close button of mobile sidebar"
|
||||
},
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": {
|
||||
"message": "← Back to main menu",
|
||||
"description": "The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)"
|
||||
},
|
||||
"theme.docs.sidebar.toggleSidebarButtonAriaLabel": {
|
||||
"message": "Toggle navigation bar",
|
||||
"description": "The ARIA label for hamburger menu button of mobile navigation"
|
||||
},
|
||||
"theme.docs.sidebar.expandButtonTitle": {
|
||||
"message": "Expand sidebar",
|
||||
"description": "The ARIA label and title attribute for expand button of doc sidebar"
|
||||
},
|
||||
"theme.docs.sidebar.expandButtonAriaLabel": {
|
||||
"message": "Expand sidebar",
|
||||
"description": "The ARIA label and title attribute for expand button of doc sidebar"
|
||||
},
|
||||
"theme.ErrorPageContent.tryAgain": {
|
||||
"message": "Try again",
|
||||
"description": "The label of the button to try again rendering when the React error boundary captures an error"
|
||||
},
|
||||
"theme.common.skipToMainContent": {
|
||||
"message": "Skip to main content",
|
||||
"description": "The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation"
|
||||
},
|
||||
"theme.tags.tagsPageTitle": {
|
||||
"message": "Tags",
|
||||
"description": "The title of the tag list page"
|
||||
},
|
||||
"homepage.hero.button.playground": {
|
||||
"message": "Playground"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 279 KiB |
|
Before Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 61 KiB |
@@ -1,80 +0,0 @@
|
||||
---
|
||||
title: Tango LowCode Builder Released!
|
||||
authors: [wwsun]
|
||||
tags: [netease, tango, open-source]
|
||||
---
|
||||
|
||||
## 📝 Introduction
|
||||
|
||||
Tango is a low-code builder framework for quickly building low-code platforms or tools. With Tango, you only need a few lines of code to build a basic low-code builder. The Tango low-code builder reads the source code of the frontend project directly, executes and renders the frontend view based on the source code, and provides users with low-code visual building capabilities. User's building operations will be translated into modifications to the source code. With low-code tools or platforms built using Tango, it is possible to achieve the effect of "source code in, source code out" and seamlessly integrate with the existing development flow within the enterprise.
|
||||
|
||||
### ✨ Features
|
||||
|
||||
- After actual testing in the production environment of NetEase Cloud Music, it can be flexibly integrated and applied to low-code platforms, local development tools, and more.
|
||||
- Driven by source code AST, with no private DSL and protocols.
|
||||
- Provides real-time code output capability, supports source code input and output.
|
||||
- Out-of-the-box front-end low-code designer, providing flexible and easy-to-use designer React components.
|
||||
- Developed using TypeScript, providing complete type definition files.
|
||||
|
||||
### 🏗️ Code-based Low-Code Solution
|
||||
|
||||
Due to the fact that the engine kernel is completely based on the source code-driven implementation, Tango low-code engine can achieve the visualization building capability of source code input and source code output, without providing any private intermediate products. If the company already has a complete R&D system (code hosting, building, deployment, CDN), Tango low-code engine can be directly used to integrate with existing services to build a low-code development platform.
|
||||
|
||||

|
||||
|
||||
### 📄 Code in, Code out
|
||||
|
||||
Due to the engine kernel being completely driven by source code, the Tango low-code engine can achieve the visual construction capability of source code in and source code out, without providing any private intermediate products. If the company already has a complete development system (code hosting, building, deployment, CDN), then it can directly use the Tango low-code engine to integrate with existing services to build a low-code development platform.
|
||||
|
||||

|
||||
|
||||
### 🏆 Advantages
|
||||
|
||||
Compared to the low-code building solution based on private schema, Tango low-code engine has the following advantages:
|
||||
|
||||
| Comparison Items | Schema-base low-code solution | Tango (Code-based) |
|
||||
| ------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| Applicable Scenarios | Targeted vertical building scenarios, such as forms, marketing pages, etc. | 🔥 Applicable to application building scenarios centered on source code |
|
||||
| Language Capabilities | Relies on private protocols for extension, inflexible, and difficult to align with programming language capabilities | 🔥 Based directly on JavaScript language, can use all language features, no extensibility issues |
|
||||
| Development Capability | LowCode | 🔥 LowCode + ProCode |
|
||||
| Source Code Export | Centered on schema, one-way code output, irreversible | 🔥 Source code-centered, two-way transcoding |
|
||||
| Custom Dependencies | Need to be encapsulated according to private protocol extension, high customization cost | 🔥 Existing components can be seamlessly and low-cost integrated |
|
||||
| Integrated R&D Facilities | High customization cost, additional customization required | 🔥 Low-cost integration, can directly reuse existing deployment and publishing capabilities |
|
||||
|
||||
The specific technical architecture is shown in the following figure:
|
||||
|
||||
## 📐 Architecture
|
||||
|
||||
The Tango low-code engine has been layered and decoupled in implementation, enabling independent development and maintenance of the upper-level low-code platform and the lower-level low-code engine, allowing for fast integration and deployment. In addition, Tango's low-code engine defines an open material ecosystem, allowing developers to freely contribute to extension components and configure property setters that enhance low-code materials.
|
||||
|
||||
The specific technical architecture is shown in the following diagram.
|
||||
|
||||

|
||||
|
||||
## ⏰ Milestones
|
||||
|
||||
Tango low-code engine is the core component of NetEase Cloud Music's internal low-code platform. Open sourcing involves a lot of decoupling work of core logic, which will bring us a lot of additional work in our normal work. Therefore, we plan to promote the open source of Tango low-code engine in stages.
|
||||
|
||||
1. Today we officially released the first community version of Tango low-code engine, which will include the core code library of Tango low-code engine, TangoBoot application framework, and low-code component library adapted to antd v4.
|
||||
2. We plan to release the 1.0 Beta version of the low-code engine on September 30th this year. This version will refactor the core implementation for community scenarios, remove some compatibility code we have internally in Cloud Music, and optimize the implementation of the core.
|
||||
3. We plan to release the 1.0 RC version of the low-code engine on October 30th this year. This version will ensure that the core API is basically stable and no longer has BREAKING CHANGE. At the same time, we will provide comprehensive and detailed development guides, deployment documents, and demonstration applications.
|
||||
4. The official version will be released before the end of Q4 2023, and we will further improve our open source community operation mechanism.
|
||||
|
||||

|
||||
|
||||
## 🤝 Community
|
||||
|
||||
Our open source work is actively advancing, and you can learn about our latest progress through the following information:
|
||||
|
||||
- Github Repository: <https://github.com/NetEase/tango>
|
||||
- Documentation Site: <https://netease.github.io/tango/>
|
||||
|
||||
Welcome everyone to join our community and participate in the open source construction of the Tango low-code engine. Any questions can be reported to us through [Github Issues](https://github.com/NetEase/tango/issues), and we will follow up and deal with them promptly.
|
||||
|
||||
## 💗 Acknowledgements
|
||||
|
||||
Thanks to the Netease Cloud Music Public Technical Team, the Front-End Team, the Live Broadcasting Technical Team, and all the colleagues who participated in the Tango project.
|
||||
|
||||
Thanks to the Sandpack project provided by CodeSandbox, which provides powerful browser-based code building and execution capabilities for Tango.
|
||||
|
||||
Tanks to the Babel project, which provides powerful javascript ast parsing and traversing capabilities for Tango.
|
||||
|
Before Width: | Height: | Size: 26 KiB |
@@ -1,5 +0,0 @@
|
||||
wwsun:
|
||||
name: Wells SUN
|
||||
title: Core maintainer
|
||||
url: https://github.com/wwsun
|
||||
image_url: https://github.com/wwsun.png
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"title": {
|
||||
"message": "Blog",
|
||||
"description": "The title for the blog used in SEO"
|
||||
},
|
||||
"description": {
|
||||
"message": "Blog",
|
||||
"description": "The description for the blog used in SEO"
|
||||
},
|
||||
"sidebar.title": {
|
||||
"message": "Recent posts",
|
||||
"description": "The label for the left sidebar"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"version.label": {
|
||||
"message": "Next",
|
||||
"description": "The label for version current"
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
# Directory Structure
|
||||
|
||||
## Specification
|
||||
|
||||
在目录结构上,我们约定了一套通用的目录结构
|
||||
|
||||
```md
|
||||
── src
|
||||
│ +── assets
|
||||
│ +── pages
|
||||
│ +── components
|
||||
│ +── services/index.js
|
||||
│ +── stores
|
||||
│ ├── routes.js // route config
|
||||
│ ├── global.less // global styles
|
||||
| |-- index.js // app entry
|
||||
└── package.json
|
||||
|---tango.config.json // config for low-code engine
|
||||
```
|
||||
|
||||
## Custom Components
|
||||
|
||||
This directory is used to store locally developed components, such as:
|
||||
|
||||
```jsx
|
||||
// src/components/sample.jsx
|
||||
import React from 'react';
|
||||
import { Box } from '@music163/antd';
|
||||
|
||||
export function Sample(props) {
|
||||
return <Box {...props}>sample component</Box>;
|
||||
}
|
||||
```
|
||||
|
||||
:::info
|
||||
Note that if you want the Sample component to be parsed and built by the designer, you must use MusicOne's `Box` component instead of the common `div` element. You can also use `<Box as="span" />` to change the rendered HTML element.
|
||||
:::
|
||||
@@ -1,134 +0,0 @@
|
||||
# App Entry
|
||||
|
||||
The framework creates and renders the entire application through `runApp`, and global configurations of the application can be passed in during the creation of the application.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configure the application globally through `src/index.js`, set up routes, runtime environment, service functions, state models, and more.
|
||||
|
||||
```js
|
||||
import { runApp, tangoBootConfig } from '@music163/tango-boot';
|
||||
import { message } from '@music163/antd';
|
||||
|
||||
// Configure the default display method for error messages that occur during requests, using the message component to display the message overlay.
|
||||
tangoBootConfig.toast = message;
|
||||
|
||||
runApp({
|
||||
// boot config
|
||||
boot: {},
|
||||
|
||||
// container components
|
||||
providers: [],
|
||||
|
||||
// store instances
|
||||
stores: {},
|
||||
|
||||
// service functions
|
||||
services: {},
|
||||
|
||||
// route config
|
||||
router: {},
|
||||
});
|
||||
```
|
||||
|
||||
## App Boot
|
||||
|
||||
The `boot` configuration item is used for the startup configuration of the application.
|
||||
|
||||
```js
|
||||
runApp({
|
||||
boot: {
|
||||
// react-dom mount element
|
||||
mountElement: document.querySelector('#root'),
|
||||
|
||||
// enable qiankun micro-app support, `qiankun: { appName: string }`
|
||||
qiankun: false,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Route Config
|
||||
|
||||
`router` configuration item is used to configure the front-end routing of the application, which is implemented based on `react-router`.
|
||||
|
||||
```js
|
||||
import routes from './routes';
|
||||
|
||||
runApp({
|
||||
router: {
|
||||
// 路由类型: hash | browser
|
||||
type: 'hash',
|
||||
|
||||
// 路由配置信息
|
||||
config: routes,
|
||||
|
||||
// basename,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
TangoBoot uses the [Static Routing Configuration Scheme](https://github.com/remix-run/react-router/tree/v5/packages/react-router-config) of ReactRouter. The specific routing configuration file is as follows:
|
||||
|
||||
```js
|
||||
// routes.js
|
||||
import Index from './pages/index';
|
||||
import About from './pages/about';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
exact: true,
|
||||
component: Index,
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
component: About,
|
||||
},
|
||||
{
|
||||
path: '/user/:id',
|
||||
component: About,
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
```
|
||||
|
||||
## Container Components
|
||||
|
||||
Sometimes, you may want to wrap the root component with specific container components such as multi-language configuration, state container, etc. Since `runApp` encapsulates the rendering logic of the root component, you can use the `providers` configuration option.
|
||||
|
||||
```js
|
||||
runApp({
|
||||
// Wrap the instance of the providers component in the order in which it is passed in.
|
||||
providers: [<ConfigProvider />, <LocaleProvider />],
|
||||
});
|
||||
```
|
||||
|
||||
## Environment Variables {#env}
|
||||
|
||||
Sometimes you may need to customize some environment variables to quickly determine when the application is running in different environments. You can use `tango.env` to quickly obtain the application's environment information. It should be noted that by default there will be no environment information, and you can set the desired environment variables by passing `getEnv` in `runApp` when needed.
|
||||
|
||||
For example:
|
||||
|
||||
```js
|
||||
runApp({
|
||||
// You can return an object with the environment information of the custom application.
|
||||
getEnv() {
|
||||
if (location.origin.includes('tango')) {
|
||||
// tango designer
|
||||
return 'development';
|
||||
}
|
||||
if (location.origin.includes('localhost')) {
|
||||
// local environment
|
||||
return 'local';
|
||||
}
|
||||
return 'production';
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Consume environment variables anywhere, for example:
|
||||
|
||||
```jsx
|
||||
<Box isRender={tango.env === 'development'}>A box only render in development environment</Box>
|
||||
```
|
||||
@@ -1,87 +0,0 @@
|
||||
# Global Variable
|
||||
|
||||
TangoBoot provides a global singleton mode by default, which means that you can always use this global singleton to access necessary application runtime information in your project.
|
||||
|
||||
:::tip
|
||||
Singleton means a single instance, also called "singleton pattern," which is a common design pattern. It limits the number of times a class can be instantiated to only once, so that a class has only one instance and provides a global access point to it. In the context of TangoBoot, "singleton" means that your application will always have only one instance of TangoBoot, and you can consistently obtain application-to-runtime information through this single instance.
|
||||
:::
|
||||
|
||||
## Get the global variable
|
||||
|
||||
```js
|
||||
// case1: get from package
|
||||
import tango from '@music163/tango-boot';
|
||||
|
||||
// case2: get from window
|
||||
window.tango;
|
||||
|
||||
// both are the same
|
||||
console.log(tango === window.tango); // true
|
||||
```
|
||||
|
||||
### Get data from the global `tango` variable
|
||||
|
||||
```js
|
||||
// tango
|
||||
{
|
||||
// custom configs
|
||||
config: {},
|
||||
|
||||
// custom helper functions
|
||||
helpers: {},
|
||||
|
||||
// History
|
||||
history: History;
|
||||
|
||||
// service functions
|
||||
services: {},
|
||||
|
||||
// store instances
|
||||
stores: {},
|
||||
|
||||
// builtin helpers
|
||||
closeModal: Function;
|
||||
openModal: Function;
|
||||
setStoreValue: Function;
|
||||
getStoreValue: Function;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Guide
|
||||
|
||||
### Use in store
|
||||
|
||||
```jsx
|
||||
import tango, { defineModel } from '@music163/tango-boot';
|
||||
|
||||
export default defineModel(
|
||||
{
|
||||
title: 'hello',
|
||||
|
||||
async listUsers() {
|
||||
const data = await tango.services.listUsers();
|
||||
this.title = data;
|
||||
},
|
||||
},
|
||||
'app'
|
||||
);
|
||||
```
|
||||
|
||||
### Use in page runtime
|
||||
|
||||
```jsx
|
||||
import tango, { definePage } from '@music163/tango-boot';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Box>
|
||||
{tango.stores.app.title}
|
||||
|
||||
<Button onClick={tango.services.list}>request</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default definePage(App);
|
||||
```
|
||||
@@ -1,130 +0,0 @@
|
||||
# 常用的工具函数
|
||||
|
||||
tango-boot 提供了一组便捷的工具函数,用于应对日常开发过程中的典型操作逻辑。
|
||||
|
||||
### `getUser()`
|
||||
|
||||
> 从 v1.15.0 开始提供
|
||||
|
||||
获取当前的登录用户
|
||||
|
||||
```js
|
||||
const user = tango.getUser();
|
||||
```
|
||||
|
||||
### `checkPrivilege({ appCode, privilegeCode })`
|
||||
|
||||
> 从 v1.15.0 开始提供
|
||||
|
||||
检查 PMS 权限
|
||||
|
||||
```js
|
||||
const test = await tango.checkPrivilege({ appCode, privilegeCode });
|
||||
```
|
||||
|
||||
### `copyToClipboard(text: string)`
|
||||
|
||||
复制文本到剪贴板
|
||||
|
||||
```js
|
||||
tango.copyToClipboard('hello world');
|
||||
```
|
||||
|
||||
### `navigateTo(link: string, data?: object)`
|
||||
|
||||
导航到其他前端路由
|
||||
|
||||
```js
|
||||
// 基本用法
|
||||
tango.navigateTo('/about');
|
||||
|
||||
// 带路由参数
|
||||
tango.navigateTo('/about', { the: 'query' }); // /about?the=query
|
||||
```
|
||||
|
||||
### `showToast(text: string, type: string, time: number)`
|
||||
|
||||
- text 消息正文
|
||||
- type 消息类型 `success | error | warn`
|
||||
- time 消息展示的时间,单位秒,默认为 3 秒
|
||||
|
||||
消息提示
|
||||
|
||||
```js
|
||||
tango.showToast('hello world');
|
||||
|
||||
tango.showToast('something is error', 'error', 3);
|
||||
```
|
||||
|
||||
### `openModal(modalId: string)`
|
||||
|
||||
根据弹层 ID 唤起目标弹层
|
||||
|
||||
```js
|
||||
tango.openModal('modal123');
|
||||
```
|
||||
|
||||
### `closeModal(modalId: string)`
|
||||
|
||||
根据弹层 ID 关闭目标弹层
|
||||
|
||||
```js
|
||||
tango.closeModal('modal123');
|
||||
```
|
||||
|
||||
### `formatDate(date, format)`
|
||||
|
||||
- date: `number | string | Moment` 日期值
|
||||
- format: `string` 格式化,取值参考 https://momentjs.com/docs/#/displaying/format/
|
||||
|
||||
格式化日期值
|
||||
|
||||
```js
|
||||
tango.formatDate(1659496225323, 'YYYY-MM-DD HH:mm:ss'); // 2022-08-03 11:10:25
|
||||
|
||||
tango.formatDate('2022-11-11', 'X'); // 1668096000
|
||||
```
|
||||
|
||||
### `formatNumber(number, locale, options)`
|
||||
|
||||
- number: `number`
|
||||
- locale: `string`
|
||||
- options: `Intl.NumberFormatOptions`,参考取值 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat
|
||||
|
||||
格式化数字
|
||||
|
||||
```js
|
||||
tango.formatNumber(99999); // 99,999
|
||||
```
|
||||
|
||||
### `getCurrentLanguage`
|
||||
|
||||
获取当前语种
|
||||
|
||||
```js
|
||||
tango.getCurrentLanguage(); // en-US
|
||||
```
|
||||
|
||||
### `setLanguage(localeCode)`
|
||||
|
||||
设置当前语种
|
||||
|
||||
```js
|
||||
tango.setLanguage('en-US');
|
||||
```
|
||||
|
||||
### `getLanguageList`
|
||||
|
||||
获取当前应用可用语种列表
|
||||
|
||||
```js
|
||||
tango.getLanguageList();
|
||||
```
|
||||
|
||||
### `trans(module:key)`
|
||||
|
||||
根据多语言 key 获取当前语种文案
|
||||
|
||||
```js
|
||||
tango.trans('common:add'); // 新增
|
||||
```
|
||||
@@ -1,24 +0,0 @@
|
||||
# 国际化
|
||||
|
||||
TangoBoot 本身不提供国际化功能,国际化和您的 UI 组件库相关,在云音乐,我们采用的是基于 `@music163/antd` 的 UI 层解决方案。可以借助应用入口的容器配置功能进行国际化配置。
|
||||
|
||||
:::tip
|
||||
如果您还不了解应用入口配置,可以先访问 [应用入口](./app) 一节进行了解。
|
||||
:::
|
||||
|
||||
## 配置示例
|
||||
|
||||
```js
|
||||
import { runApp } from '@music163/tango-boot';
|
||||
import { ConfigProvider } from '@music163/antd';
|
||||
import locale from '@music163/antd/es/locale/zh_CN';
|
||||
import 'moment/dist/locale/zh-cn';
|
||||
|
||||
// 设置 moment 的文案
|
||||
moment.locale('zh-cn');
|
||||
|
||||
runApp({
|
||||
// 设置 UI 组件库的文案配置
|
||||
providers: [<ConfigProvider locale={locale} />],
|
||||
});
|
||||
```
|
||||
@@ -1,138 +0,0 @@
|
||||
# TangoBoot
|
||||
|
||||
TangoBoot is a frontend application framework that serves the Tango low-code application development system. It provides standard data requests, state management, and routing solutions, as well as generic runtime utility functions, allowing developers to generate single-page applications through simple configuration.
|
||||
|
||||
## Application Architecture
|
||||
|
||||
The application architecture of TangoBoot uses the View-Model-Service three-layer model. The model layer defines Observable States, the view layer observes the changes of the model and updates automatically, and the service layer is used to create a set of service functions for the consumption of the view layer and the model layer. The diagram is shown in the figure below:
|
||||
|
||||
<img
|
||||
alt="image"
|
||||
width="600px"
|
||||
src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13760951704/985c/9706/7f18/be713816a143d3a054f51c9d1fc59b36.png"
|
||||
/>
|
||||
|
||||
## Core API
|
||||
|
||||
- `runApp` create the app entry
|
||||
- `definePage` define reactive views
|
||||
- `defineStore` define observable states
|
||||
- `defineServices` define async service functions
|
||||
|
||||
### Create app entry
|
||||
|
||||
The `index.js` is the app entry file,a simple example is:
|
||||
|
||||
```jsx
|
||||
import { runApp } from '@music163/tango-boot';
|
||||
import routes from './routes';
|
||||
import services from './services';
|
||||
import home from './stores/home';
|
||||
import counter from './stores/counter';
|
||||
import './global.less';
|
||||
|
||||
const { mount, unmount, bootstrap } = runApp({
|
||||
boot: {
|
||||
mountElement: document.querySelector('#root'),
|
||||
qiankun: false,
|
||||
},
|
||||
|
||||
stores: {
|
||||
home,
|
||||
counter,
|
||||
},
|
||||
|
||||
services,
|
||||
|
||||
router: {
|
||||
type: 'browser',
|
||||
config: routes,
|
||||
},
|
||||
});
|
||||
|
||||
export { mount, unmount, bootstrap };
|
||||
```
|
||||
|
||||
### Create Observable States
|
||||
|
||||
Defining a view model through defineStore is very simple, simply declare the state and actions.
|
||||
|
||||
```jsx
|
||||
import { defineStore } from '@music163/tango-boot';
|
||||
|
||||
const counter = defineStore({
|
||||
num: 0,
|
||||
|
||||
get() {},
|
||||
|
||||
decrement: function () {
|
||||
counter.num--;
|
||||
},
|
||||
|
||||
increment: () => counter.num++,
|
||||
});
|
||||
|
||||
export default counter;
|
||||
```
|
||||
|
||||
### Create Reactive Views
|
||||
|
||||
If the view layer needs to listen for state changes, it only needs to wrap the view component with `definePage`.
|
||||
|
||||
```jsx
|
||||
import React from 'react';
|
||||
import tango, { definePage } from '@music163/tango-boot';
|
||||
|
||||
class App extends React.Component {
|
||||
increment = () => {
|
||||
tango.stores.counter.increment();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Counter: {tango.stores.counter.num}</h1>
|
||||
<button type="primary" onClick={this.increment}>
|
||||
+1
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default definePage(App);
|
||||
```
|
||||
|
||||
### Create Service Functions
|
||||
|
||||
Use `defineServices` to define your remote apis as service functions.
|
||||
|
||||
```jsx
|
||||
import { defineServices } from '@music163/tango-boot';
|
||||
|
||||
export default defineServices({
|
||||
list: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
|
||||
formatter: (res) => {
|
||||
const { data, message } = res;
|
||||
return {
|
||||
code: 200,
|
||||
list: data,
|
||||
total: data.length,
|
||||
message,
|
||||
};
|
||||
},
|
||||
},
|
||||
add: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
|
||||
method: 'post',
|
||||
},
|
||||
update: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
|
||||
method: 'post',
|
||||
},
|
||||
delete: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users?id=1',
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -1,66 +0,0 @@
|
||||
# 双向绑定
|
||||
|
||||
TangoBoot 支持快捷实现组件与视图双向绑定。
|
||||
|
||||
## 借助 `withModel` 实现双向绑定
|
||||
|
||||
`withModel` 是 TangoBoot 内置的模型绑定 HOC,支持将组件的内部状态同步到某个 store 定义的状态中。基本的用法如下:
|
||||
|
||||
```jsx
|
||||
class Input extends React.Component {
|
||||
foo() {}
|
||||
|
||||
render() {
|
||||
return <input {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
const ModelInput = withModel({
|
||||
// 设置从组件同步到 store 中的状态值
|
||||
getValueFromEvent(e: any) {
|
||||
return e.target.value;
|
||||
},
|
||||
})(Input);
|
||||
```
|
||||
|
||||
现在 `ModelInput` 获取了双向绑定的能力,其 `value` 变化后的值将会通过 `onChange` 事件同步给绑定的模型变量
|
||||
|
||||
```jsx
|
||||
const Store = defineStore(
|
||||
{
|
||||
name: 'alice',
|
||||
},
|
||||
'user'
|
||||
);
|
||||
|
||||
const ModelApp = defineView((props) => {
|
||||
return (
|
||||
<div>
|
||||
<ModelInput model="user.name" />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
## 配置选项
|
||||
|
||||
`withModel` 的定义如下:
|
||||
|
||||
```jsx
|
||||
withModel(options)(Component);
|
||||
```
|
||||
|
||||
其中 options 的配置如下:
|
||||
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
| ----------------- | ------------------ | ---------- | -------------------------- |
|
||||
| name | string | | 组件的 displayName |
|
||||
| valuePropName | string | "value" | 组件的受控值 |
|
||||
| trigger | string | "onChange" | 组件值变化时的回调函数 |
|
||||
| getValueFromEvent | `(...args) => any` | | 从回调函数参数中的取值方法 |
|
||||
|
||||
借助 `withModel` 增强后,Component 组件将会获得如下属性:
|
||||
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
| ------ | ------ | ------ | ------------------------ |
|
||||
| model | string | | 绑定的模型变量的变量路径 |
|
||||
@@ -1,41 +0,0 @@
|
||||
# Pages
|
||||
|
||||
TangoBoot 内置提供了一个创建 reactive 视图的模型,用户无需关注复杂的依赖关系,只需要将视图组件使用 `definePage` 包裹即可。
|
||||
|
||||
## definePage
|
||||
|
||||
基于传递进来的组件创建一个 reactive 视图。reactive 视图会根据 store 数据的变化来自动触发视图重新渲染。例如
|
||||
|
||||
```jsx
|
||||
import React from 'react';
|
||||
import { definePage, defineStore } from '@music163/tango-boot';
|
||||
|
||||
const user = defineStore({ name: 'Bob' });
|
||||
|
||||
export default definePage(() => <div>Hello {user.name}!</div>);
|
||||
```
|
||||
|
||||
`definePage` 内部为被包裹的组件实现了一个最佳的 `shouldComponentUpdate` 或 `memo`。
|
||||
|
||||
使用 `definePage` 包裹后的组件还会默认接收一些额外的属性,以便于开发者便捷的获取部分状态信息。具体包括:
|
||||
|
||||
- `routerData` 路由数据:包括 `params` 路由参数,`query` 查询参数,`pathname` 路径名
|
||||
- `stores` 模型索引
|
||||
- `services` 服务索引
|
||||
|
||||
## defineView
|
||||
|
||||
`defineView` 是一个更加轻量的实现 reactive 视图的包裹器,推荐所有在视图中使用的本地组件都使用它来进行包裹。与 `definePage` 不同的是,他没有额外的属性透传进来。
|
||||
|
||||
```jsx
|
||||
export default defineView(() => (
|
||||
<div>
|
||||
<div>Hello {user.name}!</div>
|
||||
<div>Your first post is: {timeline.posts[0]}</div>
|
||||
</div>
|
||||
));
|
||||
```
|
||||
|
||||
:::info
|
||||
`defineView` 的实现依赖了 [react-easy-state](https://github.com/RisingStack/react-easy-state#creating-reactive-views),感兴趣的同学可以访问对应的链接了解更详细的内容。
|
||||
:::
|
||||
@@ -1,33 +0,0 @@
|
||||
# 组件实例获取
|
||||
|
||||
TangoBoot 支持快捷设置和获取组件的实例。
|
||||
|
||||
## 借助 `withRef` 注册组件实例
|
||||
|
||||
`withRef` 是 TangoBoot 内置的实例注册 HOC,可以快捷的进行组件实例的注册和获取。基本的用法如下:
|
||||
|
||||
```tsx
|
||||
class Input extends React.Component {
|
||||
foo() {}
|
||||
|
||||
render() {
|
||||
return <input {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
const ClassInput = withRef()(Input);
|
||||
```
|
||||
|
||||
借助 `withRef` 包裹组件后,当新的组件实例被设置 `id` 属性时,会自动将对应的实例注册到 `tango.refs` 中。例如:
|
||||
|
||||
```tsx
|
||||
export function Basic() {
|
||||
const inputRef = useRef();
|
||||
useEffect(() => {
|
||||
console.log(tango.refs.classInput, inputRef.current);
|
||||
}, []);
|
||||
|
||||
// 设置 id 属性后,会自动组件实例到 `tango.refs` 中
|
||||
return <ClassInput id="classInput" ref={inputRef} />;
|
||||
}
|
||||
```
|
||||
@@ -1,90 +0,0 @@
|
||||
# Async Services
|
||||
|
||||
TangoBoot provides a configuration-based method for creating data service functions. By using `defineServices`, it is very easy to declare a set of asynchronous service functions for consumption by the view layer or model layer.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```js
|
||||
import { defineServices } from '@music163/tango-boot';
|
||||
|
||||
const services = defineServices({
|
||||
list: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
|
||||
},
|
||||
add: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
|
||||
method: 'post',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
call the service function:
|
||||
|
||||
```js
|
||||
async () => {
|
||||
// simple usage
|
||||
await services.list();
|
||||
|
||||
// call with params, payload the the data passed to request body(for 'get' method, will be query parameters), config the axios request config
|
||||
await services.add(payload, config);
|
||||
};
|
||||
```
|
||||
|
||||
## Request Config
|
||||
|
||||
`defineServices` use [axios](https://github.com/axios/axios) to send requests, so you can pass in the axios request config as the second parameter of the service function.
|
||||
|
||||
|
||||
```js
|
||||
import { defineServices } from '@music163/tango-boot';
|
||||
|
||||
export default defineServices({
|
||||
list: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
|
||||
formatter: (res) => {
|
||||
const { data, message } = res;
|
||||
return {
|
||||
code: 200,
|
||||
list: data,
|
||||
total: data.length,
|
||||
message,
|
||||
};
|
||||
},
|
||||
// you can pass other axios config here
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Following is the common configs:
|
||||
|
||||
- `url` request url
|
||||
- `method` request method,default is `GET`
|
||||
- `GET`
|
||||
- `POST`
|
||||
- `PUT`
|
||||
- `DELETE`
|
||||
- `formatter` format the response data
|
||||
- `requestType` encoding type of request body (content-type), default value is `json`
|
||||
- `json` By default, serialize JavaScript objects to JSON.
|
||||
- `x-www-form-urlencoded` equals to `application/x-www-form-urlencoded`
|
||||
- `headers` request header
|
||||
- `timeout` timeout of request
|
||||
|
||||
more configs should find in [axios documentation](https://axios-http.com/zh/docs/req_config)。
|
||||
|
||||
:::tip Encoding Type
|
||||
By default, TangoBoot serializes JavaScript objects to JSON. To send data in `application/x-www-form-urlencoded` format, you can set the `requestType` to `x-www-form-urlencoded`.
|
||||
|
||||
`application/x-www-form-urlencoded` data is encoded as key-value pairs separated by '&', with keys and values separated by '='. Non-alphanumeric characters are [percent-encoded](https://developer.mozilla.org/zh-CN/docs/Glossary/percent-encoding), which is why this type does not support binary data (use multipart/form-data instead).
|
||||
:::
|
||||
|
||||
## Error Messages
|
||||
|
||||
The default presentation method of the message when a configuration request error occurs is to use the `message` component for message overlay display. It is recommended to configure this in the application's entry file.
|
||||
|
||||
```js
|
||||
import { tangoBootConfig } from '@music163/tango-boot';
|
||||
import { message } from '@music163/antd';
|
||||
|
||||
tangoBootConfig.toast = message;
|
||||
```
|
||||
@@ -1,214 +0,0 @@
|
||||
# States
|
||||
|
||||
TangoBoot 采用了一个轻量级的使用 Reactive 的状态管理方案,开发者可以非常轻松的进行页面状态的管理。一个最基本的示例如下所示:
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
import { defineStore, definePage } from '@music163/tango-boot';
|
||||
|
||||
const counter = defineStore({
|
||||
num: 0,
|
||||
increment: () => counter.num++,
|
||||
});
|
||||
|
||||
// 当状态变化的时候,视图会自动刷新
|
||||
export default definePage(() => <button onClick={counter.increment}>{counter.num}</button>);
|
||||
```
|
||||
|
||||
值得注意的是,开发者必须借助 `defineStore` 来定义状态,它会帮助你创建一个 observable 的状态对象。借助 `definePage` 来定义视图,它会帮你订一个 reactive 的视图组件,并且在自动监听状态的变化,按需进行 UI 更新。
|
||||
|
||||
## 创建状态模型 Stores
|
||||
|
||||
基本语法为 `defineStore(storeObject: object, namespace?: string)`
|
||||
|
||||
- `storeObject` 为原始的状态对象
|
||||
- `namespace` 为该 Store 对应的命名空间,如果你定义了,可以借助 `tango.stores[namespace]` 来获取该引用
|
||||
|
||||
一个基本的例子如下:
|
||||
|
||||
```js
|
||||
import { defineStore } from '@music163/tango-boot';
|
||||
|
||||
const user = defineStore({ name: 'Rick' });
|
||||
// stores behave like normal JS objects
|
||||
user.name = 'Bob';
|
||||
```
|
||||
|
||||
### 复杂的数据结构
|
||||
|
||||
```js
|
||||
// stores can include any valid JS structure
|
||||
// including nested data, arrays, Maps, Sets, getters, setters, inheritance, ...
|
||||
const user = defineStore({
|
||||
profile: {
|
||||
firstName: 'Bob',
|
||||
lastName: 'Smith',
|
||||
get name() {
|
||||
return `${user.profile.firstName} ${user.profile.lastName}`;
|
||||
},
|
||||
},
|
||||
hobbies: ['programming', 'sports'],
|
||||
friends: new Map(),
|
||||
});
|
||||
|
||||
// stores may be mutated in any syntactically valid way
|
||||
user.profile.firstName = 'Bob';
|
||||
delete user.profile.lastName;
|
||||
user.hobbies.push('reading');
|
||||
user.friends.set('id', otherUser);
|
||||
```
|
||||
|
||||
### 异步方法
|
||||
|
||||
```js
|
||||
const userStore = defineStore({
|
||||
user: {},
|
||||
async fetchUser() {
|
||||
userStore.user = await fetch('/user');
|
||||
|
||||
// or use this
|
||||
// this.user = await fetch('/user');
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 多个 Store
|
||||
|
||||
useStore.js
|
||||
|
||||
```js
|
||||
import { store } from '@music163/tango-boot';
|
||||
|
||||
const userStore = defineStore(
|
||||
{
|
||||
user: {},
|
||||
async fetchUser() {
|
||||
userStore.user = await fetch('/user');
|
||||
},
|
||||
},
|
||||
'userStore',
|
||||
);
|
||||
|
||||
export default userStore;
|
||||
```
|
||||
|
||||
recipesStore.js
|
||||
|
||||
```js
|
||||
import tango, { store } from '@music163/tango-boot';
|
||||
import userStore from './userStore';
|
||||
|
||||
const recipesStore = defineStore(
|
||||
{
|
||||
recipes: [],
|
||||
|
||||
// 方式1:直接用调用
|
||||
async fetchRecipes() {
|
||||
recipesStore.recipes = await fetch(`/recipes?user=${userStore.user.id}`);
|
||||
},
|
||||
|
||||
// 方法2:使用 this 调用,仅支持 method property,不支持箭头函数
|
||||
async fetchRecipes2() {
|
||||
this.recipes = await fetch(`/recipes?user=${userStore.user.id}`);
|
||||
},
|
||||
|
||||
// 方法3:使用 tango 全局变量调用,你也可以使用这种方式引入其他的 store
|
||||
async fetchRecipes3() {
|
||||
tango.stores.recipesStore.recipes = await fetch(
|
||||
`/recipes?user=${tango.stores.userStore.user.id}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
'recipesStore',
|
||||
);
|
||||
|
||||
export default recipesStore;
|
||||
```
|
||||
|
||||
## 创建 Reactive 视图
|
||||
|
||||
你可以借助 `definePage` 创建视图,也可以直接包裹您的原始组件。基本用法为 `definePage(Component)`,借助 `definePage` 可以帮你自动监听状态的变化,并在变化时自动触发视图的重新渲染。
|
||||
|
||||
一个简单的例子如下:
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
import { view, store } from '@music163/tango-boot';
|
||||
|
||||
// this is a global state store
|
||||
const user = defineStore({ name: 'Bob' });
|
||||
|
||||
// this is re-rendered whenever user.name changes
|
||||
export default definePage(() => (
|
||||
<div>
|
||||
<input value={user.name} onChange={(ev) => (user.name = ev.target.value)} />
|
||||
<div>Hello {user.name}!</div>
|
||||
</div>
|
||||
));
|
||||
```
|
||||
|
||||
## 实现视图与状态双向绑定
|
||||
|
||||
TangoBoot 提供了一个名为 `withModel` 的 [HOC](https://zh-hans.reactjs.org/docs/higher-order-components.html),可以借助它来实现视图与状态的双向绑定。
|
||||
|
||||
使用方法如下:
|
||||
|
||||
```js
|
||||
withModel(options)(BaseComponent);
|
||||
```
|
||||
|
||||
### 参数配置
|
||||
|
||||
其中可配置的 options 参数包括:
|
||||
|
||||
- `name` 用于配置包裹后组件的 displayName,可选
|
||||
- `valuePropName` 绑定的 value 属性名,用于实现双向绑定的受控逻辑,默认为 `value`
|
||||
- `trigger` 设置收集字段值变更的时机,默认为 `onChange`
|
||||
- `getValueFromEvent` 设置从事件回调中获取 value 值的方法,默认为 `val => val`,直接返回 trigger 的第一个参数
|
||||
|
||||
嵌套 `whitModel` 后,组件将会获得两个新增的属性:
|
||||
|
||||
- `model` 用于绑定 Store 中的状态
|
||||
- `innerRef` 用于获取内部组件的 [ref](https://zh-hans.reactjs.org/docs/refs-and-the-dom.html) 引用
|
||||
|
||||
### 双向绑定示例
|
||||
|
||||
基本用法如下:
|
||||
|
||||
```jsx
|
||||
import tango, { withModel, defineView, defineStore } from '@music163/tango-boot';
|
||||
|
||||
// 定义一个基本的 Input 组件
|
||||
class Input extends React.Component {
|
||||
foo() {}
|
||||
|
||||
render() {
|
||||
return <input {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
// 实现双向绑定
|
||||
const ModelInput = withModel({
|
||||
getValueFromEvent(e) {
|
||||
return e.target.value;
|
||||
},
|
||||
})(Input);
|
||||
|
||||
// 定义一个 Store
|
||||
defineStore(
|
||||
{
|
||||
name: 'alice',
|
||||
},
|
||||
'user',
|
||||
);
|
||||
|
||||
// 双向绑定验证
|
||||
const ModelApp = defineView((props) => {
|
||||
return (
|
||||
<div>
|
||||
<ModelInput model="user.name" />
|
||||
<div>{tango.stores.user.name}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
```
|
||||