├── .eslintrc ├── .github └── workflows │ ├── ci.yml │ └── deploy.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmrc ├── .prettierrc ├── LICENSE ├── README.md ├── README.zh-CN.md ├── apps ├── playground │ ├── .gitignore │ ├── .umirc.ts │ ├── package.json │ ├── plugin.ts │ ├── src │ │ ├── assets │ │ │ └── yay.jpg │ │ ├── components │ │ │ ├── foo-setter.tsx │ │ │ ├── index.ts │ │ │ └── other-panel.tsx │ │ ├── helpers │ │ │ ├── index.tsx │ │ │ ├── mail-files.ts │ │ │ ├── mock-files.ts │ │ │ └── prototypes.ts │ │ ├── layouts │ │ │ ├── index.less │ │ │ └── index.tsx │ │ └── pages │ │ │ ├── docs.tsx │ │ │ ├── index.tsx │ │ │ └── mail.tsx │ ├── tsconfig.json │ └── typings.d.ts └── storybook │ ├── .storybook │ ├── main.js │ ├── preview-head.html │ └── preview.js │ ├── README.md │ ├── package.json │ ├── src │ ├── editor.stories.tsx │ ├── sandbox.stories.tsx │ ├── setting-form.stories.tsx │ └── ui │ │ ├── action-select.stories.tsx │ │ ├── action.stories.tsx │ │ ├── chat.stories.tsx │ │ ├── classname-input.stories.tsx │ │ ├── copy.stories.tsx │ │ ├── drag-panel.stories.tsx │ │ ├── input-code.stories.tsx │ │ ├── input-list.stories.tsx │ │ ├── menu.stories.tsx │ │ ├── select-list.stories.tsx │ │ ├── tag-select.stories.tsx │ │ ├── toggle-button.stories.tsx │ │ └── var-tree.stories.tsx │ ├── tsconfig.json │ └── yarn-error.log ├── babel.config.js ├── commitlint.config.js ├── jest.config.js ├── lerna.json ├── package.json ├── packages ├── context │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── context.ts │ │ └── index.ts │ ├── tsconfig.json │ └── tsconfig.prod.json ├── core │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── factory.ts │ │ ├── helpers │ │ │ ├── assert.ts │ │ │ ├── ast │ │ │ │ ├── generate.ts │ │ │ │ ├── index.ts │ │ │ │ ├── parse.ts │ │ │ │ └── traverse.ts │ │ │ ├── code-helpers.ts │ │ │ ├── id-generator.ts │ │ │ ├── index.ts │ │ │ ├── object.ts │ │ │ ├── prototype.ts │ │ │ ├── schema-helpers.ts │ │ │ └── string.ts │ │ ├── index.ts │ │ ├── models │ │ │ ├── abstract-code-workspace.ts │ │ │ ├── abstract-file.ts │ │ │ ├── abstract-js-file.ts │ │ │ ├── abstract-json-file.ts │ │ │ ├── abstract-view-node.ts │ │ │ ├── abstract-workspace.ts │ │ │ ├── designer.ts │ │ │ ├── drag-source.ts │ │ │ ├── drop-target.ts │ │ │ ├── engine.ts │ │ │ ├── file.ts │ │ │ ├── history.ts │ │ │ ├── index.ts │ │ │ ├── interfaces.ts │ │ │ ├── js-app-entry-file.ts │ │ │ ├── js-file.ts │ │ │ ├── js-local-components-entry-file.ts │ │ │ ├── js-route-config-file.ts │ │ │ ├── js-service-file.ts │ │ │ ├── js-store-entry-file.ts │ │ │ ├── js-store-file.ts │ │ │ ├── js-view-file.ts │ │ │ ├── json-file.ts │ │ │ ├── select-source.ts │ │ │ ├── view-node.ts │ │ │ └── workspace.ts │ │ └── types.ts │ ├── tests │ │ ├── assert.test.ts │ │ ├── ast.test.ts │ │ ├── engine.test.ts │ │ ├── helpers.test.ts │ │ └── proto.test.ts │ ├── tsconfig.json │ ├── tsconfig.prod.json │ └── typedoc.json ├── designer │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── components │ │ │ ├── components-popover.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── drag-box.tsx │ │ │ ├── file-errors-overlay.tsx │ │ │ ├── index.ts │ │ │ ├── input-kv.tsx │ │ │ ├── menu.tsx │ │ │ ├── variable-tree-modal.tsx │ │ │ └── variable-tree │ │ │ │ ├── add-service.tsx │ │ │ │ ├── add-store.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── service-preview.tsx │ │ │ │ ├── value-detail.tsx │ │ │ │ └── value-preview.tsx │ │ ├── context-menu │ │ │ ├── copy-node.tsx │ │ │ ├── delete-node.tsx │ │ │ ├── index.tsx │ │ │ ├── paste-node.tsx │ │ │ └── view-source.tsx │ │ ├── context.ts │ │ ├── designer-panel.tsx │ │ ├── designer.tsx │ │ ├── dnd │ │ │ ├── dnd-query.ts │ │ │ ├── hotkey.ts │ │ │ ├── index.ts │ │ │ └── use-dnd.ts │ │ ├── editor.tsx │ │ ├── helpers │ │ │ ├── dom.ts │ │ │ ├── index.ts │ │ │ └── template.ts │ │ ├── index.ts │ │ ├── sandbox │ │ │ ├── index.ts │ │ │ ├── navigator.tsx │ │ │ └── sandbox.tsx │ │ ├── selection-menu │ │ │ ├── copy-node.tsx │ │ │ ├── delete-node.tsx │ │ │ ├── index.ts │ │ │ ├── more-actions.tsx │ │ │ ├── select-parent-node.tsx │ │ │ └── view-source.tsx │ │ ├── setters │ │ │ ├── action-list-setter.tsx │ │ │ ├── choice-setter.tsx │ │ │ ├── classname-setter.tsx │ │ │ ├── code-setter.tsx │ │ │ ├── column-setter.tsx │ │ │ ├── css-setter.tsx │ │ │ ├── date-setter.tsx │ │ │ ├── enum-setter.tsx │ │ │ ├── event-setter.tsx │ │ │ ├── index.ts │ │ │ ├── json-setter.tsx │ │ │ ├── jsx-setter.tsx │ │ │ ├── list-setter.tsx │ │ │ ├── model-setter.tsx │ │ │ ├── option-setter.tsx │ │ │ ├── picker-setter.tsx │ │ │ ├── render-setter.tsx │ │ │ ├── router-setter.tsx │ │ │ └── style-setter.tsx │ │ ├── setting-panel.tsx │ │ ├── sidebar │ │ │ ├── components-panel.tsx │ │ │ ├── datasource-panel │ │ │ │ ├── index.tsx │ │ │ │ ├── interface-config.tsx │ │ │ │ └── proxy-config.tsx │ │ │ ├── dependency-panel.tsx │ │ │ ├── history-panel.tsx │ │ │ ├── index.ts │ │ │ ├── outline-panel │ │ │ │ ├── components-tree.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── state-tree.tsx │ │ │ ├── resizable-box.tsx │ │ │ ├── sidebar.tsx │ │ │ └── variable-panel.tsx │ │ ├── simulator │ │ │ ├── bottom-bar.tsx │ │ │ ├── error-boundary.tsx │ │ │ ├── ghost.tsx │ │ │ ├── index.tsx │ │ │ ├── insertion.tsx │ │ │ ├── mask.tsx │ │ │ ├── selection-mask.tsx │ │ │ ├── selection.tsx │ │ │ ├── simulator.tsx │ │ │ └── viewport.tsx │ │ ├── themes │ │ │ ├── default.ts │ │ │ ├── index.ts │ │ │ └── light.ts │ │ ├── toolbar │ │ │ ├── history.tsx │ │ │ ├── index.ts │ │ │ ├── mode-switch.tsx │ │ │ ├── preview.tsx │ │ │ ├── route-switch.tsx │ │ │ ├── toggle-panel.tsx │ │ │ ├── toolbar.tsx │ │ │ └── viewport-switch.tsx │ │ ├── types │ │ │ └── index.ts │ │ ├── widgets.ts │ │ ├── workspace-panel.tsx │ │ └── workspace-view.tsx │ ├── tsconfig.json │ ├── tsconfig.prod.json │ └── typedoc.json ├── helpers │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── helpers │ │ │ ├── array.ts │ │ │ ├── assert.ts │ │ │ ├── code-helper.ts │ │ │ ├── constants.ts │ │ │ ├── dom.ts │ │ │ ├── enums.ts │ │ │ ├── events.ts │ │ │ ├── function.ts │ │ │ ├── index.ts │ │ │ ├── logger.ts │ │ │ ├── object.ts │ │ │ ├── react-helper.ts │ │ │ └── string.ts │ │ ├── hoc │ │ │ ├── compose.ts │ │ │ ├── index.ts │ │ │ └── with-dnd.tsx │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── use-boolean.ts │ │ │ ├── use-callback-ref.ts │ │ │ └── use-controllable.ts │ │ ├── index.ts │ │ ├── stores │ │ │ ├── index.ts │ │ │ └── list-store.ts │ │ └── types │ │ │ ├── advanced.ts │ │ │ ├── base.ts │ │ │ ├── index.ts │ │ │ └── prototype.ts │ ├── tests │ │ ├── assert.test.ts │ │ ├── code-helper.test.ts │ │ └── helpers.test.ts │ ├── tsconfig.json │ ├── tsconfig.prod.json │ ├── typedoc.json │ └── yarn-error.log ├── sandbox │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── code-sandbox │ │ │ ├── helper.ts │ │ │ ├── iframe-protocol.ts │ │ │ ├── index.tsx │ │ │ ├── loading.tsx │ │ │ └── manager.ts │ │ ├── index.ts │ │ └── types.ts │ ├── tsconfig.json │ └── tsconfig.prod.json ├── setting-form │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── context.tsx │ │ ├── form-item.tsx │ │ ├── form-model.tsx │ │ ├── form-object.tsx │ │ ├── form-ui.tsx │ │ ├── form.tsx │ │ ├── helpers.ts │ │ ├── index.ts │ │ ├── setters │ │ │ ├── bool-setter.tsx │ │ │ ├── code-setter.tsx │ │ │ ├── id-setter.tsx │ │ │ ├── index.ts │ │ │ ├── number-setter.tsx │ │ │ ├── register.tsx │ │ │ └── text-setter.tsx │ │ └── types.ts │ ├── tsconfig.json │ ├── tsconfig.prod.json │ └── typedoc.json ├── spec │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── tango-config.json └── ui │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ ├── action-select.tsx │ ├── action.tsx │ ├── chat-input.tsx │ ├── classname-input.tsx │ ├── code-editor.tsx │ ├── collapse-panel.tsx │ ├── color-tag.tsx │ ├── config-form.tsx │ ├── context-action.tsx │ ├── copy-clipboard.tsx │ ├── drag-panel.tsx │ ├── error-boundary.tsx │ ├── file-explorer │ │ ├── directory.tsx │ │ ├── file.tsx │ │ ├── index.ts │ │ └── module-list.tsx │ ├── iconfont.tsx │ ├── icons │ │ ├── code-outlined.tsx │ │ ├── create-icon.tsx │ │ ├── dual-outlined.tsx │ │ ├── index.ts │ │ ├── line-dashed-outlined.tsx │ │ ├── line-solid-outlined.tsx │ │ ├── open-panel-filled-left-outlined.tsx │ │ ├── open-panel-filled-right-outlined.tsx │ │ ├── open-panel-left-outlined.tsx │ │ ├── open-panel-right-outlined.tsx │ │ ├── package-outlined.tsx │ │ ├── pop-out-outlined.tsx │ │ ├── redo-outlined.tsx │ │ └── undo-outlined.tsx │ ├── index.ts │ ├── input-code.tsx │ ├── input-list.tsx │ ├── input-style-code.tsx │ ├── json-view.tsx │ ├── lang │ │ └── css-object.ts │ ├── menu.tsx │ ├── panel.tsx │ ├── popover.tsx │ ├── search.tsx │ ├── select-action.tsx │ ├── select-list.tsx │ ├── tabs.tsx │ ├── tag-select.tsx │ └── toggle-button.tsx │ ├── tsconfig.json │ └── tsconfig.prod.json ├── public ├── dashboard-builder.png ├── mail-builder.png └── rn-builder.png ├── tsconfig.json ├── tsconfig.prod.json ├── typedoc.base.json ├── typedoc.json └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint-config-ali/typescript/react", "prettier"], 3 | "ignorePatterns": ["**/dist/**/*", "**/lib/**/*", "**/node_modules/**/*", "scripts/**/*"], 4 | "rules": { 5 | "@typescript-eslint/consistent-type-definitions": "off", 6 | "@typescript-eslint/dot-notation": "off", 7 | "@typescript-eslint/no-unused-vars": "warn", 8 | "import/no-cycle": "off", 9 | "no-nested-ternary": "off", 10 | "no-useless-return": "off", 11 | "no-param-reassign": "off", 12 | "prefer-destructuring": "off" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | # Event设置为main分支的pull request事件, 3 | # 这里的main分支相当于master分支,github项目新建是把main设置为默认分支,我懒得改了所以就保持这样吧 4 | on: 5 | pull_request: 6 | branches: main 7 | jobs: 8 | # 只需要定义一个job并命名为CI 9 | CI: 10 | runs-on: ubuntu-latest 11 | steps: 12 | # 拉取项目代码 13 | - name: Checkout repository 14 | uses: actions/checkout@v2 15 | # 给当前环境下载node 16 | - name: Use Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: '18.x' 20 | # 检查缓存 21 | # 如果key命中缓存则直接将缓存的文件还原到 path 目录,从而减少流水线运行时间 22 | # 若 key 没命中缓存时,在当前Job成功完成时将自动创建一个新缓存 23 | - name: Cache 24 | # 缓存命中结果会存储在steps.[id].outputs.cache-hit里,该变量在继后的step中可读 25 | id: cache-dependencies 26 | uses: actions/cache@v3 27 | with: 28 | # 缓存文件目录的路径 29 | path: | 30 | **/node_modules 31 | # key中定义缓存标志位的生成方式。runner.OS指当前环境的系统。外加对yarn.lock内容生成哈希码作为key值,如果yarn.lock改变则代表依赖有变化。 32 | # 这里用yarn.lock而不是package.json是因为package.json中还有version和description之类的描述项目但和依赖无关的属性 33 | key: ${{runner.OS}}-${{hashFiles('**/yarn.lock')}} 34 | # 安装依赖 35 | - name: Installing Dependencies 36 | # 如果缓存标志位没命中,则执行该step。否则就跳过该step 37 | if: steps.cache-dependencies.outputs.cache-hit != 'true' 38 | run: yarn install 39 | # 预构建 40 | - name: Prebuild 41 | run: yarn build 42 | # 运行代码扫描 43 | - name: Running Lint 44 | # 通过前面章节定义的命令行执行代码扫描 45 | run: yarn eslint 46 | # 运行自动化测试 47 | - name: Running Test 48 | # 通过前面章节定义的命令行执行自动化测试 49 | run: yarn test 50 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy Github Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ['main'] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: 'pages' 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | # Single deploy job since we're just deploying 25 | deploy: 26 | environment: 27 | name: github-pages 28 | url: ${{ steps.deployment.outputs.page_url }} 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | - name: Set up Node 34 | uses: actions/setup-node@v3 35 | with: 36 | node-version: 18 37 | cache: 'npm' 38 | - name: Install dependencies 39 | run: yarn install 40 | - name: Build 41 | run: yarn build 42 | - name: Build docs 43 | run: yarn typedoc 44 | - name: Setup Pages 45 | uses: actions/configure-pages@v3 46 | - name: Upload artifact 47 | uses: actions/upload-pages-artifact@v2 48 | with: 49 | # Upload dist repository 50 | path: './docs' 51 | - name: Deploy to GitHub Pages 52 | id: deployment 53 | uses: actions/deploy-pages@v2 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | lib/ 4 | coverage/ 5 | 6 | /docs 7 | 8 | .umi/ 9 | .umi-production/ 10 | .log 11 | .eslintcache 12 | *.pem 13 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit "$1" -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged --allow-empty 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmmirror.com/ 2 | 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "singleQuote": true, 4 | "printWidth": 100 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 NetEase 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/playground/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.env.local 3 | /.umirc.local.ts 4 | /config/config.local.ts 5 | /src/.umi 6 | /src/.umi-production 7 | /src/.umi-test 8 | /dist 9 | .swc 10 | -------------------------------------------------------------------------------- /apps/playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "version": "0.0.0", 4 | "private": true, 5 | "author": "Wells ", 6 | "scripts": { 7 | "dev": "cross-env HOST=local.netease.com PORT=8001 umi dev", 8 | "build": "umi build", 9 | "postinstall": "umi setup", 10 | "setup": "umi setup", 11 | "start": "npm run dev" 12 | }, 13 | "dependencies": { 14 | "@ant-design/icons": "^4.8.0", 15 | "@music163/antd": "^0.2.4", 16 | "antd": "^4.24.2", 17 | "coral-system": "^1.0.5", 18 | "umi": "^4.2.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/playground/plugin.ts: -------------------------------------------------------------------------------- 1 | import type { IApi } from 'umi'; 2 | 3 | export default (api: IApi) => { 4 | api.addMiddlewares(() => { 5 | return (req, res, next) => { 6 | res.setHeader('Origin-Agent-Cluster', '?0'); 7 | next(); 8 | }; 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /apps/playground/src/assets/yay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEase/tango/c48a49961a3b0e8b49f8a617d893e0b4f047147f/apps/playground/src/assets/yay.jpg -------------------------------------------------------------------------------- /apps/playground/src/components/foo-setter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function FooSetter({ value, ...rest }: any) { 4 | return fooSetter: {value}; 5 | } 6 | -------------------------------------------------------------------------------- /apps/playground/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './foo-setter'; 2 | -------------------------------------------------------------------------------- /apps/playground/src/components/other-panel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Row, Switch } from 'antd'; 3 | import { observer } from '@music163/tango-context'; 4 | 5 | const OtherPanel = observer(({ autoRemove, setAutoRemove }: any) => { 6 | const onChange = (checked: boolean) => { 7 | setAutoRemove(checked); 8 | }; 9 | return ( 10 | 11 | 12 | 18 | 19 | ); 20 | }); 21 | 22 | export default OtherPanel; 23 | -------------------------------------------------------------------------------- /apps/playground/src/layouts/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | @import '~antd/dist/antd.less'; // 引入官方提供的 less 样式入口文件 3 | 4 | @primary-color: #2f54eb; 5 | @border-radius-base: 2px; 6 | 7 | -------------------------------------------------------------------------------- /apps/playground/src/layouts/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Outlet } from 'umi'; 3 | import './index.less'; 4 | 5 | export default function Layout() { 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /apps/playground/src/pages/docs.tsx: -------------------------------------------------------------------------------- 1 | const DocsPage = () => { 2 | return ( 3 |
4 |

This is umi docs.

5 |
6 | ); 7 | }; 8 | 9 | export default DocsPage; 10 | -------------------------------------------------------------------------------- /apps/playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./src/.umi/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /apps/playground/typings.d.ts: -------------------------------------------------------------------------------- 1 | import 'umi/typings'; 2 | -------------------------------------------------------------------------------- /apps/storybook/.storybook/main.js: -------------------------------------------------------------------------------- 1 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); 2 | 3 | module.exports = { 4 | stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], 5 | 6 | addons: ['@storybook/addon-links', '@storybook/addon-essentials'], 7 | 8 | babel: async (config) => { 9 | config.plugins.push('babel-plugin-styled-components'); 10 | return config; 11 | }, 12 | 13 | typescript: { 14 | reactDocgen: false, 15 | }, 16 | 17 | webpack: async (config) => { 18 | if (config.mode === 'production') { 19 | config.devtool = false; 20 | } 21 | 22 | if (config.resolve.plugins === null) { 23 | config.resolve.plugins = []; 24 | } 25 | 26 | config.resolve.plugins.push(new TsconfigPathsPlugin()); 27 | 28 | // @see https://github.com/graphql/graphql-js/issues/1272#issuecomment-393903706 29 | config.module.rules.push({ 30 | test: /\.mjs$/, 31 | include: /node_modules/, 32 | type: 'javascript/auto', 33 | }); 34 | 35 | return config; 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /apps/storybook/.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/storybook/.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SystemProvider } from 'coral-system'; 3 | import 'antd/dist/antd.css'; 4 | 5 | export const parameters = { 6 | actions: { argTypesRegex: '^on.*' }, 7 | controls: { 8 | matchers: { 9 | color: /(background|color)$/i, 10 | date: /Date$/, 11 | }, 12 | }, 13 | }; 14 | 15 | const withSystemProvider = (Story, context) => { 16 | return ( 17 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | export const decorators = [withSystemProvider]; 24 | -------------------------------------------------------------------------------- /apps/storybook/README.md: -------------------------------------------------------------------------------- 1 | # `docs` 2 | 3 | 开发调试用的文档 4 | -------------------------------------------------------------------------------- /apps/storybook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook", 3 | "version": "0.0.0", 4 | "private": "true", 5 | "description": "> tango-apps docs", 6 | "license": "MIT", 7 | "author": "wwsun ", 8 | "files": [ 9 | "lib" 10 | ], 11 | "scripts": { 12 | "build": "echo \"skip\"", 13 | "build-storybook": "build-storybook", 14 | "storybook": "start-storybook -p 6008" 15 | }, 16 | "dependencies": { 17 | "@music163/tango-setting-form": "*", 18 | "@music163/tango-ui": "*", 19 | "mobx": "6.13.2", 20 | "mobx-react-lite": "4.0.7" 21 | }, 22 | "devDependencies": { 23 | "@ant-design/icons": "^4.8.0", 24 | "@storybook/addon-actions": "^6.5.14", 25 | "@storybook/addon-essentials": "^6.5.14", 26 | "@storybook/addon-links": "^6.5.14", 27 | "@storybook/react": "^6.5.14", 28 | "antd": "^4.24.2", 29 | "babel-plugin-styled-components": "^2.0.7", 30 | "tsconfig-paths-webpack-plugin": "^3.5.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/storybook/src/editor.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box } from 'coral-system'; 3 | import { SingleMonacoEditor } from '@music163/tango-ui'; 4 | 5 | export default { 6 | title: 'Editor', 7 | }; 8 | 9 | const code = ` 10 | import React from 'react'; 11 | import { definePage } from '@music163/tango-boot'; 12 | import { Layout, Page, Section, Button } from '@music163/antd'; 13 | 14 | function About(props) { 15 | const { stores } = props; 16 | 17 | const increment = () => { 18 | stores.counter.increment(); 19 | }; 20 | 21 | return ( 22 | 23 | 24 |
25 |

Counter: {stores.counter.num}

26 | 29 |

原生html元素不可拖拽

30 |
31 |
32 |
33 | ); 34 | } 35 | 36 | export default definePage(About); 37 | `; 38 | 39 | export function Basic() { 40 | return ( 41 | 42 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /apps/storybook/src/sandbox.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box } from 'coral-system'; 3 | import { CodeSandbox } from '@music163/tango-sandbox'; 4 | import { JsonView } from '@music163/tango-ui'; 5 | 6 | export default { 7 | title: 'CodeSandbox', 8 | }; 9 | 10 | const entryFilename = '/index.js'; 11 | 12 | const packageJson = { 13 | name: 'demo', 14 | private: true, 15 | main: entryFilename, 16 | dependencies: { 17 | react: '^17.0.1', 18 | 'react-dom': '^17.0.1', 19 | }, 20 | }; 21 | 22 | const entryFile = ` 23 | import React from 'react'; 24 | import ReactDOM from "react-dom"; 25 | import App from './src'; 26 | 27 | const rootElement = document.getElementById("root"); 28 | 29 | ReactDOM.render( 30 | , 31 | rootElement 32 | ); 33 | `; 34 | 35 | const srcAppFile = ` 36 | import React from 'react'; 37 | 38 | export default function App() { 39 | return
hello world
; 40 | } 41 | `; 42 | 43 | const files = { 44 | '/package.json': { 45 | code: JSON.stringify(packageJson), 46 | }, 47 | [entryFilename]: { 48 | code: entryFile, 49 | }, 50 | '/src/index.js': { code: srcAppFile }, 51 | }; 52 | 53 | export function Basic() { 54 | return ( 55 | 56 | 65 | 66 | 67 | 68 | 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /apps/storybook/src/ui/action-select.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ActionSelect } from '@music163/tango-ui'; 3 | 4 | export default { 5 | title: 'UI/ActionSelect', 6 | component: ActionSelect, 7 | }; 8 | 9 | const options = [ 10 | { label: 'action1', value: 'action1' }, 11 | { label: 'action2', value: 'action2' }, 12 | { label: 'action3', value: 'action3' }, 13 | ]; 14 | 15 | export const Basic = { 16 | args: { 17 | defaultText: '选择动作', 18 | options, 19 | onSelect: console.log, 20 | }, 21 | }; 22 | 23 | export const showInput = { 24 | args: { 25 | text: '选择动作', 26 | options, 27 | showInput: true, 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /apps/storybook/src/ui/action.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Action } from '@music163/tango-ui'; 3 | import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons'; 4 | import { Space } from 'antd'; 5 | 6 | export default { 7 | title: 'UI/Action', 8 | component: Action, 9 | }; 10 | 11 | export const Basic = { 12 | args: { 13 | icon: , 14 | tooltip: '添加服务函数', 15 | }, 16 | }; 17 | 18 | export const Link = { 19 | args: { 20 | icon: , 21 | tooltip: '查看帮助文档', 22 | href: 'https://music.163.com', 23 | }, 24 | }; 25 | 26 | export const Outline = { 27 | args: { 28 | icon: , 29 | tooltip: '添加服务函数', 30 | shape: 'outline', 31 | }, 32 | }; 33 | 34 | export const Small = { 35 | args: { 36 | size: 'small', 37 | icon: , 38 | tooltip: '添加服务函数', 39 | }, 40 | }; 41 | 42 | export const Disabled = () => { 43 | return ( 44 | 45 | } tooltip="添加服务函数" disabled /> 46 | } tooltip="添加服务函数" disabled shape="outline" /> 47 | } 49 | tooltip="查看帮助文档" 50 | href="https://music.163.com" 51 | disabled 52 | /> 53 | 54 | ); 55 | }; 56 | 57 | export const List = () => { 58 | return ( 59 | 60 | } tooltip="添加服务函数" /> 61 | } 63 | tooltip="查看帮助文档" 64 | href="https://music.163.com" 65 | /> 66 | 67 | ); 68 | }; 69 | -------------------------------------------------------------------------------- /apps/storybook/src/ui/chat.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ChatInput } from '@music163/tango-ui'; 3 | 4 | export default { 5 | title: 'UI/ChatGPT', 6 | }; 7 | 8 | export function Basic() { 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /apps/storybook/src/ui/classname-input.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { ClassNameInput } from '@music163/tango-ui'; 3 | 4 | export default { 5 | title: 'UI/ClassNameInput', 6 | }; 7 | 8 | export function Basic() { 9 | return ; 10 | } 11 | 12 | export function Controlled() { 13 | const [value, setValue] = useState(''); 14 | return ; 15 | } 16 | -------------------------------------------------------------------------------- /apps/storybook/src/ui/copy.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CopyClipboard } from '@music163/tango-ui'; 3 | 4 | export default { 5 | title: 'UI/Copy', 6 | }; 7 | 8 | export function Basic() { 9 | return ( 10 | 11 | {(copied) => } 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /apps/storybook/src/ui/drag-panel.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DragPanel } from '@music163/tango-ui'; 3 | import { Box, Text } from 'coral-system'; 4 | import { Button } from 'antd'; 5 | 6 | export default { 7 | title: 'UI/DragPanel', 8 | }; 9 | 10 | export function Basic() { 11 | return ( 12 | <> 13 | 内容} 18 | footer={底部信息} 19 | > 20 | 21 | 22 | 内容} 27 | footer={底部信息} 28 | > 29 | 38 | 39 | 40 | ); 41 | } 42 | 43 | export function FooterCustom() { 44 | return ( 45 | 内容} 50 | footer={(close) => ( 51 | 52 | 底部信息 53 | 56 | 57 | )} 58 | > 59 | 60 | 61 | ); 62 | } 63 | 64 | export function ResizeablePanel() { 65 | return ( 66 | 74 | 内容 75 | 76 | } 77 | footer={底部信息} 78 | > 79 | 80 | 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /apps/storybook/src/ui/input-code.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Action, InputCode, InputStyleCode } from '@music163/tango-ui'; 3 | import { BlockOutlined } from '@ant-design/icons'; 4 | 5 | export default { 6 | title: 'UI/InputCode', 7 | }; 8 | 9 | const context = { 10 | stores: { 11 | foo: { 12 | loading: false, 13 | action: () => {}, 14 | }, 15 | }, 16 | services: { 17 | list: () => {}, 18 | get: () => {}, 19 | }, 20 | }; 21 | 22 | export function Basic() { 23 | return ( 24 | } size="small" />} 26 | autoCompleteContext={{ tango: context }} 27 | /> 28 | ); 29 | } 30 | 31 | export function CSS() { 32 | return } size="small" />} />; 33 | } 34 | 35 | const code = ` 36 | function foo() { 37 | console.log("test"); 38 | console.log("test"); 39 | console.log("test"); 40 | console.log("test"); 41 | console.log("test"); 42 | console.log("test"); 43 | console.log("test"); 44 | console.log("test"); 45 | console.log("test"); 46 | console.log("test"); 47 | console.log("test"); 48 | console.log("test"); 49 | console.log("test"); 50 | console.log("test"); 51 | console.log("test"); 52 | console.log("test"); 53 | console.log("test"); 54 | console.log("test"); 55 | console.log("test"); 56 | console.log("test"); 57 | console.log("test"); 58 | console.log("test"); 59 | console.log("test"); 60 | console.log("test"); 61 | console.log("test"); 62 | console.log("test"); 63 | } 64 | `; 65 | 66 | export function Readonly() { 67 | return ; 68 | } 69 | 70 | export function Inset() { 71 | return ; 72 | } 73 | -------------------------------------------------------------------------------- /apps/storybook/src/ui/input-list.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { InputList } from '@music163/tango-ui'; 3 | 4 | export default { 5 | title: 'UI/InputList', 6 | }; 7 | 8 | export function Basic() { 9 | const [value, setValue] = useState([]); 10 | return ; 11 | } 12 | -------------------------------------------------------------------------------- /apps/storybook/src/ui/menu.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Menu } from '@music163/tango-ui'; 3 | 4 | export default { 5 | title: 'UI/Menu', 6 | }; 7 | 8 | export function Basic() { 9 | return ( 10 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /apps/storybook/src/ui/select-list.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { SelectList } from '@music163/tango-ui'; 3 | 4 | export default { 5 | title: 'UI/SelectList', 6 | }; 7 | 8 | const options = [ 9 | { 10 | value: 'alice', 11 | label: 'Alice', 12 | }, 13 | { 14 | value: 'jack', 15 | label: 'Jack', 16 | }, 17 | { 18 | value: 'lucy', 19 | label: 'Lucy', 20 | }, 21 | { 22 | value: 'bob', 23 | label: 'Bob', 24 | }, 25 | ]; 26 | 27 | export function Basic() { 28 | const [value, setValue] = useState([]); 29 | return ; 30 | } 31 | 32 | export function UniqueValue() { 33 | const [value, setValue] = useState([]); 34 | return ; 35 | } 36 | -------------------------------------------------------------------------------- /apps/storybook/src/ui/tag-select.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TagSelect } from '@music163/tango-ui'; 3 | 4 | export default { 5 | title: 'UI/TagSelect', 6 | }; 7 | 8 | export function Basic() { 9 | return ( 10 | ({ label: item, value: item }))} 12 | onChange={console.log} 13 | /> 14 | ); 15 | } 16 | 17 | export function SingleMode() { 18 | return ( 19 | ({ label: item, value: item }))} 21 | mode="single" 22 | onChange={console.log} 23 | /> 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /apps/storybook/src/ui/toggle-button.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ToggleButton, UndoOutlined, RedoOutlined } from '@music163/tango-ui'; 3 | import { Box, Group, Flex } from 'coral-system'; 4 | 5 | export default { 6 | title: 'UI/ToggleButton', 7 | }; 8 | 9 | export function Basic() { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | } 37 | 38 | export function DarkMode() { 39 | return ( 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | ); 65 | } 66 | 67 | export function ButtonGroup() { 68 | return ( 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /apps/storybook/src/ui/var-tree.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { VariableTree } from '@music163/tango-designer/src/components'; 3 | import { Box } from 'coral-system'; 4 | 5 | export default { 6 | title: 'Designer/VariableTree', 7 | }; 8 | 9 | const data = [ 10 | { 11 | title: 'Stores', 12 | key: 'stores', 13 | selectable: false, 14 | children: [ 15 | { 16 | title: 'user', 17 | key: 'stores.user', 18 | children: [ 19 | { 20 | title: 'name', 21 | key: 'stores.user.name', 22 | raw: '"Tom"', 23 | }, 24 | { 25 | title: 'age', 26 | key: 'stores.user.age', 27 | raw: '18', 28 | }, 29 | ], 30 | }, 31 | { 32 | title: 'book', 33 | key: 'stores.book', 34 | children: [ 35 | { 36 | title: 'name', 37 | key: 'stores.book.name', 38 | raw: '"JavaScript 高级程序设计"', 39 | }, 40 | { 41 | title: 'price', 42 | key: 'stores.book.price', 43 | raw: '99.99', 44 | }, 45 | ], 46 | }, 47 | ], 48 | }, 49 | { 50 | title: 'Services', 51 | key: 'services', 52 | selectable: false, 53 | children: [ 54 | { 55 | title: 'add', 56 | key: 'services.add', 57 | }, 58 | { 59 | title: 'multiply', 60 | key: 'services.multiply', 61 | }, 62 | ], 63 | }, 64 | ]; 65 | 66 | export function Basic() { 67 | return ( 68 | 69 | 70 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /apps/storybook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ES2022", 5 | "module": "esnext" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * BabelConfig for Jest 3 | */ 4 | module.exports = { 5 | presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-react', '@babel/preset-typescript'], 6 | }; 7 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'scope-case': [0], 5 | 'subject-case': [0], 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 | "version": "independent", 4 | "packages": ["packages/*"], 5 | "npmClient": "yarn", 6 | "command": { 7 | "version": { 8 | "message": "chore(release): publish" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/context/README.md: -------------------------------------------------------------------------------- 1 | # tango-apps-context 2 | 3 | > React context of tango-apps core 4 | 5 | ## Usage 6 | 7 | install 8 | 9 | ```bash 10 | yarn add @music163/tango-context 11 | ``` 12 | 13 | usage 14 | 15 | ```jsx 16 | import { observer, useWorkspace } from '@music/tango-apps-context'; 17 | 18 | export const SampleWidget = observer(() => { 19 | const ws = useWorkspace(); 20 | return
; 21 | }); 22 | ``` 23 | -------------------------------------------------------------------------------- /packages/context/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@music163/tango-context", 3 | "version": "1.1.10", 4 | "description": "react context for tango-apps", 5 | "keywords": [ 6 | "react", 7 | "hooks" 8 | ], 9 | "author": "wwsun ", 10 | "homepage": "", 11 | "license": "MIT", 12 | "main": "lib/cjs/index.js", 13 | "module": "lib/esm/index.js", 14 | "types": "lib/esm/index.d.ts", 15 | "files": [ 16 | "dist", 17 | "lib" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/netease/tango.git" 22 | }, 23 | "scripts": { 24 | "clean": "rimraf lib/", 25 | "build": "yarn clean && yarn build:esm && yarn build:cjs", 26 | "build:esm": "tsc --project tsconfig.prod.json --outDir lib/esm/ --module ES2020", 27 | "build:cjs": "tsc --project tsconfig.prod.json --outDir lib/cjs/ --module CommonJS", 28 | "prepublishOnly": "yarn build" 29 | }, 30 | "peerDependencies": { 31 | "react": ">= 16.8" 32 | }, 33 | "dependencies": { 34 | "@music163/tango-core": "^1.4.4", 35 | "@music163/tango-helpers": "^1.2.4", 36 | "mobx-react-lite": "4.0.7" 37 | }, 38 | "publishConfig": { 39 | "access": "public", 40 | "registry": "https://registry.npmjs.org/" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/context/src/index.ts: -------------------------------------------------------------------------------- 1 | export { observer } from 'mobx-react-lite'; 2 | export * from './context'; 3 | -------------------------------------------------------------------------------- /packages/context/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/context/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.prod.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": ["src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # `core` 2 | 3 | 搭建引擎 4 | 5 | ## 如何使用 6 | 7 | ```js 8 | import { createEngine } from '@music163/tango-core'; 9 | 10 | const engine = createEngine(); 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@music163/tango-core", 3 | "version": "1.4.4", 4 | "description": "tango core", 5 | "author": "wwsun ", 6 | "homepage": "", 7 | "license": "MIT", 8 | "main": "lib/cjs/index.js", 9 | "module": "lib/esm/index.js", 10 | "types": "lib/esm/index.d.ts", 11 | "files": [ 12 | "dist", 13 | "lib" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/netease/tango.git" 18 | }, 19 | "scripts": { 20 | "clean": "rimraf lib/", 21 | "build": "yarn clean && yarn build:esm && yarn build:cjs", 22 | "build:esm": "tsc --project tsconfig.prod.json --outDir lib/esm/ --module ES2020", 23 | "build:cjs": "tsc --project tsconfig.prod.json --outDir lib/cjs/ --module CommonJS", 24 | "prepublishOnly": "yarn build" 25 | }, 26 | "dependencies": { 27 | "@babel/generator": "^7.25.6", 28 | "@babel/parser": "^7.25.6", 29 | "@babel/traverse": "^7.25.6", 30 | "@babel/types": "^7.25.6", 31 | "@music163/tango-helpers": "^1.2.4", 32 | "@types/babel__generator": "^7.6.7", 33 | "@types/babel__traverse": "^7.20.6", 34 | "mobx": "6.13.2", 35 | "path-browserify": "^1.0.1" 36 | }, 37 | "peerDependencies": { 38 | "mobx": "6.9.0" 39 | }, 40 | "publishConfig": { 41 | "access": "public", 42 | "registry": "https://registry.npmjs.org/" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/core/src/factory.ts: -------------------------------------------------------------------------------- 1 | import { MenuDataType } from '@music163/tango-helpers'; 2 | import { Designer, DesignerViewType, Engine, SimulatorNameType } from './models'; 3 | import { AbstractWorkspace } from './models/abstract-workspace'; 4 | 5 | interface ICreateEngineOptions { 6 | /** 7 | * 自定义工作区 8 | */ 9 | workspace?: AbstractWorkspace; 10 | /** 11 | * 菜单信息 12 | */ 13 | menuData?: MenuDataType; 14 | /** 15 | * 默认的模拟器模式 16 | */ 17 | defaultSimulatorMode?: SimulatorNameType; 18 | /** 19 | * 默认激活的侧边栏 20 | */ 21 | defaultActiveSidebarPanel?: string; 22 | /** 23 | * 默认激活的视图 24 | */ 25 | defaultActiveView?: DesignerViewType; 26 | } 27 | 28 | /** 29 | * Designer 实例化工厂函数 30 | * @param options 31 | * @returns 32 | */ 33 | export function createEngine({ 34 | workspace, 35 | defaultActiveView = 'design', 36 | defaultSimulatorMode = 'desktop', 37 | defaultActiveSidebarPanel = '', 38 | menuData, 39 | }: ICreateEngineOptions) { 40 | const engine = new Engine({ 41 | workspace, 42 | designer: new Designer({ 43 | workspace, 44 | simulator: defaultSimulatorMode, 45 | activeView: defaultActiveView, 46 | activeSidebarPanel: defaultActiveSidebarPanel, 47 | menuData, 48 | }), 49 | }); 50 | 51 | return engine; 52 | } 53 | -------------------------------------------------------------------------------- /packages/core/src/helpers/assert.ts: -------------------------------------------------------------------------------- 1 | import { isValidExpressionCode } from './ast'; 2 | 3 | const defineServiceHandlerNames = ['defineServices', 'createServices']; 4 | const sfHandlerPattern = new RegExp(`^(${defineServiceHandlerNames.join('|')})$`); 5 | 6 | /** 7 | * 判断给定的函数名是否是 defineServices 8 | * @param name 9 | * @returns 10 | */ 11 | export function isDefineService(name: string) { 12 | return sfHandlerPattern.test(name); 13 | } 14 | 15 | const defineStoreHandlerName = 'defineStore'; 16 | 17 | /** 18 | * 判断给定的函数名是否是 defineStore 19 | * @param name 20 | * @returns 21 | */ 22 | export function isDefineStore(name: string) { 23 | return defineStoreHandlerName === name; 24 | } 25 | 26 | /** 27 | * 是否是 tango 的变量引用 28 | * @example tango.stores.app.name 29 | * @example tango.stores?.app?.name 30 | * 31 | * @param name 32 | * @returns 33 | */ 34 | export function isTangoVariable(name: string) { 35 | return /^tango\??\.(stores|services)\??\./.test(name) && name.split('.').length > 2; 36 | } 37 | 38 | const templatePattern = /^{(.+)}$/; 39 | 40 | /** 41 | * 判断给定字符串是否被表达式容器`{expCode}`包裹 42 | * @param code 43 | * @deprecated 新版改为 {{code}} 作为容器,使用 isWrappedCode 代替 44 | */ 45 | export function isWrappedByExpressionContainer(code: string, isStrict = true) { 46 | if (isStrict && isValidExpressionCode(code)) { 47 | return false; 48 | } 49 | return templatePattern.test(code); 50 | } 51 | -------------------------------------------------------------------------------- /packages/core/src/helpers/ast/index.ts: -------------------------------------------------------------------------------- 1 | export * from './generate'; 2 | export * from './parse'; 3 | export * from './traverse'; 4 | -------------------------------------------------------------------------------- /packages/core/src/helpers/code-helpers.ts: -------------------------------------------------------------------------------- 1 | import { getCodeOfWrappedCode, isWrappedCode } from '@music163/tango-helpers'; 2 | import { value2node, expression2code, code2expression, node2value } from './ast'; 3 | 4 | /** 5 | * js value 转为代码字符串 6 | * @example 1 => 1 7 | * @example hello => "hello" 8 | * @example { foo: bar } => {{ foo: bar }} 9 | * @example [1,2,3] => {[1,2,3]} 10 | * 11 | * @param val js value 12 | * @returns 表达式代码 13 | */ 14 | export function value2code(val: any) { 15 | if (val === undefined) { 16 | return ''; 17 | } 18 | 19 | if (val === null) { 20 | return 'null'; 21 | } 22 | 23 | let ret; 24 | switch (typeof val) { 25 | case 'string': { 26 | if (isWrappedCode(val)) { 27 | ret = getCodeOfWrappedCode(val); 28 | } else { 29 | ret = `"${val}"`; 30 | } 31 | break; 32 | } 33 | case 'boolean': 34 | case 'function': 35 | case 'number': 36 | ret = String(val); 37 | break; 38 | default: { 39 | // other cases, including array, object, null, undefined 40 | const node = value2node(val); 41 | ret = expression2code(node); 42 | break; 43 | } 44 | } 45 | 46 | return ret; 47 | } 48 | 49 | export const value2expressionCode = value2code; 50 | 51 | /** 52 | * 代码字符串转为具体的 js value 53 | * @example `() => {}` 返回 undefined 54 | * 55 | * @param rawCode 代码字符串 56 | * @returns 返回解析后的 js value,包括:string, number, boolean, simpleObject, simpleArray 57 | */ 58 | export function code2value(rawCode: string) { 59 | const node = code2expression(rawCode); 60 | const value = node2value(node); 61 | if (isWrappedCode(value)) { 62 | // 能转的就转,转不能的就返回空 63 | return; 64 | } 65 | return value; 66 | } 67 | -------------------------------------------------------------------------------- /packages/core/src/helpers/id-generator.ts: -------------------------------------------------------------------------------- 1 | import { camelCase } from '@music163/tango-helpers'; 2 | 3 | type IdGeneratorOptionsType = { prefix?: string }; 4 | /** 5 | * ID 生成器 6 | */ 7 | export class IdGenerator { 8 | /** 9 | * ID 前缀 10 | */ 11 | private readonly prefix: string; 12 | /** 13 | * 记录组件 ID 记录 14 | */ 15 | private map = new Map(); 16 | 17 | constructor(options?: IdGeneratorOptionsType) { 18 | this.prefix = options?.prefix ? encodeURIComponent(options.prefix) : undefined; 19 | } 20 | 21 | /** 22 | * 更新组件记录 23 | * @param component 24 | */ 25 | setItem(component: string, id?: string) { 26 | if (this.map.has(component)) { 27 | const record = this.map.get(component); 28 | if (id && !record.includes(id)) { 29 | record.push(id); 30 | } 31 | this.map.set(component, record); 32 | } else { 33 | const value = id ? [id] : []; 34 | this.map.set(component, value); 35 | } 36 | } 37 | 38 | /** 39 | * 获取组件 ID 40 | * @param component 组件名, 如 Button, DatePicker 41 | * @param codeId 用户自定义的 ID 42 | * @returns 43 | */ 44 | generateId(component: string, codeId?: string) { 45 | // FIXME: 使用 size 这里可能存在冲突的风险 46 | const size = this.map.get(component)?.length + 1 || 1; 47 | const id = codeId || `${camelCase(component)}${size}`; 48 | this.setItem(component, id); 49 | 50 | let fullId = `${component}:${id}`; 51 | if (this.prefix) { 52 | fullId = `${this.prefix}:${fullId}`; 53 | } 54 | 55 | return { id, fullId }; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/core/src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ast'; 2 | export * from './assert'; 3 | export * from './code-helpers'; 4 | export * from './string'; 5 | export * from './object'; 6 | export * from './prototype'; 7 | export * from './schema-helpers'; 8 | export * from './id-generator'; 9 | -------------------------------------------------------------------------------- /packages/core/src/helpers/object.ts: -------------------------------------------------------------------------------- 1 | import { Dict } from '@music163/tango-helpers'; 2 | import { IImportDeclarationPayload, IImportSpecifierSourceData } from '../types'; 3 | 4 | /** 5 | * 导入列表解析为导入声明对象 6 | * @param names 7 | * @param nameMap 8 | * @returns 9 | */ 10 | export function namesToImportDeclarations( 11 | names: string[], 12 | nameMap: Dict, 13 | ) { 14 | const map: Dict = {}; 15 | names.forEach((name) => { 16 | const mod = nameMap[name]; 17 | if (mod) { 18 | updateMod(map, mod.source, name, mod.isDefault, !map[mod.source]); 19 | } 20 | }); 21 | return Object.keys(map).map((sourcePath) => ({ 22 | sourcePath, 23 | ...map[sourcePath], 24 | })) as IImportDeclarationPayload[]; 25 | } 26 | 27 | function updateMod( 28 | map: any, 29 | fromPackage: string, 30 | specifier: string, 31 | isDefault = false, 32 | shouldInit = true, 33 | ) { 34 | if (shouldInit) { 35 | map[fromPackage] = {}; 36 | } 37 | if (isDefault) { 38 | map[fromPackage].defaultSpecifier = specifier; 39 | } else if (map[fromPackage].specifiers) { 40 | map[fromPackage].specifiers.push(specifier); 41 | } else { 42 | map[fromPackage].specifiers = [specifier]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/core/src/helpers/schema-helpers.ts: -------------------------------------------------------------------------------- 1 | import { Dict, parseDndId, uuid } from '@music163/tango-helpers'; 2 | 3 | // { id: 1, component, props: { id: 1 }, children: [ { id: 2, component } ] } 4 | export function deepCloneNode(obj: any, component?: string): any { 5 | function deepCloneObject() { 6 | const target: Dict = {}; 7 | for (const key in obj) { 8 | if (Object.hasOwn(obj, key)) { 9 | target[key] = deepCloneNode(obj[key], component); 10 | } 11 | } 12 | return target; 13 | } 14 | 15 | function deepCloneElementNode() { 16 | const target: any = deepCloneObject(); 17 | let name = component || target.component; 18 | if (target.id) { 19 | const dnd = parseDndId(target.id); 20 | name = dnd.component || name; 21 | } 22 | target.id = uuid(`${name}:`); 23 | return target; 24 | } 25 | 26 | if (!obj || typeof obj !== 'object') { 27 | return obj; 28 | } 29 | 30 | if (Array.isArray(obj)) { 31 | return obj.map((item) => deepCloneNode(item, component)); 32 | } 33 | 34 | if (!obj.component) { 35 | return deepCloneObject(); 36 | } 37 | 38 | return deepCloneElementNode(); 39 | } 40 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './models'; 2 | export * from './factory'; 3 | export * from './helpers'; 4 | export * from './types'; 5 | 6 | export { default as generator } from '@babel/generator'; 7 | export { default as traverse } from '@babel/traverse'; 8 | export * from '@babel/parser'; 9 | -------------------------------------------------------------------------------- /packages/core/src/models/abstract-file.ts: -------------------------------------------------------------------------------- 1 | import { AbstractWorkspace } from './abstract-workspace'; 2 | import type { FileType, IFileConfig } from '../types'; 3 | 4 | /** 5 | * 普通文件抽象基类,不进行 AST 解析 6 | */ 7 | export abstract class AbstractFile { 8 | readonly workspace: AbstractWorkspace; 9 | /** 10 | * 文件名 11 | */ 12 | readonly filename: string; 13 | 14 | /** 15 | * 文件类型 16 | */ 17 | readonly type: FileType; 18 | 19 | /** 20 | * 最近修改的时间戳 21 | */ 22 | lastModified: number; 23 | 24 | /** 25 | * 文件解析是否出错 26 | */ 27 | isError: boolean; 28 | 29 | /** 30 | * 文件解析错误消息 31 | */ 32 | errorMessage: string; 33 | 34 | _code: string; 35 | _cleanCode: string; 36 | 37 | get code() { 38 | return this._code; 39 | } 40 | 41 | // FIXME: cleanCode 是不是只有 viewFile 有 ???? 42 | get cleanCode() { 43 | return this._cleanCode; 44 | } 45 | 46 | constructor(workspace: AbstractWorkspace, props: IFileConfig, isSyncCode = true) { 47 | this.workspace = workspace; 48 | this.filename = props.filename; 49 | this.type = props.type; 50 | this.lastModified = Date.now(); 51 | this.isError = false; 52 | 53 | // 这里主要是为了解决 umi ts 编译错误的问题,@see https://github.com/umijs/umi/issues/7594 54 | if (isSyncCode) { 55 | this.update(props.code); 56 | } 57 | } 58 | 59 | /** 60 | * 更新文件内容 61 | */ 62 | abstract update(code?: string): void; 63 | } 64 | -------------------------------------------------------------------------------- /packages/core/src/models/abstract-view-node.ts: -------------------------------------------------------------------------------- 1 | import { Dict } from '@music163/tango-helpers'; 2 | import { AbstractFile } from './abstract-file'; 3 | 4 | export interface IViewNodeInitConfig { 5 | id: string; 6 | component: string; 7 | rawNode: RawNodeType; 8 | file: ViewFileType; 9 | } 10 | 11 | export abstract class AbstractViewNode { 12 | /** 13 | * 节点 ID 14 | */ 15 | readonly id: string; 16 | 17 | /** 18 | * 节点对应的组件名 19 | */ 20 | readonly component: string; 21 | 22 | readonly rawNode: RawNodeType; 23 | 24 | /** 25 | * 节点所属的文件对象 26 | */ 27 | file: ViewFileType; 28 | 29 | /** 30 | * 节点所属的文件对象 31 | */ 32 | props: Record; 33 | 34 | /** 35 | * 节点的位置信息 36 | */ 37 | abstract get loc(): unknown; 38 | 39 | constructor(props: IViewNodeInitConfig) { 40 | this.id = props.id; 41 | this.component = props.component; 42 | this.rawNode = props.rawNode; 43 | this.file = props.file; 44 | } 45 | 46 | /** 47 | * 销毁当前节点,清空文件和节点的关联关系 48 | */ 49 | destroy() { 50 | this.file = undefined; 51 | } 52 | 53 | /** 54 | * 返回克隆后的 ast 节点 55 | * @param overrideProps 额外设置给克隆节点的属性 56 | * @returns 返回克隆的原始节点 57 | */ 58 | abstract cloneRawNode(overrideProps?: Dict): RawNodeType; 59 | } 60 | -------------------------------------------------------------------------------- /packages/core/src/models/drag-source.ts: -------------------------------------------------------------------------------- 1 | import { action, computed, makeObservable, observable } from 'mobx'; 2 | import { ISelectedItemData } from '@music163/tango-helpers'; 3 | import { DropTarget } from './drop-target'; 4 | import { AbstractWorkspace } from './abstract-workspace'; 5 | 6 | /** 7 | * 拖拽来源类,被拖拽的物体 8 | */ 9 | export class DragSource { 10 | /** 11 | * 是否处于拖拽状态 12 | */ 13 | isDragging: boolean; 14 | 15 | /** 16 | * 选中的目标元素数据 17 | */ 18 | data: ISelectedItemData; 19 | 20 | /** 21 | * 放置目标 22 | */ 23 | dropTarget: DropTarget; 24 | 25 | private readonly workspace: AbstractWorkspace; 26 | 27 | get node() { 28 | return this.workspace.getNode(this.data?.id, this.data?.filename); 29 | } 30 | 31 | /** 32 | * 获取对应的 prototype 33 | */ 34 | get prototype() { 35 | return this.workspace.getPrototype(this.data?.name); 36 | } 37 | 38 | get id() { 39 | return this.data?.id; 40 | } 41 | 42 | get name() { 43 | return this.data?.name; 44 | } 45 | 46 | get bounding() { 47 | return this.data?.bounding; 48 | } 49 | 50 | constructor(workspace: AbstractWorkspace) { 51 | this.workspace = workspace; 52 | this.data = null; 53 | this.isDragging = false; 54 | this.dropTarget = new DropTarget(workspace); 55 | 56 | makeObservable(this, { 57 | data: observable, 58 | isDragging: observable, 59 | set: action, 60 | clear: action, 61 | node: computed, 62 | prototype: computed, 63 | }); 64 | } 65 | 66 | /** 67 | * 更新选中数据 68 | * @param props 69 | */ 70 | set(data: ISelectedItemData) { 71 | this.data = data; 72 | this.isDragging = !!data; 73 | } 74 | 75 | /** 76 | * 重置 77 | */ 78 | clear() { 79 | this.data = null; 80 | this.isDragging = false; 81 | this.dropTarget.clear(); 82 | } 83 | 84 | /** 85 | * 获取对应的 node 86 | * @deprecated 87 | */ 88 | getNode() { 89 | return this.node; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /packages/core/src/models/drop-target.ts: -------------------------------------------------------------------------------- 1 | import { action, computed, makeObservable, observable } from 'mobx'; 2 | import { ISelectedItemData } from '@music163/tango-helpers'; 3 | import { AbstractWorkspace } from './abstract-workspace'; 4 | 5 | export enum DropMethod { 6 | ReplaceNode = 'replaceNode', // 替换节点 7 | InsertBefore = 'insertBefore', // 插入节点,放置在前面 8 | InsertAfter = 'insertAfter', // 插入节点,放置在后面 9 | InsertChild = 'insertChild', // 插入子节点,放置在最后 10 | InsertFirstChild = 'insertFirstChild', // 插入子节点,放置在最前 11 | } 12 | 13 | /** 14 | * 放置目标类 15 | */ 16 | export class DropTarget { 17 | /** 18 | * 插入方法 19 | */ 20 | method: DropMethod; 21 | /** 22 | * 放置的目标元素数据 23 | */ 24 | data: ISelectedItemData; 25 | 26 | private readonly workspace: AbstractWorkspace; 27 | 28 | get node() { 29 | return this.workspace.getNode(this.data.id, this.data.filename); 30 | } 31 | 32 | /** 33 | * 获取对应的 prototype 34 | */ 35 | get prototype() { 36 | return this.data?.name ? this.workspace.getPrototype(this.data?.name) : null; 37 | } 38 | 39 | get id() { 40 | return this.data?.id; 41 | } 42 | 43 | get bounding() { 44 | return this.data?.bounding; 45 | } 46 | 47 | get display() { 48 | return this.data?.display; 49 | } 50 | 51 | constructor(workspace: AbstractWorkspace) { 52 | this.workspace = workspace; 53 | this.method = DropMethod.InsertAfter; 54 | this.data = null; 55 | 56 | makeObservable(this, { 57 | method: observable, 58 | data: observable, 59 | set: action, 60 | clear: action, 61 | node: computed, 62 | }); 63 | } 64 | 65 | set(data: ISelectedItemData, method: DropMethod) { 66 | this.data = data; 67 | this.method = method; 68 | } 69 | 70 | /** 71 | * 重置 72 | */ 73 | clear() { 74 | this.data = null; 75 | } 76 | 77 | /** 78 | * 获取对应的 node 79 | * @deprecated 80 | */ 81 | getNode() { 82 | return this.node; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /packages/core/src/models/engine.ts: -------------------------------------------------------------------------------- 1 | import { AbstractWorkspace } from './abstract-workspace'; 2 | import { Designer } from './designer'; 3 | 4 | /** 5 | * 设计器引擎 6 | */ 7 | export class Engine { 8 | /** 9 | * 工作区状态 10 | */ 11 | workspace: AbstractWorkspace; 12 | /** 13 | * 设计器状态 14 | */ 15 | designer: Designer; 16 | 17 | constructor(options: Pick) { 18 | this.workspace = options.workspace; 19 | this.designer = options.designer; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/core/src/models/file.ts: -------------------------------------------------------------------------------- 1 | import { action, computed, makeObservable, observable } from 'mobx'; 2 | import { isNil } from '@music163/tango-helpers'; 3 | import type { IFileConfig } from '../types'; 4 | import { AbstractWorkspace } from './abstract-workspace'; 5 | import { AbstractFile } from './abstract-file'; 6 | 7 | export class TangoFile extends AbstractFile { 8 | constructor(workspace: AbstractWorkspace, props: IFileConfig) { 9 | super(workspace, props, false); 10 | this.update(props.code); 11 | makeObservable(this, { 12 | _code: observable, 13 | _cleanCode: observable, 14 | code: computed, 15 | cleanCode: computed, 16 | update: action, 17 | }); 18 | } 19 | 20 | update(code?: string) { 21 | if (!isNil(code)) { 22 | this.lastModified = Date.now(); 23 | this._code = code; 24 | this._cleanCode = code; 25 | } 26 | this.workspace.onFilesChange([this.filename]); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/src/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './abstract-workspace'; 2 | export * from './abstract-code-workspace'; 3 | export * from './abstract-file'; 4 | export * from './abstract-js-file'; 5 | export * from './abstract-json-file'; 6 | export * from './abstract-view-node'; 7 | export * from './designer'; 8 | export * from './drag-source'; 9 | export * from './drop-target'; 10 | export * from './engine'; 11 | export * from './file'; 12 | export * from './history'; 13 | export * from './interfaces'; 14 | export * from './js-app-entry-file'; 15 | export * from './js-file'; 16 | export * from './js-local-components-entry-file'; 17 | export * from './js-route-config-file'; 18 | export * from './js-service-file'; 19 | export * from './js-store-entry-file'; 20 | export * from './js-store-file'; 21 | export * from './js-view-file'; 22 | export * from './json-file'; 23 | export * from './select-source'; 24 | export * from './view-node'; 25 | export * from './workspace'; 26 | -------------------------------------------------------------------------------- /packages/core/src/models/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { Dict } from '@music163/tango-helpers'; 2 | import { 3 | InsertChildPositionType, 4 | IImportSpecifierSourceData, 5 | IImportSpecifierData, 6 | } from '../types'; 7 | import { IdGenerator } from '../helpers'; 8 | import { AbstractViewNode } from './abstract-view-node'; 9 | 10 | export interface IViewFile { 11 | /** 12 | * 文件名 13 | */ 14 | filename: string; 15 | 16 | /** 17 | * ID 生成器 18 | */ 19 | idGenerator: IdGenerator; 20 | 21 | /** 22 | * 通过导入组件名查找组件来自的包 23 | */ 24 | importMap?: Dict; 25 | 26 | /** 27 | * 判断节点是否存在 28 | * @param codeId 节点 ID 29 | * @returns 存在返回 true,否则返回 false 30 | */ 31 | hasNodeByCodeId?: (codeId: string) => boolean; 32 | 33 | listImportSources?: () => string[]; 34 | listModals?: () => Array<{ label: string; value: string }>; 35 | listForms?: () => Record; 36 | 37 | update: (code?: string, isFormatCode?: boolean, refreshWorkspace?: boolean) => void; 38 | 39 | /** 40 | * 添加新的导入符号 41 | * @param source 导入来源 42 | * @param newSpecifiers 新导入符号列表 43 | * @returns this 44 | */ 45 | addImportSpecifiers: (source: string, newSpecifiers: IImportSpecifierData[]) => IViewFile; 46 | 47 | getNode: (targetNodeId: string) => AbstractViewNode; 48 | 49 | removeNode: (targetNodeId: string) => this; 50 | 51 | insertChild: (targetNodeId: string, newNode: any, position?: InsertChildPositionType) => this; 52 | 53 | insertAfter: (targetNodeId: string, newNode: any) => this; 54 | 55 | insertBefore: (targetNodeId: string, newNode: any) => this; 56 | 57 | replaceNode: (targetNodeId: string, newNode: any) => this; 58 | 59 | replaceViewChildren: (rawNodes: any[]) => this; 60 | 61 | updateNodeAttribute: ( 62 | nodeId: string, 63 | attrName: string, 64 | attrValue?: any, 65 | relatedImports?: string[], 66 | ) => this; 67 | 68 | updateNodeAttributes: ( 69 | nodeId: string, 70 | config: Record, 71 | relatedImports?: string[], 72 | ) => this; 73 | 74 | get nodes(): Map; 75 | get nodesTree(): object[]; 76 | get tree(): any; 77 | /** 78 | * 文件中的代码 79 | */ 80 | get code(): string; 81 | } 82 | -------------------------------------------------------------------------------- /packages/core/src/models/js-app-entry-file.ts: -------------------------------------------------------------------------------- 1 | import { traverseEntryFile } from '../helpers'; 2 | import { IFileConfig } from '../types'; 3 | import { AbstractJsFile } from './abstract-js-file'; 4 | import { AbstractWorkspace } from './abstract-workspace'; 5 | 6 | export class JsAppEntryFile extends AbstractJsFile { 7 | routerType: string; 8 | 9 | constructor(workspace: AbstractWorkspace, props: IFileConfig) { 10 | super(workspace, props, false); 11 | this.update(props.code, true, false); 12 | } 13 | 14 | _analysisAst() { 15 | const config = traverseEntryFile(this.ast); 16 | this.routerType = config?.router?.type; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/src/models/js-file.ts: -------------------------------------------------------------------------------- 1 | import { action, computed, makeObservable, observable } from 'mobx'; 2 | import { IFileConfig } from '../types'; 3 | import { AbstractWorkspace } from './abstract-workspace'; 4 | import { AbstractJsFile } from './abstract-js-file'; 5 | 6 | /** 7 | * 普通 JS 文件 8 | */ 9 | export class JsFile extends AbstractJsFile { 10 | constructor(workspace: AbstractWorkspace, props: IFileConfig) { 11 | super(workspace, props, false); 12 | this.update(props.code, true, false); 13 | 14 | makeObservable(this, { 15 | _code: observable, 16 | _cleanCode: observable, 17 | isError: observable, 18 | errorMessage: observable, 19 | code: computed, 20 | cleanCode: computed, 21 | update: action, 22 | updateAst: action, 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/core/src/models/js-local-components-entry-file.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { action, computed, makeObservable, observable } from 'mobx'; 3 | import { IExportSpecifierData, IFileConfig } from '../types'; 4 | import { traverseComponentsEntryFile } from '../helpers'; 5 | import { AbstractWorkspace } from './abstract-workspace'; 6 | import { AbstractJsFile } from './abstract-js-file'; 7 | 8 | /** 9 | * 本地组件目录的入口文件,例如 '/components/index.js' 或 `/blocks/index.js` 10 | */ 11 | export class JsLocalComponentsEntryFile extends AbstractJsFile { 12 | exportList: Record; 13 | 14 | constructor(workspace: AbstractWorkspace, props: IFileConfig) { 15 | super(workspace, props, false); 16 | this.update(props.code, true, false); 17 | makeObservable(this, { 18 | _code: observable, 19 | _cleanCode: observable, 20 | exportList: observable, 21 | code: computed, 22 | cleanCode: computed, 23 | update: action, 24 | }); 25 | } 26 | 27 | _analysisAst() { 28 | const baseDir = path.dirname(this.filename); 29 | const { exportMap } = traverseComponentsEntryFile(this.ast, baseDir); 30 | this.exportList = exportMap; 31 | Object.keys(this.exportList).forEach((key) => { 32 | this.workspace.componentPrototypes.set(key, { 33 | name: key, 34 | exportType: 'namedExport', 35 | package: baseDir, 36 | type: 'element', 37 | }); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/core/src/models/js-store-entry-file.ts: -------------------------------------------------------------------------------- 1 | import { action, computed, makeObservable, observable, toJS } from 'mobx'; 2 | import { traverseStoreEntryFile, addStoreToEntryFile, removeStoreToEntryFile } from '../helpers'; 3 | import { IFileConfig } from '../types'; 4 | import { AbstractWorkspace } from './abstract-workspace'; 5 | import { AbstractJsFile } from './abstract-js-file'; 6 | 7 | /** 8 | * stores 入口文件 9 | */ 10 | export class JsStoreEntryFile extends AbstractJsFile { 11 | _stores: string[] = []; 12 | 13 | get stores() { 14 | return toJS(this._stores); 15 | } 16 | 17 | constructor(workspace: AbstractWorkspace, props: IFileConfig) { 18 | super(workspace, props, false); 19 | this.update(props.code, true, false); 20 | 21 | makeObservable(this, { 22 | _stores: observable, 23 | _code: observable, 24 | _cleanCode: observable, 25 | isError: observable, 26 | errorMessage: observable, 27 | stores: computed, 28 | code: computed, 29 | cleanCode: computed, 30 | update: action, 31 | updateAst: action, 32 | }); 33 | } 34 | 35 | _analysisAst() { 36 | this._stores = traverseStoreEntryFile(this.ast); 37 | } 38 | 39 | /** 40 | * 新建模型 41 | * @param name 42 | */ 43 | addStore(name: string) { 44 | this.ast = addStoreToEntryFile(this.ast, name); 45 | return this; 46 | } 47 | 48 | /** 49 | * 删除模型 50 | * @param name 51 | */ 52 | removeStore(name: string) { 53 | this.ast = removeStoreToEntryFile(this.ast, name); 54 | return this; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/core/src/models/js-store-file.ts: -------------------------------------------------------------------------------- 1 | import { action, computed, makeObservable, observable } from 'mobx'; 2 | import { 3 | traverseStoreFile, 4 | getModuleNameByFilename, 5 | addStoreState, 6 | updateStoreState, 7 | removeStoreState, 8 | } from '../helpers'; 9 | import { IFileConfig, IStorePropertyData } from '../types'; 10 | import { AbstractWorkspace } from './abstract-workspace'; 11 | import { AbstractJsFile } from './abstract-js-file'; 12 | 13 | /** 14 | * 状态模型模块 15 | */ 16 | export class JsStoreFile extends AbstractJsFile { 17 | /** 18 | * 模块名 19 | */ 20 | name: string; 21 | 22 | namespace: string; 23 | 24 | states: IStorePropertyData[]; 25 | 26 | actions: IStorePropertyData[]; 27 | 28 | constructor(workspace: AbstractWorkspace, props: IFileConfig) { 29 | super(workspace, props, false); 30 | this.name = getModuleNameByFilename(props.filename); 31 | this.update(props.code, true, false); 32 | 33 | makeObservable(this, { 34 | states: observable, 35 | actions: observable, 36 | _code: observable, 37 | _cleanCode: observable, 38 | cleanCode: computed, 39 | code: computed, 40 | update: action, 41 | }); 42 | } 43 | 44 | /** 45 | * 添加状态属性 46 | * @param stateName 47 | * @param initValue 48 | */ 49 | addState(stateName: string, initValue: string) { 50 | this.ast = addStoreState(this.ast, stateName, initValue); 51 | return this; 52 | } 53 | 54 | /** 55 | * 移除状态 56 | */ 57 | removeState(stateName: string) { 58 | this.ast = removeStoreState(this.ast, stateName); 59 | return this; 60 | } 61 | 62 | /** 63 | * 更新状态代码 64 | * @param stateName 状态名 65 | * @param code 代码 66 | */ 67 | updateState(stateName: string, code: string) { 68 | this.ast = updateStoreState(this.ast, stateName, code); 69 | return this; 70 | } 71 | 72 | _analysisAst() { 73 | const { namespace, states, actions } = traverseStoreFile(this.ast); 74 | this.namespace = namespace || this.name; 75 | this.states = states; 76 | this.actions = actions; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/core/src/models/json-file.ts: -------------------------------------------------------------------------------- 1 | import { action, computed, makeObservable, observable, toJS } from 'mobx'; 2 | import type { IFileConfig } from '../types'; 3 | import { AbstractWorkspace } from './abstract-workspace'; 4 | import { AbstractJsonFile } from './abstract-json-file'; 5 | 6 | export class JsonFile extends AbstractJsonFile { 7 | get json(): object { 8 | return toJS(this._object); 9 | } 10 | 11 | constructor(workspace: AbstractWorkspace, props: IFileConfig) { 12 | super(workspace, props); 13 | makeObservable(this, { 14 | _code: observable, 15 | _cleanCode: observable, 16 | _object: observable, 17 | code: computed, 18 | cleanCode: computed, 19 | json: computed, 20 | update: action, 21 | setValue: action, 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/src/models/view-node.ts: -------------------------------------------------------------------------------- 1 | import { JSXElement } from '@babel/types'; 2 | import { Dict } from '@music163/tango-helpers'; 3 | import { cloneJSXElement, getJSXElementAttributes } from '../helpers'; 4 | import { JsViewFile } from './js-view-file'; 5 | import { AbstractViewNode, IViewNodeInitConfig } from './abstract-view-node'; 6 | 7 | /** 8 | * 视图节点类 9 | */ 10 | export class JsxViewNode extends AbstractViewNode { 11 | get loc() { 12 | return this.rawNode?.loc; 13 | } 14 | 15 | constructor(props: IViewNodeInitConfig) { 16 | super(props); 17 | this.props = getJSXElementAttributes(cloneJSXElement(props.rawNode)); 18 | } 19 | 20 | /** 21 | * 返回克隆后的 ast 节点 22 | * @param overrideProps 额外设置给克隆节点的属性 23 | * @returns 24 | */ 25 | cloneRawNode(overrideProps?: Dict) { 26 | return cloneJSXElement(this.rawNode, overrideProps); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/src/models/workspace.ts: -------------------------------------------------------------------------------- 1 | import { action, computed, makeObservable, observable } from 'mobx'; 2 | import { IWorkspaceInitConfig } from './abstract-workspace'; 3 | import { AbstractCodeWorkspace } from './abstract-code-workspace'; 4 | import { FileType } from '../types'; 5 | 6 | /** 7 | * 工作区 8 | */ 9 | export class Workspace extends AbstractCodeWorkspace { 10 | constructor(options?: IWorkspaceInitConfig) { 11 | super(options); 12 | makeObservable(this, { 13 | files: observable, 14 | activeRoute: observable, 15 | activeFile: observable, 16 | activeViewFile: observable, 17 | pages: computed, 18 | bizComps: computed, 19 | fileErrors: computed, 20 | isValid: computed, 21 | setActiveRoute: action, 22 | setActiveFile: action, 23 | addFile: action, 24 | removeFile: action, 25 | }); 26 | } 27 | 28 | addFile(filename: string, code: string, fileType?: FileType) { 29 | super.addFile(filename, code, fileType); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/tests/assert.test.ts: -------------------------------------------------------------------------------- 1 | import { isTangoVariable } from '../src/helpers'; 2 | 3 | describe('assert', () => { 4 | it('isTangoVariable', () => { 5 | expect(isTangoVariable('tango.stores.app.name')).toBeTruthy(); 6 | expect(isTangoVariable('tango.stores?.app?.name')).toBeTruthy(); 7 | expect(isTangoVariable('tango.stores.app?.name')).toBeTruthy(); 8 | // expect(isTangoVariable('tango.copyToClipboard')).toBeTruthy(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /packages/core/tests/engine.test.ts: -------------------------------------------------------------------------------- 1 | import { createEngine, Workspace } from '../src'; 2 | 3 | describe('engine', () => { 4 | it('engine init', () => { 5 | const engine = createEngine({ 6 | workspace: new Workspace({ 7 | entry: '/src/index.js', 8 | }), 9 | }); 10 | expect(engine.workspace.entry).toEqual('/src/index.js'); 11 | }); 12 | 13 | it('engine init without required files', () => { 14 | const engine = createEngine({ 15 | workspace: new Workspace({ 16 | entry: '/src/index.js', 17 | files: [ 18 | { 19 | filename: '/src/index.js', 20 | code: 'console.log("hello")', 21 | }, 22 | { 23 | filename: '/package.json', 24 | code: JSON.stringify({ name: 'sample' }), 25 | }, 26 | ], 27 | }), 28 | }); 29 | expect(engine.workspace.activeViewModule).toBeUndefined(); 30 | expect(engine.workspace.routeModule).toBeUndefined(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /packages/core/tests/proto.test.ts: -------------------------------------------------------------------------------- 1 | import { propDataToKeyValueString } from '../src/helpers'; 2 | 3 | describe('prototype helpers', () => { 4 | it('propDataToKeyValueString', () => { 5 | // basic 6 | expect(propDataToKeyValueString({ name: 'foo', initValue: 'bar' })).toEqual('foo="bar"'); 7 | expect(propDataToKeyValueString({ name: 'foo', initValue: 1 })).toEqual('foo={1}'); 8 | expect(propDataToKeyValueString({ name: 'foo', initValue: false })).toEqual('foo={false}'); 9 | expect(propDataToKeyValueString({ name: 'foo', initValue: null })).toEqual('foo={null}'); 10 | expect(propDataToKeyValueString({ name: 'foo', initValue: [] })).toEqual('foo={[]}'); 11 | expect(propDataToKeyValueString({ name: 'foo', initValue: {} })).toEqual('foo={{}}'); 12 | expect(propDataToKeyValueString({ name: 'foo', initValue: () => {} })).toEqual( 13 | 'foo={() => {}}', 14 | ); 15 | expect(propDataToKeyValueString({ name: 'foo', initValue: { foo: 'bar' } })).toEqual( 16 | 'foo={{ foo: "bar" }}', 17 | ); 18 | expect(propDataToKeyValueString({ name: 'foo', initValue: [{ foo: 'bar' }] })).toEqual( 19 | 'foo={[{ foo: "bar" }]}', 20 | ); 21 | 22 | // wrapped code 23 | expect( 24 | propDataToKeyValueString({ name: 'foo', initValue: '{{}}' }), 25 | ).toEqual('foo={}'); 26 | expect(propDataToKeyValueString({ name: 'foo', initValue: '{{tango}}' })).toEqual( 27 | 'foo={tango}', 28 | ); 29 | expect(propDataToKeyValueString({ name: 'foo', initValue: '{{"bar"}}' })).toEqual( 30 | 'foo={"bar"}', 31 | ); 32 | expect(propDataToKeyValueString({ name: 'foo', initValue: '{{() => {}}}' })).toEqual( 33 | 'foo={() => {}}', 34 | ); 35 | 36 | // compatible with old version 37 | expect(propDataToKeyValueString({ name: 'foo', initValue: '{() => {}}' })).toEqual( 38 | 'foo={() => {}}', 39 | ); 40 | expect( 41 | propDataToKeyValueString({ name: 'foo', initValue: '{}' }), 42 | ).toEqual('foo={}'); 43 | // expect(propDataToKeyValueString({ name: 'foo', initValue: '{tango}' })).toEqual('foo={tango}'); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/core/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.prod.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": ["src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../typedoc.base.json"], 3 | "entryPoints": ["src/index.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/designer/README.md: -------------------------------------------------------------------------------- 1 |

Tango LowCode Designer

2 |
3 | 4 | A source code based low-code designer from the NetEase Cloud Music Develop Team. 5 | 6 |
7 | 8 | ## 📄 Usage 9 | 10 | Install the low-code designer 11 | 12 | ```bash 13 | npm install @music163/tango-designer 14 | ``` 15 | 16 | Initialize the low-code designer engine 17 | 18 | ```js 19 | import { createEngine } form '@music163/tango-designer'; 20 | 21 | // init designer engine 22 | const engine = createEngine({ 23 | entry: '/src/index.js', 24 | files: sampleFiles, 25 | componentPrototypes: prototypes as any, 26 | }); 27 | ``` 28 | 29 | Initialize the drag-and-drop engine 30 | 31 | ```js 32 | import { DndQuery } form '@music163/tango-designer'; 33 | 34 | const sandboxQuery = new DndQuery({ 35 | context: 'iframe', 36 | }); 37 | ``` 38 | -------------------------------------------------------------------------------- /packages/designer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@music163/tango-designer", 3 | "version": "1.4.6", 4 | "description": "lowcode designer", 5 | "keywords": [ 6 | "react" 7 | ], 8 | "author": "wwsun ", 9 | "homepage": "", 10 | "license": "MIT", 11 | "main": "lib/cjs/index.js", 12 | "module": "lib/esm/index.js", 13 | "types": "lib/esm/index.d.ts", 14 | "files": [ 15 | "dist", 16 | "lib" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/netease/tango.git" 21 | }, 22 | "scripts": { 23 | "clean": "rimraf lib/", 24 | "build": "yarn clean && yarn build:esm && yarn build:cjs", 25 | "build:esm": "tsc --project tsconfig.prod.json --outDir lib/esm/ --module ES2020", 26 | "build:cjs": "tsc --project tsconfig.prod.json --outDir lib/cjs/ --module CommonJS", 27 | "prepublishOnly": "yarn build" 28 | }, 29 | "peerDependencies": { 30 | "react": ">= 16.8", 31 | "styled-components": ">= 4" 32 | }, 33 | "dependencies": { 34 | "@ant-design/icons": "^4.8.0", 35 | "@music163/request": "^0.2.0", 36 | "@music163/tango-context": "^1.1.10", 37 | "@music163/tango-core": "^1.4.4", 38 | "@music163/tango-helpers": "^1.2.4", 39 | "@music163/tango-sandbox": "^1.0.13", 40 | "@music163/tango-setting-form": "^1.2.15", 41 | "@music163/tango-ui": "^1.4.5", 42 | "antd": "^4.24.2", 43 | "cash-dom": "^8.1.2", 44 | "classnames": "^2.5.1", 45 | "color": "^4.2.3", 46 | "coral-system": "^1.0.5", 47 | "cssjson": "^2.1.3", 48 | "date-fns": "^2.29.2", 49 | "lodash-es": "^4.17.21", 50 | "moment": "^2.30.1", 51 | "react-color": "^2.19.3", 52 | "react-resizable": "^3.0.5", 53 | "semver": "^7.6.2" 54 | }, 55 | "devDependencies": { 56 | "@types/color": "^3.0.5", 57 | "@types/react-color": "^3.0.9", 58 | "@types/react-resizable": "^3.0.6" 59 | }, 60 | "publishConfig": { 61 | "access": "public", 62 | "registry": "https://registry.npmjs.org/" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/designer/src/components/drag-box.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, HTMLCoralProps } from 'coral-system'; 3 | 4 | type DragBoxProps = { 5 | index?: number; 6 | onMove?: Function; 7 | } & HTMLCoralProps<'div'>; 8 | 9 | export const DragBox = ({ children, index, onMove, ...rest }: DragBoxProps) => { 10 | const handleStartDrag = (ev: React.DragEvent) => { 11 | ev.dataTransfer.setData('text', `${index}`); 12 | }; 13 | 14 | const handleEndDrag = (ev: React.DragEvent) => { 15 | ev.dataTransfer.dropEffect = 'move'; 16 | }; 17 | 18 | const handleDrop = (ev: React.DragEvent) => { 19 | const idx = ev.dataTransfer.getData('text'); 20 | 21 | onMove?.(+idx, index); 22 | }; 23 | 24 | const handleDragOver = (ev: React.DragEvent) => { 25 | // 阻止默认行为,触发 onDrop 26 | ev.preventDefault(); 27 | // TODO: 优化动画交互 28 | }; 29 | 30 | return ( 31 | 39 | {children} 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /packages/designer/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './context-menu'; 2 | export * from './drag-box'; 3 | export * from './input-kv'; 4 | export * from './variable-tree'; 5 | export * from './variable-tree-modal'; 6 | export * from './components-popover'; 7 | export * from './file-errors-overlay'; 8 | -------------------------------------------------------------------------------- /packages/designer/src/components/variable-tree-modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { ModalProps, Modal } from 'antd'; 3 | import { Box } from 'coral-system'; 4 | import { IVariableTreeNode, noop, useBoolean } from '@music163/tango-helpers'; 5 | import { VariableTree, VariableTreeProps } from './variable-tree'; 6 | 7 | interface VariableTreeModalProps extends VariableTreeProps { 8 | trigger?: React.ReactElement; 9 | title?: ModalProps['title']; 10 | modalProps?: ModalProps; 11 | } 12 | 13 | export function VariableTreeModal({ 14 | trigger, 15 | title, 16 | modalProps, 17 | onSelect = noop, 18 | ...rest 19 | }: VariableTreeModalProps) { 20 | const [node, setNode] = useState(); 21 | const [visible, { on, off }] = useBoolean(false); 22 | return ( 23 | 24 | {React.cloneElement(trigger, { onClick: on })} 25 | { 33 | if (node) { 34 | onSelect(node); 35 | off(); 36 | } 37 | }} 38 | width="60%" 39 | {...modalProps} 40 | > 41 | 42 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /packages/designer/src/components/variable-tree/value-preview.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { isFunction, isNil, isString } from '@music163/tango-helpers'; 3 | import { InputCode, JsonView, JsonViewProps } from '@music163/tango-ui'; 4 | import { Empty } from 'antd'; 5 | 6 | interface ValuePreviewProps { 7 | value?: unknown; 8 | /** 9 | * 选择预览结点的回调 10 | */ 11 | onCopy?: JsonViewProps['onCopy']; 12 | } 13 | 14 | export function ValuePreview({ value, onCopy }: ValuePreviewProps) { 15 | if (isNil(value)) { 16 | return ; 17 | } 18 | 19 | if (isFunction(value)) { 20 | return ; 21 | } 22 | 23 | if (typeof value === 'object') { 24 | return ; 25 | } 26 | 27 | return ( 28 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /packages/designer/src/context-menu/copy-node.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useWorkspace, observer } from '@music163/tango-context'; 3 | import { ContextAction } from '@music163/tango-ui'; 4 | import { CopyOutlined } from '@ant-design/icons'; 5 | 6 | export const CopyNodeContextAction = observer(() => { 7 | const workspace = useWorkspace(); 8 | return ( 9 | } 11 | hotkey="Command+C" 12 | onClick={() => { 13 | workspace.copySelectedNode(); 14 | }} 15 | > 16 | 复制节点 17 | 18 | ); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/designer/src/context-menu/delete-node.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useWorkspace, observer } from '@music163/tango-context'; 3 | import { ContextAction } from '@music163/tango-ui'; 4 | import { DeleteOutlined } from '@ant-design/icons'; 5 | 6 | export const DeleteNodeContextAction = observer(() => { 7 | const workspace = useWorkspace(); 8 | return ( 9 | } 11 | hotkey="Backspace" 12 | onClick={() => { 13 | workspace.removeSelectedNode(); 14 | }} 15 | > 16 | 删除节点 17 | 18 | ); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/designer/src/context-menu/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './copy-node'; 2 | export * from './delete-node'; 3 | export * from './paste-node'; 4 | export * from './view-source'; 5 | -------------------------------------------------------------------------------- /packages/designer/src/context-menu/paste-node.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useWorkspace, observer } from '@music163/tango-context'; 3 | import { ContextAction } from '@music163/tango-ui'; 4 | import { SnippetsOutlined } from '@ant-design/icons'; 5 | 6 | export const PasteNodeContextAction = observer(() => { 7 | const workspace = useWorkspace(); 8 | return ( 9 | } 11 | hotkey="Command+V" 12 | onClick={() => { 13 | workspace.pasteSelectedNode(); 14 | }} 15 | > 16 | 粘贴节点 17 | 18 | ); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/designer/src/context-menu/view-source.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDesigner, observer } from '@music163/tango-context'; 3 | import { ContextAction } from '@music163/tango-ui'; 4 | import { CodeOutlined } from '@ant-design/icons'; 5 | 6 | export const ViewSourceContextAction = observer(() => { 7 | const designer = useDesigner(); 8 | return ( 9 | } 11 | onClick={() => { 12 | designer.setActiveView('code'); 13 | }} 14 | > 15 | 查看源码 16 | 17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/designer/src/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from '@music163/tango-helpers'; 2 | import type { DndQuery } from './dnd'; 3 | 4 | export interface IDesignerContext { 5 | /** 6 | * 沙箱查询实例 7 | */ 8 | sandboxQuery: DndQuery; 9 | /** 10 | * 预览沙箱查询实例 11 | */ 12 | previewSandboxQuery: DndQuery; 13 | /** 14 | * 远程服务 15 | */ 16 | remoteServices: Record; 17 | } 18 | 19 | const [DesignerProvider, useDesigner] = createContext({ 20 | name: 'DesignerContext', 21 | }); 22 | 23 | export { DesignerProvider }; 24 | 25 | export const useSandboxQuery = () => { 26 | return useDesigner().sandboxQuery; 27 | }; 28 | 29 | export const usePreviewSandboxQuery = () => { 30 | return useDesigner().previewSandboxQuery; 31 | }; 32 | 33 | export const useRemoteServices = () => { 34 | return useDesigner().remoteServices; 35 | }; 36 | -------------------------------------------------------------------------------- /packages/designer/src/designer-panel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box } from 'coral-system'; 3 | import { ReactComponentProps } from '@music163/tango-helpers'; 4 | 5 | export interface DesignerPanelProps extends ReactComponentProps { 6 | /** 7 | * 品牌图标 8 | */ 9 | logo?: React.ReactNode; 10 | /** 11 | * 项目描述 12 | */ 13 | description?: React.ReactNode; 14 | /** 15 | * 主行动点 16 | */ 17 | actions?: React.ReactNode; 18 | /** 19 | * 自定义头部节点 20 | */ 21 | header?: React.ReactNode; 22 | } 23 | 24 | /** 25 | * 设计器面板 26 | */ 27 | export function DesignerPanel(props: DesignerPanelProps) { 28 | const { header, logo, description, actions, children } = props; 29 | return ( 30 | 31 | {header ?? ( 32 | 43 | 44 | {logo} 45 | {description} 46 | 47 | {actions} 48 | 49 | )} 50 | 56 | {children} 57 | 58 | 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /packages/designer/src/designer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { SystemProvider, extendTheme } from 'coral-system'; 3 | import { ConfigProvider } from 'antd'; 4 | import { TangoEngineProvider, ITangoEngineContext } from '@music163/tango-context'; 5 | import zhCN from 'antd/lib/locale/zh_CN'; 6 | import { DesignerProvider, IDesignerContext } from './context'; 7 | import defaultTheme from './themes/default'; 8 | import { DndQuery } from './dnd'; 9 | import { DESIGN_SANDBOX_ID, PREVIEW_SANDBOX_ID } from './helpers'; 10 | 11 | const builtinSandboxQuery = new DndQuery({ 12 | context: `#${DESIGN_SANDBOX_ID}`, 13 | }); 14 | 15 | const builtinPreviewSandboxQuery = new DndQuery({ 16 | context: `#${PREVIEW_SANDBOX_ID}`, 17 | }); 18 | 19 | export interface DesignerProps extends Partial, ITangoEngineContext { 20 | /** 21 | * 主题包 22 | */ 23 | theme?: any; 24 | children?: React.ReactNode; 25 | } 26 | 27 | /** 28 | * 设计器状态和设置容器 29 | * @param props 30 | * @returns 31 | */ 32 | export function Designer(props: DesignerProps) { 33 | const { 34 | engine, 35 | config, 36 | theme: themeProp, 37 | sandboxQuery = builtinSandboxQuery, 38 | previewSandboxQuery = builtinPreviewSandboxQuery, 39 | remoteServices = {}, 40 | children, 41 | } = props; 42 | const theme = useMemo(() => extendTheme(themeProp, defaultTheme), [themeProp]); 43 | return ( 44 | 45 | 46 | 47 | 48 | {children} 49 | 50 | 51 | 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /packages/designer/src/dnd/hotkey.ts: -------------------------------------------------------------------------------- 1 | import { Dict } from '@music163/tango-helpers'; 2 | 3 | /** 4 | * 快捷键 5 | */ 6 | export class Hotkey { 7 | private readonly hotkeyMap: Dict = {}; 8 | 9 | constructor(hotkeys: Record) { 10 | Object.keys(hotkeys).forEach((hotkey) => { 11 | const keys = hotkey.split(','); 12 | keys.forEach((key) => { 13 | if (key) { 14 | key = fixKey(key); 15 | this.hotkeyMap[key] = hotkeys[hotkey]; 16 | } 17 | }); 18 | }); 19 | } 20 | 21 | run(hotkey: string) { 22 | const callback = this.hotkeyMap[hotkey]; 23 | callback?.(); 24 | } 25 | } 26 | 27 | function fixKey(key: string) { 28 | if (key === 'esc') { 29 | return 'escape'; 30 | } 31 | return key.replaceAll('command', 'meta'); 32 | } 33 | -------------------------------------------------------------------------------- /packages/designer/src/dnd/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-dnd'; 2 | export * from './dnd-query'; 3 | -------------------------------------------------------------------------------- /packages/designer/src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dom'; 2 | export * from './template'; 3 | -------------------------------------------------------------------------------- /packages/designer/src/helpers/template.ts: -------------------------------------------------------------------------------- 1 | const newStoreTemplate = ` 2 | import { defineStore } from '@music163/tango-boot'; 3 | export default defineStore({ 4 | }); 5 | `; 6 | 7 | export const CODE_TEMPLATES = { 8 | newStoreTemplate, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/designer/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './context'; 2 | export * from './designer'; 3 | export * from './designer-panel'; 4 | export * from './workspace-panel'; 5 | export * from './setting-panel'; 6 | export * from './workspace-view'; 7 | export * from './simulator'; 8 | export * from './dnd'; 9 | export * from './editor'; 10 | export * from './sandbox'; 11 | export * from './sidebar'; 12 | export * from './toolbar'; 13 | export * from './selection-menu'; 14 | export * from './widgets'; 15 | export * from './themes'; 16 | export * from './components'; 17 | 18 | export { register as registerSetter } from '@music163/tango-setting-form'; 19 | 20 | export type { FormItemComponentProps, IFormItemCreateOptions } from '@music163/tango-setting-form'; 21 | -------------------------------------------------------------------------------- /packages/designer/src/sandbox/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sandbox'; 2 | -------------------------------------------------------------------------------- /packages/designer/src/selection-menu/copy-node.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useWorkspace, observer } from '@music163/tango-context'; 3 | import { SelectAction } from '@music163/tango-ui'; 4 | import { CopyOutlined } from '@ant-design/icons'; 5 | 6 | export const CopyNodeAction = observer(() => { 7 | const workspace = useWorkspace(); 8 | return ( 9 | { 12 | workspace.cloneSelectedNode(); 13 | }} 14 | > 15 | 16 | 17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/designer/src/selection-menu/delete-node.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useWorkspace, observer } from '@music163/tango-context'; 3 | import { SelectAction } from '@music163/tango-ui'; 4 | import { DeleteOutlined } from '@ant-design/icons'; 5 | 6 | export const DeleteNodeAction = observer(() => { 7 | const workspace = useWorkspace(); 8 | return ( 9 | { 12 | workspace.removeSelectedNode(); 13 | }} 14 | > 15 | 16 | 17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/designer/src/selection-menu/index.ts: -------------------------------------------------------------------------------- 1 | export * from './copy-node'; 2 | export * from './delete-node'; 3 | export * from './more-actions'; 4 | export * from './select-parent-node'; 5 | export * from './view-source'; 6 | -------------------------------------------------------------------------------- /packages/designer/src/selection-menu/more-actions.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer } from '@music163/tango-context'; 3 | import { SelectAction } from '@music163/tango-ui'; 4 | import { MoreOutlined } from '@ant-design/icons'; 5 | import { Dropdown } from 'antd'; 6 | import { ContextMenu } from '../components'; 7 | 8 | export const MoreActionsAction = observer(() => { 9 | return ( 10 | }> 11 | 12 | 13 | 14 | 15 | ); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/designer/src/selection-menu/select-parent-node.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useWorkspace, observer } from '@music163/tango-context'; 3 | import { IconFont, SelectAction } from '@music163/tango-ui'; 4 | 5 | export const SelectParentNodeAction = observer(() => { 6 | const workspace = useWorkspace(); 7 | 8 | return ( 9 | { 12 | workspace.selectSource.selectParent(); 13 | }} 14 | > 15 | 16 | 17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/designer/src/selection-menu/view-source.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer, useDesigner } from '@music163/tango-context'; 3 | import { SelectAction, CodeOutlined } from '@music163/tango-ui'; 4 | 5 | export const ViewSourceAction = observer(() => { 6 | const designer = useDesigner(); 7 | return ( 8 | { 11 | designer.setActiveView('code'); 12 | }} 13 | > 14 | 15 | 16 | ); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/designer/src/setters/action-list-setter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Select } from 'antd'; 3 | import { FormItemComponentProps } from '@music163/tango-setting-form'; 4 | import { ListSetter, NewOptionFormFieldType } from './list-setter'; 5 | 6 | const defaultNewOptionValue = { 7 | key: '1', 8 | label: '按钮', 9 | }; 10 | 11 | const shapes = [ 12 | { label: '普通按钮', value: 'button' }, 13 | { label: '文本按钮', value: 'text' }, 14 | ]; 15 | 16 | const buttonTypes = [ 17 | { label: '主要', value: 'primary' }, 18 | { label: '次要', value: 'secondary' }, 19 | { label: '普通', value: 'normal' }, 20 | ]; 21 | 22 | const types = [ 23 | { label: '普通动作', value: 'action' }, 24 | { label: '确认提示', value: 'confirm' }, 25 | ]; 26 | 27 | const optionFormFields: NewOptionFormFieldType[] = [ 28 | { label: 'key', name: 'key', required: true }, 29 | { label: '文本', name: 'label', required: true }, 30 | { label: '外观', name: 'shape', component: , 35 | }, 36 | { label: '类型', name: 'actionType', component: { 24 | const option = optionsStore.getNode(val); 25 | const detail = option?.relatedImports 26 | ? { relatedImports: option.relatedImports } 27 | : undefined; 28 | onChange?.(val, detail); 29 | }} 30 | {...props} 31 | /> 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /packages/designer/src/setters/render-setter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useMemo, useState } from 'react'; 2 | import { ActionSelect, InputCode } from '@music163/tango-ui'; 3 | import { FormItemComponentProps } from '@music163/tango-setting-form'; 4 | import { Box } from 'coral-system'; 5 | import { Dict, IOptionItem } from '@music163/tango-helpers'; 6 | import { getCallbackValue } from './code-setter'; 7 | 8 | export interface RenderSetterProps { 9 | text?: string; 10 | options?: IOptionItem[]; 11 | fallbackOption?: IOptionItem; 12 | } 13 | 14 | const defaultOptions: IOptionItem[] = [ 15 | { label: '取消自定义', value: '' }, 16 | { label: '自定义渲染', value: 'Box', render: '() => ' }, 17 | ]; 18 | 19 | /** 20 | * Render Props Setters 21 | */ 22 | export function RenderSetter({ 23 | value, 24 | onChange, 25 | text = '自定义渲染为', 26 | options = defaultOptions, 27 | template = `() => {{content}}`, 28 | fallbackOption, 29 | }: FormItemComponentProps & RenderSetterProps) { 30 | const [inputValue, setInputValue] = useState(value || ''); 31 | useEffect(() => { 32 | setInputValue(value); 33 | }, [value]); 34 | 35 | const optionsMap = useMemo(() => { 36 | return options.reduce((prev, cur) => { 37 | prev[cur.value] = cur; 38 | return prev; 39 | }, {}); 40 | }, [options]); 41 | 42 | const onSelect = useCallback( 43 | (key: string) => { 44 | const option = optionsMap[key] || fallbackOption; 45 | const next = option?.render || getCallbackValue(option.renderBody, template); 46 | if (next) { 47 | onChange(next, { relatedImports: option.relatedImports }); 48 | } else { 49 | onChange(undefined); 50 | } 51 | }, 52 | [optionsMap, fallbackOption, onChange, template], 53 | ); 54 | return ( 55 | 56 | 57 | {inputValue && ( 58 | setInputValue(val)} 61 | onBlur={() => onChange(inputValue)} 62 | /> 63 | )} 64 | 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /packages/designer/src/setters/router-setter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Box } from 'coral-system'; 3 | import { Input, Radio } from 'antd'; 4 | import { Dict, isValidUrl } from '@music163/tango-helpers'; 5 | import { FormItemComponentProps } from '@music163/tango-setting-form'; 6 | import { useWorkspaceData } from '@music163/tango-context'; 7 | import { PickerSetter } from './picker-setter'; 8 | 9 | const routerModeMap: Dict = { 10 | link: '链接', 11 | router: '路由', 12 | both: '', 13 | }; 14 | 15 | export function RouterSetter(props: FormItemComponentProps) { 16 | const [type, setType] = useState<'input' | 'select'>(() => { 17 | return isValidUrl(props.value) ? 'input' : 'select'; 18 | }); 19 | const [input, setInput] = useState(props.value); 20 | const { routeOptions } = useWorkspaceData(); 21 | 22 | const displayType = props.type || 'both'; 23 | useEffect(() => { 24 | displayType === 'router' ? setType('select') : setType('input'); 25 | }, [displayType]); 26 | return ( 27 | 28 | 29 | {['both', 'router'].includes(displayType) && ( 30 | setType(e.target.value)} 35 | > 36 | 选择路由 37 | 自定义输入 38 | 39 | )} 40 | 41 | {type === 'select' && ( 42 | 43 | )} 44 | {type === 'input' && ( 45 | setInput(e.target.value)} 50 | onBlur={() => props?.onChange(input)} 51 | /> 52 | )} 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /packages/designer/src/sidebar/datasource-panel/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box } from 'coral-system'; 3 | import { Tabs } from '@music163/tango-ui'; 4 | import InterfaceConfig from './interface-config'; 5 | import ProxyConfig from './proxy-config'; 6 | 7 | export function DataSourcePanel(props: Record) { 8 | return ( 9 | 10 | , 20 | }, 21 | { 22 | key: 'proxy', 23 | label: '代理', 24 | children: , 25 | }, 26 | ]} 27 | /> 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /packages/designer/src/sidebar/datasource-panel/interface-config.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer, useWorkspace, useWorkspaceData } from '@music163/tango-context'; 3 | import { Box } from 'coral-system'; 4 | import { VariableTree } from '../../components'; 5 | import { useSandboxQuery } from '../../context'; 6 | 7 | // 移除掉不必要的属性 8 | export function shapeServiceValues(val: any) { 9 | const shapeValues = { ...val }; 10 | delete shapeValues.type; 11 | return shapeValues; 12 | } 13 | 14 | const DataSourceView = observer(() => { 15 | const sandbox = useSandboxQuery(); 16 | const workspace = useWorkspace(); 17 | const { serviceVariables } = useWorkspaceData(); 18 | const serviceModules = Object.keys(workspace.serviceModules).map((key) => ({ 19 | label: key === 'index' ? '默认模块' : key, 20 | value: key, 21 | })); 22 | 23 | return ( 24 | 25 | { 30 | workspace.removeServiceFunction(variableKey); 31 | }} 32 | onAddService={(data) => { 33 | const { name, moduleName, ...payload } = shapeServiceValues(data); 34 | workspace.addServiceFunction(name, payload, moduleName); 35 | }} 36 | onUpdateService={(data) => { 37 | const { name, moduleName, ...payload } = shapeServiceValues(data); 38 | workspace.updateServiceFunction(name, payload, moduleName); 39 | }} 40 | getServiceData={(serviceKey) => { 41 | const data = workspace.getServiceFunction(serviceKey); 42 | return { 43 | name: data.name, 44 | moduleName: data.moduleName, 45 | method: 'get', 46 | ...data.config, 47 | }; 48 | }} 49 | /> 50 | 51 | ); 52 | }); 53 | 54 | export default DataSourceView; 55 | -------------------------------------------------------------------------------- /packages/designer/src/sidebar/history-panel.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import cx from 'classnames'; 3 | import { Box, css } from 'coral-system'; 4 | import { format } from 'date-fns'; 5 | import { observer, useWorkspace } from '@music163/tango-context'; 6 | 7 | export const HistoryPanel = observer(() => { 8 | const { history } = useWorkspace(); 9 | 10 | return ( 11 | 12 | {history.list.map((item, index) => ( 13 | 18 | ))} 19 | 20 | ); 21 | }); 22 | 23 | const itemStyle = css` 24 | line-height: 2.5; 25 | cursor: pointer; 26 | user-select: none; 27 | 28 | &:hover { 29 | background-color: var(--tango-colors-gray-10); 30 | } 31 | 32 | &.active { 33 | color: var(--tango-colors-brand); 34 | } 35 | `; 36 | 37 | function HistoryItem({ data, ...rest }: any) { 38 | const { time, message } = data; 39 | const formatTime = useMemo(() => { 40 | return format(time, 'yyyy/MM/dd HH:mm:mm'); 41 | }, [time]); 42 | return ( 43 | 44 | {message} 45 | {formatTime} 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /packages/designer/src/sidebar/index.ts: -------------------------------------------------------------------------------- 1 | export * from './datasource-panel'; 2 | export * from './outline-panel'; 3 | export * from './components-panel'; 4 | export * from './dependency-panel'; 5 | export * from './history-panel'; 6 | export * from './variable-panel'; 7 | export * from './sidebar'; 8 | export * from './resizable-box'; 9 | -------------------------------------------------------------------------------- /packages/designer/src/sidebar/outline-panel/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box } from 'coral-system'; 3 | import { CollapsePanel } from '@music163/tango-ui'; 4 | import { StateTree } from './state-tree'; 5 | import { ComponentsTree, ComponentsTreeProps } from './components-tree'; 6 | 7 | export interface OutlineViewProps extends ComponentsTreeProps { 8 | /** 9 | * 展示状态视图 10 | */ 11 | showStateView?: boolean; 12 | } 13 | 14 | export function OutlinePanel({ showStateView = true, ...treeProps }: OutlineViewProps) { 15 | return ( 16 | 17 | 24 | 25 | 26 | {showStateView && } 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /packages/designer/src/sidebar/variable-panel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from 'antd'; 3 | import { FileAddOutlined } from '@ant-design/icons'; 4 | import { useBoolean } from '@music163/tango-helpers'; 5 | import { Panel } from '@music163/tango-ui'; 6 | import { observer, useWorkspace, useWorkspaceData } from '@music163/tango-context'; 7 | import { VariableTree } from '../components'; 8 | import { CODE_TEMPLATES } from '../helpers'; 9 | import { AddStoreForm } from '../components/variable-tree/add-store'; 10 | 11 | export interface VariablePanelProps { 12 | newStoreTemplate?: string; 13 | } 14 | 15 | export const VariablePanel = observer( 16 | ({ newStoreTemplate = CODE_TEMPLATES.newStoreTemplate }: VariablePanelProps) => { 17 | const [isAdd, { on, off }] = useBoolean(); 18 | const workspace = useWorkspace(); 19 | const { storeVariables } = useWorkspaceData(); 20 | const storeNames = storeVariables.map((item) => item.title) as string[]; 21 | 22 | return ( 23 | } onClick={on}> 29 | 新建模型 30 | 31 | } 32 | bodyProps={{ p: 'm', height: '100%' }} 33 | > 34 | {isAdd ? ( 35 | { 39 | workspace.addStoreFile(values.name, newStoreTemplate); 40 | off(); 41 | }} 42 | /> 43 | ) : ( 44 | { 49 | workspace.addStoreState(storeName, data.name, data.initialValue); 50 | }} 51 | onRemoveStoreVariable={(variableKey) => { 52 | workspace.removeStoreVariable(variableKey); 53 | }} 54 | onUpdateStoreVariable={(variableKey, code) => { 55 | workspace.updateStoreVariable(variableKey, code); 56 | }} 57 | /> 58 | )} 59 | 60 | ); 61 | }, 62 | ); 63 | -------------------------------------------------------------------------------- /packages/designer/src/simulator/bottom-bar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, HTMLCoralProps, css } from 'coral-system'; 3 | import { Breadcrumb } from 'antd'; 4 | import { observer, useWorkspace, useDesigner } from '@music163/tango-context'; 5 | 6 | const itemWrapperStyle = css` 7 | user-select: none; 8 | 9 | &:hover { 10 | color: var(--tango-colors-brand); 11 | } 12 | `; 13 | 14 | const ItemWrapper = (props: HTMLCoralProps<'div'>) => { 15 | return ; 16 | }; 17 | 18 | export const BottomBar = observer(() => { 19 | const workspace = useWorkspace(); 20 | const designer = useDesigner(); 21 | 22 | if (workspace.selectSource.size !== 1) { 23 | return null; 24 | } 25 | 26 | const parents = workspace.selectSource?.first?.parents || []; 27 | const reversedParents = [...parents].reverse(); 28 | const length = parents.length; 29 | 30 | return ( 31 | 40 | 41 | {reversedParents.map((parent, index) => ( 42 | { 45 | workspace.selectSource.select({ 46 | ...parent, 47 | parents: parents.slice(length - index), 48 | }); 49 | }} 50 | > 51 | {parent.name} 52 | 53 | ))} 54 | 55 | {workspace.selectSource.first.name} 56 | 57 | 58 | 59 | ); 60 | }); 61 | -------------------------------------------------------------------------------- /packages/designer/src/simulator/error-boundary.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface Props { 4 | children?: React.ReactNode; 5 | } 6 | 7 | interface State { 8 | hasError: boolean; 9 | } 10 | 11 | // TODO: enable ErrorBoundary 12 | export class ErrorBoundary extends React.Component { 13 | state = { 14 | hasError: false, 15 | }; 16 | 17 | static getDerivedStateFromError(error: any) { 18 | // Update state so the next render will show the fallback UI. 19 | return { hasError: true }; 20 | } 21 | 22 | componentDidCatch(error: any, errorInfo: any) { 23 | // You can also log the error to an error reporting service 24 | // logErrorToMyService(error, errorInfo); 25 | } 26 | 27 | render() { 28 | if (this.state.hasError) { 29 | // You can render any custom fallback UI 30 | return

Something went wrong.

; 31 | } 32 | 33 | return this.props.children; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/designer/src/simulator/ghost.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { observer, useWorkspace } from '@music163/tango-context'; 4 | import { DRAG_GHOST_ID } from '../helpers'; 5 | 6 | const GhostWrapper = styled.div` 7 | display: inline-block; 8 | position: absolute; 9 | top: -50%; 10 | left: -50%; 11 | z-index: -1; 12 | background-color: rgba(30, 167, 253, 0.5); 13 | color: #fff; 14 | font-size: 12px; 15 | line-height: 2; 16 | padding: 0 12px; 17 | pointer-events: none; 18 | white-space: nowrap; 19 | `; 20 | 21 | export const Ghost = observer(() => { 22 | const workspace = useWorkspace(); 23 | const text = workspace.dragSource.name || ''; 24 | return {text}; 25 | }); 26 | -------------------------------------------------------------------------------- /packages/designer/src/simulator/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './simulator'; 2 | export * from './viewport'; 3 | export * from './bottom-bar'; 4 | -------------------------------------------------------------------------------- /packages/designer/src/simulator/insertion.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, css } from 'coral-system'; 3 | import { DropMethod } from '@music163/tango-core'; 4 | import { observer, useWorkspace } from '@music163/tango-context'; 5 | 6 | const insertionStyle = css` 7 | display: none; 8 | z-index: 1000; 9 | position: absolute; 10 | pointer-events: none; 11 | transition: left 0.2s ease-out, top 0.2s ease-out; 12 | `; 13 | 14 | /** 15 | * 插入提示符 16 | */ 17 | export const InsertionPrompt = observer(() => { 18 | const workspace = useWorkspace(); 19 | const dragSource = workspace.dragSource; 20 | const dropTarget = dragSource.dropTarget; 21 | 22 | if (!dragSource.isDragging || !dropTarget.bounding) { 23 | // 没有拖拽,或没有着陆点的轮廓信息,不渲染 24 | return null; 25 | } 26 | 27 | let style: React.CSSProperties; 28 | const bounding = dropTarget.bounding; 29 | switch (dropTarget.display) { 30 | case 'inline': 31 | case 'inline-block': 32 | case 'inline-flex': 33 | case 'inline-grid': { 34 | if (dropTarget.method === DropMethod.InsertBefore) { 35 | style = { 36 | display: 'block', 37 | left: bounding.left, 38 | top: bounding.top, 39 | width: 2, 40 | height: bounding.height, 41 | }; 42 | } else if (dropTarget.method === DropMethod.InsertAfter) { 43 | style = { 44 | display: 'block', 45 | left: bounding.left + bounding.width, 46 | top: bounding.top, 47 | width: 2, 48 | height: bounding.height, 49 | }; 50 | } 51 | break; 52 | } 53 | default: { 54 | if (dropTarget.method === DropMethod.InsertBefore) { 55 | style = { 56 | display: 'block', 57 | left: bounding.left, 58 | top: bounding.top, 59 | width: bounding.width, 60 | height: 2, 61 | }; 62 | } else if (dropTarget.method === DropMethod.InsertAfter) { 63 | style = { 64 | display: 'block', 65 | left: bounding.left, 66 | top: bounding.top + bounding.height, 67 | width: bounding.width, 68 | height: 2, 69 | }; 70 | } 71 | } 72 | } 73 | 74 | return ; 75 | }); 76 | -------------------------------------------------------------------------------- /packages/designer/src/simulator/mask.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cx from 'classnames'; 3 | import styled from 'styled-components'; 4 | import { DropMethod } from '@music163/tango-core'; 5 | import { ElementBoundingType } from '@music163/tango-helpers'; 6 | import { observer, useWorkspace } from '@music163/tango-context'; 7 | 8 | const MaskWrapper = styled.div` 9 | position: absolute; 10 | top: 0; 11 | left: 0; 12 | pointer-events: none; 13 | 14 | &.dragging { 15 | background-color: rgba(23, 97, 144, 0.26); 16 | } 17 | 18 | &.dropping { 19 | background-color: rgba(38, 169, 252, 0.34); 20 | } 21 | `; 22 | 23 | interface MaskProps { 24 | dragging?: boolean; 25 | dropping?: boolean; 26 | bounding?: ElementBoundingType; 27 | } 28 | 29 | const Mask = observer(({ dragging, dropping, bounding }: MaskProps) => { 30 | if (!bounding) { 31 | return null; 32 | } 33 | 34 | const clazz = cx({ 35 | dragging, 36 | dropping, 37 | }); 38 | 39 | const style = { 40 | width: bounding.width, 41 | height: bounding.height, 42 | transform: `translate(${bounding.left}px, ${bounding.top}px)`, 43 | }; 44 | 45 | return ; 46 | }); 47 | 48 | const showDroppingMaskMethods = [DropMethod.InsertChild, DropMethod.ReplaceNode]; 49 | 50 | /** 51 | * 拖拽遮罩提示 52 | */ 53 | export const DraggingMask = observer(() => { 54 | const workspace = useWorkspace(); 55 | const dragSource = workspace.dragSource; 56 | const dropTarget = dragSource.dropTarget; 57 | 58 | const dragging = !!dragSource.id; 59 | const dropping = !!( 60 | dragSource.isDragging && 61 | showDroppingMaskMethods.includes(dropTarget.method) && 62 | dropTarget.bounding 63 | ); 64 | 65 | return ( 66 | 67 | 68 | 69 | 70 | ); 71 | }); 72 | -------------------------------------------------------------------------------- /packages/designer/src/simulator/selection-mask.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box } from 'coral-system'; 3 | import { observer, useWorkspace } from '@music163/tango-context'; 4 | 5 | export const SelectionMask = observer(() => { 6 | const {selectSource} = useWorkspace(); 7 | const display = selectSource.start?.point.x && selectSource.start?.point.y ? 'block' : 'none'; 8 | return ( 9 | 16 | ); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/designer/src/simulator/simulator.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css, Box, HTMLCoralProps } from 'coral-system'; 3 | import { observer, useDesigner } from '@music163/tango-context'; 4 | 5 | export interface SimulatorProps { 6 | children?: React.ReactNode; 7 | } 8 | 9 | /** 10 | * PC Simulator 11 | * 页面模拟容器,支持模拟多种设备 12 | */ 13 | export const Simulator = observer(({ children }: SimulatorProps) => { 14 | const designer = useDesigner(); 15 | const Sim = designer.simulator.name === 'desktop' ? DesktopSimulator : MobileSimulator; 16 | return {children}; 17 | }); 18 | 19 | function DesktopSimulator({ children }: HTMLCoralProps<'div'>) { 20 | return ( 21 | 22 | {children} 23 | 24 | ); 25 | } 26 | 27 | const mobileStyle = css` 28 | --device-viewport-width: 360px; 29 | --device-viewport-height: 640px; 30 | position: relative; 31 | margin: auto; 32 | border: 16px black solid; 33 | border-top-width: 60px; 34 | border-bottom-width: 60px; 35 | border-radius: 36px; 36 | 37 | &::before { 38 | content: ''; 39 | display: block; 40 | width: 60px; 41 | height: 5px; 42 | position: absolute; 43 | top: -30px; 44 | left: 50%; 45 | transform: translate(-50%, -50%); 46 | background: #333; 47 | border-radius: 10px; 48 | } 49 | 50 | &::after { 51 | content: ''; 52 | display: block; 53 | width: 35px; 54 | height: 35px; 55 | position: absolute; 56 | left: 50%; 57 | bottom: -65px; 58 | transform: translate(-50%, -50%); 59 | background: #333; 60 | border-radius: 50%; 61 | } 62 | 63 | .MobileSimulatorDeviceFrame { 64 | width: var(--device-viewport-width); 65 | height: var(--device-viewport-height); 66 | background: white; 67 | } 68 | `; 69 | 70 | function MobileSimulator({ children }: HTMLCoralProps<'div'>) { 71 | return ( 72 | 73 | 74 | {children} 75 | 76 | 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /packages/designer/src/themes/default.ts: -------------------------------------------------------------------------------- 1 | import { extendTheme } from 'coral-system'; 2 | 3 | export default extendTheme({ 4 | colors: { 5 | primary: { 6 | 10: '#e8f3ff', 7 | 20: '#bedaff', 8 | 30: '#94bfff', 9 | 40: '#6aa1ff', 10 | 50: '#4080ff', 11 | 60: '#165dff', 12 | 70: '#0e42d2', 13 | }, 14 | line: { 15 | normal: '#e5e6eb', 16 | }, 17 | background: { 18 | normal: '#f3f3f4', 19 | secondary: '#e5e6eb', 20 | }, 21 | text: { 22 | title: '#1d2129', 23 | body: '#4e5969', 24 | note: '#86909c', 25 | placeholder: '#c9cdd4', 26 | }, 27 | custom: { 28 | topNavBg: '#222', 29 | topNavColor: '#fff', 30 | topNavBorderColor: '#222', 31 | toolbarDividerColor: 'gray.60', 32 | toolbarButtonBg: 'rgba(223, 223, 223, 0.08)', 33 | toolbarButtonBgHover: '#4080ff', 34 | toolbarButtonBgDisabled: 'rgba(223,223,223, 0.08)', 35 | toolbarButtonBgActive: '#4080ff', 36 | toolbarButtonTextColor: '#FFF', 37 | toolbarButtonTextColorHover: '#FFF', 38 | toolbarButtonTextColorDisabled: 'hsla(0,0%,100%,0.3)', 39 | toolbarButtonTextColorActive: '#fff', 40 | sidebarBg: '#fff', 41 | sidebarExpandBg: '#fff', 42 | sidebarItemActiveBg: '#f2f3f5', 43 | sidebarItemHoverBg: '#f2f3f5', 44 | viewportBg: '#f0f2f5', 45 | }, 46 | }, 47 | radii: { 48 | s: '2px', 49 | m: '2px', 50 | l: '4px', 51 | xl: '8px', 52 | xxl: '16px', 53 | }, 54 | }); 55 | -------------------------------------------------------------------------------- /packages/designer/src/themes/index.ts: -------------------------------------------------------------------------------- 1 | export { default as themeDefault } from './default'; 2 | export { default as themeLight } from './light'; 3 | -------------------------------------------------------------------------------- /packages/designer/src/themes/light.ts: -------------------------------------------------------------------------------- 1 | import { extendTheme } from 'coral-system'; 2 | 3 | export default extendTheme({ 4 | colors: { 5 | custom: { 6 | topNavBg: '#FFF', 7 | topNavColor: '#333', 8 | topNavBorderColor: 'rgb(229,230,235)', 9 | toolbarDividerColor: 'rgb(229,230,235)', 10 | toolbarButtonBg: 'rgb(242,243,245)', 11 | toolbarButtonBgHover: 'rgb(229,230,235)', 12 | toolbarButtonBgDisabled: 'rgb(247,248,250)', 13 | toolbarButtonBgActive: 'colors.brand', 14 | toolbarButtonTextColor: 'colors.text2', 15 | toolbarButtonTextColorHover: 'colors.text2', 16 | toolbarButtonTextColorDisabled: 'colors.text4', 17 | toolbarButtonTextColorActive: '#FFF', 18 | sidebarBg: '#fff', 19 | sidebarExpandBg: '#fff', 20 | sidebarItemActiveBg: '#f2f3f5', 21 | sidebarItemHoverBg: '#f2f3f5', 22 | viewportBg: '#f0f2f5', 23 | }, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /packages/designer/src/toolbar/history.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box } from 'coral-system'; 3 | import { ToggleButton, RedoOutlined, UndoOutlined } from '@music163/tango-ui'; 4 | import { observer, useWorkspace } from '@music163/tango-context'; 5 | 6 | export const HistoryTool = observer(() => { 7 | const workspace = useWorkspace(); 8 | const disabled = !workspace.isValid; 9 | return ( 10 | 11 | { 16 | workspace.history.back(); 17 | workspace.selectSource.clear(); 18 | }} 19 | > 20 | 21 | 22 | { 27 | workspace.history.forward(); 28 | workspace.selectSource.clear(); 29 | }} 30 | > 31 | 32 | 33 | 34 | ); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/designer/src/toolbar/index.ts: -------------------------------------------------------------------------------- 1 | export * from './history'; 2 | export * from './mode-switch'; 3 | export * from './preview'; 4 | export * from './route-switch'; 5 | export * from './viewport-switch'; 6 | export * from './toggle-panel'; 7 | export * from './toolbar'; -------------------------------------------------------------------------------- /packages/designer/src/toolbar/mode-switch.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Group } from 'coral-system'; 3 | import { ToggleButton, CodeOutlined, DualOutlined } from '@music163/tango-ui'; 4 | import { observer, useDesigner, useWorkspace } from '@music163/tango-context'; 5 | import { BorderOutlined } from '@ant-design/icons'; 6 | 7 | export const ModeSwitchTool = observer(() => { 8 | const workspace = useWorkspace(); 9 | const designer = useDesigner(); 10 | 11 | return ( 12 | 13 | { 17 | workspace.syncFiles(); // 保证 ast 与 code 是同步的 18 | designer.setActiveView('design'); 19 | }} 20 | tooltip="设计视图" 21 | > 22 | 23 | 24 | { 28 | designer.setActiveView('code'); // 切换到源码视图 29 | designer.setActiveSidebarPanel(''); // 关闭左侧面板 30 | }} 31 | tooltip="源码视图" 32 | > 33 | 34 | 35 | { 39 | designer.setActiveView('dual'); // 切换到双屏视图 40 | designer.setActiveSidebarPanel(''); // 关闭左侧面板 41 | }} 42 | tooltip="双屏视图" 43 | > 44 | 45 | 46 | 47 | ); 48 | }); 49 | -------------------------------------------------------------------------------- /packages/designer/src/toolbar/preview.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ToggleButton } from '@music163/tango-ui'; 3 | import { EyeOutlined } from '@ant-design/icons'; 4 | import { observer, useDesigner, useWorkspace } from '@music163/tango-context'; 5 | 6 | export const PreviewTool = observer(() => { 7 | const designer = useDesigner(); 8 | const workspace = useWorkspace(); 9 | 10 | return ( 11 | { 16 | const nextIsPreview = !designer.isPreview; 17 | designer.toggleIsPreview(nextIsPreview); 18 | if (nextIsPreview && designer.activeView === 'code') { 19 | designer.setActiveView('design'); 20 | } 21 | }} 22 | tooltip={designer.isPreview ? '切换到设计模式' : '切换到预览模式'} 23 | > 24 | 25 | 26 | ); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/designer/src/toolbar/toggle-panel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer, useDesigner } from '@music163/tango-context'; 3 | import { Box } from 'coral-system'; 4 | import { 5 | OpenPanelFilledLeftOutlined, 6 | OpenPanelFilledRightOutlined, 7 | OpenPanelLeftOutlined, 8 | OpenPanelRightOutlined, 9 | ToggleButton, 10 | } from '@music163/tango-ui'; 11 | 12 | export const TogglePanelTool = observer(() => { 13 | const designer = useDesigner(); 14 | return ( 15 | 16 | { 20 | designer.setActiveSidebarPanel(designer.activeSidebarPanel ? '' : 'outline'); 21 | }} 22 | > 23 | {designer.activeSidebarPanel ? : } 24 | 25 | designer.toggleRightPanel()} 29 | disabled={designer.activeView === 'code'} 30 | > 31 | {designer.showRightPanel ? : } 32 | 33 | 34 | ); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/designer/src/toolbar/viewport-switch.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Group } from 'coral-system'; 3 | import { ToggleButton } from '@music163/tango-ui'; 4 | import { DesktopOutlined, MobileOutlined } from '@ant-design/icons'; 5 | import { observer, useDesigner } from '@music163/tango-context'; 6 | 7 | /** 8 | * @deprecated 9 | */ 10 | export const ViewportSwitchTool = observer(() => { 11 | const designer = useDesigner(); 12 | return ( 13 | 14 | { 18 | designer.setSimulator('desktop'); 19 | }} 20 | tooltip="桌面视图" 21 | > 22 | 23 | 24 | { 28 | designer.setSimulator('phone'); 29 | }} 30 | tooltip="手机视图" 31 | > 32 | 33 | 34 | 35 | ); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/designer/src/types/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 选择模式 3 | * point 点选 4 | * area 区域选择 5 | */ 6 | export type SelectModeType = 'point' | 'area'; 7 | -------------------------------------------------------------------------------- /packages/designer/src/workspace-panel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box } from 'coral-system'; 3 | import { ReactComponentProps } from '@music163/tango-helpers'; 4 | 5 | export type WorkspacePanelProps = ReactComponentProps; 6 | 7 | export function WorkspacePanel({ children }: WorkspacePanelProps) { 8 | return ( 9 | 17 | {children} 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /packages/designer/src/workspace-view.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, HTMLCoralProps } from 'coral-system'; 3 | import { observer, useDesigner } from '@music163/tango-context'; 4 | import { DesignerViewType } from '@music163/tango-core'; 5 | import cx from 'classnames'; 6 | import { ComponentsPopover, FileErrorsOverlay } from './components'; 7 | 8 | export interface WorkspaceViewProps extends HTMLCoralProps<'div'> { 9 | /** 10 | * 视图面板模式,对应 Workspace 的模式 11 | */ 12 | mode?: DesignerViewType; 13 | } 14 | 15 | export const WorkspaceView = observer((props: WorkspaceViewProps) => { 16 | const { mode = 'design', children, className, ...rest } = props; 17 | const designer = useDesigner(); 18 | const display = mode !== designer.activeView ? 'none' : 'block'; 19 | // 移动端模式小屏幕适配,可能会溢出屏幕 20 | const overflow = 21 | designer.simulator.name === 'phone' ? 'auto' : designer.isPreview ? 'auto' : 'hidden'; 22 | return ( 23 | 31 | {children} 32 | {/* 添加组件弹层 */} 33 | {display === 'block' && } 34 | {designer.activeView === 'design' && } 35 | 36 | ); 37 | }); 38 | -------------------------------------------------------------------------------- /packages/designer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/designer/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.prod.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": ["src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/designer/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../typedoc.base.json"], 3 | "entryPoints": ["src/index.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/helpers/README.md: -------------------------------------------------------------------------------- 1 | # `@music163/tango-helpers` 2 | 3 | > 提供 tango-apps 的公共类型定义和工具函数等等 4 | 5 | ## Usage 6 | 7 | ```ts 8 | import { foo } from '@music163/tango-helpers'; 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/helpers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@music163/tango-helpers", 3 | "version": "1.2.4", 4 | "description": "Shared types, helpers, and hooks of tango-apps", 5 | "keywords": [ 6 | "shared", 7 | "helper", 8 | "utils", 9 | "types" 10 | ], 11 | "author": "wwsun ", 12 | "homepage": "", 13 | "license": "MIT", 14 | "sideEffects": false, 15 | "main": "lib/cjs/index.js", 16 | "module": "lib/esm/index.js", 17 | "types": "lib/esm/index.d.ts", 18 | "files": [ 19 | "dist", 20 | "lib" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/netease/tango.git" 25 | }, 26 | "scripts": { 27 | "clean": "rimraf lib/", 28 | "build": "yarn clean && yarn build:esm && yarn build:cjs", 29 | "build:esm": "tsc --project tsconfig.prod.json --outDir lib/esm/ --module ES2020", 30 | "build:cjs": "tsc --project tsconfig.prod.json --outDir lib/cjs/ --module CommonJS", 31 | "prepublishOnly": "yarn build" 32 | }, 33 | "peerDependencies": { 34 | "react": ">= 16.8" 35 | }, 36 | "dependencies": { 37 | "hoist-non-react-statics": "^3.3.2", 38 | "lodash.get": "^4.4.2", 39 | "lodash.set": "^4.3.2" 40 | }, 41 | "publishConfig": { 42 | "access": "public", 43 | "registry": "https://registry.npmjs.org/" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/helpers/src/helpers/array.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 对数组进行去重,适用于简单数组,对象为根据引用进行去重 3 | * @param arr 输入数组 4 | * @returns 去重后的数组 5 | */ 6 | export function uniq(arr: any[]) { 7 | const set = new Set(arr); 8 | return Array.from(set); 9 | } 10 | 11 | /** 12 | * 对象数组转为对象 13 | * @example [{ a: 'a' }, { b: 'b' }] -> { a: 'a', b: 'b' } 14 | * @param arr 15 | * @param getKey 16 | * @param getValue 17 | * @returns 18 | */ 19 | export function array2object( 20 | arr: Array>, 21 | getKey: (item: any) => any, 22 | getValue?: (item: any) => any, 23 | ) { 24 | return arr.reduce((prev, cur) => { 25 | if (cur) { 26 | const key = getKey(cur); 27 | prev[key] = getValue ? getValue(cur) : cur; 28 | } 29 | return prev; 30 | }, {}); 31 | } 32 | 33 | /** 34 | * 树形嵌套数据的过滤 35 | * @param array 输入的数组 36 | * @param predict 断言函数,过滤出符合判断函数的数据 37 | * @param childrenProp 子属性的名字 38 | * @param onlyLeaf 是否子探测叶子结点,即存在子节点的父节点不执行断言函数 39 | */ 40 | export function filterTreeData( 41 | array: T[], 42 | predict: (leaf: T) => boolean, 43 | childrenProp = 'children', 44 | onlyLeaf = false, 45 | ) { 46 | const reducer = (result: T[], current: any) => { 47 | if (current[childrenProp]) { 48 | const newChildren = current[childrenProp].reduce(reducer, []); 49 | if (newChildren.length) { 50 | result.push({ 51 | ...current, 52 | [childrenProp]: newChildren, 53 | }); 54 | if (!onlyLeaf) return result; 55 | } 56 | if (onlyLeaf) return result; 57 | } 58 | if (predict(current)) { 59 | result.push(current); 60 | } 61 | 62 | return result; 63 | }; 64 | 65 | return array.reduce(reducer, []); 66 | } 67 | 68 | export function mapTreeData( 69 | treeData: T[], 70 | mapper: (node: T) => any, 71 | childrenProp = 'children', 72 | ) { 73 | return treeData.map((node: any) => { 74 | const newNode = mapper(node); 75 | if (node[childrenProp]) { 76 | newNode[childrenProp] = mapTreeData(node[childrenProp], mapper, childrenProp); 77 | } 78 | return newNode; 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /packages/helpers/src/helpers/assert.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * condition 为 false 的时候打印 msg 3 | * @param condition 4 | * @param msg 5 | */ 6 | export function assert(condition: any, msg?: string) { 7 | console.assert(condition, msg); 8 | } 9 | 10 | export function isString(str: any): str is string { 11 | return typeof str === 'string'; 12 | } 13 | 14 | export function isFunction(fn: any) { 15 | return typeof fn === 'function'; 16 | } 17 | 18 | export function isNumber(num: any): num is number { 19 | return typeof num === 'number'; 20 | } 21 | 22 | export function isBoolean(obj: any): obj is boolean { 23 | return typeof obj === 'boolean'; 24 | } 25 | 26 | export function isObject(obj: any): obj is Record { 27 | return typeof obj === 'object' && !Array.isArray(obj) && obj !== null; 28 | } 29 | 30 | export function isPlainObject(obj: any) { 31 | return Object.prototype.toString.call(obj) === '[object Object]'; 32 | } 33 | 34 | export function isPromise(obj: any) { 35 | return obj && obj.then && isFunction(obj.then); 36 | } 37 | 38 | /** 39 | * 判断值是否是 null 或 undefined 40 | * @param val 41 | * @returns true if val is null or undefined 42 | */ 43 | export function isNil(val: any) { 44 | return val == null; 45 | } 46 | 47 | /** 48 | * 是否是状态变量的 path 49 | * @param key 50 | * @returns 51 | */ 52 | export function isStoreVariablePath(key: string) { 53 | return key === 'pageStore' || /^stores\.[a-zA-Z0-9]+\.\w+$/.test(key); 54 | } 55 | 56 | /** 57 | * 是否是服务变量的 path 58 | * @param key 59 | * @returns 60 | */ 61 | export function isServiceVariablePath(key: string) { 62 | return /^services\.[a-zA-Z0-9]+/.test(key); 63 | } 64 | 65 | /** 66 | * 是否在 Tango 设计模式 67 | */ 68 | export function isInTangoDesignMode() { 69 | return !!(window as any).__TANGO_DESIGNER__; 70 | } 71 | 72 | /** 73 | * 是否是 macOS 或 iOS like 设备 74 | */ 75 | export function isApplePlatform() { 76 | return /Mac|iPhone|iPad|iPod/i.test(navigator.platform); 77 | } 78 | -------------------------------------------------------------------------------- /packages/helpers/src/helpers/constants.ts: -------------------------------------------------------------------------------- 1 | export const SLOT = { 2 | /** 3 | * DND 追踪标记,通常值为 {{pagePath}}:{{componentName}} 4 | */ 5 | dnd: 'data-dnd', 6 | /** 7 | * ID 追踪标记 8 | */ 9 | id: 'data-id', 10 | }; 11 | -------------------------------------------------------------------------------- /packages/helpers/src/helpers/dom.ts: -------------------------------------------------------------------------------- 1 | import { Dict } from '../types'; 2 | 3 | /** 4 | * 获取元素上的 data 属性集 5 | */ 6 | export function getElementDataProps(element: HTMLElement): Dict { 7 | return { 8 | ...element.dataset, 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /packages/helpers/src/helpers/enums.ts: -------------------------------------------------------------------------------- 1 | export enum TangoEventName { 2 | OpenModal = 'tango_openModal', 3 | 4 | ViewChange = 'tango_viewChange', 5 | 6 | DesignerAction = 'tango_designerAction', 7 | } 8 | 9 | export enum ExpressionInstruction { 10 | InlineExpression = 'tango_inlineExpression', 11 | } 12 | -------------------------------------------------------------------------------- /packages/helpers/src/helpers/events.ts: -------------------------------------------------------------------------------- 1 | import { TangoEventName } from './enums'; 2 | 3 | type EventListenerCallbackType = (event: any) => void; 4 | 5 | export const events = { 6 | /** 7 | * 绑定事件监听器 8 | * @param node HTML 元素 9 | * @param eventName 事件名 10 | * @param callback 监听器函数 11 | * @param useCapture 是否开启事件捕获 12 | * @returns { off } off 用于快速取消事件监听 13 | */ 14 | on( 15 | node: HTMLElement | Document, 16 | eventName: string, 17 | callback: EventListenerCallbackType, 18 | useCapture = false, 19 | ) { 20 | if (node.addEventListener) { 21 | node.addEventListener(eventName, callback, useCapture); 22 | } 23 | 24 | return { 25 | off: () => events.off(node, eventName, callback, useCapture), 26 | }; 27 | }, 28 | 29 | /** 30 | * 取消事件监听器 31 | * @param node HTML 元素 32 | * @param eventName 事件名 33 | * @param callback 事件监听器 34 | * @param useCapture 是否开启事件捕获 35 | */ 36 | off( 37 | node: HTMLElement | Document, 38 | eventName: string, 39 | callback: EventListenerCallbackType, 40 | useCapture = false, 41 | ) { 42 | if (node.removeEventListener) { 43 | node.removeEventListener(eventName, callback, useCapture); 44 | } 45 | }, 46 | }; 47 | 48 | /** 49 | * 触发 tangoEvent 50 | * @param element 发起事件的元素 51 | * @param eventName 事件名 52 | * @param eventPayload 携带的参数 53 | */ 54 | export function dispatchTangoEvent( 55 | element: E, 56 | eventName: TangoEventName, 57 | eventPayload?: P, 58 | ) { 59 | const target = element || document.body; 60 | target.dispatchEvent( 61 | new CustomEvent(eventName, { 62 | bubbles: true, 63 | detail: eventPayload, 64 | }), 65 | ); 66 | } 67 | 68 | function getKeyboardEventFns(e: React.KeyboardEvent) { 69 | const fns = []; 70 | if (e.metaKey) { 71 | fns.push('meta'); 72 | } 73 | if (e.ctrlKey) { 74 | fns.push('ctrl'); 75 | } 76 | if (e.shiftKey) { 77 | fns.push('shift'); 78 | } 79 | if (e.altKey) { 80 | fns.push('alt'); 81 | } 82 | return fns.join('+'); 83 | } 84 | 85 | /** 86 | * 获取按键的快捷信息 87 | */ 88 | export function getHotkey(e: React.KeyboardEvent) { 89 | const fn = getKeyboardEventFns(e); 90 | const char = e.key.toLowerCase(); 91 | return fn ? `${fn}+${char}` : char; 92 | } 93 | -------------------------------------------------------------------------------- /packages/helpers/src/helpers/function.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from './assert'; 2 | 3 | export function noop(...args: any[]) {} 4 | 5 | export function runIfFn(valueOrFn: any, ...args: any[]) { 6 | return isFunction(valueOrFn) ? valueOrFn(...args) : valueOrFn; 7 | } 8 | 9 | export function callAll(...fns: any[]) { 10 | return function mergedFn(...args: any[]) { 11 | fns.forEach((fn) => { 12 | if (isFunction(fn)) { 13 | fn?.(...args); 14 | } 15 | }); 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /packages/helpers/src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './array'; 2 | export * from './assert'; 3 | export * from './constants'; 4 | export * from './dom'; 5 | export * from './enums'; 6 | export * from './events'; 7 | export * from './function'; 8 | export * from './logger'; 9 | export * from './string'; 10 | export * from './object'; 11 | export * from './react-helper'; 12 | export * from './code-helper'; 13 | -------------------------------------------------------------------------------- /packages/helpers/src/helpers/logger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | export const logger = { 4 | group(title: string, body: string) { 5 | console.groupCollapsed(title); 6 | console.log(body); 7 | console.groupEnd(); 8 | }, 9 | 10 | log(msg: string) { 11 | console.log('[Log]: %s', msg); 12 | }, 13 | 14 | warn(msg: string) { 15 | warning(false, msg); 16 | }, 17 | 18 | error(...args: any) { 19 | console.error('[Error]: ', ...args); 20 | }, 21 | }; 22 | 23 | /** 24 | * 如果条件为 false 则打印警告日志 25 | * @example warning(truthyValue, 'This should not log a warning'); 26 | * @example warning(falsyValue, 'This should log a warning'); 27 | * @see https://github.com/alexreardon/tiny-warning 28 | * 29 | * @param condition 打印警告日志的条件 30 | * @param message 警告日志 31 | */ 32 | export function warning(condition: boolean, message: string) { 33 | if (condition) { 34 | return; 35 | } 36 | 37 | console.warn(`Warning: ${message}`); 38 | } 39 | 40 | /** 41 | * 如果条件为 false 则抛出错误 42 | * @see https://github.com/alexreardon/tiny-invariant 43 | * @param condition 条件 44 | * @param message 错误消息 45 | */ 46 | export function invariant(condition: boolean, message: string | (() => string)) { 47 | if (condition) { 48 | return; 49 | } 50 | 51 | const text = typeof message === 'function' ? message() : message; 52 | throw new Error(`Invariant failed: ${text}`); 53 | } 54 | -------------------------------------------------------------------------------- /packages/helpers/src/helpers/object.ts: -------------------------------------------------------------------------------- 1 | import get from 'lodash.get'; 2 | import set from 'lodash.set'; 3 | import { Dict } from '../types'; 4 | 5 | /** 6 | * 合并 target 和 source 对象,source 对象的优先级更高,如存在重名,覆盖 target 中的 key 7 | * @param target target object 8 | * @param source source object 9 | * @return new merged object, target will not be modified 10 | */ 11 | export function merge(target: object, source: object) { 12 | return { 13 | ...(target || {}), 14 | ...(source || {}), 15 | }; 16 | } 17 | 18 | /** 19 | * 从目标上下文中根据 keyPath 获取对应的值 20 | * @param context the object to query 21 | * @param keyPath the path of the property to get 22 | * @returns 23 | */ 24 | export function getValue(context: any, keyPath: string) { 25 | keyPath = keyPath.replaceAll('?.', '.'); 26 | return get(context, keyPath); 27 | } 28 | 29 | /** 30 | * Sets the value at path of object. If a portion of path doesn't exist, it's created. Arrays are created for missing index properties while objects are created for all other missing properties 31 | * @param context 32 | * @param keyPath 33 | * @param value 34 | * @returns 35 | */ 36 | export function setValue(context: any, keyPath: string, value: any) { 37 | return set(context, keyPath, value); 38 | } 39 | 40 | /** 41 | * 浅拷贝目标对象,并清除对象上的 undefined value 42 | * @param obj 43 | */ 44 | export function clone(obj: any, withUndefined = true) { 45 | if (!obj || typeof obj !== 'object') { 46 | return obj; 47 | } 48 | 49 | const target: Dict = {}; 50 | for (const key in obj) { 51 | if (withUndefined) { 52 | target[key] = obj[key]; 53 | } else if (obj[key] !== undefined) { 54 | target[key] = obj[key]; 55 | } 56 | } 57 | return target; 58 | } 59 | 60 | export function omit(obj: any, keys: string[]) { 61 | const target = clone(obj); 62 | for (const key of keys) { 63 | delete target[key]; 64 | } 65 | return target; 66 | } 67 | 68 | export function pick(obj: any, keys: string[]) { 69 | const target: Dict = {}; 70 | for (const key of keys) { 71 | target[key] = obj[key]; 72 | } 73 | return target; 74 | } 75 | -------------------------------------------------------------------------------- /packages/helpers/src/hoc/compose.ts: -------------------------------------------------------------------------------- 1 | export const compose = (...funcs: ((...args: any[]) => any)[]) => 2 | funcs.reduce( 3 | (a, b) => 4 | (...args) => 5 | a(b(...args)), 6 | (arg) => arg, 7 | ); 8 | -------------------------------------------------------------------------------- /packages/helpers/src/hoc/index.ts: -------------------------------------------------------------------------------- 1 | export * from './compose'; 2 | export * from './with-dnd'; 3 | -------------------------------------------------------------------------------- /packages/helpers/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-boolean'; 2 | export * from './use-controllable'; 3 | export * from './use-callback-ref'; 4 | -------------------------------------------------------------------------------- /packages/helpers/src/hooks/use-boolean.ts: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from 'react'; 2 | 3 | type InitialStateType = boolean | (() => boolean); 4 | 5 | export function useBoolean(initialState: InitialStateType = false) { 6 | const [value, setValue] = useState(initialState); 7 | 8 | const on = useCallback(() => { 9 | setValue(true); 10 | }, []); 11 | 12 | const off = useCallback(() => { 13 | setValue(false); 14 | }, []); 15 | 16 | const toggle = useCallback(() => { 17 | setValue((prev) => !prev); 18 | }, []); 19 | 20 | return [value, { on, off, toggle }] as const; 21 | } 22 | -------------------------------------------------------------------------------- /packages/helpers/src/hooks/use-callback-ref.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react'; 2 | 3 | /** 4 | * @param callback 5 | * @param deps 6 | * @returns 7 | */ 8 | export function useCallbackRef any>( 9 | callback: T | undefined, 10 | deps: React.DependencyList = [], 11 | ) { 12 | const callbackRef = useRef(callback); 13 | 14 | useEffect(() => { 15 | callbackRef.current = callback; 16 | }); 17 | 18 | // eslint-disable-next-line react-hooks/exhaustive-deps 19 | return useCallback(((...args) => callbackRef.current?.(...args)) as T, deps); 20 | } 21 | -------------------------------------------------------------------------------- /packages/helpers/src/hooks/use-controllable.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo, useState } from 'react'; 2 | import { useCallbackRef } from './use-callback-ref'; 3 | 4 | export function useControllableProp(prop: T | undefined, state: T) { 5 | const controlled = typeof prop !== 'undefined'; 6 | const value = controlled ? prop : state; 7 | return useMemo<[boolean, T]>(() => [controlled, value], [controlled, value]); 8 | } 9 | 10 | export interface UseControllableStateProps { 11 | value?: T; 12 | defaultValue?: T | (() => T); 13 | onChange?: (value: T) => void; 14 | shouldUpdate?: (prev: T, next: T) => boolean; 15 | } 16 | 17 | export function useControllableState(props: UseControllableStateProps) { 18 | const { 19 | value: valueProp, 20 | defaultValue, 21 | onChange, 22 | shouldUpdate = (prev, next) => prev !== next, 23 | } = props; 24 | 25 | const onChangeProp = useCallbackRef(onChange); 26 | const shouldUpdateProp = useCallbackRef(shouldUpdate); 27 | 28 | const [uncontrolledState, setUncontrolledState] = useState(defaultValue as T); 29 | 30 | const controlled = valueProp !== undefined; 31 | const value = controlled ? valueProp : uncontrolledState; 32 | 33 | const setValue = useCallback( 34 | (next: React.SetStateAction) => { 35 | const setter = next as (prevState?: T) => T; 36 | const nextValue = typeof next === 'function' ? setter(value) : next; 37 | 38 | if (!shouldUpdateProp(value, nextValue)) { 39 | return; 40 | } 41 | 42 | if (!controlled) { 43 | setUncontrolledState(nextValue); 44 | } 45 | 46 | onChangeProp(nextValue); 47 | }, 48 | [controlled, onChangeProp, value, shouldUpdateProp], 49 | ); 50 | 51 | return [value, setValue] as [T, React.Dispatch>]; 52 | } 53 | -------------------------------------------------------------------------------- /packages/helpers/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './helpers'; 2 | export * from './types'; 3 | export * from './hoc'; 4 | export * from './hooks'; 5 | export * from './stores'; 6 | -------------------------------------------------------------------------------- /packages/helpers/src/stores/index.ts: -------------------------------------------------------------------------------- 1 | export * from './list-store'; 2 | -------------------------------------------------------------------------------- /packages/helpers/src/stores/list-store.ts: -------------------------------------------------------------------------------- 1 | import { warning } from '../helpers'; 2 | import { Dict } from '../types'; 3 | 4 | type ListStoreOptionsType = { 5 | data: T[]; 6 | keyProp?: string; 7 | labelProp?: string; 8 | childrenProp?: string; 9 | }; 10 | 11 | export class ListStore { 12 | private nodeMap: Map; 13 | private keyProp: string; 14 | private childrenProp: string; 15 | 16 | get nodes() { 17 | return this.nodeMap.values(); 18 | } 19 | 20 | constructor(options: ListStoreOptionsType) { 21 | this.keyProp = options.keyProp || 'value'; 22 | this.childrenProp = options.childrenProp || 'children'; 23 | this.nodeMap = new Map(); 24 | this.visitNodes(options.data); 25 | } 26 | 27 | getNode(key: string) { 28 | if (!key) { 29 | return; 30 | } 31 | 32 | return this.nodeMap.get(key); 33 | } 34 | 35 | private visitNodes(nodes: T[]) { 36 | if (!Array.isArray(nodes) || nodes.length === 0) { 37 | return; 38 | } 39 | 40 | nodes.forEach((node) => { 41 | const key = node[this.keyProp]; 42 | this.nodeMap.set(key, node); 43 | warning( 44 | this.nodeMap.has(key), 45 | `duplicate value '${key}' detected! All node values should be unique!`, 46 | ); 47 | this.nodeMap.set(key, node); 48 | if (node[this.childrenProp]) { 49 | this.visitNodes(node[this.childrenProp]); 50 | } 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/helpers/src/types/base.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 基础扩展类型 3 | */ 4 | 5 | export type StringOrNumber = string | number; 6 | 7 | /** 8 | * 字典对象类型 9 | */ 10 | export type Dict = { 11 | [key: string]: T; 12 | }; 13 | 14 | export type PartialRecord = { 15 | [P in K]?: T; 16 | }; 17 | 18 | export interface ReactComponentProps { 19 | children?: React.ReactNode; 20 | style?: React.CSSProperties; 21 | className?: string; 22 | } 23 | -------------------------------------------------------------------------------- /packages/helpers/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base'; 2 | export * from './advanced'; 3 | export * from './prototype'; 4 | -------------------------------------------------------------------------------- /packages/helpers/tests/assert.test.ts: -------------------------------------------------------------------------------- 1 | import { isPlainObject } from '../src/helpers'; 2 | 3 | describe('helpers/assert', () => { 4 | it('isPlainObject', () => { 5 | expect(isPlainObject({})).toBeTruthy(); // true 6 | expect(isPlainObject({ aaa: 'aaa' })).toBeTruthy(); // true 7 | expect(isPlainObject([])).toBeFalsy(); // false 8 | expect(isPlainObject(null)).toBeFalsy(); // false 9 | expect(isPlainObject(new Date())).toBeFalsy(); // false 10 | expect(isPlainObject(/regex/)).toBeFalsy(); // false 11 | expect(isPlainObject(123)).toBeFalsy(); // false 12 | expect(isPlainObject('string')).toBeFalsy(); // false 13 | expect(isPlainObject(true)).toBeFalsy(); // false 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/helpers/tests/code-helper.test.ts: -------------------------------------------------------------------------------- 1 | import { isWrappedCode } from '../src/helpers'; 2 | 3 | describe('helpers/codeHelper', () => { 4 | it('isWrappedCode', () => { 5 | expect(isWrappedCode('{{[]}}')).toBeTruthy(); // true 6 | expect(isWrappedCode('{{{}}}')).toBeTruthy(); // true 7 | expect(isWrappedCode('{{this.foo}}')).toBeTruthy(); // true 8 | expect(isWrappedCode('{{123}}')).toBeTruthy(); // true 9 | expect(isWrappedCode('{{() => {}}}')).toBeTruthy(); // true 10 | expect(isWrappedCode('{{}}')).toBeTruthy(); // true 11 | expect(isWrappedCode('{{<>\n\n}}')).toBeTruthy(); // true 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/helpers/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/helpers/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.prod.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": ["src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/helpers/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../typedoc.base.json"], 3 | "entryPoints": ["src/index.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/sandbox/README.md: -------------------------------------------------------------------------------- 1 | # `sandbox` 2 | 3 | 渲染沙箱 4 | -------------------------------------------------------------------------------- /packages/sandbox/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@music163/tango-sandbox", 3 | "version": "1.0.13", 4 | "description": "sandbox of tango apps", 5 | "author": "wwsun ", 6 | "homepage": "", 7 | "license": "MIT", 8 | "main": "lib/cjs/index.js", 9 | "module": "lib/esm/index.js", 10 | "types": "lib/esm/index.d.ts", 11 | "files": [ 12 | "dist", 13 | "lib" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/netease/tango.git" 18 | }, 19 | "scripts": { 20 | "clean": "rimraf dist/ && rimraf lib/", 21 | "build": "yarn clean && yarn build:esm && yarn build:cjs", 22 | "build:esm": "tsc --project tsconfig.prod.json --outDir lib/esm/ --module ES2020", 23 | "build:cjs": "tsc --project tsconfig.prod.json --outDir lib/cjs/ --module CommonJS", 24 | "prepublishOnly": "yarn build" 25 | }, 26 | "peerDependencies": { 27 | "react": ">= 16.8", 28 | "styled-components": ">= 4" 29 | }, 30 | "dependencies": { 31 | "@ant-design/icons": "^4.8.0", 32 | "@music163/tango-core": "^1.4.4", 33 | "@music163/tango-helpers": "^1.2.4", 34 | "crypto-js": "^4.1.1", 35 | "lodash.isequal": "4.5.0", 36 | "react-frame-component": "^5.2.4" 37 | }, 38 | "devDependencies": { 39 | "@types/crypto-js": "^4.1.3" 40 | }, 41 | "publishConfig": { 42 | "access": "public", 43 | "registry": "https://registry.npmjs.org/" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/sandbox/src/code-sandbox/helper.ts: -------------------------------------------------------------------------------- 1 | import { IFiles, IDependencies } from '../types'; 2 | 3 | export function getPackageJSON(dependencies: IDependencies = {}, entry = '/index.js') { 4 | return JSON.stringify( 5 | { 6 | name: 'sandpack-project', 7 | main: entry, 8 | dependencies, 9 | }, 10 | null, 11 | 2, 12 | ); 13 | } 14 | 15 | // 整理 files 16 | // 如果 files 中没有 package.json 文件,则通过 dependencies 和 entry 动态生成 package.json 文件 17 | export function createMissingPackageJSON( 18 | files: IFiles, 19 | dependencies?: IDependencies, 20 | entry?: string, 21 | ) { 22 | const newFiles = { ...files }; 23 | 24 | if (!newFiles['/package.json']) { 25 | if (!dependencies) { 26 | throw new Error( 27 | 'No dependencies specified, please specify either a package.json or dependencies.', 28 | ); 29 | } 30 | 31 | if (!entry) { 32 | throw new Error( 33 | "No entry specified, please specify either a package.json with 'main' field or dependencies.", 34 | ); 35 | } 36 | 37 | newFiles['/package.json'] = { 38 | code: getPackageJSON(dependencies, entry), 39 | }; 40 | } 41 | 42 | return newFiles; 43 | } 44 | 45 | function isSameOrigin(url1: string, url2: string) { 46 | try { 47 | const u1 = new URL(url1); 48 | const u2 = new URL(url2); 49 | return ( 50 | u1.protocol === u2.protocol && 51 | u1.hostname.split('.').slice(-2).join('.') === u2.hostname.split('.').slice(-2).join('.') 52 | ); 53 | } catch (e) { 54 | console.log(e, 'from isSameOrigin'); 55 | return false; 56 | } 57 | } 58 | 59 | export function changeRoute(iframeEl: HTMLIFrameElement, path = '/', routerMode = 'history') { 60 | const _isSameOrigin = isSameOrigin(window.location.href, iframeEl.src); 61 | if (!_isSameOrigin) { 62 | console.warn('无法跨域设置 iframe 中的路由'); 63 | return; 64 | } 65 | if (iframeEl) { 66 | try { 67 | if (routerMode === 'hash') { 68 | iframeEl.contentWindow.location.hash = path; 69 | } else { 70 | iframeEl.contentWindow.history.pushState({ key: '642dmeli', state: null }, null, path); 71 | const popevent = new PopStateEvent('popstate', { state: { key: '642dmeli' } }); 72 | iframeEl.contentWindow.dispatchEvent(popevent); 73 | } 74 | } catch (e) { 75 | console.log(e, 'from changeStartRoute'); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/sandbox/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './code-sandbox'; 2 | export type { CodeSandboxProps } from './types'; 3 | -------------------------------------------------------------------------------- /packages/sandbox/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/sandbox/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.prod.json", 3 | "include": ["./src"], 4 | "exclude": ["**/__tests__/**/*"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/setting-form/README.md: -------------------------------------------------------------------------------- 1 | # `setting-form` 2 | 3 | 高性能属性配置表单 4 | 5 | ## Setter 规范 6 | 7 | 必须提供以下属性支持受控使用 8 | 9 | - `value` 受控的值 10 | - `onChange(value, detail)` 值变化时的回调 11 | - value 需要为简单数据类型 12 | - detail 可以 13 | 14 | ## Setter 选择 15 | 16 | - actionSetter 函数设置器 17 | - boolSetter 布尔值设置器 18 | - choiceSetter 单选项设置器(平铺的单选项) 19 | - expressionSetter 表达式设置器 20 | - jsonSetter JSON 设置器 21 | - numberSetter 数字设置器 22 | - pickerSetter 下拉列表设置器 23 | - textSetter 文本设置器 24 | -------------------------------------------------------------------------------- /packages/setting-form/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@music163/tango-setting-form", 3 | "version": "1.2.15", 4 | "description": "setting form of tango-apps", 5 | "author": "wwsun ", 6 | "homepage": "", 7 | "license": "MIT", 8 | "main": "lib/cjs/index.js", 9 | "module": "lib/esm/index.js", 10 | "types": "lib/esm/index.d.ts", 11 | "files": [ 12 | "dist", 13 | "lib" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/netease/tango.git" 18 | }, 19 | "scripts": { 20 | "clean": "rimraf dist/ && rimraf lib/", 21 | "build": "yarn clean && yarn build:esm && yarn build:cjs", 22 | "build:esm": "tsc --project tsconfig.prod.json --outDir lib/esm/ --module ES2020", 23 | "build:cjs": "tsc --project tsconfig.prod.json --outDir lib/cjs/ --module CommonJS", 24 | "prepublishOnly": "yarn build" 25 | }, 26 | "peerDependencies": { 27 | "react": ">= 16.8" 28 | }, 29 | "dependencies": { 30 | "@ant-design/icons": "^4.8.0", 31 | "@music163/tango-core": "^1.4.4", 32 | "@music163/tango-helpers": "^1.2.4", 33 | "@music163/tango-ui": "^1.4.5", 34 | "antd": "^4.24.2", 35 | "coral-system": "^1.0.5", 36 | "mobx": "6.13.2", 37 | "mobx-react-lite": "4.0.7" 38 | }, 39 | "publishConfig": { 40 | "access": "public", 41 | "registry": "https://registry.npmjs.org/" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/setting-form/src/context.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | import { IFormModel } from './form-model'; 3 | 4 | const FormModelContext = createContext(null); 5 | FormModelContext.displayName = 'ModelContext'; 6 | 7 | export const FormModelProvider = FormModelContext.Provider; 8 | 9 | export const useFormModel = () => { 10 | return useContext(FormModelContext); 11 | }; 12 | 13 | export interface FormVariableContextType { 14 | /** 15 | * 是否允许表单项切换到表达式设置器 16 | */ 17 | disableSwitchExpressionSetter?: boolean; 18 | /** 19 | * 是否展示表单项的副标题 20 | */ 21 | showItemSubtitle?: boolean; 22 | } 23 | 24 | const FormVariableContext = createContext(null); 25 | FormVariableContext.displayName = 'FormVariableContext'; 26 | 27 | export const FormVariableProvider = FormVariableContext.Provider; 28 | export const useFormVariable = () => useContext(FormVariableContext); 29 | -------------------------------------------------------------------------------- /packages/setting-form/src/helpers.ts: -------------------------------------------------------------------------------- 1 | import * as mobx from 'mobx'; 2 | 3 | export function isValidNestProps(props?: unknown) { 4 | return Array.isArray(props) && props.length > 0; 5 | } 6 | 7 | export function splitToPath(name: string) { 8 | // 可以考虑一下 foo.bar[0].buzz 中 [0] 的情况 9 | return name.split('.'); 10 | } 11 | 12 | function isNumericKey(key: string) { 13 | return String(Number.parseInt(key, 10)) === key; 14 | } 15 | 16 | /** lodash.get(...) for mobx observables */ 17 | export function observableGetIn(obj: any, key: string | string[], defaultValue?: any) { 18 | const path = Array.isArray(key) ? key : splitToPath(key); 19 | 20 | let target = obj; 21 | 22 | for (let i = 0; i < path.length; i += 1) { 23 | if (!mobx.isObservable(target)) { 24 | return defaultValue; 25 | } 26 | target = mobx.get(target, path[i]); 27 | } 28 | if (target === undefined) { 29 | return defaultValue; 30 | } 31 | return target; 32 | } 33 | 34 | /** lodash.set(...) for mobx observables */ 35 | export function observableSetIn(obj: unknown, key: string | string[], value: unknown) { 36 | const path = Array.isArray(key) ? key : splitToPath(key); 37 | const lastPartIndex = path.length - 1; 38 | 39 | let target = obj; 40 | 41 | for (let i = 0; i < lastPartIndex; i += 1) { 42 | const part = path[i]; 43 | if (mobx.get(target, part) == null) { 44 | if (isNumericKey(path[i + 1])) { 45 | mobx.set(target, part, []); 46 | } else { 47 | mobx.set(target, part, {}); 48 | } 49 | } 50 | target = mobx.get(target, part); 51 | if (!mobx.isObservable(target)) { 52 | return; 53 | } 54 | } 55 | if (mobx.isObservable(target)) { 56 | mobx.set(target, path[lastPartIndex], value); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/setting-form/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './form'; 2 | export * from './form-item'; 3 | export * from './form-object'; 4 | export * from './form-model'; 5 | export * from './context'; 6 | export * from './setters'; 7 | -------------------------------------------------------------------------------- /packages/setting-form/src/setters/bool-setter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch } from 'antd'; 3 | import { FormItemComponentProps } from '../form-item'; 4 | 5 | export function BoolSetter({ value, onChange, ...props }: FormItemComponentProps) { 6 | return onChange?.(val)} {...props} />; 7 | } 8 | -------------------------------------------------------------------------------- /packages/setting-form/src/setters/code-setter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { FormItemComponentProps } from '../form-item'; 3 | import { InputCode } from '@music163/tango-ui'; 4 | 5 | export function CodeSetter({ 6 | value: valueProp, 7 | onChange, 8 | ...rest 9 | }: FormItemComponentProps) { 10 | const [value, setValue] = useState(valueProp || ''); 11 | return ( 12 | { 14 | setValue(val); 15 | }} 16 | onBlur={() => { 17 | onChange?.(value); 18 | }} 19 | value={value} 20 | {...rest} 21 | /> 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /packages/setting-form/src/setters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bool-setter'; 2 | export * from './code-setter'; 3 | export * from './id-setter'; 4 | export * from './number-setter'; 5 | export * from './text-setter'; 6 | export * from './register'; 7 | -------------------------------------------------------------------------------- /packages/setting-form/src/setters/number-setter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { InputNumber, Slider } from 'antd'; 3 | import { FormItemComponentProps } from '../form-item'; 4 | 5 | const style = { 6 | width: '100%', 7 | }; 8 | 9 | export function NumberSetter({ onChange, ...props }: FormItemComponentProps) { 10 | return ( 11 | { 15 | if (val === null) { 16 | val = undefined; 17 | } 18 | onChange && onChange(val); 19 | }} 20 | {...props} 21 | /> 22 | ); 23 | } 24 | 25 | export function SliderSetter(props: FormItemComponentProps) { 26 | return ; 27 | } 28 | -------------------------------------------------------------------------------- /packages/setting-form/src/setters/register.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CodeSetter } from './code-setter'; 3 | import { BoolSetter } from './bool-setter'; 4 | import { IdSetter } from './id-setter'; 5 | import { TextAreaSetter, TextSetter } from './text-setter'; 6 | import { NumberSetter, SliderSetter } from './number-setter'; 7 | import { IFormItemCreateOptions, register } from '../form-item'; 8 | 9 | const BASIC_SETTERS: IFormItemCreateOptions[] = [ 10 | { 11 | name: 'codeSetter', 12 | alias: ['expSetter', 'expressionSetter'], 13 | component: CodeSetter, 14 | type: 'code', 15 | }, 16 | { 17 | name: 'textSetter', 18 | component: TextSetter, 19 | }, 20 | { 21 | name: 'textAreaSetter', 22 | component: TextAreaSetter, 23 | }, 24 | { 25 | name: 'boolSetter', 26 | component: BoolSetter, 27 | }, 28 | { 29 | name: 'numberSetter', 30 | component: NumberSetter, 31 | }, 32 | { 33 | name: 'sliderSetter', 34 | component: SliderSetter, 35 | }, 36 | { 37 | name: 'idSetter', 38 | component: IdSetter, 39 | }, 40 | { 41 | name: 'imageSetter', 42 | render: (props) => , 43 | }, 44 | ]; 45 | 46 | let registered = false; 47 | 48 | /** 49 | * 注册内置的基础类型 Setter 50 | */ 51 | export function registerBuiltinSetters() { 52 | if (registered) { 53 | // 防止重复注册 54 | return; 55 | } 56 | 57 | // 预注册基础 Setter 58 | BASIC_SETTERS.forEach(register); 59 | registered = true; 60 | } 61 | -------------------------------------------------------------------------------- /packages/setting-form/src/setters/text-setter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Input } from 'antd'; 3 | import { FormItemComponentProps } from '../form-item'; 4 | 5 | const noop = () => {}; 6 | 7 | export function TextSetter({ 8 | value: valueProp, 9 | onChange = noop, 10 | placeholder = '请输入文本', 11 | ...props 12 | }: FormItemComponentProps) { 13 | const [valueState, setValue] = useState(valueProp); 14 | 15 | useEffect(() => { 16 | setValue(valueProp); 17 | }, [valueProp]); 18 | 19 | return ( 20 | setValue(e.target.value)} 25 | onBlur={() => { 26 | if (valueState !== valueProp) { 27 | onChange(valueState); 28 | } 29 | }} 30 | {...props} 31 | /> 32 | ); 33 | } 34 | 35 | const autoSize = { 36 | minRows: 2, 37 | maxRows: 6, 38 | }; 39 | 40 | export function TextAreaSetter({ 41 | value: valueProp, 42 | onChange = noop, 43 | placeholder = '请输入文本', 44 | ...props 45 | }: FormItemComponentProps) { 46 | const [valueState, setValue] = useState(valueProp); 47 | 48 | useEffect(() => { 49 | setValue(valueProp); 50 | }, [valueProp]); 51 | 52 | return ( 53 | setValue(e.target.value)} 58 | onBlur={() => { 59 | if (valueState !== valueProp) { 60 | onChange(valueState); 61 | } 62 | }} 63 | autoSize={autoSize} 64 | {...props} 65 | /> 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /packages/setting-form/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface ISetterOnChangeCallbackDetail { 2 | relatedImports?: string[]; 3 | rawCode?: string; 4 | } 5 | -------------------------------------------------------------------------------- /packages/setting-form/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/setting-form/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.prod.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": ["src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/setting-form/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../typedoc.base.json"], 3 | "entryPoints": ["src/index.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/spec/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## 1.0.2 (2023-12-13) 7 | 8 | ### Bug Fixes 9 | 10 | - update variable tree with help messages and styling & add tangoConfig schema ([5e7e285](https://github.com/NetEase/tango/commit/5e7e285452b46888b447991b0e8548b6392acb3a)) 11 | -------------------------------------------------------------------------------- /packages/spec/README.md: -------------------------------------------------------------------------------- 1 | # Tango Config Spec 2 | 3 | This is the specification for the Tango Config format. 4 | -------------------------------------------------------------------------------- /packages/spec/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tango-spec", 3 | "version": "1.0.2", 4 | "description": "tango config spec", 5 | "keywords": [ 6 | "json", 7 | "schema" 8 | ], 9 | "author": "wwsun ", 10 | "homepage": "https://github.com/NetEase/tango#readme", 11 | "license": "MIT", 12 | "main": "index.js", 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/NetEase/tango.git" 16 | }, 17 | "scripts": { 18 | "test": "node ./__tests__/spec.test.js" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/NetEase/tango/issues" 22 | }, 23 | "publishConfig": { 24 | "access": "public", 25 | "registry": "https://registry.npmjs.org/" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/ui/README.md: -------------------------------------------------------------------------------- 1 | # `tango-ui` 2 | 3 | > UI Widgets of tango-apps 4 | 5 | ## Usage 6 | 7 | WIP 8 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@music163/tango-ui", 3 | "version": "1.4.5", 4 | "description": "ui widgets of tango", 5 | "keywords": [ 6 | "react", 7 | "ui", 8 | "widgets" 9 | ], 10 | "author": "wwsun ", 11 | "homepage": "", 12 | "license": "MIT", 13 | "main": "lib/cjs/index.js", 14 | "module": "lib/esm/index.js", 15 | "types": "lib/esm/index.d.ts", 16 | "files": [ 17 | "dist", 18 | "lib" 19 | ], 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/netease/tango.git" 23 | }, 24 | "scripts": { 25 | "clean": "rimraf lib/", 26 | "build": "yarn clean && yarn build:esm && yarn build:cjs", 27 | "build:esm": "tsc --project tsconfig.prod.json --outDir lib/esm/ --module ES2020", 28 | "build:cjs": "tsc --project tsconfig.prod.json --outDir lib/cjs/ --module CommonJS", 29 | "prepublishOnly": "yarn build" 30 | }, 31 | "peerDependencies": { 32 | "react": ">= 16.8", 33 | "styled-components": ">= 4" 34 | }, 35 | "dependencies": { 36 | "@ant-design/icons": "^4.8.0", 37 | "@codemirror/autocomplete": "^6.16.0", 38 | "@codemirror/lang-javascript": "^6.2.2", 39 | "@codemirror/lint": "^6.7.1", 40 | "@codemirror/search": "^6.5.6", 41 | "@music163/tango-helpers": "^1.2.4", 42 | "@uiw/react-codemirror": "^4.22.0", 43 | "antd": "^4.24.2", 44 | "classnames": "^2.5.1", 45 | "coral-system": "^1.0.5", 46 | "eslint-linter-browserify": "^8.51.0", 47 | "react-draggable": "^4.4.5", 48 | "react-json-view": "^1.21.3", 49 | "react-monaco-editor-lite": "^1.3.16" 50 | }, 51 | "publishConfig": { 52 | "access": "public", 53 | "registry": "https://registry.npmjs.org/" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/ui/src/code-editor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SingleEditor, SingleEditorIProps } from 'react-monaco-editor-lite'; 3 | import { Box } from 'coral-system'; 4 | 5 | export interface SingleMonacoEditorProps extends SingleEditorIProps { 6 | language?: string; 7 | hasBorder?: boolean; 8 | } 9 | 10 | export function SingleMonacoEditor(props: SingleMonacoEditorProps) { 11 | const { 12 | language, 13 | options: optionsProp = {}, 14 | height = '100%', 15 | width = '100%', 16 | hasBorder = false, 17 | ...rest 18 | } = props; 19 | const options = { 20 | language, 21 | ...optionsProp, 22 | }; 23 | 24 | const boxProps = hasBorder 25 | ? { 26 | border: 'solid', 27 | borderColor: 'line.normal', 28 | borderRadius: 's', 29 | } 30 | : {}; 31 | 32 | return ( 33 | 34 | 35 | 36 | ); 37 | } 38 | 39 | export { MultiEditor } from 'react-monaco-editor-lite'; 40 | export type { MultiEditorIProps as MultiEditorProps } from 'react-monaco-editor-lite'; 41 | -------------------------------------------------------------------------------- /packages/ui/src/color-tag.tsx: -------------------------------------------------------------------------------- 1 | import { Tag, TagProps, Tooltip } from 'antd'; 2 | import React from 'react'; 3 | 4 | interface ColorTagProps extends TagProps { 5 | tooltip?: string; 6 | } 7 | 8 | export function ColorTag({ tooltip, children, ...rest }: ColorTagProps) { 9 | const tag = {children}; 10 | if (tooltip) { 11 | return {tag}; 12 | } 13 | return tag; 14 | } 15 | -------------------------------------------------------------------------------- /packages/ui/src/context-action.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { Menu, Space } from 'antd'; 3 | import { Box, css } from 'coral-system'; 4 | import { Dict, isApplePlatform } from '@music163/tango-helpers'; 5 | 6 | const contextActionStyle = css` 7 | display: flex; 8 | gap: 8px; 9 | align-items: center; 10 | 11 | .ContextActionContent { 12 | flex: 1; 13 | overflow: hidden; 14 | text-overflow: ellipsis; 15 | white-space: nowrap; 16 | } 17 | 18 | .ContextActionExtra { 19 | flex: none; 20 | color: var(--tango-colors-gray-40); 21 | } 22 | `; 23 | 24 | interface ContextActionProps { 25 | icon?: React.ReactNode; 26 | children?: React.ReactNode; 27 | hotkey?: string; 28 | extra?: React.ReactNode; 29 | onClick?: () => void; 30 | className?: string; 31 | disabled?: boolean; 32 | key?: string; 33 | } 34 | 35 | export function ContextAction({ 36 | icon, 37 | children, 38 | hotkey, 39 | extra, 40 | disabled, 41 | onClick, 42 | className, 43 | key, 44 | }: ContextActionProps) { 45 | const normalizedHotKey = useMemo(() => { 46 | if (!hotkey) { 47 | return null; 48 | } 49 | const keyMap: Dict = isApplePlatform() 50 | ? { 51 | command: '⌘', 52 | meta: '⌘', 53 | ctrl: '^', 54 | control: '^', 55 | alt: '⌥', 56 | option: '⌥', 57 | shift: '⇧', 58 | '+': '', 59 | } 60 | : { 61 | command: 'Ctrl', 62 | meta: 'Win', 63 | ctrl: 'Ctrl', 64 | control: 'Ctrl', 65 | alt: 'Alt', 66 | option: 'Alt', 67 | shift: 'Shift', 68 | }; 69 | const regexp = new RegExp(Object.keys(keyMap).join('|').replace(/\+/, '\\+'), 'ig'); 70 | return hotkey.replace(regexp, (match) => keyMap[match.toLowerCase()]); 71 | }, [hotkey]); 72 | 73 | return ( 74 | 75 | 76 | {children} 77 | 78 | 79 | {normalizedHotKey} 80 | {extra} 81 | 82 | 83 | 84 | 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /packages/ui/src/copy-clipboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | 3 | export interface CopyClipboardProps { 4 | /** 5 | * 要拷贝到内容 6 | */ 7 | text: string; 8 | /** 9 | * 拷贝成功后到回调 10 | */ 11 | onCopy?: () => void; 12 | children?: 13 | | React.ReactNode 14 | | ((data: { copied: boolean; onClick: Function }) => React.ReactElement); 15 | } 16 | 17 | export function CopyClipboard({ onCopy, text, children }: CopyClipboardProps) { 18 | const [copied, setCopied] = useState(false); 19 | const timeoutRef = useRef(); 20 | 21 | useEffect(() => { 22 | return () => { 23 | if (timeoutRef.current) { 24 | clearTimeout(timeoutRef.current); 25 | } 26 | }; 27 | }, []); 28 | 29 | const onClick = () => { 30 | if (timeoutRef.current) { 31 | clearTimeout(timeoutRef.current); 32 | } 33 | navigator.clipboard.writeText(text); 34 | setCopied(true); 35 | onCopy?.(); 36 | timeoutRef.current = setTimeout(() => { 37 | setCopied(false); 38 | }, 1000); 39 | }; 40 | 41 | if (typeof children === 'function') { 42 | return React.cloneElement(children({ copied, onClick }), { 43 | onClick, 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/ui/src/error-boundary.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box } from 'coral-system'; 3 | import { logger } from '@music163/tango-helpers'; 4 | 5 | interface ErrorBoundaryProps { 6 | children: React.ReactNode; 7 | } 8 | 9 | interface ErrorBoundaryState { 10 | hasError: boolean; 11 | } 12 | 13 | export class ErrorBoundary extends React.Component { 14 | static getDerivedStateFromError(error: any) { 15 | // Update state so the next render will show the fallback UI. 16 | logger.error(error); 17 | return { hasError: true }; 18 | } 19 | 20 | constructor(props: ErrorBoundaryProps) { 21 | super(props); 22 | this.state = { hasError: false }; 23 | } 24 | 25 | componentDidCatch(error: any, errorInfo: any) { 26 | // You can also log the error to an error reporting service 27 | logger.error(error, errorInfo); 28 | } 29 | 30 | render() { 31 | if (this.state.hasError) { 32 | // You can render any custom fallback UI 33 | return ( 34 | 35 | Something went wrong. 36 | 37 | ); 38 | } 39 | 40 | return this.props.children; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/ui/src/file-explorer/directory.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { File } from './file'; 3 | import { ModuleList } from './module-list'; 4 | 5 | export interface Props { 6 | prefixedPath: string; 7 | files: string[]; 8 | selectFile: (path: string) => void; 9 | activePath: string; 10 | depth: number; 11 | } 12 | 13 | interface State { 14 | open: boolean; 15 | } 16 | 17 | export class Directory extends React.Component { 18 | state = { 19 | open: true, 20 | }; 21 | 22 | toggleOpen = () => { 23 | this.setState((state) => ({ open: !state.open })); 24 | }; 25 | 26 | render() { 27 | const { prefixedPath, files, selectFile, activePath, depth } = this.props; 28 | 29 | return ( 30 |
31 | 37 | 38 | {this.state.open ? ( 39 | 46 | ) : null} 47 |
48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/ui/src/file-explorer/file.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css, Box } from 'coral-system'; 3 | import { FolderOpenOutlined, FolderOutlined, FileOutlined } from '@ant-design/icons'; 4 | 5 | export interface Props { 6 | path: string; 7 | selectFile?: (path: string) => void; 8 | active?: boolean; 9 | onClick?: React.MouseEventHandler; 10 | depth: number; 11 | isDirOpen?: boolean; 12 | } 13 | 14 | const fileItemStyle = css` 15 | user-select: none; 16 | transition: all 0.15s ease-in-out; 17 | 18 | > .anticon { 19 | margin-right: 4px; 20 | } 21 | 22 | &:hover { 23 | background-color: var(--tango-colors-gray-10); 24 | } 25 | 26 | &[data-active='true'] { 27 | background-color: var(--tango-colors-gray-30); 28 | } 29 | `; 30 | 31 | export class File extends React.PureComponent { 32 | selectFile = () => { 33 | if (this.props.selectFile) { 34 | this.props.selectFile(this.props.path); 35 | } 36 | }; 37 | 38 | render() { 39 | const fileName = this.props.path.split('/').filter(Boolean).pop(); 40 | 41 | return ( 42 | 54 | {this.props.selectFile ? : } 55 | {fileName} 56 | 57 | ); 58 | } 59 | } 60 | 61 | function DirectoryIcon({ isOpen }: { isOpen?: Props['isDirOpen'] }) { 62 | return isOpen ? : ; 63 | } 64 | -------------------------------------------------------------------------------- /packages/ui/src/file-explorer/index.ts: -------------------------------------------------------------------------------- 1 | export { ModuleList as FileExplorer } from './module-list'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/file-explorer/module-list.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Directory } from './directory'; 3 | import { File } from './file'; 4 | 5 | export interface ModuleListProps { 6 | prefixedPath: string; 7 | files: string[]; 8 | selectFile: (path: string) => void; 9 | activePath: string; 10 | depth?: number; 11 | } 12 | 13 | export class ModuleList extends React.PureComponent { 14 | render() { 15 | const { depth = 0, activePath, selectFile, prefixedPath, files } = this.props; 16 | 17 | const fileListWithoutPrefix = files 18 | .filter((file) => file.startsWith(prefixedPath)) 19 | .map((file) => file.substring(prefixedPath.length)); 20 | 21 | const directoriesToShow = new Set( 22 | fileListWithoutPrefix 23 | .filter((file) => file.includes('/')) 24 | .map((file) => `${prefixedPath}${file.split('/')[0]}/`), 25 | ); 26 | 27 | const filesToShow = fileListWithoutPrefix 28 | .filter((file) => !file.includes('/')) 29 | .map((file) => ({ path: `${prefixedPath}${file}` })); 30 | 31 | return ( 32 |
33 | {Array.from(directoriesToShow).map((dir) => ( 34 | 42 | ))} 43 | 44 | {filesToShow.map((file) => ( 45 | 52 | ))} 53 |
54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/ui/src/iconfont.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon from '@ant-design/icons'; 3 | import type { IconFontProps } from '@ant-design/icons/es/components/IconFont'; 4 | 5 | /** 6 | * 只能在 Designer 里面用,其他地方用不了,依赖 iconfont 脚本提前载入 7 | */ 8 | export function IconFont(props: IconFontProps) { 9 | const { type, children, ...restProps } = props; 10 | 11 | // children > type 12 | let content: React.ReactNode = null; 13 | if (props.type) { 14 | content = ; 15 | } 16 | if (children) { 17 | content = children; 18 | } 19 | return {content}; 20 | } 21 | 22 | IconFont.displayName = 'IconFont'; 23 | -------------------------------------------------------------------------------- /packages/ui/src/icons/code-outlined.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createIcon } from './create-icon'; 3 | 4 | const CodeOutlinedSvg = () => ( 5 | 12 | 13 | 14 | 18 | 19 | 20 | ); 21 | 22 | export const CodeOutlined = createIcon(CodeOutlinedSvg, 'CodeOutlined'); 23 | -------------------------------------------------------------------------------- /packages/ui/src/icons/create-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon from '@ant-design/icons'; 3 | import type { CustomIconComponentProps } from '@ant-design/icons/lib/components/Icon'; 4 | 5 | export function createIcon(component: React.ComponentType, displayName?: string) { 6 | const CustomIcon = (props: Partial) => ( 7 | 8 | ); 9 | if (displayName) { 10 | CustomIcon.displayName = displayName; 11 | } 12 | return CustomIcon; 13 | } 14 | -------------------------------------------------------------------------------- /packages/ui/src/icons/dual-outlined.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createIcon } from './create-icon'; 3 | 4 | const DualOutlinedSvg = () => ( 5 | 13 | 14 | 15 | ); 16 | 17 | export const DualOutlined = createIcon(DualOutlinedSvg, 'DualOutlined'); 18 | -------------------------------------------------------------------------------- /packages/ui/src/icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-icon'; 2 | export * from './code-outlined'; 3 | export * from './line-dashed-outlined'; 4 | export * from './line-solid-outlined'; 5 | export * from './package-outlined'; 6 | export * from './open-panel-filled-left-outlined'; 7 | export * from './open-panel-filled-right-outlined'; 8 | export * from './open-panel-left-outlined'; 9 | export * from './open-panel-right-outlined'; 10 | export * from './undo-outlined'; 11 | export * from './pop-out-outlined'; 12 | export * from './redo-outlined'; 13 | export * from './dual-outlined'; 14 | -------------------------------------------------------------------------------- /packages/ui/src/icons/line-dashed-outlined.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createIcon } from './create-icon'; 3 | 4 | const LineDashedOutlinedSvg = () => ( 5 | 13 | 14 | 15 | ); 16 | 17 | export const LineDashedOutlined = createIcon(LineDashedOutlinedSvg, 'LineSolidOutlined'); 18 | -------------------------------------------------------------------------------- /packages/ui/src/icons/line-solid-outlined.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createIcon } from './create-icon'; 3 | 4 | const LineSolidOutlinedSvg = () => ( 5 | 13 | 14 | 15 | ); 16 | 17 | export const LineSolidOutlined = createIcon(LineSolidOutlinedSvg, 'LineSolidOutlined'); 18 | -------------------------------------------------------------------------------- /packages/ui/src/icons/open-panel-filled-left-outlined.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createIcon } from './create-icon'; 3 | 4 | const OpenPanelFilledLeftOutlinedSvg = () => ( 5 | 12 | 13 | 14 | 15 | ); 16 | 17 | export const OpenPanelFilledLeftOutlined = createIcon( 18 | OpenPanelFilledLeftOutlinedSvg, 19 | 'OpenPanelFilledLeftOutlined', 20 | ); 21 | -------------------------------------------------------------------------------- /packages/ui/src/icons/open-panel-filled-right-outlined.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createIcon } from './create-icon'; 3 | 4 | const OpenPanelFilledRightOutlinedSvg = () => ( 5 | 12 | 13 | 14 | 15 | ); 16 | 17 | export const OpenPanelFilledRightOutlined = createIcon( 18 | OpenPanelFilledRightOutlinedSvg, 19 | 'OpenPanelFilledRightOutlined', 20 | ); 21 | -------------------------------------------------------------------------------- /packages/ui/src/icons/open-panel-left-outlined.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createIcon } from './create-icon'; 3 | 4 | const OpenPanelLeftOutlinedSvg = () => ( 5 | 12 | 13 | 14 | 15 | ); 16 | 17 | export const OpenPanelLeftOutlined = createIcon(OpenPanelLeftOutlinedSvg, 'OpenPanelLeftOutlined'); 18 | -------------------------------------------------------------------------------- /packages/ui/src/icons/open-panel-right-outlined.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createIcon } from './create-icon'; 3 | 4 | const OpenPanelRightOutlinedSvg = () => ( 5 | 12 | 13 | 14 | 15 | ); 16 | 17 | export const OpenPanelRightOutlined = createIcon( 18 | OpenPanelRightOutlinedSvg, 19 | 'OpenPanelRightOutlined', 20 | ); 21 | -------------------------------------------------------------------------------- /packages/ui/src/icons/package-outlined.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createIcon } from './create-icon'; 3 | 4 | const PackageOutlinedSvg = () => ( 5 | 13 | 14 | 15 | ); 16 | 17 | export const PackageOutlined = createIcon(PackageOutlinedSvg, 'PackageOutlined'); 18 | -------------------------------------------------------------------------------- /packages/ui/src/icons/pop-out-outlined.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createIcon } from './create-icon'; 3 | 4 | const Svg = () => ( 5 | 13 | 14 | 15 | ); 16 | 17 | export const PopOutOutlined = createIcon(Svg, 'PopOutOutlined'); 18 | -------------------------------------------------------------------------------- /packages/ui/src/icons/redo-outlined.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createIcon } from './create-icon'; 3 | 4 | const RedoOutlinedSvg = () => ( 5 | 12 | 13 | 14 | ); 15 | 16 | export const RedoOutlined = createIcon(RedoOutlinedSvg, 'RedoOutlined'); 17 | -------------------------------------------------------------------------------- /packages/ui/src/icons/undo-outlined.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createIcon } from './create-icon'; 3 | 4 | const UndoOutlinedSvg = () => ( 5 | 12 | 13 | 14 | ); 15 | 16 | export const UndoOutlined = createIcon(UndoOutlinedSvg, 'UndoOutlined'); 17 | -------------------------------------------------------------------------------- /packages/ui/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './icons'; 2 | export * from './action'; 3 | export { Action as IconButton } from './action'; // 兼容旧版本 4 | export * from './action-select'; 5 | export * from './code-editor'; 6 | export * from './chat-input'; 7 | export * from './error-boundary'; 8 | export * from './file-explorer'; 9 | export * from './collapse-panel'; 10 | export * from './color-tag'; 11 | export * from './iconfont'; 12 | export * from './menu'; 13 | export * from './panel'; 14 | export * from './toggle-button'; 15 | export * from './input-code'; 16 | export * from './input-style-code'; 17 | export * from './input-list'; 18 | export * from './search'; 19 | export * from './json-view'; 20 | export * from './select-list'; 21 | export * from './config-form'; 22 | export * from './tabs'; 23 | export * from './select-action'; 24 | export * from './copy-clipboard'; 25 | export * from './tag-select'; 26 | export * from './popover'; 27 | export * from './drag-panel'; 28 | export * from './context-action'; 29 | export * from './classname-input'; 30 | -------------------------------------------------------------------------------- /packages/ui/src/json-view.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactJsonView, { ReactJsonViewProps } from 'react-json-view'; 3 | 4 | export type JsonViewProps = Omit & { 5 | /** 6 | * 允许复制 7 | */ 8 | enableCopy?: boolean; 9 | /** 10 | * 点击复制按钮的回调 11 | * @param valuePath value 的路径 12 | * @param data { src, namespace, name } 13 | * @returns 14 | */ 15 | onCopy?: (valuePath: string, data: any) => string; 16 | }; 17 | 18 | export function JsonView({ enableCopy, onCopy, ...rest }: JsonViewProps) { 19 | return ( 20 | { 28 | const valuePath = getJsonValuePath(data.namespace); 29 | const ret = onCopy?.(valuePath, data); 30 | navigator.clipboard.writeText(ret || valuePath); 31 | } 32 | : false 33 | } 34 | {...rest} 35 | /> 36 | ); 37 | } 38 | 39 | const numPattern = /^\d+$/; 40 | 41 | function getJsonValuePath(namespace: any[]): string { 42 | if (!Array.isArray(namespace)) { 43 | return; 44 | } 45 | 46 | if (namespace[0] === false) { 47 | namespace = namespace.slice(1); 48 | } 49 | 50 | return [...namespace].reduce((prev, cur) => { 51 | if (numPattern.test(cur)) { 52 | prev = `${prev}[${cur}]`; 53 | } else { 54 | prev = prev ? [prev, cur].join('?.') : cur; 55 | } 56 | return prev; 57 | }, ''); 58 | } 59 | -------------------------------------------------------------------------------- /packages/ui/src/search.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text } from 'coral-system'; 3 | import { Input, InputProps } from 'antd'; 4 | import { SearchOutlined } from '@ant-design/icons'; 5 | 6 | export interface SearchProps extends Omit { 7 | width?: string; 8 | onChange?: (value: string) => void; 9 | /** 10 | * value 是否转义 11 | */ 12 | escapeValue?: boolean; 13 | } 14 | 15 | export function Search({ 16 | placeholder = '请输入关键词', 17 | onChange, 18 | width, 19 | escapeValue = true, 20 | style: styleProp = {}, 21 | ...rest 22 | }: SearchProps) { 23 | const style = { 24 | width, 25 | ...styleProp, 26 | }; 27 | return ( 28 | { 31 | let next = e.target.value; 32 | if (escapeValue) { 33 | next = next.replaceAll('\\', '\\\\'); 34 | } 35 | onChange?.(next); 36 | }} 37 | suffix={ 38 | 39 | 40 | 41 | } 42 | allowClear 43 | style={style} 44 | {...rest} 45 | /> 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /packages/ui/src/select-action.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Tooltip } from 'antd'; 3 | import { Box, css, HTMLCoralProps } from 'coral-system'; 4 | 5 | export interface SelectActionProps extends HTMLCoralProps<'div'> { 6 | tooltip?: string; 7 | } 8 | 9 | const selectionActionStyle = css` 10 | &:hover { 11 | background-color: var(--tango-colors-primary-40); 12 | } 13 | `; 14 | 15 | export function SelectAction({ tooltip, children, ...rest }: SelectActionProps) { 16 | return ( 17 | 18 | 28 | {children} 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /packages/ui/src/tabs.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import cx from 'classnames'; 4 | import { Tabs as AntTabs, TabsProps as AntTabsProps } from 'antd'; 5 | 6 | const StyledTabs = styled(AntTabs)` 7 | .ant-tabs-nav { 8 | padding-left: 12px; 9 | padding-right: 12px; 10 | margin: 0; 11 | } 12 | 13 | &.sticky .ant-tabs-nav { 14 | position: sticky; 15 | top: ${(props) => props.$stickyOffset}; 16 | z-index: 2; 17 | background: #fff; 18 | } 19 | 20 | &.ant-tabs-centered .ant-tabs-nav { 21 | margin: 0; 22 | } 23 | `; 24 | 25 | export interface TabsProps extends AntTabsProps { 26 | isTabBarSticky?: boolean; 27 | tabBarStickyOffset?: number; 28 | } 29 | 30 | export function Tabs({ 31 | centered, 32 | isTabBarSticky = false, 33 | tabBarStickyOffset = 0, 34 | className, 35 | ...rest 36 | }: TabsProps) { 37 | const classNames = cx(className, { sticky: isTabBarSticky }); 38 | 39 | return ( 40 | 48 | ); 49 | } 50 | 51 | Tabs.TabPane = AntTabs.TabPane; 52 | -------------------------------------------------------------------------------- /packages/ui/src/tag-select.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box } from 'coral-system'; 3 | import { Tag } from 'antd'; 4 | import { useControllableState, warning } from '@music163/tango-helpers'; 5 | 6 | export interface TagSelectProps { 7 | /** 8 | * 默认值 9 | */ 10 | defaultValue?: string[]; 11 | /** 12 | * 受控值 13 | */ 14 | value?: string[]; 15 | /** 16 | * 值变化时的回调 17 | */ 18 | onChange?: (value: string[]) => void; 19 | /** 20 | * 选项列表 21 | */ 22 | options?: Array<{ label: string; value: string; disabled?: boolean }>; 23 | /** 24 | * 选择模式,单选或多选 25 | */ 26 | mode?: 'single' | 'multiple'; 27 | className?: string; 28 | style?: React.CSSProperties; 29 | } 30 | 31 | export function TagSelect({ 32 | value: valueProp, 33 | defaultValue = [], 34 | onChange, 35 | options, 36 | mode = 'multiple', 37 | ...rest 38 | }: TagSelectProps) { 39 | const [value, setValue] = useControllableState({ 40 | value: valueProp, 41 | defaultValue, 42 | onChange, 43 | }); 44 | 45 | if (mode === 'single' && value.length > 1) { 46 | warning(false, '单选模式下 value 只能有一个值!'); 47 | } 48 | 49 | return ( 50 | 51 | {options.map((option) => ( 52 | { 57 | if (mode === 'multiple') { 58 | if (checked) { 59 | setValue([...value, option.value]); 60 | } else { 61 | setValue(value.filter((item) => item !== option.value)); 62 | } 63 | } else { 64 | checked ? setValue([option.value]) : setValue([]); 65 | } 66 | }} 67 | > 68 | {option.label} 69 | 70 | ))} 71 | 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.prod.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": ["src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /public/dashboard-builder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEase/tango/c48a49961a3b0e8b49f8a617d893e0b4f047147f/public/dashboard-builder.png -------------------------------------------------------------------------------- /public/mail-builder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEase/tango/c48a49961a3b0e8b49f8a617d893e0b4f047147f/public/mail-builder.png -------------------------------------------------------------------------------- /public/rn-builder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEase/tango/c48a49961a3b0e8b49f8a617d893e0b4f047147f/public/rn-builder.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.prod.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "noEmit": true, 6 | "sourceMap": true, 7 | "resolveJsonModule": true, 8 | "strictFunctionTypes": true, 9 | "paths": { 10 | "@music163/tango-helpers": ["packages/helpers/src/index.ts"], 11 | "@music163/tango-core": ["packages/core/src/index.ts"], 12 | "@music163/tango-context": ["packages/context/src/index.ts"], 13 | "@music163/tango-ui": ["packages/ui/src/index.ts"], 14 | "@music163/tango-sandbox": ["packages/sandbox/src/index.ts"], 15 | "@music163/tango-setting-form": ["packages/setting-form/src/index.ts"], 16 | "@music163/tango-designer": ["packages/designer/src/index.ts"] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "module": "ES2015", 5 | "lib": ["dom", "esnext"], 6 | "importHelpers": true, 7 | "declaration": true, 8 | "sourceMap": false, 9 | "moduleResolution": "node", 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "ignoreDeprecations": "5.0", 14 | "allowSyntheticDefaultImports": true, 15 | "strictNullChecks": false, 16 | "jsx": "react", 17 | "esModuleInterop": true 18 | }, 19 | "exclude": ["node_modules", "dist"] 20 | } 21 | -------------------------------------------------------------------------------- /typedoc.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "includeVersion": true 4 | } 5 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "name": "Tango LowCode Builder", 4 | "entryPoints": ["packages/core", "packages/designer","packages/setting-form", "packages/helpers"], 5 | "entryPointStrategy": "packages", 6 | "exclude": ["packages/**/tests/**/*"], 7 | "out": "docs", 8 | "logLevel": "Verbose" 9 | } 10 | --------------------------------------------------------------------------------