chore: import upstream snapshot with attribution
Deploy Github Pages / deploy (push) Failing after 1s

This commit is contained in:
wehub-resource-sync
2026-07-13 12:30:43 +08:00
commit 2d398e1894
356 changed files with 90411 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
{
"extends": ["eslint-config-ali/typescript/react", "prettier"],
"ignorePatterns": ["**/dist/**/*", "**/lib/**/*", "**/node_modules/**/*", "scripts/**/*"],
"rules": {
"@typescript-eslint/consistent-type-definitions": "off",
"@typescript-eslint/dot-notation": "off",
"@typescript-eslint/no-unused-vars": "warn",
"import/no-cycle": "off",
"no-nested-ternary": "off",
"no-useless-return": "off",
"no-param-reassign": "off",
"prefer-destructuring": "off"
}
}
+49
View File
@@ -0,0 +1,49 @@
name: CI
# Event设置为main分支的pull request事件,
# 这里的main分支相当于master分支,github项目新建是把main设置为默认分支,我懒得改了所以就保持这样吧
on:
pull_request:
branches: main
jobs:
# 只需要定义一个job并命名为CI
CI:
runs-on: ubuntu-latest
steps:
# 拉取项目代码
- name: Checkout repository
uses: actions/checkout@v2
# 给当前环境下载node
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '18.x'
# 检查缓存
# 如果key命中缓存则直接将缓存的文件还原到 path 目录,从而减少流水线运行时间
# 若 key 没命中缓存时,在当前Job成功完成时将自动创建一个新缓存
- name: Cache
# 缓存命中结果会存储在steps.[id].outputs.cache-hit里,该变量在继后的step中可读
id: cache-dependencies
uses: actions/cache@v3
with:
# 缓存文件目录的路径
path: |
**/node_modules
# key中定义缓存标志位的生成方式。runner.OS指当前环境的系统。外加对yarn.lock内容生成哈希码作为key值,如果yarn.lock改变则代表依赖有变化。
# 这里用yarn.lock而不是package.json是因为package.json中还有version和description之类的描述项目但和依赖无关的属性
key: ${{runner.OS}}-${{hashFiles('**/yarn.lock')}}
# 安装依赖
- name: Installing Dependencies
# 如果缓存标志位没命中,则执行该step。否则就跳过该step
if: steps.cache-dependencies.outputs.cache-hit != 'true'
run: yarn install
# 预构建
- name: Prebuild
run: yarn build
# 运行代码扫描
- name: Running Lint
# 通过前面章节定义的命令行执行代码扫描
run: yarn eslint
# 运行自动化测试
- name: Running Test
# 通过前面章节定义的命令行执行自动化测试
run: yarn test
+53
View File
@@ -0,0 +1,53 @@
# Simple workflow for deploying static content to GitHub Pages
name: Deploy Github Pages
on:
# Runs on pushes targeting the default branch
push:
branches: ['main']
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow one concurrent deployment
concurrency:
group: 'pages'
cancel-in-progress: true
jobs:
# Single deploy job since we're just deploying
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- name: Install dependencies
run: yarn install
- name: Build
run: yarn build
- name: Build docs
run: yarn typedoc
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Upload artifact
uses: actions/upload-pages-artifact@v2
with:
# Upload dist repository
path: './docs'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2
+12
View File
@@ -0,0 +1,12 @@
node_modules/
dist/
lib/
coverage/
/docs
.umi/
.umi-production/
.log
.eslintcache
*.pem
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn commitlint --edit "$1"
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn lint-staged --allow-empty
+2
View File
@@ -0,0 +1,2 @@
registry=https://registry.npmmirror.com/
+5
View File
@@ -0,0 +1,5 @@
{
"trailingComma": "all",
"singleQuote": true,
"printWidth": 100
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 NetEase
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+125
View File
@@ -0,0 +1,125 @@
<p align="center">
<img width="200" src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30218210645/b186/3974/338b/2ddfa3cd042cf988ca452686552f8462.png" />
</p>
<h1 align="center">Tango LowCode Builder</h1>
<div align="center">
A source code based low-code builder.
[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/NetEase/tango/blob/main/LICENSE)
[![NPM version](https://img.shields.io/npm/v/@music163/tango-designer.svg?style=flat-square)](http://npmjs.org/package/@music163/tango-designer)
<img src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30108735057/7ba9/dced/9ac3/420f6e04b371dd47de06e7d71142560d.gif" alt="preview" />
</div>
English | [简体中文](/README.zh-CN.md)
## 📄 Documentation
You can view the detailed usage guide through the following links:
- Document site: <https://netease.github.io/tango-site/>
- Playground application: <https://tango-demo.musicfe.com/designer/>
## ✨ Features
- Tested in the production environment of NetEase Cloud Music, can be flexibly integrated into low-code platforms, local development tools, etc.
- Based on source code AST, with no private DSL and protocol
Real-time code generation capability, supporting source code in and source code out
- Out-of-the-box front-end low-code designer, providing flexible and easy-to-use designer React components
- Developed using TypeScript, providing complete type definition files
## 💡 Examples
You can use tango to build lowcode builders, for example:
| Preview | Description |
| ---------------------------------------------------------------------------------------------- | ----------------------- |
| ![image](https://github.com/NetEase/tango/assets/6984035/56a71741-4536-4336-803a-d81d51128a76) | Internal App Builder |
| ![dashboard app](public/dashboard-builder.png) | Dashboard App Builder |
| ![image](https://github.com/NetEase/tango/assets/6984035/790d88e1-d716-4f99-868a-31dda25e9fb1) | Mobile App Builder |
| ![rn app](public/rn-builder.png) | ReactNative App Builder |
| ![mail app](public/mail-builder.png) | Email Builder |
## 🌐 Compatibility
- Modern browsersChrome >= 80, Edge >= 80, last 2 safari versions, last 2 firefox versions
## 💻 Development
### Environment
- Node `>= 18`
- Yarn `>= 1.22 && < 2`
### Development Quick Start
```bash
# clone the repo
git clone https://github.com/NetEase/tango.git
# enter the project root
cd tango
# install dependencies
yarn
# start the designer playground app
yarn start
```
### Local Https certificate
If you need to use https in the local development environment, you can use the following command to generate a certificate:
```bash
brew install mkcert
# install the local certificate
mkcert -install
# enter the playground app directory
cd apps/playground
# generate a certificate
mkcert local.netease.com # or change to your own domain name
```
## 💬 Community
Join NetEase Tango Community to share your ideas, suggestions, or questions and connect with other users and contributors.
- Discord: <https://discord.gg/B6hkGTe4Rz>
- [Usage Trends](https://npm-compare.com/@music163/tango-helpers,@music163/tango-context,@music163/tango-core,@music163/tango-setting-form,@music163/tango-sandbox,@music163/tango-ui,@music163/tango-designer)
## 🤝 Contributing
Please read the [github contribution guide](https://docs.github.com/en/get-started/quickstart/contributing-to-projects) first。
- Clone the repository
- Create a branch
- Commit and push your code
- Open a Pull Request
## 💗 Acknowledgments
Thanks to the NetEase Cloud Music Front-end team, Public Technology team, Live Broadcasting Technology team, and all the developers who participated in the Tango project.
Thank you to CodeSandbox for providing the [Sandpack](https://sandpack.codesandbox.io/) project, which provides powerful online code execution capabilities for Tango.
## 📣 Product Promotion
![](https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/31629770956/da9e/3a74/4e00/7c69cf46a713f1b008bd1243b5b1ab1c.png)
Don't waste your time restoring the UI, try the NetEase Cloud Music "Seal D2C" development tool! Easily turn your design into code, support React, RN, Vue, WeChat apps and other multi-end scenarios, what you see is what you get!
Experience "Seal D2C" now:
- I'm a Figma user: <https://www.figma.com/community/plugin/1174548852019950797/seal-figma-to-code-d2c/>
- I'm a MasterGo user: <https://mastergo.com/community/plugin/98956774428196/>
## 📄 License
This project is licensed under the terms of the [MIT license](./LICENSE)
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`NetEase/tango`
- 原始仓库:https://github.com/NetEase/tango
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
+126
View File
@@ -0,0 +1,126 @@
<p align="center">
<img width="200" src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30218210645/b186/3974/338b/2ddfa3cd042cf988ca452686552f8462.png" />
</p>
<h1 align="center">Tango 低代码设计器</h1>
<div align="center">
一个源码驱动的低代码设计器框架
[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/NetEase/tango/blob/main/LICENSE)
[![NPM version](https://img.shields.io/npm/v/@music163/tango-designer.svg?style=flat-square)](http://npmjs.org/package/@music163/tango-designer)
<img src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30108735057/7ba9/dced/9ac3/420f6e04b371dd47de06e7d71142560d.gif" alt="preview" />
</div>
简体中文 | [English](/README.md)
## 📄 文档
可以通过下面的链接查看详细的使用指南:
- 官方文档站点: <https://netease.github.io/tango-site/>
- 演示应用: <https://tango-demo.musicfe.com/designer/>
## ✨ 特性
- 经历网易云音乐内网生产环境的实际检验,可灵活集成应用于低代码平台,本地开发工具等
- 提供基于源码 AST 驱动的低代码引擎,无私有 DSL 和协议
- 提供实时出码能力,支持源码进,源码出,适配多种源码开发场景
- 提供开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件
- 使用 TypeScript 开发,提供完整的类型定义文件和完善的文档支持
## 💡 例子
你可以使用 Tango 快速构建多种类型的基于源码的低代码搭建工具,例如:
| 预览图 | 说明 |
| ---------------------------------------------------------------------------------------------- | -------------------- |
| ![image](https://github.com/NetEase/tango/assets/6984035/56a71741-4536-4336-803a-d81d51128a76) | 中后台系统搭建 |
| ![dashboard app](public/dashboard-builder.png) | 仪表盘应用搭建 |
| ![image](https://github.com/NetEase/tango/assets/6984035/790d88e1-d716-4f99-868a-31dda25e9fb1) | H5活动页面搭建 |
| ![rn app](public/rn-builder.png) | ReactNative 应用搭建 |
| ![mail app](public/mail-builder.png) | 营销类邮件搭建 |
## 🌐 兼容环境
- 现代浏览器(Chrome >= 80, Edge >= 80, last 2 safari versions, last 2 firefox versions
## 💻 开发
### 推荐开发环境
- Node `>= 18`
- Yarn `>= 1.22 && < 2`
### 本地开发调试方法
```bash
# 下载仓库
git clone https://github.com/NetEase/tango.git
# 进入项目根目录
cd tango
# 安装依赖
yarn
# 启动设计器示例
yarn start
```
### 本地 https 证书
如果需要在本地开发环境中使用 https,可以使用以下命令生成证书:
```bash
brew install mkcert
# 将 mkcert 添加到本地根 CA,仅在本地生效
mkcert -install
# 进入 playground 应用目录
cd apps/playground
# 为网站生成一个由 mkcert 签名的证书
mkcert local.netease.com
```
## 💬 社区讨论
参与 NetEase Tango 的社区,以分享您的想法、建议或问题,并与其他用户和贡献者建立联系。
- Discord: <https://discord.gg/B6hkGTe4Rz>
- [使用趋势](https://npm-compare.com/@music163/tango-helpers,@music163/tango-context,@music163/tango-core,@music163/tango-setting-form,@music163/tango-sandbox,@music163/tango-ui,@music163/tango-designer)
## 🤝 参与共建
请先阅读 [贡献指南](https://docs.github.com/en/get-started/quickstart/contributing-to-projects)。
- 克隆仓库
- 创建分支
- 提交代码
- 合并修改 git rebase master
- 发起 Pull Request
## 💗 致谢
感谢网易云音乐公共技术团队,大前端团队,直播技术团队,以及所有参与过 Tango 项目的开发者。
感谢 CodeSandbox 提供的 [Sandpack](https://sandpack.codesandbox.io/) 项目,为 Tango 提供了强大的基于浏览器的代码构建与执行能力。
## 📣 产品推广
![](https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/31629770956/da9e/3a74/4e00/7c69cf46a713f1b008bd1243b5b1ab1c.png)
不要再浪费时间还原 UI 啦,快来试试网易云音乐「海豹 D2C」研发工具吧!轻松将设计稿转为代码,支持 React、RN、Vue、微信小程序等多端场景,所见即所得!
立即体验「海豹 D2C」:
- 我是 Figma 用户:<https://www.figma.com/community/plugin/1174548852019950797/seal-figma-to-code-d2c/>
- 我是 MasterGo 用户:<https://mastergo.com/community/plugin/98956774428196/>
## 📄 开源协议
此项目遵循 [MIT 开源协议](./LICENSE)
+9
View File
@@ -0,0 +1,9 @@
/node_modules
/.env.local
/.umirc.local.ts
/config/config.local.ts
/src/.umi
/src/.umi-production
/src/.umi-test
/dist
.swc
+65
View File
@@ -0,0 +1,65 @@
import path from 'path';
import { defineConfig } from 'umi';
const resolvePackageIndex = (relativeEntry: string) =>
path.resolve(__dirname, '../../packages/', relativeEntry);
export default defineConfig({
routes: [
{ path: '/', component: 'index' },
{ path: '/mail', component: 'mail' },
{ path: '/docs', component: 'docs' },
],
npmClient: 'yarn',
mfsu: false,
codeSplitting: false,
alias: {
'@music163/tango-helpers': resolvePackageIndex('helpers/src/index.ts'),
'@music163/tango-core': resolvePackageIndex('core/src/index.ts'),
'@music163/tango-context': resolvePackageIndex('context/src/index.ts'),
'@music163/tango-ui': resolvePackageIndex('ui/src/index.ts'),
'@music163/tango-designer': resolvePackageIndex('designer/src/index.ts'),
'@music163/tango-sandbox': resolvePackageIndex('sandbox/src/index.ts'),
'@music163/tango-setting-form': resolvePackageIndex('setting-form/src/index.ts'),
},
externals: {
react: 'React',
'react-dom': 'ReactDOM',
'styled-components': 'styled',
moment: 'moment',
antd: 'antd',
},
headScripts: [
'https://unpkg.com/react@18.2.0/umd/react.development.js',
'https://unpkg.com/react-dom@18.2.0/umd/react-dom.development.js',
'https://unpkg.com/react-is@18.2.0/umd/react-is.production.min.js',
'https://unpkg.com/moment/min/moment-with-locales.js',
'https://unpkg.com/styled-components@5.3.11/dist/styled-components.js',
'https://unpkg.com/antd@4.24.13/dist/antd-with-locales.min.js',
'https://unpkg.com/prettier@2.6.0/standalone.js',
'https://unpkg.com/prettier@2.6.0/parser-babel.js',
],
https: {
key: path.resolve(__dirname, 'local.netease.com-key.pem'),
cert: path.resolve(__dirname, 'local.netease.com.pem'),
http2: false,
},
chainWebpack: (config: any) => {
// @see https://github.com/graphql/graphql-js/issues/1272#issuecomment-393903706
config.module
.rule('mjs')
.test(/\.mjs$/)
.include.add(/node_modules/)
.end()
.type('javascript/auto');
config.module.rule('js').include.add(resolvePackageIndex('context/src'));
config.module.rule('js').include.add(resolvePackageIndex('core/src'));
config.module.rule('js').include.add(resolvePackageIndex('designer/src'));
config.module.rule('js').include.add(resolvePackageIndex('helpers/src'));
config.module.rule('js').include.add(resolvePackageIndex('sandbox/src'));
config.module.rule('js').include.add(resolvePackageIndex('setting-form/src'));
config.module.rule('js').include.add(resolvePackageIndex('ui/src'));
return config;
},
monorepoRedirect: {},
});
+20
View File
@@ -0,0 +1,20 @@
{
"name": "playground",
"version": "0.0.0",
"private": true,
"author": "Wells <ww.sun@outlook.com>",
"scripts": {
"dev": "cross-env HOST=local.netease.com PORT=8001 umi dev",
"build": "umi build",
"postinstall": "umi setup",
"setup": "umi setup",
"start": "npm run dev"
},
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/antd": "^0.2.4",
"antd": "^4.24.2",
"coral-system": "^1.0.5",
"umi": "^4.2.3"
}
}
+10
View File
@@ -0,0 +1,10 @@
import type { IApi } from 'umi';
export default (api: IApi) => {
api.addMiddlewares(() => {
return (req, res, next) => {
res.setHeader('Origin-Agent-Cluster', '?0');
next();
};
});
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

@@ -0,0 +1,5 @@
import React from 'react';
export function FooSetter({ value, ...rest }: any) {
return <code {...rest}>fooSetter: {value}</code>;
}
+1
View File
@@ -0,0 +1 @@
export * from './foo-setter';
@@ -0,0 +1,22 @@
import React from 'react';
import { Row, Switch } from 'antd';
import { observer } from '@music163/tango-context';
const OtherPanel = observer(({ autoRemove, setAutoRemove }: any) => {
const onChange = (checked: boolean) => {
setAutoRemove(checked);
};
return (
<Row>
<label></label>
<Switch
checkedChildren="开启"
unCheckedChildren="关闭"
onChange={onChange}
checked={autoRemove}
/>
</Row>
);
});
export default OtherPanel;
+260
View File
@@ -0,0 +1,260 @@
import React from 'react';
import { Box, Group, Text } from 'coral-system';
import { Avatar, Space, Switch } from 'antd';
import { BranchesOutlined, MenuOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import { registerSetter } from '@music163/tango-designer';
import type { IVariableTreeNode } from '@music163/tango-helpers';
import { FooSetter } from '../components';
import { ToggleButton } from '@music163/tango-ui';
export * from './mock-files';
export const bootHelperVariables: IVariableTreeNode[] = [
{
key: '$helpers',
title: '工具函数',
children: [
{
title: '设置变量值',
key: '() => tango.setStoreValue("variableName", "variableValue")',
type: 'function',
help: '设置状态值,tango.setStoreValue("variableName", "variableValue")',
},
{
title: '获取变量值',
key: '() => tango.getStoreValue("variableName")',
type: 'function',
help: '获取状态值',
},
{ title: '打开弹窗', key: '() => tango.openModal("")', type: 'function' },
{ title: '关闭弹窗', key: '() => tango.closeModal("")', type: 'function' },
{ title: '切换路由', key: '() => tango.navigateTo("/")', type: 'function' },
{
title: '拷贝到剪贴板',
key: '() => tango.copyToClipboard("hello")',
type: 'function',
},
],
},
];
// 注册自定义 setter
registerSetter({
name: 'fooSetter',
component: FooSetter,
});
// 平台 Logo
export function Logo() {
return (
<Box width="50px" display="flex" alignItems="center" justifyContent="center" fontSize="14px">
<ToggleButton
shape="text"
items={[
{ label: '回到首页', key: 'home' },
{ label: '文档', key: 'doc' },
{ label: '设置', key: 'setting' },
]}
>
<MenuOutlined />
</ToggleButton>
</Box>
);
}
// 项目信息
export function ProjectDetail() {
return (
<Box display="flex" alignItems="center" columnGap="l">
<Text className="ProjectName" fontSize="18px" fontWeight="bold" truncated>
community-test
</Text>
<Text className="BranchName" as="code" fontSize="14px" truncated>
<BranchesOutlined /> feature/list
</Text>
</Box>
);
}
export function SidebarFooter() {
return (
<Space direction="vertical" align="center">
<QuestionCircleOutlined style={{ fontSize: 20 }} />
<Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" />
</Space>
);
}
interface ActionsProps {
defaultChecked?: boolean;
// eslint-disable-next-line react/no-unused-prop-types
onChange?: (checked: boolean) => void;
}
// 平台核心行动点
export function Actions({ defaultChecked }: ActionsProps) {
return (
<Group spacingX="8px">
<Switch defaultChecked={defaultChecked} checkedChildren="新版" unCheckedChildren="老版" />
</Group>
);
}
export const menuData: any = {
// 常用组件
common: [
{
title: '基本',
items: [
'Button',
'ButtonGroup',
'ActionList',
'Action',
'Image',
'Text',
'MultilineText',
'Link',
'Title',
'Paragraph',
'Icon',
],
},
{
title: '数据和逻辑',
items: ['When', 'DataProvider', 'Interval', 'Each'],
},
{
title: '基础布局',
items: [
'Section',
'Columns',
'Box',
'Divider',
'Space',
'Tabs',
'Toolbar',
'Modal',
'Drawer',
'SnippetButtonGroup',
'Snippet2ColumnLayout',
'Snippet3ColumnLayout',
'SnippetSuccessResult',
],
},
{
title: '表单表格',
items: [
'XTable',
'XEditableTable',
// 'Table',
'XForm',
'XFormItem',
'XStepForms',
// 'SearchForm',
// 'Form',
// 'FormItem',
],
},
{
title: '常用图表',
items: [
'ChartContainer',
'BarChart',
'LineChart',
'PieChart',
'FunnelChart',
'ScatterChart',
'RadarChart',
'WordCloud',
],
},
{
title: '导航',
items: [
'Breadcrumb',
'Dropdown',
'Menu',
// 'PageHeader',
'Pagination',
'Steps',
],
},
{
title: '数据录入',
items: [
'AutoComplete',
'Cascader',
'Checkbox',
'CheckboxGroup',
'DatePicker',
'DateRangePicker',
'WeekPicker',
'MonthPicker',
'YearPicker',
'Input',
'InputNumber',
'InputKV',
'Mentions',
'Radio',
'RadioGroup',
'Rate',
'Select',
'Slider',
'Switch',
'Search',
'TextArea',
'TimePicker',
'TimeRangePicker',
'Transfer',
'TreeSelect',
'Upload',
'NosUpload',
],
},
{
title: '数据展示',
items: [
'Avatar',
'Badge',
'RibbonBadge',
'Calendar',
'Card',
'Carousel',
'Collapse',
'Comment',
'Descriptions',
'Empty',
'Image',
'List',
'Popover',
'Statistic',
'Table',
'Tag',
'CheckableTag',
'Skeleton',
'SkeletonAvatar',
'SkeletonButton',
'SkeletonInput',
'SkeletonImage',
'SkeletonNode',
'Spin',
'Timeline',
'Tooltip',
'Tree',
],
},
{
title: '反馈',
items: [
'Alert',
'Drawer',
// 'Message',
'Modal',
'Notification',
'Popconfirm',
'Progress',
'Result',
],
},
],
};
+179
View File
@@ -0,0 +1,179 @@
const packageJson = {
name: 'demo',
private: true,
dependencies: {
'@music163/tango-mail': '0.2.4',
'@music163/tango-boot': '0.2.5',
react: '17.0.2',
'react-dom': '17.0.2',
'prop-types': '15.7.2',
tslib: '2.5.0',
},
};
const tangoConfigJson = {
designerConfig: {
autoGenerateComponentId: true,
},
packages: {
react: {
version: '17.0.2',
library: 'React',
type: 'dependency',
resources: ['https://unpkg.com/react@{{version}}/umd/react.development.js'],
},
'react-dom': {
version: '17.0.2',
library: 'ReactDOM',
type: 'dependency',
resources: ['https://unpkg.com/react-dom@{{version}}/umd/react-dom.development.js'],
},
'react-is': {
version: '16.13.1',
library: 'ReactIs',
type: 'dependency',
resources: ['https://unpkg.com/react-is@{{version}}/umd/react-is.production.min.js'],
},
'@music163/tango-boot': {
description: '云音乐低代码运行时框架',
version: '0.2.5',
library: 'TangoBoot',
type: 'baseDependency',
resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'],
// resources: ['http://localhost:9001/boot.js'],
},
'@music163/tango-mail': {
description: 'TangoMail 基础物料',
version: '0.2.4',
library: 'TangoMail',
type: 'baseDependency',
resources: ['https://unpkg.com/@music163/tango-mail@{{version}}/dist/index.js'],
designerResources: ['https://unpkg.com/@music163/tango-mail@{{version}}/dist/designer.js'],
},
},
};
const routesCode = `
import Mail from "./pages/mail";
const routes = [
{
path: '/',
exact: true,
component: Mail,
},
];
export default routes;
`;
const entryCode = `
import { runApp } from '@music163/tango-boot';
import routes from './routes';
runApp({
boot: {
mountElement: document.querySelector('#root'),
qiankun: false,
},
router: {
type: 'browser',
config: routes,
},
});
`;
const viewHomeCode = `
import React from 'react';
import {
Body,
Button,
Container,
Head,
Hr,
Html,
Preview,
Section,
Text,
} from '@music163/tango-mail';
const WelcomeEmail = () => (
<Html>
<Head />
<Preview>The sales intelligence platform that helps you uncover qualified leads.</Preview>
<Body style={main}>
<Container style={container}>
<Text style={paragraph}>Hi wells,</Text>
<Text style={paragraph}>
Welcome to Koala, the sales intelligence platform that helps you uncover qualified leads
and close deals faster.
</Text>
<Section style={btnContainer}>
<Button style={button} href="https://getkoala.com">
Get started
</Button>
</Section>
<Text style={paragraph}>
Best,
The Koala team
</Text>
<Hr style={hr} />
<Text style={footer}>470 Noor Ave STE B #1148, South San Francisco, CA 94080</Text>
</Container>
</Body>
</Html>
);
const main = {
backgroundColor: '#ffffff',
fontFamily:
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif',
};
const container = {
margin: '0 auto',
padding: '20px 0 48px',
};
const paragraph = {
fontSize: '16px',
lineHeight: '26px',
};
const btnContainer = {
textAlign: 'center',
};
const button = {
backgroundColor: '#5F51E8',
borderRadius: '3px',
color: '#fff',
fontSize: '16px',
textDecoration: 'none',
textAlign: 'center',
display: 'block',
padding: '12px',
};
const hr = {
borderColor: '#cccccc',
margin: '20px 0',
};
const footer = {
color: '#8898aa',
fontSize: '12px',
};
export default WelcomeEmail;
`;
export const mailFiles = [
{ filename: '/package.json', code: JSON.stringify(packageJson) },
{ filename: '/tango.config.json', code: JSON.stringify(tangoConfigJson) },
{ filename: '/README.md', code: '# readme' },
{ filename: '/src/index.js', code: entryCode },
{ filename: '/src/pages/mail.js', code: viewHomeCode },
{ filename: '/src/routes.js', code: routesCode },
];
+462
View File
@@ -0,0 +1,462 @@
const packageJson = {
name: 'demo',
private: true,
dependencies: {
'@music163/antd': '0.2.5',
'@music163/tango-boot': '0.3.5',
react: '17.0.2',
'react-dom': '17.0.2',
'prop-types': '15.7.2',
tslib: '2.5.0',
},
};
const tangoConfigJson = {
designerConfig: {
autoGenerateComponentId: true,
},
packages: {
react: {
version: '17.0.2',
library: 'React',
type: 'dependency',
resources: ['https://unpkg.com/react@{{version}}/umd/react.development.js'],
},
'react-dom': {
version: '17.0.2',
library: 'ReactDOM',
type: 'dependency',
resources: ['https://unpkg.com/react-dom@{{version}}/umd/react-dom.development.js'],
},
'react-is': {
version: '16.13.1',
library: 'ReactIs',
type: 'dependency',
resources: ['https://unpkg.com/react-is@{{version}}/umd/react-is.production.min.js'],
},
'styled-components': {
version: '5.3.5',
library: 'styled',
type: 'dependency',
resources: ['https://unpkg.com/styled-components@{{version}}/dist/styled-components.min.js'],
},
moment: {
version: '2.29.4',
library: 'moment',
type: 'dependency',
resources: ['https://unpkg.com/moment@{{version}}/moment.js'],
},
'@music163/tango-boot': {
description: '云音乐低代码运行时框架',
version: '0.3.5',
library: 'TangoBoot',
type: 'baseDependency',
resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'],
// resources: ['http://localhost:9001/boot.js'],
},
'@music163/antd': {
description: '云音乐低代码中后台应用基础物料',
version: '0.2.6',
library: 'TangoAntd',
type: 'baseDependency',
resources: [
'https://unpkg.com/@music163/antd@{{version}}/dist/index.js',
// 'http://localhost:9002/designer.js',
'https://unpkg.com/antd@4.24.13/dist/antd.css',
],
designerResources: [
'https://unpkg.com/@music163/antd@{{version}}/dist/designer.js',
'https://unpkg.com/antd@4.24.13/dist/antd.css',
],
},
},
};
const helperCode = `
export function registerComponentPrototype(proto) {
if (!proto) return;
if (!window.localTangoComponentPrototypes) {
window.localTangoComponentPrototypes = {};
}
if (proto.name) {
window.localTangoComponentPrototypes[proto.name] = proto;
}
}
`;
const routesCode = `
import Index from "./pages/list";
import Detail from "./pages/detail";
const routes = [
{
path: '/',
exact: true,
component: Index
},
{
path: '/detail',
exact: true,
component: Detail
},
];
export default routes;
`;
const storeIndexCode = `
export { default as app } from './app';
export { default as counter } from './counter';
`;
const entryCode = `
import { runApp } from '@music163/tango-boot';
import routes from './routes';
import './services';
import './stores';
import './index.less';
runApp({
boot: {
mountElement: document.querySelector('#root'),
qiankun: false,
},
router: {
type: 'browser',
config: routes,
},
});
`;
const storeCounter = `
import { defineStore } from '@music163/tango-boot';
const counter = defineStore({
// state
num: 0,
// action
increment: () => counter.num++,
decrement: () => {
counter.num--;
},
}, 'counter');
export default counter;
`;
const viewHomeCode = `
import React from "react";
import { definePage } from "@music163/tango-boot";
import {
Page,
Section,
Box,
Button,
Input,
FormilyForm,
FormilyFormItem,
Table,
} from "@music163/antd";
import { Space } from "@music163/antd";
import { LocalButton } from "../components";
import { OutButton } from "../components";
class App extends React.Component {
render() {
return (
<Page title={tango.stores.app.title} subTitle={<><Button>hello</Button></>}>
<Section tid="section0">
<Box></Box>
</Section>
<Section tid="section1" title="Section Title">
your input: <Input tid="input1" defaultValue="hello" />
copy input: <Input value={tango.page.input1?.value} />
<Table
columns={[
{ title: "姓名", dataIndex: "name", key: "name" },
{ title: "年龄", dataIndex: "age", key: "age" },
{ title: "住址", dataIndex: "address", key: "address" },
]}
tid="table1"
/>
</Section>
<Section tid="section2">
<Space tid="space1">
<LocalButton />
<OutButton />
<Button tid="button1">button</Button>
</Space>
</Section>
<Section title="区块标题" tid="section3">
<FormilyForm tid="formilyForm1">
<FormilyFormItem name="input1" component="Input" label="表单项" />
<FormilyFormItem name="select1" component="Select" label="表单项" />
</FormilyForm>
</Section>
<Section title="原生 DOM" tid="section4">
<h1 style={{ ...{ color: "red" }, fontSize: 64 }}>
hello world
</h1>
<div
style={{
border: "1px solid #ccc",
borderRadius: "5px",
backgroundColor: "#f4f4f4",
}}
>
<form onSubmit={tango.stores.app.submitForm} style={{
padding: "10px",
}}>
<div>
<label>Username:</label>
<input
type="text"
id="username"
name="username"
style={{ margin: "5px" }}
/>
</div>
<div>
<label>Password:</label>
<input
type="password"
id="password"
name="password"
style={{ margin: "5px" }}
/>
</div>
<div>
<label>Role:</label>
<select id="role" name="role" style={{ margin: "5px" }}>
<option value="admin">Admin</option>
<option value="user">User</option>
</select>
</div>
<div>
<button
type="submit"
style={{
marginTop: '5px',
padding: "3px 10px",
backgroundColor: "#007bff",
color: "#fff",
border: "none",
borderRadius: "5px",
}}
>
Submit
</button>
</div>
</form>
</div>
</Section>
</Page>
);
}
}
export default definePage(App);
`;
export const emptyPageCode = `
import React from "react";
import { definePage } from "@music163/tango-boot";
import {
Page,
Section,
} from "@music163/antd";
function App() {
return (<Page title="Detail Page">
<Section></Section>
</Page>)
}
export default definePage(App);
`;
const componentsButtonCode = `
import React from 'react';
import { registerComponentPrototype } from '../utils';
export default function MyButton(props) {
return <button {...props}>my button</button>
}
registerComponentPrototype({
name: 'LocalButton',
title: 'Local Button',
exportType: 'namedExport',
package: '/src/components',
props: [
{ name: 'background', title: '背景色', setter: 'colorSetter' },
],
});
`;
const outButtonCode = `
import React from 'react';
export default function OutButton(props) {
return <button {...props}>Out button (from other file)</button>
}
`;
const componentsInputCode = `
import React from 'react';
import { registerComponentPrototype } from '../utils';
export default function MyInput(props) {
return <input {...props} />;
}
registerComponentPrototype({
name: 'LocalInput',
title: 'Local Input',
exportType: 'namedExport',
package: '/src/components',
props: [
{ name: 'color', title: '文本色', setter: 'colorSetter' },
],
});
`;
const componentsEntryCode = `
export { default as LocalButton } from './button';
export { default as LocalInput } from './input';
export { default as OutButton } from './out-button';
`;
const storeApp = `
import { defineStore } from '@music163/tango-boot';
export default defineStore({
title: 'Page Title',
array: [1, 2, 3],
test: function() {
console.log('test');
},
submitForm: function(event) {
event.preventDefault();
const formData = new FormData(event.target);
const username = formData.get("username");
const password = formData.get("password");
const role = formData.get("role");
console.log("Submitted Data:", { username, password, role });
}
}, 'app');
`;
const serviceCode = `
import { defineServices } from '@music163/tango-boot';
import './sub';
export default defineServices({
longLongLongLongLongLongLongLongGet: {
url: 'https://nei.hz.netease.com/api/apimock-v2/cc974ffbaa7a85c77f30e4ce67deb67f/api/getUserProfile',
formatter: res => res.data,
headers: {
'Content-Type': 'application/json',
}
},
get: {
url: 'https://nei.hz.netease.com/api/apimock-v2/cc974ffbaa7a85c77f30e4ce67deb67f/api/getUserProfile',
formatter: res => res.data,
headers: {
'Content-Type': 'application/json',
}
},
list: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/anchor-list-normal',
},
add: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
method: 'post',
description: '新增用户'
},
update: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
method: 'post',
description: '更新用户'
},
delete: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users?id=1',
description: '删除用户'
},
});
`;
const subServiceCode = `
import { defineServices } from '@music163/tango-boot';
export default defineServices({
list: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/anchor-list-normal',
},
}, {
namespace: 'sub',
});
`;
const lessCode = `
body {
font-size: 12px;
}
`;
const cssCode = `
* {
margin: 0;
padding: 0;
}
p {
color: red;
}
`;
export const sampleFiles = [
{ filename: '/package.json', code: JSON.stringify(packageJson) },
{ filename: '/tango.config.json', code: JSON.stringify(tangoConfigJson) },
{ filename: '/README.md', code: '# readme' },
{ filename: '/src/index.less', code: lessCode },
{ filename: '/src/style.css', code: cssCode },
{ filename: '/src/index.js', code: entryCode },
{ filename: '/src/pages/list.js', code: viewHomeCode },
{ filename: '/src/pages/detail.js', code: emptyPageCode },
{ filename: '/src/components/button.js', code: componentsButtonCode },
{ filename: '/src/components/out-button.js', code: outButtonCode },
{ filename: '/src/components/input.js', code: componentsInputCode },
{ filename: '/src/components/index.js', code: componentsEntryCode },
{ filename: '/src/routes.js', code: routesCode },
{ filename: '/src/stores/index.js', code: storeIndexCode },
{ filename: '/src/stores/app.js', code: storeApp },
{ filename: '/src/stores/counter.js', code: storeCounter },
{ filename: '/src/services/index.js', code: serviceCode },
{ filename: '/src/services/sub.js', code: subServiceCode },
{ filename: '/src/utils/index.js', code: helperCode },
];
export const genDefaultPage = (index: number) => ({
name: 'new-page',
code: `
import React from 'react';
import tango, { definePage } from '@music163/tango-boot';
import { Page, Section } from '@music163/antd';
function App() {
return (
<Page title="空白模板${index}">
<Section></Section>
</Page>
)
}
export default definePage(App);
`,
});
+305
View File
@@ -0,0 +1,305 @@
import * as basePrototypes from '@music163/antd/prototypes';
import type { IComponentPrototype, Dict } from '@music163/tango-helpers';
const sampleBlockCode = `
<Section>
<Result
status="success"
title="Successfully Purchased Cloud Server ECS!"
subTitle="Order number: 2017182818828182881 Cloud server configuration takes 1-5 minutes, please wait."
extra={[
<Button type="primary" key="console">
Go Console
</Button>,
<Button key="buy">Buy Again</Button>,
]}
/>
</Section>
`;
const SnippetSuccessResult: IComponentPrototype = {
name: 'SnippetSuccessResult',
title: '成功结果',
icon: 'icon-tupian',
type: 'snippet',
package: '@music163/antd',
initChildren: sampleBlockCode,
relatedImports: ['Section', 'Result', 'Button'],
};
const Snippet2ColumnLayout: IComponentPrototype = {
name: 'Snippet2ColumnLayout',
title: '两列布局',
icon: 'icon-columns',
type: 'snippet',
package: '@music163/antd',
initChildren: `
<Columns columns={12}>
<Column colSpan={6}></Column>
<Column colSpan={6}></Column>
</Columns>
`,
relatedImports: ['Columns', 'Column'],
};
const Snippet3ColumnLayout: IComponentPrototype = {
name: 'Snippet3ColumnLayout',
title: '三列布局',
icon: 'icon-column-3',
type: 'snippet',
package: '@music163/antd',
initChildren: `
<Columns columns={12}>
<Column colSpan={4}>
</Column>
<Column colSpan={4}>
</Column>
<Column colSpan={4}>
</Column>
</Columns>
`,
relatedImports: ['Columns', 'Column'],
};
const SnippetButtonGroup: IComponentPrototype = {
name: 'SnippetButtonGroup',
title: '按钮组',
icon: 'icon-anniuzu',
type: 'snippet',
package: '@music163/antd',
initChildren: `
<Space>
<Button type="primary">按钮1</Button>
<Button>按钮2</Button>
</Space>
`,
relatedImports: ['Space', 'Button'],
};
// hack some prototypes
basePrototypes['Section'].siblingNames = [
'SnippetButtonGroup',
'Snippet2ColumnLayout',
'Snippet3ColumnLayout',
'SnippetSuccessResult',
];
export const nativeDomPrototypes = () => {
const doms = [
'div',
'span',
'h1',
'h2',
'p',
'a',
'img',
'ul',
'ol',
'li',
'input',
'button',
'form',
'table',
'tr',
'td',
'header',
'footer',
'nav',
'section',
'article',
'aside',
'main',
'video',
'audio',
'label',
'select',
'option',
'textarea',
'iframe',
];
const componentPrototypes: Dict<IComponentPrototype> = doms.reduce(
(acc: Dict<IComponentPrototype>, tag) => {
acc[tag] = {
name: tag,
title: tag,
hasChildren: true,
package: '',
type: 'element',
props: [
{
name: 'style',
title: '样式',
group: 'style',
setter: 'expressionSetter',
setterProps: {
expressionType: 'cssObject',
},
},
{
name: 'className',
title: '类名',
setter: 'classNameSetter',
},
{
name: 'id',
title: 'ID',
setter: 'textSetter',
},
{
name: 'onClick',
title: '点击事件',
setter: 'actionSetter',
group: 'event',
},
],
};
return acc;
},
{},
);
return componentPrototypes;
};
// iconfont: https://www.iconfont.cn/manage/index?manage_type=myprojects&projectId=2891794
const prototypes: Dict<IComponentPrototype> = {
...nativeDomPrototypes(),
...(basePrototypes as any),
SnippetSuccessResult,
Snippet2ColumnLayout,
Snippet3ColumnLayout,
SnippetButtonGroup,
Section: {
...basePrototypes['Section'],
props: [
// ...(basePrototypes['Section'].props as any),
{
name: 'title',
title: '标题',
setter: 'textSetter',
},
{
name: 'extra',
title: '额外内容',
setter: 'renderSetter',
options: [
{
label: '按钮组',
value: 'ButtonGroup',
renderBody:
'<ButtonGroup><Button>button1</Button><Button>button2</Button></ButtonGroup>',
relatedImports: ['ButtonGroup', 'Button'],
},
],
template: '(v) => {\n return {{content}};\n}',
},
{
name: 'className',
title: '类名',
setter: 'classNameSetter',
group: 'style',
},
],
},
Box: {
name: 'Box',
title: '盒子',
icon: 'icon-mianban',
type: 'container',
package: '@music163/antd',
hasChildren: true,
siblingNames: ['Box'],
props: [
{
name: 'aaa',
title: 'aaa',
setter: 'textSetter',
},
{
name: 'bbb',
title: 'bbb',
setter: 'textSetter',
deprecated: true,
},
{
name: 'ccc',
title: 'ccc',
setter: 'textSetter',
deprecated: true,
},
{
name: 'd',
title: 'd',
setter: 'textSetter',
},
{
name: 'onClick',
title: '点击事件',
setter: 'eventSetter',
template: '(e) => {\n {{content}}\n}',
tip: '回调参数说明:e 为事件对象',
},
{
name: 'renderFoo',
title: 'foo自定义渲染',
setter: 'renderSetter',
options: [
{
label: '占位空间',
value: 'Box',
render: '() => <Box>test</Box>',
relatedImports: ['Box'],
},
{
label: '占位文本',
value: 'Text',
render: '() => <Text>text</Text>',
relatedImports: ['Text'],
},
],
},
{
name: 'className',
title: '类名',
setter: 'classNameSetter',
group: 'style',
},
],
},
Columns: {
name: 'Columns',
type: 'container',
icon: 'icon-column-4',
package: '@music163/antd',
hasChildren: true,
childrenNames: ['Column'],
},
Column: {
name: 'Column',
type: 'container',
icon: 'icon-juxing',
package: '@music163/antd',
hasChildren: true,
siblingNames: ['Column'],
},
Text: {
name: 'Text',
type: 'element',
icon: 'icon-wenzi',
package: '@music163/antd',
initChildren: '文本内容',
},
Placeholder: {
name: 'Placeholder',
type: 'element',
package: '@music163/antd',
},
ButtonGroup: {
name: 'ButtonGroup',
type: 'element',
package: '@music163/antd',
hasChildren: true,
childrenNames: ['Button'],
},
};
export default prototypes;
+6
View File
@@ -0,0 +1,6 @@
@import '~antd/es/style/themes/default.less';
@import '~antd/dist/antd.less'; // 引入官方提供的 less 样式入口文件
@primary-color: #2f54eb;
@border-radius-base: 2px;
+7
View File
@@ -0,0 +1,7 @@
import React from 'react';
import { Outlet } from 'umi';
import './index.less';
export default function Layout() {
return <Outlet />;
}
+9
View File
@@ -0,0 +1,9 @@
const DocsPage = () => {
return (
<div>
<p>This is umi docs.</p>
</div>
);
};
export default DocsPage;
+219
View File
@@ -0,0 +1,219 @@
import { Box } from 'coral-system';
import { Button, Form, Input, Modal, Space } from 'antd';
import {
Designer,
DesignerPanel,
SettingPanel,
Sidebar,
Toolbar,
WorkspacePanel,
WorkspaceView,
CodeEditor,
Sandbox,
DndQuery,
themeLight,
} from '@music163/tango-designer';
import { createEngine, Workspace } from '@music163/tango-core';
import prototypes from '../helpers/prototypes';
import { Logo, ProjectDetail, bootHelperVariables, emptyPageCode, sampleFiles } from '../helpers';
import {
ApiOutlined,
AppstoreAddOutlined,
BuildOutlined,
FunctionOutlined,
PlusOutlined,
createFromIconfontCN,
} from '@ant-design/icons';
import { Action, PackageOutlined } from '@music163/tango-ui';
import { useState } from 'react';
const menuData = {
common: [
{
title: '常用',
items: [
'Button',
'Section',
'Columns',
'Column',
'Box',
'Text',
'Space',
'Typography',
'Title',
'Paragraph',
'Table',
'Each',
],
},
{
title: '输入',
items: ['Input', 'InputNumber', 'Select'],
},
{
title: 'Formily表单',
items: ['FormilyForm', 'FormilyFormItem', 'FormilySubmit', 'FormilyReset'],
},
{
title: '数据展示',
items: ['Comment'],
},
],
};
// 1. 实例化工作区
const workspace = new Workspace({
entry: '/src/index.js',
files: sampleFiles,
prototypes,
});
// inject workspace to window for debug
(window as any).__workspace__ = workspace;
// 2. 引擎初始化
const engine = createEngine({
workspace,
menuData,
defaultActiveView: 'design', // dual code design
});
// @ts-ignore
window.__workspace__ = workspace;
// 3. 沙箱初始化
const sandboxQuery = new DndQuery({
context: 'iframe',
});
// 4. 图标库初始化(物料面板和组件树使用了 iconfont 里的图标)
createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/c/font_2891794_151xsllxqd7.js',
});
/**
* 5. 平台初始化,访问 https://local.netease.com:6006/
*/
export default function App() {
const [showNewPageModal, setShowNewPageModal] = useState(false);
const [form] = Form.useForm();
return (
<Designer
theme={themeLight}
engine={engine}
config={{
customActionVariables: bootHelperVariables,
customExpressionVariables: bootHelperVariables,
}}
sandboxQuery={sandboxQuery}
>
<DesignerPanel
logo={<Logo />}
description={<ProjectDetail />}
actions={
<Box px="l">
<Toolbar>
<Toolbar.Item key="routeSwitch" placement="left" activeViews={['design']} />
<Toolbar.Item key="addPage" placement="left" activeViews={['design']}>
<Action
tooltip="添加页面"
shape="outline"
icon={<PlusOutlined />}
onClick={() => setShowNewPageModal(true)}
/>
</Toolbar.Item>
<Toolbar.Item key="history" placement="left" activeViews={['design']} />
<Toolbar.Item key="preview" placement="left" activeViews={['design']} />
<Toolbar.Item key="togglePanel" placement="right" activeViews={['design']} />
<Toolbar.Item key="modeSwitch" placement="right" />
<Toolbar.Separator />
<Toolbar.Item placement="right">
<Space>
<Button type="primary"></Button>
</Space>
</Toolbar.Item>
</Toolbar>
<Modal
title="添加新页面"
open={showNewPageModal}
onCancel={() => setShowNewPageModal(false)}
footer={null}
>
<Form
form={form}
onFinish={(values) => {
workspace.addViewFile(values.name, emptyPageCode);
setShowNewPageModal(false);
}}
layout="vertical"
>
<Form.Item label="文件名" name="name" required rules={[{ required: true }]}>
<Input placeholder="请输入文件名" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
</Button>
</Form.Item>
</Form>
</Modal>
</Box>
}
>
<Sidebar>
<Sidebar.Item
key="components"
label="组件"
icon={<AppstoreAddOutlined />}
widgetProps={{
menuData,
}}
/>
<Sidebar.Item key="outline" label="结构" icon={<BuildOutlined />} />
<Sidebar.Item
key="variables"
label="变量"
icon={<FunctionOutlined />}
isFloat
width={800}
/>
<Sidebar.Item key="dataSource" label="接口" icon={<ApiOutlined />} isFloat width={800} />
<Sidebar.Item
key="dependency"
label="依赖"
icon={<PackageOutlined />}
isFloat
width={800}
/>
</Sidebar>
<WorkspacePanel>
<WorkspaceView mode="code">
<CodeEditor />
</WorkspaceView>
<WorkspaceView mode="design">
<Sandbox
onMessage={(e) => {
if (e.type === 'done') {
const sandboxWindow: any = sandboxQuery.window;
// if (sandboxWindow.TangoAntd) {
// if (sandboxWindow.TangoAntd.menuData) {
// setMenuData(sandboxWindow.TangoAntd.menuData);
// }
// if (sandboxWindow.TangoAntd.prototypes) {
// workspace.setComponentPrototypes(sandboxWindow.TangoAntd.prototypes);
// }
// }
if (sandboxWindow.localTangoComponentPrototypes) {
workspace.setComponentPrototypes(sandboxWindow.localTangoComponentPrototypes);
}
}
}}
navigatorExtra={<Button size="small">hello world</Button>}
/>
</WorkspaceView>
</WorkspacePanel>
<SettingPanel />
</DesignerPanel>
</Designer>
);
}
+135
View File
@@ -0,0 +1,135 @@
import React, { useState } from 'react';
import { Box } from 'coral-system';
import { Button, Space } from 'antd';
import {
Designer,
DesignerPanel,
SettingPanel,
Sidebar,
Toolbar,
WorkspacePanel,
WorkspaceView,
CodeEditor,
Sandbox,
DndQuery,
themeLight,
} from '@music163/tango-designer';
import { createEngine, Workspace } from '@music163/tango-core';
import { Logo, ProjectDetail, bootHelperVariables } from '@/helpers';
import {
AppstoreAddOutlined,
BuildOutlined,
ClusterOutlined,
createFromIconfontCN,
} from '@ant-design/icons';
import { mailFiles } from '@/helpers/mail-files';
// 1. 实例化工作区
const workspace = new Workspace({
entry: '/src/index.js',
files: mailFiles,
});
// 2. 引擎初始化
const engine = createEngine({
workspace,
});
// @ts-ignore
window.__mailWorkspace__ = workspace;
// 3. 沙箱初始化
const sandboxQuery = new DndQuery({
context: 'iframe',
});
// 4. 图标库初始化(物料面板和组件树使用了 iconfont 里的图标)
createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/c/font_2891794_151xsllxqd7.js',
});
/**
* 5. 平台初始化,访问 https://local.netease.com:6006/
*/
export default function App() {
const [menuLoading, setMenuLoading] = useState(true);
const [menuData, setMenuData] = useState(false);
return (
<Designer
theme={themeLight}
engine={engine}
config={{
customActionVariables: bootHelperVariables,
customExpressionVariables: bootHelperVariables,
}}
sandboxQuery={sandboxQuery}
>
<DesignerPanel
logo={<Logo />}
description={<ProjectDetail />}
actions={
<Box px="l">
<Toolbar>
<Toolbar.Item key="history" placement="left" />
<Toolbar.Item key="preview" placement="left" />
<Toolbar.Item key="modeSwitch" placement="right" />
<Toolbar.Item key="togglePanel" placement="right" />
<Toolbar.Separator />
<Toolbar.Item placement="right">
<Space>
<Button type="primary"></Button>
</Space>
</Toolbar.Item>
</Toolbar>
</Box>
}
>
<Sidebar>
<Sidebar.Item
key="components"
label="组件"
icon={<AppstoreAddOutlined />}
widgetProps={{
menuData: menuData as any,
loading: menuLoading,
}}
/>
<Sidebar.Item key="outline" label="结构" icon={<BuildOutlined />} />
<Sidebar.Item
key="dependency"
label="依赖"
icon={<ClusterOutlined />}
isFloat
width={800}
/>
</Sidebar>
<WorkspacePanel>
<WorkspaceView mode="design">
<Sandbox
onMessage={(e) => {
if (e.type === 'done') {
const sandboxWindow: any = sandboxQuery.window;
if (sandboxWindow.TangoMail) {
if (sandboxWindow.TangoMail.menuData) {
setMenuData(sandboxWindow.TangoMail.menuData);
engine.designer.setMenuData(sandboxWindow.TangoMail.menuData);
}
if (sandboxWindow.TangoMail.prototypes) {
workspace.setComponentPrototypes(sandboxWindow.TangoMail.prototypes);
}
}
setMenuLoading(false);
}
}}
navigatorExtra={<Button size="small">hello world</Button>}
/>
</WorkspaceView>
<WorkspaceView mode="code">
<CodeEditor />
</WorkspaceView>
</WorkspacePanel>
<SettingPanel />
</DesignerPanel>
</Designer>
);
}
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "./src/.umi/tsconfig.json"
}
+1
View File
@@ -0,0 +1 @@
import 'umi/typings';
+37
View File
@@ -0,0 +1,37 @@
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
module.exports = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
babel: async (config) => {
config.plugins.push('babel-plugin-styled-components');
return config;
},
typescript: {
reactDocgen: false,
},
webpack: async (config) => {
if (config.mode === 'production') {
config.devtool = false;
}
if (config.resolve.plugins === null) {
config.resolve.plugins = [];
}
config.resolve.plugins.push(new TsconfigPathsPlugin());
// @see https://github.com/graphql/graphql-js/issues/1272#issuecomment-393903706
config.module.rules.push({
test: /\.mjs$/,
include: /node_modules/,
type: 'javascript/auto',
});
return config;
},
};
@@ -0,0 +1 @@
<!--insert head here-->
+23
View File
@@ -0,0 +1,23 @@
import React from 'react';
import { SystemProvider } from 'coral-system';
import 'antd/dist/antd.css';
export const parameters = {
actions: { argTypesRegex: '^on.*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};
const withSystemProvider = (Story, context) => {
return (
<SystemProvider prefix="--tango">
<Story {...context} />
</SystemProvider>
);
};
export const decorators = [withSystemProvider];
+3
View File
@@ -0,0 +1,3 @@
# `docs`
开发调试用的文档
+32
View File
@@ -0,0 +1,32 @@
{
"name": "storybook",
"version": "0.0.0",
"private": "true",
"description": "> tango-apps docs",
"license": "MIT",
"author": "wwsun <sunweiwei01@corp.netease.com>",
"files": [
"lib"
],
"scripts": {
"build": "echo \"skip\"",
"build-storybook": "build-storybook",
"storybook": "start-storybook -p 6008"
},
"dependencies": {
"@music163/tango-setting-form": "*",
"@music163/tango-ui": "*",
"mobx": "6.13.2",
"mobx-react-lite": "4.0.7"
},
"devDependencies": {
"@ant-design/icons": "^4.8.0",
"@storybook/addon-actions": "^6.5.14",
"@storybook/addon-essentials": "^6.5.14",
"@storybook/addon-links": "^6.5.14",
"@storybook/react": "^6.5.14",
"antd": "^4.24.2",
"babel-plugin-styled-components": "^2.0.7",
"tsconfig-paths-webpack-plugin": "^3.5.2"
}
}
+45
View File
@@ -0,0 +1,45 @@
import React from 'react';
import { Box } from 'coral-system';
import { SingleMonacoEditor } from '@music163/tango-ui';
export default {
title: 'Editor',
};
const code = `
import React from 'react';
import { definePage } from '@music163/tango-boot';
import { Layout, Page, Section, Button } from '@music163/antd';
function About(props) {
const { stores } = props;
const increment = () => {
stores.counter.increment();
};
return (
<Layout>
<Page title="About Page" height="100vh">
<Section>
<h1>Counter: {stores.counter.num}</h1>
<Button type="primary" onClick={increment}>
+1
</Button>
<p>原生html元素不可拖拽</p>
</Section>
</Page>
</Layout>
);
}
export default definePage(About);
`;
export function Basic() {
return (
<Box border="solid" borderColor="line.normal" height="400px">
<SingleMonacoEditor defaultValue={code} height="100%" onBlur={console.log} />
</Box>
);
}
+70
View File
@@ -0,0 +1,70 @@
import React from 'react';
import { Box } from 'coral-system';
import { CodeSandbox } from '@music163/tango-sandbox';
import { JsonView } from '@music163/tango-ui';
export default {
title: 'CodeSandbox',
};
const entryFilename = '/index.js';
const packageJson = {
name: 'demo',
private: true,
main: entryFilename,
dependencies: {
react: '^17.0.1',
'react-dom': '^17.0.1',
},
};
const entryFile = `
import React from 'react';
import ReactDOM from "react-dom";
import App from './src';
const rootElement = document.getElementById("root");
ReactDOM.render(
<App />,
rootElement
);
`;
const srcAppFile = `
import React from 'react';
export default function App() {
return <div style={{ background: 'yellow', color: 'red' }}>hello world</div>;
}
`;
const files = {
'/package.json': {
code: JSON.stringify(packageJson),
},
[entryFilename]: {
code: entryFile,
},
'/src/index.js': { code: srcAppFile },
};
export function Basic() {
return (
<Box display="flex" height="100vh">
<CodeSandbox
template="create-react-app"
bundlerURL="https://codesandbox.fn.netease.com"
files={files}
entry={entryFilename}
style={{
flex: 1,
}}
/>
<Box width="30%">
<JsonView src={files} />
</Box>
</Box>
);
}
+560
View File
@@ -0,0 +1,560 @@
import React from 'react';
import { FormModel, SettingForm, register } from '@music163/tango-setting-form';
import { IComponentPrototype } from '@music163/tango-helpers';
import { BUILT_IN_SETTERS } from '@music163/tango-designer/src/setters';
import { Box } from 'coral-system';
import { JsonView } from '@music163/tango-ui';
import { toJS } from 'mobx';
import { observer } from 'mobx-react-lite';
import { Card } from 'antd';
import { createFromIconfontCN } from '@ant-design/icons';
const BLACK_LIST = ['codeSetter', 'eventSetter', 'modelSetter', 'routerSetter'];
BUILT_IN_SETTERS.filter((setter) => !BLACK_LIST.includes(setter.name)).forEach(register);
createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/c/font_2891794_151xsllxqd7.js',
});
export default {
title: 'SettingForm',
};
/**
* 表单值预览
*/
const FormValuePreview = observer(({ model }: { model: FormModel }) => {
const data = toJS(model.values);
return <JsonView src={data} />;
});
interface SettingFormDemoProps {
initValues?: object;
prototype?: IComponentPrototype;
}
function SettingFormDemo({ initValues, prototype }: SettingFormDemoProps) {
const model = new FormModel(initValues, { onChange: console.log });
return (
<Box display="flex">
<Box flex="0 0 320px" overflow="hidden">
<SettingForm
model={model}
prototype={prototype}
showIdentifier={{
identifierKey: 'tid',
getIdentifier: () => `time${Date.now()}`,
}}
/>
</Box>
<Box position="relative">
<Card title="表单状态预览" style={{ position: 'sticky', top: 0 }}>
<FormValuePreview model={model} />
</Card>
</Box>
</Box>
);
}
const prototypeHasBasicProps: IComponentPrototype = {
name: 'Sample',
title: '演示组件',
package: 'sample-pkg',
type: 'element',
docs: 'https://4x-ant-design.antgroup.com/components/slider-cn',
props: [
{
name: 'code',
title: 'codeSetter',
setter: 'codeSetter',
},
{
name: 'style',
title: 'codeSetter(cssObject)',
setter: 'codeSetter',
setterProps: {
expressionType: 'cssObject',
},
},
{
name: 'text',
title: 'textSetter',
setter: 'textSetter',
},
{
name: 'text2',
title: 'textAreaSetter',
setter: 'textAreaSetter',
},
{
name: 'src',
title: 'imageSetter',
setter: 'imageSetter',
},
{
name: 'number',
title: 'numberSetter',
setter: 'numberSetter',
},
{
name: 'number2',
title: 'sliderSetter',
setter: 'sliderSetter',
},
{
name: 'bool',
title: 'boolSetter',
setter: 'boolSetter',
},
{
name: 'enum',
title: 'enumSetter',
setter: 'enumSetter',
},
{
name: 'list',
title: 'listSetter',
setter: 'listSetter',
},
],
};
export function Basic() {
return (
<SettingFormDemo
initValues={{
bool: true,
enum: {
aaa: 'aaa',
bbb: 'bbb',
ccc: 'ccc',
},
list: [{ key: 1 }, { key: 2 }],
}}
prototype={prototypeHasBasicProps}
/>
);
}
export function DeprecatedProp() {
return (
<SettingFormDemo
prototype={{
name: 'Deprecated',
package: 'sample-pkg',
type: 'element',
props: [
{
name: 'number',
title: 'numberSetter',
setter: 'numberSetter',
tip: '这是一个文本属性',
docs: 'https://4x-ant-design.antgroup.com/components/slider-cn',
deprecated: '使用 text2 替代',
},
{
name: 'number1',
title: 'sliderSetter',
setter: 'sliderSetter',
},
],
}}
/>
);
}
export function Validate() {
return (
<SettingFormDemo
prototype={{
name: 'Validate',
package: 'sample-pkg',
type: 'element',
props: [
{
name: 'number',
title: 'numberSetter',
setter: 'numberSetter',
validate: (value) => {
if (!value && value !== 0) {
return '必填';
}
if (value < 0) {
return '必须大于 0';
}
if (value > 10) {
return '必须小于等于 10';
}
},
},
],
}}
/>
);
}
export function ObjectSetter() {
return (
<SettingFormDemo
prototype={{
name: 'Object',
package: 'sample-pkg',
type: 'element',
props: [
{
name: 'text',
title: 'textSetter',
setter: 'textSetter',
},
{
name: 'object',
title: 'objectSetter',
setter: 'objectSetter',
props: [
{
name: 'text',
title: 'textSetter',
setter: 'textSetter',
},
{
name: 'number',
title: 'numberSetter',
setter: 'numberSetter',
},
],
},
],
}}
/>
);
}
export function InitValues() {
return (
<SettingFormDemo
initValues={{
bool: true,
bool1: '{{true}}',
style: {
background: 'red',
},
object: {
text: 'text',
number: 10,
},
object1: {
text: 'text',
number: '{{tango.stores.user?.age}}',
},
// 只会有一种情况传下来的是字符串,就是用户代码里存在 rest operator,这时候不需要额外处理,提示用户就使用代码模式
object2: '{{{ text: "text22", number: 22, ...{ extra: "some" } }}}',
list: [{ key: 'aaa' }, { key: 'bbb' }], // list object
list1: "{{[{ key: 'aaa' }, { key: 'bbb' }]}}", // raw code
}}
prototype={{
name: 'InitValues',
package: 'sample-pkg',
type: 'element',
props: [
{
name: 'bool',
title: 'value初始化',
setter: 'boolSetter',
},
{
name: 'bool1',
title: 'value初始化',
setter: 'boolSetter',
},
{
name: 'bool2',
title: '无初值',
setter: 'boolSetter',
},
{
name: 'style',
title: 'codeSetter',
setter: 'codeSetter',
setterProps: {
expressionType: 'cssObject',
},
},
{
name: 'object',
props: [
{
name: 'text',
title: 'text',
setter: 'textSetter',
},
{
name: 'number',
title: 'number',
setter: 'numberSetter',
},
],
},
{
name: 'object1',
props: [
{
name: 'text',
title: 'text',
setter: 'textSetter',
},
{
name: 'number',
title: 'number',
setter: 'numberSetter',
},
],
},
{
name: 'object2',
props: [
{
name: 'text',
title: 'text',
setter: 'textSetter',
},
{
name: 'number',
title: 'number',
setter: 'numberSetter',
},
],
},
{
name: 'list',
title: 'listSetter',
setter: 'listSetter',
},
{
name: 'list1',
title: 'listSetter',
setter: 'listSetter',
},
],
}}
/>
);
}
export function Lite() {
return (
<Box width={320} border="solid" borderColor="line2">
<SettingForm
showSearch={false}
showGroups={false}
showItemSubtitle={false}
prototype={prototypeHasBasicProps}
disableSwitchExpressionSetter
/>
</Box>
);
}
export function HideToggleCode() {
const model = new FormModel({});
return (
<Box width={320} border="solid" borderColor="line2">
<SettingForm model={model} prototype={prototypeHasBasicProps} disableSwitchExpressionSetter />
</Box>
);
}
const prototypeHasExtraProps: IComponentPrototype = {
name: 'ExtraProps',
type: 'element',
package: '@music163/antd',
props: [
{
name: 'choice',
title: 'choiceSetter',
setter: 'choiceSetter',
options: [
{ label: '选项1', value: '1' },
{ label: '选项2', value: '2' },
{ label: '选项3', value: '3' },
],
},
{
name: 'picker',
title: 'pickerSetter',
setter: 'pickerSetter',
options: [
{ label: '选项1', value: '1' },
{ label: '选项2', value: '2' },
{ label: '选项3', value: '3' },
],
},
{
name: 'actionList',
title: 'actionListSetter',
setter: 'actionListSetter',
},
{
name: 'list',
title: 'listSetter',
setter: 'listSetter',
},
{
name: 'options',
title: 'optionSetter',
setter: 'optionSetter',
},
{
name: 'columns',
title: 'tableColumnsSetter',
setter: 'tableColumnsSetter',
},
{
name: 'css',
title: 'cssSetter',
setter: 'cssSetter',
},
{
name: 'date',
title: 'dateSetter',
setter: 'dateSetter',
},
{
name: 'dateRange',
title: 'dateRangeSetter',
setter: 'dateRangeSetter',
},
{
name: 'time',
title: 'timeSetter',
setter: 'timeSetter',
},
{
name: 'time',
title: 'timeRangeSetter',
setter: 'timeRangeSetter',
},
{
name: 'enum',
title: 'enumSetter',
setter: 'enumSetter',
},
{
name: 'event',
title: 'eventSetter',
setter: 'eventSetter',
},
{
name: 'json',
title: 'jsonSetter',
setter: 'jsonSetter',
},
{
name: 'jsx',
title: 'jsxSetter',
setter: 'jsxSetter',
},
{
name: 'render',
title: 'renderPropsSetter',
setter: 'renderPropsSetter',
},
{
name: 'cell',
title: 'tableCellSetter',
setter: 'tableCellSetter',
},
{
name: 'expandable',
title: 'tableExpandableSetter',
setter: 'tableExpandableSetter',
},
{
name: 'router',
title: 'routerSetter',
setter: 'routerSetter',
},
],
};
export function ExtraSetters() {
return (
<SettingFormDemo
initValues={{
router: 'www.163.com',
expression: `{ foo: 'foo' }`,
object: {
name: 'Alice',
},
image:
'https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13270238619/2cc5/0782/1d6e/009b96bf90c557b9bbde09b1687a2c80.png',
}}
prototype={prototypeHasExtraProps}
/>
);
}
export function StyleProps() {
return (
<SettingFormDemo
prototype={{
name: 'Box',
title: 'Box',
type: 'element',
package: '@music163/antd',
props: [
{
name: 'style',
title: 'styleSetter',
setter: 'styleSetter',
},
{
name: 'display',
title: 'displaySetter',
setter: 'displaySetter',
},
{
name: 'flexDirection',
title: 'flexDirectionSetter',
setter: 'flexDirectionSetter',
},
{
name: 'flexGap',
title: 'flexGapSetter',
setter: 'flexGapSetter',
},
{
name: 'flexJustifyContent',
title: 'flexJustifyContentSetter',
setter: 'flexJustifyContentSetter',
},
{
name: 'flexAlignItems',
title: 'flexAlignItemsSetter',
setter: 'flexAlignItemsSetter',
},
{
name: 'spacing',
title: 'spacingSetter',
setter: 'spacingSetter',
},
{
name: 'color',
title: 'colorSetter',
setter: 'colorSetter',
},
{
name: 'bg',
title: 'bgSetter',
setter: 'bgSetter',
},
{
name: 'border',
title: 'borderSetter',
setter: 'borderSetter',
},
],
}}
/>
);
}
@@ -0,0 +1,29 @@
import React from 'react';
import { ActionSelect } from '@music163/tango-ui';
export default {
title: 'UI/ActionSelect',
component: ActionSelect,
};
const options = [
{ label: 'action1', value: 'action1' },
{ label: 'action2', value: 'action2' },
{ label: 'action3', value: 'action3' },
];
export const Basic = {
args: {
defaultText: '选择动作',
options,
onSelect: console.log,
},
};
export const showInput = {
args: {
text: '选择动作',
options,
showInput: true,
},
};
+68
View File
@@ -0,0 +1,68 @@
import React from 'react';
import { Action } from '@music163/tango-ui';
import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import { Space } from 'antd';
export default {
title: 'UI/Action',
component: Action,
};
export const Basic = {
args: {
icon: <PlusOutlined />,
tooltip: '添加服务函数',
},
};
export const Link = {
args: {
icon: <QuestionCircleOutlined />,
tooltip: '查看帮助文档',
href: 'https://music.163.com',
},
};
export const Outline = {
args: {
icon: <PlusOutlined />,
tooltip: '添加服务函数',
shape: 'outline',
},
};
export const Small = {
args: {
size: 'small',
icon: <PlusOutlined />,
tooltip: '添加服务函数',
},
};
export const Disabled = () => {
return (
<Space>
<Action icon={<PlusOutlined />} tooltip="添加服务函数" disabled />
<Action icon={<PlusOutlined />} tooltip="添加服务函数" disabled shape="outline" />
<Action
icon={<QuestionCircleOutlined />}
tooltip="查看帮助文档"
href="https://music.163.com"
disabled
/>
</Space>
);
};
export const List = () => {
return (
<Space>
<Action icon={<PlusOutlined />} tooltip="添加服务函数" />
<Action
icon={<QuestionCircleOutlined />}
tooltip="查看帮助文档"
href="https://music.163.com"
/>
</Space>
);
};
+10
View File
@@ -0,0 +1,10 @@
import React from 'react';
import { ChatInput } from '@music163/tango-ui';
export default {
title: 'UI/ChatGPT',
};
export function Basic() {
return <ChatInput />;
}
@@ -0,0 +1,15 @@
import React, { useState } from 'react';
import { ClassNameInput } from '@music163/tango-ui';
export default {
title: 'UI/ClassNameInput',
};
export function Basic() {
return <ClassNameInput defaultValue="hello world" onChange={console.log} />;
}
export function Controlled() {
const [value, setValue] = useState('');
return <ClassNameInput value={value} onChange={setValue} />;
}
+14
View File
@@ -0,0 +1,14 @@
import React from 'react';
import { CopyClipboard } from '@music163/tango-ui';
export default {
title: 'UI/Copy',
};
export function Basic() {
return (
<CopyClipboard text="hello">
{(copied) => <button>{copied ? 'copied' : 'copy'}</button>}
</CopyClipboard>
);
}
@@ -0,0 +1,82 @@
import React from 'react';
import { DragPanel } from '@music163/tango-ui';
import { Box, Text } from 'coral-system';
import { Button } from 'antd';
export default {
title: 'UI/DragPanel',
};
export function Basic() {
return (
<>
<DragPanel
title="弹层标题"
extra="右上角内容"
width={350}
body={<Box p="m"></Box>}
footer={<Text></Text>}
>
<Button></Button>
</DragPanel>
<DragPanel
title="弹层标题"
extra="右上角内容"
width={350}
body={<Box p="m"></Box>}
footer={<Text></Text>}
>
<Button
style={{
position: 'absolute',
bottom: 20,
right: 20,
}}
>
</Button>
</DragPanel>
</>
);
}
export function FooterCustom() {
return (
<DragPanel
title="弹层标题"
extra="右上角内容"
width={350}
body={<Box p="m"></Box>}
footer={(close) => (
<Box display="flex" justifyContent="space-between">
<Text></Text>
<Button size="small" onClick={() => close()}>
</Button>
</Box>
)}
>
<Button></Button>
</DragPanel>
);
}
export function ResizeablePanel() {
return (
<DragPanel
title="弹层标题"
resizeable
extra="右上角内容"
width={350}
height={400}
body={
<Box p="m" textAlign="center" background="#fa8484">
</Box>
}
footer={<Text></Text>}
>
<Button></Button>
</DragPanel>
);
}
@@ -0,0 +1,72 @@
import React from 'react';
import { Action, InputCode, InputStyleCode } from '@music163/tango-ui';
import { BlockOutlined } from '@ant-design/icons';
export default {
title: 'UI/InputCode',
};
const context = {
stores: {
foo: {
loading: false,
action: () => {},
},
},
services: {
list: () => {},
get: () => {},
},
};
export function Basic() {
return (
<InputCode
suffix={<Action icon={<BlockOutlined />} size="small" />}
autoCompleteContext={{ tango: context }}
/>
);
}
export function CSS() {
return <InputStyleCode enableESLint suffix={<Action icon={<BlockOutlined />} size="small" />} />;
}
const code = `
function foo() {
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
}
`;
export function Readonly() {
return <InputCode shape="inset" readOnly value={code} />;
}
export function Inset() {
return <InputCode shape="inset" />;
}
@@ -0,0 +1,11 @@
import React, { useState } from 'react';
import { InputList } from '@music163/tango-ui';
export default {
title: 'UI/InputList',
};
export function Basic() {
const [value, setValue] = useState([]);
return <InputList value={value} onChange={setValue} />;
}
+19
View File
@@ -0,0 +1,19 @@
import React from 'react';
import { Menu } from '@music163/tango-ui';
export default {
title: 'UI/Menu',
};
export function Basic() {
return (
<Menu
activeKey="2"
items={[
{ key: '1', label: 'bob', note: 'male', deletable: true },
{ key: '2', label: 'alice', note: 'female', deletable: true },
{ key: '3', label: 'tom', deletable: true },
]}
></Menu>
);
}
@@ -0,0 +1,35 @@
import React, { useState } from 'react';
import { SelectList } from '@music163/tango-ui';
export default {
title: 'UI/SelectList',
};
const options = [
{
value: 'alice',
label: 'Alice',
},
{
value: 'jack',
label: 'Jack',
},
{
value: 'lucy',
label: 'Lucy',
},
{
value: 'bob',
label: 'Bob',
},
];
export function Basic() {
const [value, setValue] = useState([]);
return <SelectList value={value} onChange={setValue} options={options} />;
}
export function UniqueValue() {
const [value, setValue] = useState([]);
return <SelectList isUniqueValue value={value} onChange={setValue} options={options} />;
}
@@ -0,0 +1,25 @@
import React from 'react';
import { TagSelect } from '@music163/tango-ui';
export default {
title: 'UI/TagSelect',
};
export function Basic() {
return (
<TagSelect
options={['Movies', 'Books', 'Music', 'Sports'].map((item) => ({ label: item, value: item }))}
onChange={console.log}
/>
);
}
export function SingleMode() {
return (
<TagSelect
options={['Movies', 'Books', 'Music', 'Sports'].map((item) => ({ label: item, value: item }))}
mode="single"
onChange={console.log}
/>
);
}
@@ -0,0 +1,81 @@
import React from 'react';
import { ToggleButton, UndoOutlined, RedoOutlined } from '@music163/tango-ui';
import { Box, Group, Flex } from 'coral-system';
export default {
title: 'UI/ToggleButton',
};
export function Basic() {
return (
<Box>
<Box display="flex" columnGap="l" p="l">
<ToggleButton>
<UndoOutlined />
</ToggleButton>
<ToggleButton selected>
<RedoOutlined />
</ToggleButton>
<ToggleButton disabled>
<RedoOutlined />
</ToggleButton>
</Box>
<Box display="flex" columnGap="l" p="l">
<ToggleButton type="primary">
<UndoOutlined />
</ToggleButton>
<ToggleButton type="primary" selected>
<RedoOutlined />
</ToggleButton>
<ToggleButton type="primary" disabled>
<RedoOutlined />
</ToggleButton>
</Box>
</Box>
);
}
export function DarkMode() {
return (
<Box>
<Flex gap="l" bg="#222" p="l">
<ToggleButton shape="ghost">
<UndoOutlined />
</ToggleButton>
<ToggleButton shape="ghost" selected>
<RedoOutlined />
</ToggleButton>
<ToggleButton shape="ghost" disabled>
<UndoOutlined />
</ToggleButton>
</Flex>
<Flex gap="l" bg="#222" p="l">
<ToggleButton shape="text">
<UndoOutlined />
</ToggleButton>
<ToggleButton shape="text" selected>
<RedoOutlined />
</ToggleButton>
<ToggleButton shape="text" disabled>
<RedoOutlined />
</ToggleButton>
</Flex>
</Box>
);
}
export function ButtonGroup() {
return (
<Group attached>
<ToggleButton>
<UndoOutlined />
</ToggleButton>
<ToggleButton selected>
<RedoOutlined />
</ToggleButton>
<ToggleButton disabled>
<RedoOutlined />
</ToggleButton>
</Group>
);
}
@@ -0,0 +1,72 @@
import React from 'react';
import { VariableTree } from '@music163/tango-designer/src/components';
import { Box } from 'coral-system';
export default {
title: 'Designer/VariableTree',
};
const data = [
{
title: 'Stores',
key: 'stores',
selectable: false,
children: [
{
title: 'user',
key: 'stores.user',
children: [
{
title: 'name',
key: 'stores.user.name',
raw: '"Tom"',
},
{
title: 'age',
key: 'stores.user.age',
raw: '18',
},
],
},
{
title: 'book',
key: 'stores.book',
children: [
{
title: 'name',
key: 'stores.book.name',
raw: '"JavaScript 高级程序设计"',
},
{
title: 'price',
key: 'stores.book.price',
raw: '99.99',
},
],
},
],
},
{
title: 'Services',
key: 'services',
selectable: false,
children: [
{
title: 'add',
key: 'services.add',
},
{
title: 'multiply',
key: 'services.multiply',
},
],
},
];
export function Basic() {
return (
<Box width={600} border="solid">
<VariableTree dataSource={data} />
</Box>
);
}
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "ES2022",
"module": "esnext"
}
}
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
/**
* BabelConfig for Jest
*/
module.exports = {
presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-react', '@babel/preset-typescript'],
};
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'scope-case': [0],
'subject-case': [0],
},
};
+195
View File
@@ -0,0 +1,195 @@
/**
* For a detailed explanation regarding each configuration property, visit:
* https://jestjs.io/docs/configuration
*/
/** @type {import('jest').Config} */
const config = {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/private/var/folders/q_/7k4h88k50pn0p6423z7smp9r0000gn/T/jest_dx",
// Automatically clear mock calls, instances, contexts and results before every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
coverageDirectory: 'coverage',
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// Indicates which provider should be used to instrument code for coverage
// coverageProvider: "babel",
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// The default configuration for fake timers
// fakeTimers: {
// "enableGlobally": false
// },
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "mjs",
// "cjs",
// "jsx",
// "ts",
// "tsx",
// "json",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: undefined,
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state before every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state and implementation before every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
testEnvironment: 'jsdom',
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
testMatch: ['**/tests/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",
// A map from regular expressions to paths to transformers
// transform: undefined,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/",
// "\\.pnp\\.[^\\/]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};
module.exports = config;
+11
View File
@@ -0,0 +1,11 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "independent",
"packages": ["packages/*"],
"npmClient": "yarn",
"command": {
"version": {
"message": "chore(release): publish"
}
}
}
+73
View File
@@ -0,0 +1,73 @@
{
"name": "tango-community",
"private": true,
"workspaces": [
"packages/*",
"apps/*"
],
"scripts": {
"start": "yarn workspace playground dev",
"start:docs": "yarn workspace storybook storybook",
"build": "lerna run build",
"typedoc": "typedoc",
"docs": "yarn typedoc && open docs/index.html",
"test": "jest",
"test:watch": "jest --watch",
"eslint": "eslint packages/**/src/*.{ts,tsx} --cache",
"publish": "lerna publish",
"ver": "lerna version --no-private --conventional-commits",
"release": "yarn eslint && yarn build && yarn ver && lerna publish from-git",
"release:npm": "yarn eslint && yarn build && yarn ver && lerna publish from-package",
"release:beta": "yarn eslint && yarn build && yarn ver && lerna publish from-package --dist-tag beta",
"up": "yarn upgrade-interactive --latest",
"prepare": "husky install"
},
"devDependencies": {
"@babel/preset-env": "^7.25.4",
"@babel/preset-react": "^7.24.7",
"@babel/preset-typescript": "^7.24.7",
"@commitlint/cli": "^18.4.3",
"@commitlint/config-conventional": "^18.4.3",
"@types/jest": "^29.5.7",
"@types/lodash-es": "^4.17.10",
"@types/lodash.get": "^4.4.8",
"@types/lodash.isequal": "^4.5.7",
"@types/lodash.set": "^4.3.8",
"@types/react": "^18.2.34",
"@types/react-dom": "^18.2.14",
"@types/styled-components": "^5.1.29",
"@typescript-eslint/eslint-plugin": "^6.13.2",
"@typescript-eslint/parser": "^6.13.2",
"conventional-changelog-cli": "^4.1.0",
"copyfiles": "^2.4.1",
"cross-env": "^7.0.3",
"eslint": "^8.55.0",
"eslint-config-ali": "^14.0.2",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"http-server": "^14.1.0",
"husky": "^8.0.3",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"lerna": "^8.1.2",
"lint-staged": "^15.2.2",
"prettier": "^3.2.5",
"react": "^17.0.0",
"react-dom": "^17.0.0",
"styled-components": "^5.3.6",
"typedoc": "^0.26.7",
"typescript": "^5.3.3"
},
"engines": {
"node": ">=18"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
]
}
}
+166
View File
@@ -0,0 +1,166 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.10](https://github.com/netease/tango/compare/@music163/tango-context@1.1.9...@music163/tango-context@1.1.10) (2024-09-09)
### Bug Fixes
- service support set description field ([#206](https://github.com/netease/tango/issues/206)) ([79f9a8c](https://github.com/netease/tango/commit/79f9a8c1791424f35f194ab2a9aa7c263ef39f40))
## [1.1.9](https://github.com/netease/tango/compare/@music163/tango-context@1.1.8...@music163/tango-context@1.1.9) (2024-09-02)
**Note:** Version bump only for package @music163/tango-context
## [1.1.8](https://github.com/netease/tango/compare/@music163/tango-context@1.1.7...@music163/tango-context@1.1.8) (2024-08-06)
**Note:** Version bump only for package @music163/tango-context
## [1.1.7](https://github.com/netease/tango/compare/@music163/tango-context@1.1.6...@music163/tango-context@1.1.7) (2024-08-05)
**Note:** Version bump only for package @music163/tango-context
## [1.1.6](https://github.com/netease/tango/compare/@music163/tango-context@1.1.5...@music163/tango-context@1.1.6) (2024-08-01)
**Note:** Version bump only for package @music163/tango-context
## [1.1.5](https://github.com/netease/tango/compare/@music163/tango-context@1.1.4...@music163/tango-context@1.1.5) (2024-07-12)
**Note:** Version bump only for package @music163/tango-context
## [1.1.4](https://github.com/netease/tango/compare/@music163/tango-context@1.1.3...@music163/tango-context@1.1.4) (2024-06-20)
**Note:** Version bump only for package @music163/tango-context
## [1.1.3](https://github.com/netease/tango/compare/@music163/tango-context@1.1.2...@music163/tango-context@1.1.3) (2024-06-05)
**Note:** Version bump only for package @music163/tango-context
## [1.1.2](https://github.com/netease/tango/compare/@music163/tango-context@1.1.1...@music163/tango-context@1.1.2) (2024-06-03)
**Note:** Version bump only for package @music163/tango-context
## [1.1.1](https://github.com/netease/tango/compare/@music163/tango-context@1.1.0...@music163/tango-context@1.1.1) (2024-05-21)
**Note:** Version bump only for package @music163/tango-context
# [1.1.0](https://github.com/netease/tango/compare/@music163/tango-context@1.0.2...@music163/tango-context@1.1.0) (2024-05-17)
### Features
- refactor parse attribute value ([#149](https://github.com/netease/tango/issues/149)) ([ffaa276](https://github.com/netease/tango/commit/ffaa276b5c205ed962d37e2fdb358a703f8fad01))
## [1.0.1](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0...@music163/tango-context@1.0.1) (2024-04-22)
### Bug Fixes
- allow reload state tree & display pageStore in variable tree ([#135](https://github.com/netease/tango/issues/135)) ([2664613](https://github.com/netease/tango/commit/2664613ab263aead2a5239fa012454fc7fd5ff99))
# [1.0.0-alpha.10](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.9...@music163/tango-context@1.0.0-alpha.10) (2024-04-09)
**Note:** Version bump only for package @music163/tango-context
# [1.0.0-alpha.9](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.8...@music163/tango-context@1.0.0-alpha.9) (2024-03-26)
**Note:** Version bump only for package @music163/tango-context
# [1.0.0-alpha.8](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.7...@music163/tango-context@1.0.0-alpha.8) (2024-03-18)
**Note:** Version bump only for package @music163/tango-context
# [1.0.0-alpha.7](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.6...@music163/tango-context@1.0.0-alpha.7) (2024-03-18)
**Note:** Version bump only for package @music163/tango-context
# [1.0.0-alpha.6](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.5...@music163/tango-context@1.0.0-alpha.6) (2024-01-16)
**Note:** Version bump only for package @music163/tango-context
# [1.0.0-alpha.5](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.4...@music163/tango-context@1.0.0-alpha.5) (2023-12-29)
### Bug Fixes
- refactor VariableTree & Workspace ([#83](https://github.com/netease/tango/issues/83)) ([8c07821](https://github.com/netease/tango/commit/8c07821d93cea4dfc43f81ca948b845176821184))
# [1.0.0-alpha.4](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.3...@music163/tango-context@1.0.0-alpha.4) (2023-12-26)
**Note:** Version bump only for package @music163/tango-context
# [1.0.0-alpha.3](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.2...@music163/tango-context@1.0.0-alpha.3) (2023-12-25)
**Note:** Version bump only for package @music163/tango-context
# [1.0.0-alpha.1](https://github.com/netease/tango/compare/@music163/tango-context@0.2.12...@music163/tango-context@1.0.0-alpha.1) (2023-12-12)
**Note:** Version bump only for package @music163/tango-context
## [0.2.12](https://github.com/netease/tango/compare/@music163/tango-context@0.2.11...@music163/tango-context@0.2.12) (2023-12-04)
**Note:** Version bump only for package @music163/tango-context
## [0.2.11](https://github.com/netease/tango/compare/@music163/tango-context@0.2.10...@music163/tango-context@0.2.11) (2023-12-04)
**Note:** Version bump only for package @music163/tango-context
## [0.2.10](https://github.com/netease/tango/compare/@music163/tango-context@0.2.9...@music163/tango-context@0.2.10) (2023-11-30)
**Note:** Version bump only for package @music163/tango-context
## [0.2.9](https://github.com/netease/tango/compare/@music163/tango-context@0.2.8...@music163/tango-context@0.2.9) (2023-11-29)
**Note:** Version bump only for package @music163/tango-context
## [0.2.8](https://github.com/netease/tango/compare/@music163/tango-context@0.2.7...@music163/tango-context@0.2.8) (2023-11-27)
**Note:** Version bump only for package @music163/tango-context
## [0.2.7](https://github.com/netease/tango/compare/@music163/tango-context@0.2.6...@music163/tango-context@0.2.7) (2023-11-23)
**Note:** Version bump only for package @music163/tango-context
## [0.2.6](https://github.com/netease/tango/compare/@music163/tango-context@0.2.5...@music163/tango-context@0.2.6) (2023-11-20)
**Note:** Version bump only for package @music163/tango-context
## [0.2.5](https://github.com/netease/tango/compare/@music163/tango-context@0.2.4...@music163/tango-context@0.2.5) (2023-11-16)
**Note:** Version bump only for package @music163/tango-context
## [0.2.4](https://github.com/netease/tango/compare/@music163/tango-context@0.2.3...@music163/tango-context@0.2.4) (2023-11-02)
### Bug Fixes
- optional context config ([70fc6f8](https://github.com/netease/tango/commit/70fc6f8030202f5d340281c0424752044ed0fb83))
## [0.2.3](https://github.com/netease/tango/compare/@music163/tango-context@0.2.2...@music163/tango-context@0.2.3) (2023-10-23)
**Note:** Version bump only for package @music163/tango-context
## [0.2.2](https://github.com/netease/tango/compare/@music163/tango-context@0.2.1...@music163/tango-context@0.2.2) (2023-10-12)
### Bug Fixes
- refactor expSetter ([#41](https://github.com/netease/tango/issues/41)) ([d63517e](https://github.com/netease/tango/commit/d63517ecb936e4227e70c33e610664316625f4f4))
## [0.2.1](https://github.com/netease/tango/compare/@music163/tango-context@0.2.0...@music163/tango-context@0.2.1) (2023-09-22)
**Note:** Version bump only for package @music163/tango-context
# [0.2.0](https://github.com/netease/tango/compare/@music163/tango-context@0.1.4...@music163/tango-context@0.2.0) (2023-09-21)
### Features
- support multiple service modules & refactor workspace ([a7df29c](https://github.com/netease/tango/commit/a7df29c3debc56b187792d3e203b470e9d368ea5))
## [0.1.4](https://github.com/netease/tango/compare/@music163/tango-context@0.1.3...@music163/tango-context@0.1.4) (2023-09-18)
**Note:** Version bump only for package @music163/tango-context
## [0.1.3](https://github.com/netease/tango/compare/@music163/tango-context@0.1.2...@music163/tango-context@0.1.3) (2023-09-13)
**Note:** Version bump only for package @music163/tango-context
## 0.1.2 (2023-09-06)
**Note:** Version bump only for package @music163/tango-context
+22
View File
@@ -0,0 +1,22 @@
# tango-apps-context
> React context of tango-apps core
## Usage
install
```bash
yarn add @music163/tango-context
```
usage
```jsx
import { observer, useWorkspace } from '@music/tango-apps-context';
export const SampleWidget = observer(() => {
const ws = useWorkspace();
return <div></div>;
});
```
+42
View File
@@ -0,0 +1,42 @@
{
"name": "@music163/tango-context",
"version": "1.1.10",
"description": "react context for tango-apps",
"keywords": [
"react",
"hooks"
],
"author": "wwsun <ww.sun@outlook.com>",
"homepage": "",
"license": "MIT",
"main": "lib/cjs/index.js",
"module": "lib/esm/index.js",
"types": "lib/esm/index.d.ts",
"files": [
"dist",
"lib"
],
"repository": {
"type": "git",
"url": "git+https://github.com/netease/tango.git"
},
"scripts": {
"clean": "rimraf lib/",
"build": "yarn clean && yarn build:esm && yarn build:cjs",
"build:esm": "tsc --project tsconfig.prod.json --outDir lib/esm/ --module ES2020",
"build:cjs": "tsc --project tsconfig.prod.json --outDir lib/cjs/ --module CommonJS",
"prepublishOnly": "yarn build"
},
"peerDependencies": {
"react": ">= 16.8"
},
"dependencies": {
"@music163/tango-core": "^1.4.4",
"@music163/tango-helpers": "^1.2.4",
"mobx-react-lite": "4.0.7"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
}
+149
View File
@@ -0,0 +1,149 @@
import type { AbstractCodeWorkspace, Engine } from '@music163/tango-core';
import { IVariableTreeNode, createContext } from '@music163/tango-helpers';
export interface ITangoEngineContext {
/**
* 低代码引擎
*/
engine: Engine;
/**
* 自定义配置数据
*/
config?: {
customActionVariables?: IVariableTreeNode[];
customExpressionVariables?: IVariableTreeNode[];
};
}
const [TangoEngineProvider, useTangoEngine] = createContext<ITangoEngineContext>({
name: 'TangoEngineContext',
});
export { TangoEngineProvider };
/**
* 获取 CodeWorkspace 实例
* @returns
*/
export const useWorkspace = () => {
return useTangoEngine()?.engine.workspace as AbstractCodeWorkspace;
};
export const useDesigner = () => {
return useTangoEngine()?.engine.designer;
};
export const useWorkspaceData = () => {
const ctx = useTangoEngine();
const workspace = useWorkspace();
const modelVariables: IVariableTreeNode[] = []; // 绑定变量列表
const storeActionVariables: IVariableTreeNode[] = []; // 模型中的所有 actions
const storeVariables: IVariableTreeNode[] = []; // 模型中的所有变量
const pageStoreVariables: IVariableTreeNode[] = []; // 页面中的组件实例变量
const serviceVariables: IVariableTreeNode[] = []; // 服务中的所有变量
pageStoreVariables.push({
title: 'pageStore',
key: 'pageStore',
});
Object.values(workspace.storeModules).forEach((file) => {
const prefix = `stores.${file.name}`;
const states: IVariableTreeNode[] = file.states.map((item) => ({
title: item.name,
key: `${prefix}.${item.name}`,
raw: item.code,
showRemoveButton: true,
}));
const actions: IVariableTreeNode[] = file.actions.map((item) => ({
title: item.name,
key: `${prefix}.${item.name}`,
type: 'function',
raw: item.code,
showRemoveButton: true,
}));
modelVariables.push({
title: file.name,
key: prefix,
selectable: false,
children: states,
showAddButton: true,
});
storeActionVariables.push({
title: file.name,
key: prefix,
selectable: false,
children: actions,
});
storeVariables.push({
title: file.name,
key: prefix,
selectable: false,
children: [...states, ...actions],
showAddButton: true,
});
});
Object.values(workspace.serviceModules).forEach((file) => {
const prefix = file.name !== 'index' ? `services.${file.name}` : 'services';
serviceVariables.push({
title: file.name,
key: prefix,
selectable: false,
showAddButton: true,
children: Object.entries(file.serviceFunctions || {}).map(([key, value]) => {
const title = value.description ? `${key}(${value.description})` : key;
return {
title,
key: [prefix, key].join('.'),
type: 'function',
showRemoveButton: true,
};
}),
});
});
// 路由选项列表
const routeOptions = workspace.pages?.map((item) => ({
label: `${item.name} (${item.path})`,
value: item.path,
}));
let actionVariables: IVariableTreeNode[] = [
buildVariableOptions('数据模型', '$stores', storeActionVariables),
buildVariableOptions('服务函数', '$services', serviceVariables),
];
if (ctx.config?.customActionVariables) {
actionVariables = actionVariables.concat(ctx.config?.customActionVariables);
}
let expressionVariables: IVariableTreeNode[] = [
buildVariableOptions('当前页面组件实例', '$pageStore', pageStoreVariables),
buildVariableOptions('数据模型', '$stores', storeVariables),
buildVariableOptions('服务函数', '$services', serviceVariables),
];
if (ctx.config?.customExpressionVariables) {
expressionVariables = expressionVariables.concat(ctx.config?.customExpressionVariables);
}
return {
modelVariables: [buildVariableOptions('数据模型', 'stores', modelVariables)],
actionVariables,
storeVariables,
serviceVariables,
expressionVariables,
routeOptions,
};
};
function buildVariableOptions(title: string, key: string, children: IVariableTreeNode[]) {
return {
key,
title,
children,
};
}
+2
View File
@@ -0,0 +1,2 @@
export { observer } from 'mobx-react-lite';
export * from './context';
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json"
}
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"outDir": "./dist"
},
"include": ["src/**/*"]
}
+244
View File
@@ -0,0 +1,244 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.4.4](https://github.com/netease/tango/compare/@music163/tango-core@1.4.3...@music163/tango-core@1.4.4) (2024-09-09)
**Note:** Version bump only for package @music163/tango-core
## [1.4.3](https://github.com/netease/tango/compare/@music163/tango-core@1.4.2...@music163/tango-core@1.4.3) (2024-09-02)
**Note:** Version bump only for package @music163/tango-core
## [1.4.2](https://github.com/netease/tango/compare/@music163/tango-core@1.4.1...@music163/tango-core@1.4.2) (2024-08-06)
### Bug Fixes
- view module \_nodesTree set init value ([#194](https://github.com/netease/tango/issues/194)) ([269e304](https://github.com/netease/tango/commit/269e304d685c4fcc80635b9d04220cda333b80c4))
## [1.4.1](https://github.com/netease/tango/compare/@music163/tango-core@1.4.0...@music163/tango-core@1.4.1) (2024-08-05)
**Note:** Version bump only for package @music163/tango-core
# [1.4.0](https://github.com/netease/tango/compare/@music163/tango-core@1.3.3...@music163/tango-core@1.4.0) (2024-08-01)
### Bug Fixes
- update types ([67d602a](https://github.com/netease/tango/commit/67d602a3ec6b7f74156bb78fda6f3b4bc2676e55))
### Features
- add isError and errorMessage to File & add isAstSynced to Module ([#190](https://github.com/netease/tango/issues/190)) ([8dd289c](https://github.com/netease/tango/commit/8dd289cd7daba628e54dc6b8929f22a1e2245160))
## [1.3.3](https://github.com/netease/tango/compare/@music163/tango-core@1.3.2...@music163/tango-core@1.3.3) (2024-07-12)
### Bug Fixes
- update designer config ([#186](https://github.com/netease/tango/issues/186)) ([85c053b](https://github.com/netease/tango/commit/85c053b3db6d652b41c9873dba1366315174833f))
## [1.3.2](https://github.com/netease/tango/compare/@music163/tango-core@1.3.1...@music163/tango-core@1.3.2) (2024-06-20)
### Bug Fixes
- update designer style ([#176](https://github.com/netease/tango/issues/176)) ([0f3f0af](https://github.com/netease/tango/commit/0f3f0afdfa8aee2532a97c5c2e92ef4230397d86))
## [1.3.1](https://github.com/netease/tango/compare/@music163/tango-core@1.3.0...@music163/tango-core@1.3.1) (2024-06-05)
### Bug Fixes
- set value as jsxElement children ([#173](https://github.com/netease/tango/issues/173)) ([ae04850](https://github.com/netease/tango/commit/ae04850bb251f392575a72ca5fd960249ea9d06b))
# [1.3.0](https://github.com/netease/tango/compare/@music163/tango-core@1.2.0...@music163/tango-core@1.3.0) (2024-06-03)
### Bug Fixes
- parse app entry file & pass correct routerType to sandbox ([#168](https://github.com/netease/tango/issues/168)) ([3f3981b](https://github.com/netease/tango/commit/3f3981bb9db874b738a67fd449c579c35c58d08b))
### Features
- add context menu ([#161](https://github.com/netease/tango/issues/161)) ([28040fc](https://github.com/netease/tango/commit/28040fc00604339a40ad3216b76baf7de93a13e0))
# [1.2.0](https://github.com/netease/tango/compare/@music163/tango-core@1.1.0...@music163/tango-core@1.2.0) (2024-05-21)
### Bug Fixes
- enhance code value validate in SettingForm ([#152](https://github.com/netease/tango/issues/152)) ([791fbb1](https://github.com/netease/tango/commit/791fbb162a7147243924e01f54e9c0b586f14438))
- prototype2code & go back history error ([#156](https://github.com/netease/tango/issues/156)) ([8bf53a7](https://github.com/netease/tango/commit/8bf53a76f8a71eaf261ea68b9ee44e5bf19893aa))
- support add components with popover ([#155](https://github.com/netease/tango/issues/155)) ([f17ccbb](https://github.com/netease/tango/commit/f17ccbb7f645f8047ecd96d9f3f2185048a3b726))
### Features
- add select parent node of selected node ([#158](https://github.com/netease/tango/issues/158)) ([fe31246](https://github.com/netease/tango/commit/fe3124648325e72abfc58da8b2f8ff83301d40b8))
- supoort set default active view ([#157](https://github.com/netease/tango/issues/157)) ([f854f75](https://github.com/netease/tango/commit/f854f75b8a8c25384d290bd76d6b8bb6fb69f22d))
# [1.1.0](https://github.com/netease/tango/compare/@music163/tango-core@1.0.2...@music163/tango-core@1.1.0) (2024-05-17)
### Bug Fixes
- parse module with module alias ([#147](https://github.com/netease/tango/issues/147)) ([56d0877](https://github.com/netease/tango/commit/56d0877507b0138877eac6f36db288147b43c7d9))
### Features
- refactor parse attribute value ([#149](https://github.com/netease/tango/issues/149)) ([ffaa276](https://github.com/netease/tango/commit/ffaa276b5c205ed962d37e2fdb358a703f8fad01))
## [1.0.1](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0...@music163/tango-core@1.0.1) (2024-04-22)
**Note:** Version bump only for package @music163/tango-core
# [1.0.0-alpha.10](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.9...@music163/tango-core@1.0.0-alpha.10) (2024-04-09)
### Bug Fixes
- update setters and use tabOptions to filter props ([#129](https://github.com/netease/tango/issues/129)) ([93608d1](https://github.com/netease/tango/commit/93608d1037327afa4f755976b86427b6128ae3d0))
# [1.0.0-alpha.9](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.8...@music163/tango-core@1.0.0-alpha.9) (2024-03-26)
### Features
- quick add sibling nodes ([#127](https://github.com/netease/tango/issues/127)) ([9ed6a7d](https://github.com/netease/tango/commit/9ed6a7d1a4944d69d96e034f243b61531862e317))
# [1.0.0-alpha.8](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.7...@music163/tango-core@1.0.0-alpha.8) (2024-03-18)
**Note:** Version bump only for package @music163/tango-core
# [1.0.0-alpha.7](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.6...@music163/tango-core@1.0.0-alpha.7) (2024-03-18)
### Features
- support code id ([#111](https://github.com/netease/tango/issues/111)) ([6c65362](https://github.com/netease/tango/commit/6c65362a5d5b2297b22f30c093c7d21a979630a1))
# [1.0.0-alpha.6](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.5...@music163/tango-core@1.0.0-alpha.6) (2024-01-16)
### Bug Fixes
- 优化变量树实现 ([#90](https://github.com/netease/tango/issues/90)) ([62d403f](https://github.com/netease/tango/commit/62d403f80a5ad08c216bc0c035ffc12c2cf329d2))
# [1.0.0-alpha.5](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.4...@music163/tango-core@1.0.0-alpha.5) (2023-12-29)
### Bug Fixes
- check if store value is valid ([765ea07](https://github.com/netease/tango/commit/765ea07cd5be6528e3b326e4ecb163647774d9e5))
- refactor VariableTree & Workspace ([#83](https://github.com/netease/tango/issues/83)) ([8c07821](https://github.com/netease/tango/commit/8c07821d93cea4dfc43f81ca948b845176821184))
# [1.0.0-alpha.4](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.3...@music163/tango-core@1.0.0-alpha.4) (2023-12-26)
### Bug Fixes
- list overflow in expSetter ([4d6b67e](https://github.com/netease/tango/commit/4d6b67eecd02b31f01d1bc896c6eeb83f6b34b35))
# [1.0.0-alpha.3](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.2...@music163/tango-core@1.0.0-alpha.3) (2023-12-25)
### Bug Fixes
- export componentEntry module ([149002d](https://github.com/netease/tango/commit/149002ddddaee859333469f65c054d811bde2f7f))
# [1.0.0-alpha.1](https://github.com/netease/tango/compare/@music163/tango-core@0.2.11...@music163/tango-core@1.0.0-alpha.1) (2023-12-12)
**Note:** Version bump only for package @music163/tango-core
## [0.2.11](https://github.com/netease/tango/compare/@music163/tango-core@0.2.10...@music163/tango-core@0.2.11) (2023-12-04)
### Bug Fixes
- update FileType ([c884712](https://github.com/netease/tango/commit/c8847123312bc47ea06575f4e714dfd596893e39))
## [0.2.10](https://github.com/netease/tango/compare/@music163/tango-core@0.2.9...@music163/tango-core@0.2.10) (2023-12-04)
### Bug Fixes
- add filesFormatter to sandbox & fix update props with imports ([#71](https://github.com/netease/tango/issues/71)) ([138cbe9](https://github.com/netease/tango/commit/138cbe9b203b370aff42c1ae8086d69edacf35e9))
## [0.2.9](https://github.com/netease/tango/compare/@music163/tango-core@0.2.8...@music163/tango-core@0.2.9) (2023-11-30)
### Bug Fixes
- fix render components tree ([76a5a2c](https://github.com/netease/tango/commit/76a5a2c65920bc42b019cd1f32a3cacd0d888638))
## [0.2.8](https://github.com/netease/tango/compare/@music163/tango-core@0.2.7...@music163/tango-core@0.2.8) (2023-11-29)
### Bug Fixes
- workspace onFilesChange ([#70](https://github.com/netease/tango/issues/70)) ([accd826](https://github.com/netease/tango/commit/accd8263764c811ea8175a9cd341fc6fa6c75967))
## [0.2.7](https://github.com/netease/tango/compare/@music163/tango-core@0.2.6...@music163/tango-core@0.2.7) (2023-11-27)
### Bug Fixes
- list service functions ([91b53da](https://github.com/netease/tango/commit/91b53da38e362ae320457429037dc347adf90bd3))
- parse tango variables in view file ([d917633](https://github.com/netease/tango/commit/d91763379c4ccf9d826b717aa80a07abdaf2e3a2))
## [0.2.6](https://github.com/netease/tango/compare/@music163/tango-core@0.2.5...@music163/tango-core@0.2.6) (2023-11-23)
### Bug Fixes
- prototype to import declaration ([#67](https://github.com/netease/tango/issues/67)) ([8a1234e](https://github.com/netease/tango/commit/8a1234e565489c92f041356b7a06339eaeee48df))
- refactor update import specifiers ([#68](https://github.com/netease/tango/issues/68)) ([558f2cd](https://github.com/netease/tango/commit/558f2cd0a692c6bbc866d08250d25e2619af183f))
## [0.2.5](https://github.com/netease/tango/compare/@music163/tango-core@0.2.4...@music163/tango-core@0.2.5) (2023-11-20)
### Bug Fixes
- refactor parse expression ([#61](https://github.com/netease/tango/issues/61)) ([dbbd1dd](https://github.com/netease/tango/commit/dbbd1dddc75c532b7c9710ab0941c8680100f093))
## [0.2.4](https://github.com/netease/tango/compare/@music163/tango-core@0.2.3...@music163/tango-core@0.2.4) (2023-11-16)
### Bug Fixes
- allow override existing file via addFile ([6cf673e](https://github.com/netease/tango/commit/6cf673e7f6f330613e1d6331256a59900e8cbc68))
- export isJSXElementById from traverse ([#54](https://github.com/netease/tango/issues/54)) ([208e275](https://github.com/netease/tango/commit/208e275bb1cd98ed21d816c26179f9ebc11df223))
- parse variables from view ([089a482](https://github.com/netease/tango/commit/089a482f750e9d9a7743a6641d6e39989347d318))
- update types ([653386d](https://github.com/netease/tango/commit/653386dcc0b064b41915548129952eeabe53019f))
## [0.2.3](https://github.com/netease/tango/compare/@music163/tango-core@0.2.2...@music163/tango-core@0.2.3) (2023-11-02)
### Bug Fixes
- add onFilesChange to workspace ([#50](https://github.com/netease/tango/issues/50)) ([d775a50](https://github.com/netease/tango/commit/d775a5003ab1a14f801d3b38f7187ea15ef7d74d))
- **core:** create store without entry file ([ae806ae](https://github.com/netease/tango/commit/ae806aee3583ef5f957836e15d715035c4556867))
- **core:** logging drop action ([963bfe1](https://github.com/netease/tango/commit/963bfe1ab80fa4114637963534b2891a27538120))
- designer model add defaultActiveSidebarPanel ([#51](https://github.com/netease/tango/issues/51)) ([9dd11f7](https://github.com/netease/tango/commit/9dd11f7a6d5807c80a63c84c1ad0df5bc1ce9558))
- refactor workspace onFilesChange ([f53ce94](https://github.com/netease/tango/commit/f53ce94c6ed777d45ba6137188bfbc4566e03942))
- update types ([46d5f59](https://github.com/netease/tango/commit/46d5f59b4cd4fa71ed247f5e470e66c8c9f6d4a0))
## [0.2.2](https://github.com/netease/tango/compare/@music163/tango-core@0.2.1...@music163/tango-core@0.2.2) (2023-10-23)
### Bug Fixes
- add listImportSources to viewModule ([6bbbe63](https://github.com/netease/tango/commit/6bbbe634ad52acaf06a0d776aeff450993f7b69a))
- enhance expSetter ([#43](https://github.com/netease/tango/issues/43)) ([5ebbb42](https://github.com/netease/tango/commit/5ebbb428fb3fb786d330ab01959028443338d315))
- update interface ([0b3e632](https://github.com/netease/tango/commit/0b3e632745e30d578268458329d5702dbd729010))
## [0.2.1](https://github.com/netease/tango/compare/@music163/tango-core@0.2.0...@music163/tango-core@0.2.1) (2023-09-22)
### Bug Fixes
- add listServiceFunctions ([09a1e21](https://github.com/netease/tango/commit/09a1e2135a1f51b0a5f6c0a507ba42d2e6355c24))
# [0.2.0](https://github.com/netease/tango/compare/@music163/tango-core@0.1.4...@music163/tango-core@0.2.0) (2023-09-21)
### Features
- support multiple service modules & refactor workspace ([a7df29c](https://github.com/netease/tango/commit/a7df29c3debc56b187792d3e203b470e9d368ea5))
## [0.1.4](https://github.com/netease/tango/compare/@music163/tango-core@0.1.3...@music163/tango-core@0.1.4) (2023-09-18)
**Note:** Version bump only for package @music163/tango-core
## [0.1.3](https://github.com/netease/tango/compare/@music163/tango-core@0.1.2...@music163/tango-core@0.1.3) (2023-09-13)
### Bug Fixes
- **core:** optimize find jsxElement parent node way ([#22](https://github.com/netease/tango/issues/22)) ([19d0a45](https://github.com/netease/tango/commit/19d0a4523bb31ea9286714737a0f4f1883e1c801))
- refactor setting formItem ([b786de2](https://github.com/netease/tango/commit/b786de2f1a0e4e9141eb09fce696e45df633b232))
- refactor types ([15542d9](https://github.com/netease/tango/commit/15542d9eb2f8959597b81cae457091ee71710c83))
- update sidebar ([4d809e9](https://github.com/netease/tango/commit/4d809e9afd0d6d525850708722736847b510638e))
## 0.1.2 (2023-09-06)
### Bug Fixes
- parse service file with sub module ([468910a](https://github.com/netease/tango/commit/468910afde6aec75255f07f8af1f756025e1a237))
- refactor codes ([9392231](https://github.com/netease/tango/commit/9392231414fa1f992e206804549367c5bfee52cb))
- refactor core helpers ([f9c9cbe](https://github.com/netease/tango/commit/f9c9cbefaef7b7fa46585798834e951ded36c68a))
- refactor designer ([ea5fb24](https://github.com/netease/tango/commit/ea5fb24ba7469e28a3de3f60597c819f2f37104a))
+11
View File
@@ -0,0 +1,11 @@
# `core`
搭建引擎
## 如何使用
```js
import { createEngine } from '@music163/tango-core';
const engine = createEngine();
```
+44
View File
@@ -0,0 +1,44 @@
{
"name": "@music163/tango-core",
"version": "1.4.4",
"description": "tango core",
"author": "wwsun <ww.sun@outlook.com>",
"homepage": "",
"license": "MIT",
"main": "lib/cjs/index.js",
"module": "lib/esm/index.js",
"types": "lib/esm/index.d.ts",
"files": [
"dist",
"lib"
],
"repository": {
"type": "git",
"url": "git+https://github.com/netease/tango.git"
},
"scripts": {
"clean": "rimraf lib/",
"build": "yarn clean && yarn build:esm && yarn build:cjs",
"build:esm": "tsc --project tsconfig.prod.json --outDir lib/esm/ --module ES2020",
"build:cjs": "tsc --project tsconfig.prod.json --outDir lib/cjs/ --module CommonJS",
"prepublishOnly": "yarn build"
},
"dependencies": {
"@babel/generator": "^7.25.6",
"@babel/parser": "^7.25.6",
"@babel/traverse": "^7.25.6",
"@babel/types": "^7.25.6",
"@music163/tango-helpers": "^1.2.4",
"@types/babel__generator": "^7.6.7",
"@types/babel__traverse": "^7.20.6",
"mobx": "6.13.2",
"path-browserify": "^1.0.1"
},
"peerDependencies": {
"mobx": "6.9.0"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
}
+52
View File
@@ -0,0 +1,52 @@
import { MenuDataType } from '@music163/tango-helpers';
import { Designer, DesignerViewType, Engine, SimulatorNameType } from './models';
import { AbstractWorkspace } from './models/abstract-workspace';
interface ICreateEngineOptions {
/**
* 自定义工作区
*/
workspace?: AbstractWorkspace;
/**
* 菜单信息
*/
menuData?: MenuDataType;
/**
* 默认的模拟器模式
*/
defaultSimulatorMode?: SimulatorNameType;
/**
* 默认激活的侧边栏
*/
defaultActiveSidebarPanel?: string;
/**
* 默认激活的视图
*/
defaultActiveView?: DesignerViewType;
}
/**
* Designer 实例化工厂函数
* @param options
* @returns
*/
export function createEngine({
workspace,
defaultActiveView = 'design',
defaultSimulatorMode = 'desktop',
defaultActiveSidebarPanel = '',
menuData,
}: ICreateEngineOptions) {
const engine = new Engine({
workspace,
designer: new Designer({
workspace,
simulator: defaultSimulatorMode,
activeView: defaultActiveView,
activeSidebarPanel: defaultActiveSidebarPanel,
menuData,
}),
});
return engine;
}
+50
View File
@@ -0,0 +1,50 @@
import { isValidExpressionCode } from './ast';
const defineServiceHandlerNames = ['defineServices', 'createServices'];
const sfHandlerPattern = new RegExp(`^(${defineServiceHandlerNames.join('|')})$`);
/**
* 判断给定的函数名是否是 defineServices
* @param name
* @returns
*/
export function isDefineService(name: string) {
return sfHandlerPattern.test(name);
}
const defineStoreHandlerName = 'defineStore';
/**
* 判断给定的函数名是否是 defineStore
* @param name
* @returns
*/
export function isDefineStore(name: string) {
return defineStoreHandlerName === name;
}
/**
* 是否是 tango 的变量引用
* @example tango.stores.app.name
* @example tango.stores?.app?.name
*
* @param name
* @returns
*/
export function isTangoVariable(name: string) {
return /^tango\??\.(stores|services)\??\./.test(name) && name.split('.').length > 2;
}
const templatePattern = /^{(.+)}$/;
/**
* 判断给定字符串是否被表达式容器`{expCode}`包裹
* @param code
* @deprecated 新版改为 {{code}} 作为容器,使用 isWrappedCode 代替
*/
export function isWrappedByExpressionContainer(code: string, isStrict = true) {
if (isStrict && isValidExpressionCode(code)) {
return false;
}
return templatePattern.test(code);
}
+247
View File
@@ -0,0 +1,247 @@
/**
* ast to code
*/
import generator, { GeneratorOptions } from '@babel/generator';
import * as t from '@babel/types';
import { Dict, logger, wrapCode } from '@music163/tango-helpers';
import { formatCode } from '../string';
const defaultGeneratorOptions: GeneratorOptions = {
jsescOption: { minimal: true },
retainLines: true,
};
/**
* 将 t.File 生成为代码
* @param ast
* @param options
* @returns
*/
export function ast2code(ast: t.Node, options: GeneratorOptions = defaultGeneratorOptions) {
let code = generator(ast, {
...options,
}).code;
code = formatCode(code);
return code;
}
const bracketPattern = /^\(.+\)$/;
/**
* 将表达式生成为块级代码
* @param node
* @returns
*/
export function expression2code(node: t.Expression) {
const statement = t.expressionStatement(node);
let ret = ast2code(statement).trim();
// 移除末尾的分号
if (ret.endsWith(';')) {
ret = ret.slice(0, -1);
}
const isWrappingExpression = t.isObjectExpression(node) || t.isFunctionExpression(node);
if (isWrappingExpression && bracketPattern.test(ret)) {
// 如果是对象,输出包含 ({}),则去掉首尾的括号
ret = ret.slice(1, -1);
}
return ret;
}
/**
* 获取成员表达式的调用名
* @example Date.now() --> Date.now
* @param node
* @returns
*/
function getNameByMemberExpression(node: t.MemberExpression | t.JSXMemberExpression): string {
let objectName;
let propertyName;
if (t.isIdentifier(node.object) || t.isJSXIdentifier(node.object)) {
objectName = node.object.name;
}
if (t.isIdentifier(node.property) || t.isJSXIdentifier(node.property)) {
propertyName = node.property.name;
}
if (t.isMemberExpression(node.object) || t.isJSXMemberExpression(node.object)) {
objectName = getNameByMemberExpression(node.object);
}
if (t.isMemberExpression(node.property) || t.isJSXMemberExpression(node.property)) {
propertyName = getNameByMemberExpression(node.property);
}
return `${objectName}.${propertyName}`;
}
/**
* 将 的 jsxAttributeName 或 objectPropertyKey 转换为 js value
* @param node jsxAttributeName or objectPropertyKey
* @returns simple js value
*/
export function keyNode2value(node: t.Node) {
if (!node) {
logger.error('invalid property key', node);
return;
}
let ret;
switch (node.type) {
case 'Identifier':
case 'JSXIdentifier':
ret = node.name;
break;
case 'StringLiteral':
ret = `"${node.value}"`;
break;
case 'NumericLiteral':
ret = node.value;
break;
case 'MemberExpression':
ret = getNameByMemberExpression(node);
break;
case 'JSXMemberExpression':
ret = getNameByMemberExpression(node);
break;
default:
logger.error('unknown property key', node);
break;
}
return ret;
}
/**
* 将 t.Node 生成为字符串代码
* @param node
* @returns
*/
export function node2code(node: t.Node) {
let ret = '';
switch (node.type) {
case 'StringLiteral':
case 'NumericLiteral':
ret = node.extra.raw as string;
break;
case 'BooleanLiteral':
ret = `${node.value}`;
break;
case 'NullLiteral':
ret = 'null';
break;
default:
ret = expression2code(node as t.Expression);
break;
}
return ret;
}
/**
* 将 t.Node 生成为 js 值
* @param node ast node
* @param isWrapCode 是否包裹代码,例如 code -> {{code}}
* @returns a plain javascript value
*/
export function node2value(node: t.Node, isWrapCode = true): any {
let ret;
switch (node.type) {
case 'StringLiteral':
case 'NumericLiteral':
case 'BooleanLiteral': {
ret = node.value;
break;
}
case 'NullLiteral':
ret = null;
break;
case 'Identifier': // {{data}}
case 'MemberExpression': // {{this.props.data}}
case 'OptionalMemberExpression': // {{a?.b}}
case 'UnaryExpression': // {{!false}}
case 'ArrowFunctionExpression': // {{() => {}}}
case 'TemplateLiteral': // {{`hello ${text}`}}
case 'ConditionalExpression': // {{a ? 'foo' : 'bar'}}
case 'LogicalExpression': // {{ a || b}}
case 'BinaryExpression': // {{ a + b}}
case 'TaggedTemplateExpression': // {{css``}}
case 'CallExpression': // {{[1,2,3].map(fn)}}
case 'JSXElement': // {{<Box>hello</Box>}}
case 'JSXFragment': // {{<><Box /></>}}
ret = expression2code(node);
if (isWrapCode) {
ret = wrapCode(ret);
}
break;
case 'ObjectExpression': {
const isSimpleObject = node.properties.every(
(propertyNode) => propertyNode.type === 'ObjectProperty',
);
if (isSimpleObject) {
// simple object: { key1, key2, key3 }
ret = node.properties.reduce<Dict>((prev, propertyNode) => {
if (propertyNode.type === 'ObjectProperty') {
const key = keyNode2value(propertyNode.key);
const value = node2value(propertyNode.value, isWrapCode);
prev[key] = value; // key 可能是字符串,也可能是数字
}
return prev;
}, {});
} else {
// mixed object, object property maybe SpreadElement or ObjectMethod, e.g. { key1, fn() {}, ...obj1 }
ret = expression2code(node);
if (wrapCode) {
ret = wrapCode(ret);
}
}
break;
}
case 'ArrayExpression': {
// FIXME: 有可能会解析失败
ret = node.elements.map((elementNode) => node2value(elementNode, isWrapCode));
break;
}
default:
logger.error('unknown ast node:', node);
break;
}
return ret;
}
/**
* jsx prop value 节点转为 js value
*/
export function jsxAttributeValueNode2value(node: t.Node): any {
// e.g. <Checkbox checked /> 此时没有 value node
if (!node) {
return true;
}
let ret;
switch (node.type) {
case 'JSXExpressionContainer':
// <Foo bar={a}>
// <Foo bar={a.b}>
// <Foo bar={2.2}>
// <Foo bar={{}}>
// <Foo bar={[]}>
ret = jsxAttributeValueNode2value(node.expression);
break;
case 'ArrayExpression': {
// 数组统一处理为 code
ret = expression2code(node);
ret = wrapCode(ret);
break;
}
default: {
ret = node2value(node);
break;
}
}
return ret;
}
+3
View File
@@ -0,0 +1,3 @@
export * from './generate';
export * from './parse';
export * from './traverse';
+290
View File
@@ -0,0 +1,290 @@
/**
* code to ast
*/
import { parse, parseExpression, ParserOptions } from '@babel/parser';
import * as t from '@babel/types';
import {
logger,
getVariableContent,
isPlainObject,
Dict,
isWrappedCode,
getCodeOfWrappedCode,
} from '@music163/tango-helpers';
// @see https://babeljs.io/docs/en/babel-parser#pluginss
const babelParserConfig: ParserOptions = {
sourceType: 'module',
plugins: [
'jsx',
'doExpressions',
'objectRestSpread',
'decorators-legacy',
'classProperties',
'asyncGenerators',
'functionBind',
'dynamicImport',
'optionalChaining',
],
};
/**
* 检测代码是否是合法的代码
* @param code
* @returns true 为合法代码,false 为非法代码
*/
export function isValidCode(code: string) {
try {
parse(code, babelParserConfig);
} catch (err) {
return false;
}
return true;
}
/**
* 检测代码是否是合法的表达式代码
* 表达式是一组代码的集合,它返回一个值;每一个合法的表达式都能计算成某个值
* 只要你输入这段代码,可以形成一个值,就算是表达式
* @example x = 1; // 1
* @example 1=1
* @example 'hello'
* @example { foo: 'bar' }
* @param code
* @returns
*/
export function isValidExpressionCode(code: string) {
try {
parseExpression(code, babelParserConfig);
} catch (err) {
return false;
}
return true;
}
/**
* 将源代码解析为一棵完整的 ast 树 t.File
* @param code
* @returns
*/
export function code2ast(code: string): t.File {
return parse(code, babelParserConfig);
}
/**
* 将代码片段解析为 ast 节点
* @example <Button>hello</Button>
* @example { foo: 'foo' }
* @example [{ foo: 'bar' }]
* @example () => {}
* @param code 输入字符串
* @returns
*/
export function code2expression(code: string) {
if (!code) {
return;
}
if (code.endsWith(';')) {
code = code.slice(0, -1);
}
let expNode;
try {
expNode = t.cloneNode(parseExpression(code, babelParserConfig), false, true);
} catch (err) {
logger.error('code2expression failed, invalid code:', code);
}
return expNode;
}
/**
* 表达式代码片段转为 ast 树
* @param code
* @returns File
*/
export function expressionCode2ast(code: string) {
const node = code2expression(code);
return t.file(t.program([t.blockStatement([t.expressionStatement(node)])]));
}
/**
* 将 js 值解析为 t.Node
* @param value
* @returns
*/
export function value2node(
value: any,
):
| t.NullLiteral
| t.Identifier
| t.NumericLiteral
| t.StringLiteral
| t.BooleanLiteral
| t.Expression {
let ret;
switch (typeof value) {
case 'string':
if (isWrappedCode(value)) {
// 再检查是否是代码 {{code}},例如 {{this.foo}}, {{1}}
const innerCode = getCodeOfWrappedCode(value);
ret = code2expression(innerCode);
} else {
// 否则当成字符串处理
ret = t.stringLiteral(value);
}
break;
case 'number':
ret = t.numericLiteral(value);
break;
case 'boolean':
ret = t.booleanLiteral(value);
break;
case 'function':
ret = code2expression(String(value)) as t.ArrowFunctionExpression | t.FunctionExpression;
break;
case 'object': {
if (value === null) {
ret = t.nullLiteral();
} else if (isPlainObject(value)) {
ret = object2node(value);
} else if (Array.isArray(value)) {
ret = t.arrayExpression(value.map((val) => value2node(val)));
} else {
ret = t.identifier('undefined');
logger.error('value2node: not support value!', ret);
}
break;
}
case 'undefined':
ret = t.identifier('undefined');
break;
default: {
logger.error(`value2node: value <${value}> transform failed!`);
break;
}
}
return ret;
}
/**
* 将 js 普通对象解析为 t.Node
*/
export function object2node(
obj: Dict,
getValueNode: (value: any, key?: string) => t.Expression = value2node,
) {
if (!isPlainObject(obj)) {
return value2node(obj);
}
return t.objectExpression(
Object.keys(obj).map((key) => {
const valNode = getValueNode(obj[key], key);
return t.objectProperty(t.identifier(key), valNode);
}),
);
}
export function code2jsxAttributeValueNode(code: string) {
return t.jsxExpressionContainer(code2expression(code));
}
/**
* FIXME: 统一处理为 code2jsxAttributeValueNode
* 将 js value 转为 JSXAttributeValueNode
* @param value js value, or wrapped code
* @returns 返回 JSXAttributeValueNode,转换失败返回Node为 {undefined}
*/
export function value2jsxAttributeValueNode(value: any) {
let ret;
switch (typeof value) {
case 'string': {
if (value.length > 1) {
value = value.trim();
}
if (isWrappedCode(value)) {
const innerCode = getCodeOfWrappedCode(value);
const node = code2expression(innerCode);
ret = t.jsxExpressionContainer(node || t.identifier('undefined'));
} else {
ret = t.stringLiteral(value);
}
break;
}
default:
ret = t.jsxExpressionContainer(value2node(value));
break;
}
return ret;
}
export function value2jsxChildrenValueNode(value: any) {
let ret: t.JSXElement | t.JSXFragment | t.JSXExpressionContainer | t.JSXSpreadChild | t.JSXText;
switch (typeof value) {
case 'string':
if (isWrappedCode(value)) {
// 代码模式
const innerString = getVariableContent(value);
ret = t.jsxExpressionContainer(code2expression(innerString));
} else {
// 普通的文本
ret = t.jsxText(value);
}
break;
case 'number':
ret = t.jsxText(String(value));
break;
case 'object':
// value 为 JSXElement[]的情况下直接return
return value as t.JSXElement[];
default:
break;
}
return ret ? [ret] : [];
}
/**
* 给定具体的 value 值,生成 JSXAttribute
* @param name 属性名
* @param value 属性值代码
* @returns
*/
export function makeJSXAttribute(name: string, value: any) {
return t.jsxAttribute(t.jsxIdentifier(name), value2jsxAttributeValueNode(value));
}
export function makeJSXAttributes(props: Dict) {
return Object.keys(props).map((key) => makeJSXAttribute(key, props[key]));
}
/**
* 给定具体的 value 代码,生成 JSXAttribute
* @param name 属性名
* @param valueCode 属性值代码
* @returns
*/
export function makeJSXAttributeByCode(name: string, valueCode: string) {
return t.jsxAttribute(t.jsxIdentifier(name), code2jsxAttributeValueNode(valueCode));
}
/**
* 生成 JSXElement
* @param name
* @param attributes
* @param children
* @param selfClosing
* @returns
*/
export function makeJSXElement(
name: string,
attributes: t.JSXAttribute[],
children: t.JSXElement['children'],
selfClosing: boolean,
) {
return t.jsxElement(
t.jsxOpeningElement(t.jsxIdentifier(name), attributes),
t.jsxClosingElement(t.jsxIdentifier(name)),
children ?? [],
selfClosing,
);
}
File diff suppressed because it is too large Load Diff
+66
View File
@@ -0,0 +1,66 @@
import { getCodeOfWrappedCode, isWrappedCode } from '@music163/tango-helpers';
import { value2node, expression2code, code2expression, node2value } from './ast';
/**
* js value 转为代码字符串
* @example 1 => 1
* @example hello => "hello"
* @example { foo: bar } => {{ foo: bar }}
* @example [1,2,3] => {[1,2,3]}
*
* @param val js value
* @returns 表达式代码
*/
export function value2code(val: any) {
if (val === undefined) {
return '';
}
if (val === null) {
return 'null';
}
let ret;
switch (typeof val) {
case 'string': {
if (isWrappedCode(val)) {
ret = getCodeOfWrappedCode(val);
} else {
ret = `"${val}"`;
}
break;
}
case 'boolean':
case 'function':
case 'number':
ret = String(val);
break;
default: {
// other cases, including array, object, null, undefined
const node = value2node(val);
ret = expression2code(node);
break;
}
}
return ret;
}
export const value2expressionCode = value2code;
/**
* 代码字符串转为具体的 js value
* @example `() => {}` 返回 undefined
*
* @param rawCode 代码字符串
* @returns 返回解析后的 js value,包括:string, number, boolean, simpleObject, simpleArray
*/
export function code2value(rawCode: string) {
const node = code2expression(rawCode);
const value = node2value(node);
if (isWrappedCode(value)) {
// 能转的就转,转不能的就返回空
return;
}
return value;
}
+57
View File
@@ -0,0 +1,57 @@
import { camelCase } from '@music163/tango-helpers';
type IdGeneratorOptionsType = { prefix?: string };
/**
* ID 生成器
*/
export class IdGenerator {
/**
* ID 前缀
*/
private readonly prefix: string;
/**
* 记录组件 ID 记录
*/
private map = new Map<string, string[]>();
constructor(options?: IdGeneratorOptionsType) {
this.prefix = options?.prefix ? encodeURIComponent(options.prefix) : undefined;
}
/**
* 更新组件记录
* @param component
*/
setItem(component: string, id?: string) {
if (this.map.has(component)) {
const record = this.map.get(component);
if (id && !record.includes(id)) {
record.push(id);
}
this.map.set(component, record);
} else {
const value = id ? [id] : [];
this.map.set(component, value);
}
}
/**
* 获取组件 ID
* @param component 组件名, 如 Button, DatePicker
* @param codeId 用户自定义的 ID
* @returns
*/
generateId(component: string, codeId?: string) {
// FIXME: 使用 size 这里可能存在冲突的风险
const size = this.map.get(component)?.length + 1 || 1;
const id = codeId || `${camelCase(component)}${size}`;
this.setItem(component, id);
let fullId = `${component}:${id}`;
if (this.prefix) {
fullId = `${this.prefix}:${fullId}`;
}
return { id, fullId };
}
}
+8
View File
@@ -0,0 +1,8 @@
export * from './ast';
export * from './assert';
export * from './code-helpers';
export * from './string';
export * from './object';
export * from './prototype';
export * from './schema-helpers';
export * from './id-generator';
+44
View File
@@ -0,0 +1,44 @@
import { Dict } from '@music163/tango-helpers';
import { IImportDeclarationPayload, IImportSpecifierSourceData } from '../types';
/**
* 导入列表解析为导入声明对象
* @param names
* @param nameMap
* @returns
*/
export function namesToImportDeclarations(
names: string[],
nameMap: Dict<IImportSpecifierSourceData>,
) {
const map: Dict = {};
names.forEach((name) => {
const mod = nameMap[name];
if (mod) {
updateMod(map, mod.source, name, mod.isDefault, !map[mod.source]);
}
});
return Object.keys(map).map((sourcePath) => ({
sourcePath,
...map[sourcePath],
})) as IImportDeclarationPayload[];
}
function updateMod(
map: any,
fromPackage: string,
specifier: string,
isDefault = false,
shouldInit = true,
) {
if (shouldInit) {
map[fromPackage] = {};
}
if (isDefault) {
map[fromPackage].defaultSpecifier = specifier;
} else if (map[fromPackage].specifiers) {
map[fromPackage].specifiers.push(specifier);
} else {
map[fromPackage].specifiers = [specifier];
}
}
+207
View File
@@ -0,0 +1,207 @@
import * as t from '@babel/types';
import {
IComponentProp,
IComponentPrototype,
Dict,
logger,
uuid,
isWrappedCode,
getCodeOfWrappedCode,
wrapCodeWithJSXExpressionContainer,
} from '@music163/tango-helpers';
import { getRelativePath, isFilepath } from './string';
import type { IImportDeclarationPayload, IImportSpecifierData } from '../types';
import { code2expression } from './ast';
import { isWrappedByExpressionContainer } from './assert';
import { value2code } from './code-helpers';
export function prototype2importDeclarationData(
prototype: IComponentPrototype,
relativeFilepath?: string,
): { source: string; specifiers: IImportSpecifierData[] } {
let source = prototype.package;
const isSnippet = prototype.type === 'snippet';
if (relativeFilepath && isFilepath(source)) {
source = getRelativePath(relativeFilepath, source);
}
if (source.endsWith('.js')) {
source = source.slice(0, -3);
}
const specifiers: IImportSpecifierData[] = [];
if (prototype.exportType === 'defaultExport') {
specifiers.push({
localName: prototype.name,
type: 'ImportDefaultSpecifier',
});
} else {
// 忽略代码片段 name
[...(isSnippet ? [] : [prototype.name]), ...(prototype.relatedImports || [])].forEach(
(item) => {
specifiers.push({
localName: item,
type: 'ImportSpecifier',
});
},
);
}
return {
source,
specifiers,
};
}
/**
* 根据组件的 prototype 生成 ImportDeclarationPayload
* @deprecated
*/
export function getImportDeclarationPayloadByPrototype(
prototype: IComponentPrototype,
relativeFilepath?: string,
): IImportDeclarationPayload {
let defaultSpecifier;
let specifiers;
if (prototype.exportType === 'defaultExport') {
defaultSpecifier = prototype.name;
specifiers = prototype.relatedImports || [];
} else {
specifiers = [...(prototype.relatedImports || [])];
if (prototype.type !== 'snippet') {
specifiers.push(prototype.name);
}
}
let sourcePath = prototype.package;
if (relativeFilepath && isFilepath(sourcePath)) {
sourcePath = getRelativePath(relativeFilepath, sourcePath);
}
if (sourcePath.endsWith('.js')) {
sourcePath = sourcePath.slice(0, -3);
}
return {
defaultSpecifier,
specifiers,
sourcePath,
};
}
/**
* 基于 key-value 生成 prop={value} 字符串
* @example { name: 'foo', initValue: false } >> name={false}
* @example { name: 'foo', initValue: 1 } >> name={1}
* @example { name: 'foo', initValue: () => {} } >> name={()=>{}}
* @example { name: 'foo', initValue: { foo: 'bar' } } >> name={{ foo: 'bar' }}
* @example { name: 'foo', initValue: [{ foo: 'bar' }] } >> name={[{ foo: 'bar' }]}
* @example { name: 'foo', initValue: 'bar' } >> name="bar"
* @example { name: 'foo', initValue: '{() => {}}' } >> name={()=>{}}
* @example { name: 'foo', initValue: '{{() => {}}}' } >> name={() => {}}
* @example { name: 'foo', initValue: '{bar}' } >> name={bar}
* @returns
*/
export function propDataToKeyValueString(
item: IComponentProp,
generateValue?: (...args: any[]) => string,
) {
const key = item.name;
let value = item.initValue;
if (!value && item.autoInitValue) {
value = generateValue?.(3) || uuid(key, 3);
}
if (value === undefined) {
return;
}
switch (typeof value) {
case 'number':
case 'boolean':
case 'function': {
value = wrapCodeWithJSXExpressionContainer(String(value));
break;
}
case 'object': {
try {
value = wrapCodeWithJSXExpressionContainer(value2code(value));
} catch (err) {
logger.error(err);
}
break;
}
case 'string': {
if (isWrappedCode(value)) {
const innerCode = getCodeOfWrappedCode(value);
value = wrapCodeWithJSXExpressionContainer(innerCode);
} else if (isWrappedByExpressionContainer(value)) {
// TIP: 兼容旧版逻辑,如果是变量字符串,无需处理
} else {
value = `"${value}"`;
}
break;
}
default:
break;
}
return `${key}=${value}`;
}
/**
* Button prototype -> <Button>hello</Button>
*
* @param prototype
* @param extraProps
*/
export function prototype2code(prototype: IComponentPrototype, extraProps?: Dict) {
let code;
switch (prototype.type) {
case 'snippet':
code = prototype.initChildren || prototype.defaultChildren;
break;
default: {
const propList: IComponentProp[] = extraProps
? Object.keys(extraProps).map((key) => ({
name: key,
initValue: extraProps[key],
}))
: [];
// merge extraProps to props
const props = [...(prototype.props || []), ...propList];
const keys =
props.reduce((acc, item) => {
const pair = propDataToKeyValueString(item, (fractionDigits: number) =>
uuid(prototype.name, fractionDigits),
);
return pair ? ` ${acc} ${pair}` : acc;
}, '') || '';
if (prototype.hasChildren) {
code = `<${prototype.name} ${keys}>${
prototype.initChildren || prototype.defaultChildren || ''
}</${prototype.name}>`;
} else {
code = `<${prototype.name} ${keys.trim()} />`;
}
break;
}
}
return code;
}
/**
* 基于 prototype 信息生成 t.JSXElement
* @example ButtonPrototype -> <Button>hello</Button> -> t.JSXElement
*
* @param prototype 组件的配置信息
* @param props 额外的属性集
*/
export function prototype2jsxElement(prototype: IComponentPrototype, props?: Dict) {
const code = prototype2code(prototype, props);
return code2expression(code) as t.JSXElement;
}
@@ -0,0 +1,39 @@
import { Dict, parseDndId, uuid } from '@music163/tango-helpers';
// { id: 1, component, props: { id: 1 }, children: [ { id: 2, component } ] }
export function deepCloneNode(obj: any, component?: string): any {
function deepCloneObject() {
const target: Dict = {};
for (const key in obj) {
if (Object.hasOwn(obj, key)) {
target[key] = deepCloneNode(obj[key], component);
}
}
return target;
}
function deepCloneElementNode() {
const target: any = deepCloneObject();
let name = component || target.component;
if (target.id) {
const dnd = parseDndId(target.id);
name = dnd.component || name;
}
target.id = uuid(`${name}:`);
return target;
}
if (!obj || typeof obj !== 'object') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => deepCloneNode(item, component));
}
if (!obj.component) {
return deepCloneObject();
}
return deepCloneElementNode();
}
+201
View File
@@ -0,0 +1,201 @@
import path from 'path';
import { camelCase, upperCamelCase } from '@music163/tango-helpers';
import { FileType } from './../types';
/**
* 推断JS模块类型
*/
export function inferFileType(filename: string): FileType {
// 增加 tangoConfigJson Module
if (/\/tango\.config\.json$/.test(filename)) {
return FileType.TangoConfigJsonFile;
}
if (/\/appJson\.json$/.test(filename)) {
return FileType.AppJsonFile;
}
if (/\/package\.json$/.test(filename)) {
return FileType.PackageJsonFile;
}
if (/\/routes\.js$/.test(filename)) {
return FileType.JsRouteConfigFile;
}
// 所有 pages 下的 js 文件均认为是有效的 viewModule
if (/\/pages\/.+\.jsx?$/.test(filename)) {
return FileType.JsViewFile;
}
// 所有 pages 下的 js 文件均认为是有效的 viewModule
if (/\/pages\/.+\.schema\.json?$/.test(filename)) {
return FileType.JsonViewFile;
}
if (/\/(blocks|components)\/index\.js/.test(filename)) {
return FileType.JsLocalComponentsEntryFile;
}
if (/\/services\/.+\.js$/.test(filename)) {
return FileType.JsServiceFile;
}
if (/service\.js$/.test(filename)) {
return FileType.JsServiceFile;
}
if (/\/stores\/index\.js$/.test(filename)) {
return FileType.JsStoreEntryFile;
}
if (/\/stores\/.+\.js$/.test(filename)) {
return FileType.JsStoreFile;
}
if (/\.jsx?$/.test(filename)) {
return FileType.JsFile;
}
if (/\.json$/.test(filename)) {
return FileType.JsonFile;
}
return FileType.File;
}
/**
* 判断组件名是否合法
* @example Button -> valid
* @example isStrict ? div -> invalid : div -> valid
* @param name
* @param isStrict 是否严格模式(严格模式不匹配原生标签)
* @returns
*/
export function isValidComponentName(name: string, isStrict = false) {
if (!name) {
return false;
}
if (!isStrict) {
return true;
}
const firstChar = name.charAt(0);
return firstChar === firstChar.toUpperCase();
}
/**
* 从 filename 中解析获得 moduleName
* @example /stores/user.js -> user
* @example /services/foo-bar.js -> fooBar
* @param filename
*/
export function getModuleNameByFilename(filename: string) {
const parts = filename.split('/');
let name = parts[parts.length - 1];
name = name.split('.')[0];
return camelCase(name);
}
/**
* 基于路由名生成文件路径
* @param routePath 路由地址
* @param baseDir base dir
* @param ext 后缀名
* @returns
*/
export function getFilepath(routePath: string, baseDir: string, ext = '') {
if (routePath.startsWith('/')) {
routePath = routePath.substring(1);
}
const filename = routePath.replaceAll('/:', '@').split('/').join('-');
if (!baseDir.endsWith('/')) {
baseDir = `${baseDir}/`;
}
return `${baseDir}${filename}${ext}`;
}
export function getPrivilegeCode(appName: string, routePath: string) {
return `${appName}@${routePath.replaceAll('/', '%')}`;
}
const prettier = (window as any).prettier;
const prettierPlugins = (window as any).prettierPlugins;
/**
* 格式化代码
* @param code original source code
* @param parser prettier parser, see https://prettier.io/docs/en/options.html#parser
* @returns the formatted code
*/
export function formatCode(code: string, parser = 'babel') {
if (prettier && prettierPlugins) {
return prettier.format(code, { parser, plugins: prettierPlugins });
}
return code;
}
/**
* 是否匹配路由
* @example isPathnameMatchRoute('/user/123', '/users/:id') -> true
*/
export function isPathnameMatchRoute(pathname: string, route: string) {
if (!pathname) {
return false;
}
pathname = pathname.split('?')[0];
if (pathname === route) {
return true;
}
const str = route.replaceAll(/:\w+/gi, '\\w+');
const pt = new RegExp(`^${str}$`, 'i');
return pt.test(pathname);
}
/**
* 根据文件路径计算区块的名字
* @param filename 文件路径
* @return 返回计算后的区块名(大驼峰)
*/
export function getBlockNameByFilename(filename: string) {
const name = filename.split('/').slice(-2, -1)[0];
return upperCamelCase(name);
}
/**
* 合并两个路径
* @param root
* @param filename
* @returns
*/
export function getFullPath(root: string, filename: string) {
return path.join(root, filename);
}
/**
* 计算 targetFile 在 sourceFile 中的相对引用路径
* @param sourceFile
* @param targetFile
* @returns
*/
export function getRelativePath(sourceFile: string, targetFile: string) {
sourceFile = path.dirname(sourceFile);
return path.relative(sourceFile, targetFile);
}
/**
* 判断给定字符串是否是文件路径
* @example ./pages/index.js -- yes
* @example ../pages/index.js -- yes
* @example ../components -- yes
* @example /src/pages/index.js -- yes
* @example @music163/tango-designer -- no
* @param str
*/
export function isFilepath(str: string) {
return /^(\.\.?\/|\/).*(\.[a-z]+)?$/.test(str);
}
+8
View File
@@ -0,0 +1,8 @@
export * from './models';
export * from './factory';
export * from './helpers';
export * from './types';
export { default as generator } from '@babel/generator';
export { default as traverse } from '@babel/traverse';
export * from '@babel/parser';
@@ -0,0 +1,264 @@
import {
Dict,
isStoreVariablePath,
parseServiceVariablePath,
parseStoreVariablePath,
} from '@music163/tango-helpers';
import { inferFileType, getFilepath } from '../helpers';
import { TangoFile } from './file';
import { FileType } from '../types';
import { JsRouteConfigFile } from './js-route-config-file';
import { JsStoreEntryFile } from './js-store-entry-file';
import { JsServiceFile } from './js-service-file';
import { JsViewFile } from './js-view-file';
import { JsLocalComponentsEntryFile } from './js-local-components-entry-file';
import { JsAppEntryFile } from './js-app-entry-file';
import { JsFile } from './js-file';
import { AbstractWorkspace, IWorkspaceInitConfig } from './abstract-workspace';
import { JsonFile } from './json-file';
import { JsStoreFile } from './js-store-file';
/**
* CodeWorkspace 抽象基类
*/
export abstract class AbstractCodeWorkspace extends AbstractWorkspace {
/**
* 模型入口配置模块
*/
storeEntryModule: JsStoreEntryFile;
/**
* 状态管理模块
*/
storeModules: Record<string, JsStoreFile>;
/**
* 数据服务模块
*/
serviceModules: Record<string, JsServiceFile>;
constructor(options: IWorkspaceInitConfig) {
super(options);
this.storeModules = {};
this.serviceModules = {};
if (options?.files) {
this.addFiles(options.files);
}
}
/**
* 添加文件到工作区
* @param filename 文件名
* @param code 代码片段
* @param fileType 模块类型
*/
addFile(filename: string, code: string, fileType?: FileType) {
if (!fileType && filename === this.entry) {
fileType = FileType.JsAppEntryFile;
}
const moduleType = fileType || inferFileType(filename);
const props = {
filename,
code,
type: moduleType,
};
let module;
switch (moduleType) {
case FileType.JsAppEntryFile:
module = new JsAppEntryFile(this, props);
this.jsAppEntryFile = module;
break;
case FileType.JsStoreEntryFile:
module = new JsStoreEntryFile(this, props);
this.storeEntryModule = module;
break;
case FileType.JsLocalComponentsEntryFile:
module = new JsLocalComponentsEntryFile(this, props);
this.componentsEntryModule = module;
break;
case FileType.JsRouteConfigFile: {
module = new JsRouteConfigFile(this, props);
this.routeModule = module;
// check if activeRoute exists
const route = module.routes.find((item) => item.path === this.activeRoute);
if (!route) {
this.setActiveRoute(module.routes[0]?.path);
}
break;
}
case FileType.JsViewFile:
module = new JsViewFile(this, props);
break;
case FileType.JsServiceFile:
module = new JsServiceFile(this, props);
this.serviceModules[module.name] = module;
break;
case FileType.JsStoreFile:
module = new JsStoreFile(this, props);
this.storeModules[module.name] = module;
break;
case FileType.JsFile:
module = new JsFile(this, props);
break;
case FileType.PackageJsonFile:
module = new JsonFile(this, props);
this.packageJson = module;
break;
case FileType.TangoConfigJsonFile:
module = new JsonFile(this, props);
this.tangoConfigJson = module;
break;
case FileType.JsonFile:
module = new JsonFile(this, props);
break;
default:
module = new TangoFile(this, props);
}
this.files.set(filename, module);
}
addServiceFile(serviceName: string, code: string) {
const filename = `/src/services/${serviceName}.js`;
this.addFile(filename, code, FileType.JsServiceFile);
const indexServiceModule = this.serviceModules.index;
indexServiceModule?.addImportDeclaration(`./${serviceName}`, []).update();
}
addStoreFile(storeName: string, code: string) {
const filename = `/src/stores/${storeName}.js`;
this.addFile(filename, code);
if (!this.storeEntryModule) {
this.addFile('/src/stores/index.js', '');
}
this.storeEntryModule.addStore(storeName).update();
}
/**
* 添加新的模型文件
* @deprecated 使用 addStoreFile 代替
*/
addStoreModule(name: string, code: string) {
this.addStoreFile(name, code);
}
/**
* 删除模型文件
* @param name
*/
removeStoreModule(name: string) {
const filename = getFilepath(name, '/src/stores', '.js');
this.storeEntryModule.removeStore(name).update();
this.removeFile(filename);
}
/**
* 添加模型属性
* @param storeName
* @param stateName
* @param initValue
*/
addStoreState(storeName: string, stateName: string, initValue: string) {
this.storeModules[storeName]?.addState(stateName, initValue).update();
}
/**
* 删除模型属性
* @param storeName
* @param stateName
*/
removeStoreState(storeName: string, stateName: string) {
this.storeModules[storeName]?.removeState(stateName).update();
}
/**
* 根据变量路径删除状态变量
* @param variablePath
*/
removeStoreVariable(variablePath: string) {
const { storeName, variableName } = parseStoreVariablePath(variablePath);
this.removeStoreState(storeName, variableName);
}
/**
* 根据变量路径更新状态变量的值
* @param variablePath 变量路径
* @param code 变量代码
*/
updateStoreVariable(variablePath: string, code: string) {
if (isStoreVariablePath(variablePath)) {
const { storeName, variableName } = parseStoreVariablePath(variablePath);
this.storeModules[storeName]?.updateState(variableName, code).update();
}
}
/**
* 获取服务函数的详情
* TODO: 不要 services 前缀
* @param serviceKey `services.list` 或 `services.sub.list`
* @returns
*/
getServiceFunction(serviceKey: string) {
const { name, moduleName } = parseServiceVariablePath(serviceKey);
if (!name) {
return;
}
return {
name,
moduleName,
config: this.serviceModules[moduleName]?.serviceFunctions[name],
};
}
/**
* 获取服务函数的列表
* @returns 返回服务函数的列表 { [serviceKey: string]: Dict }
*/
listServiceFunctions() {
const ret: Record<string, Dict> = {};
Object.keys(this.serviceModules).forEach((moduleName) => {
const module = this.serviceModules[moduleName];
Object.keys(module.serviceFunctions).forEach((name) => {
const serviceKey = moduleName === 'index' ? name : [moduleName, name].join('.');
ret[serviceKey] = module.serviceFunctions[name];
});
});
return ret;
}
/**
* 更新服务函数
*/
updateServiceFunction(serviceName: string, payload: Dict, moduleName = 'index') {
this.serviceModules[moduleName].updateServiceFunction(serviceName, payload).update();
}
/**
* 新增服务函数,支持批量添加
*/
addServiceFunction(name: string, config: Dict, moduleName = 'index') {
this.serviceModules[moduleName]?.addServiceFunction(name, config).update();
}
addServiceFunctions(configs: Dict<Dict>, modName = 'index') {
this.serviceModules[modName]?.addServiceFunctions(configs).update();
}
/**
* 删除服务函数
* @param name
*/
removeServiceFunction(serviceKey: string) {
const { moduleName, name } = parseServiceVariablePath(serviceKey);
this.serviceModules[moduleName]?.deleteServiceFunction(name).update();
}
/**
* 更新服务的基础配置
*/
updateServiceBaseConfig(config: Dict, moduleName = 'index') {
this.serviceModules[moduleName]?.updateBaseConfig(config).update();
}
}
+63
View File
@@ -0,0 +1,63 @@
import { AbstractWorkspace } from './abstract-workspace';
import type { FileType, IFileConfig } from '../types';
/**
* 普通文件抽象基类,不进行 AST 解析
*/
export abstract class AbstractFile {
readonly workspace: AbstractWorkspace;
/**
* 文件名
*/
readonly filename: string;
/**
* 文件类型
*/
readonly type: FileType;
/**
* 最近修改的时间戳
*/
lastModified: number;
/**
* 文件解析是否出错
*/
isError: boolean;
/**
* 文件解析错误消息
*/
errorMessage: string;
_code: string;
_cleanCode: string;
get code() {
return this._code;
}
// FIXME: cleanCode 是不是只有 viewFile 有 ????
get cleanCode() {
return this._cleanCode;
}
constructor(workspace: AbstractWorkspace, props: IFileConfig, isSyncCode = true) {
this.workspace = workspace;
this.filename = props.filename;
this.type = props.type;
this.lastModified = Date.now();
this.isError = false;
// 这里主要是为了解决 umi ts 编译错误的问题,@see https://github.com/umijs/umi/issues/7594
if (isSyncCode) {
this.update(props.code);
}
}
/**
* 更新文件内容
*/
abstract update(code?: string): void;
}
@@ -0,0 +1,138 @@
import * as t from '@babel/types';
import { isNil } from '@music163/tango-helpers';
import {
code2ast,
ast2code,
formatCode,
traverseFile,
addImportDeclaration,
updateImportDeclaration,
} from '../helpers';
import { IFileConfig, IImportSpecifierData, ImportDeclarationDataType } from '../types';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractFile } from './abstract-file';
/**
* JS 文件抽象基类
* - ast 操纵类方法,统一返回 this,支持外层链式调用
* - observable state 统一用 _foo 格式,并提供 getter 方法
*/
export abstract class AbstractJsFile extends AbstractFile {
ast: t.File;
/**
* ast 是否与 code 保持同步
*/
isAstSynced: boolean;
/**
* 导入的依赖列表
*/
importList: ImportDeclarationDataType;
constructor(workspace: AbstractWorkspace, props: IFileConfig, isSyncCode = true) {
super(workspace, props, isSyncCode);
}
/**
* 基于最新的 ast 进行同步
* @param code 如果传入 code,则基于 code 进行同步
* @param isSyncAst 是否同步 ast
* @param isRefreshWorkspace 是否刷新 workspace
*/
update(code?: string, isSyncAst = true, isRefreshWorkspace = true) {
this.lastModified = Date.now();
try {
if (isNil(code)) {
this._syncByAst();
} else {
this._syncByCode(code, isSyncAst);
}
if (isSyncAst) {
this._analysisAst();
}
this.isAstSynced = isSyncAst;
this.isError = false;
this.errorMessage = undefined;
this.workspace.onFilesChange([this.filename]);
if (isRefreshWorkspace) {
this.workspace.refresh([this.filename]);
}
} catch (err: any) {
this.isError = true;
this.errorMessage = err.message;
}
}
/**
* 基于当前的代码重新生成 ast
*/
updateAst() {
if (!this.isAstSynced) {
try {
this.ast = code2ast(this._code);
this._analysisAst();
this.isAstSynced = true;
this.isError = false;
this.errorMessage = undefined;
} catch (err: any) {
this.isAstSynced = false;
this.isError = true;
this.errorMessage = err.message;
}
}
}
addImportDeclaration(source: string, specifiers: IImportSpecifierData[]) {
this.ast = addImportDeclaration(this.ast, source, specifiers);
return this;
}
updateImportDeclaration(source: string, specifiers: IImportSpecifierData[]) {
this.ast = updateImportDeclaration(this.ast, source, specifiers);
return this;
}
/**
* 基于最新的 ast 进行源码同步
*/
_syncByAst() {
const code = ast2code(this.ast);
this._code = code;
this._cleanCode = code;
}
/**
* 基于输入的源码进行同步
* @param code 源码
* @param isSyncAst 是否同步 ast
* @returns
*/
_syncByCode(code: string, isSyncAst = true) {
if (code === this._code) {
return;
}
// 提前格式化代码
try {
code = formatCode(code);
} catch (err) {
// err ignored, format code failed
}
this._code = code;
this._cleanCode = code;
if (isSyncAst) {
this.ast = code2ast(code);
}
}
_analysisAst() {
const { imports } = traverseFile(this.ast);
this.importList = imports;
}
}
@@ -0,0 +1,106 @@
import { getValue, isNil, logger, setValue } from '@music163/tango-helpers';
import type { IFileConfig } from '../types';
import { formatCode } from '../helpers';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractFile } from './abstract-file';
export abstract class AbstractJsonFile extends AbstractFile {
_object;
abstract get json(): object;
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false);
this._object = {};
this.update(props.code);
}
update(code?: string) {
this.lastModified = Date.now();
if (isNil(code)) {
// 基于最新的 json 同步代码
let newCode = JSON.stringify(this._object);
try {
newCode = formatCode(newCode, 'json');
} catch (err) {
logger.error(err);
return;
}
this._code = newCode;
this._cleanCode = newCode;
} else {
try {
// 基于传入的代码,同步 json 对象
code = formatCode(code, 'json');
} catch (err) {
logger.error(err);
return;
}
this._code = code;
this._cleanCode = code;
try {
const json = JSON.parse(code);
this._object = json;
} catch (err) {
logger.error(err);
}
}
this.workspace.onFilesChange([this.filename]);
}
/**
* 根据路径取值
* @param valuePath
* @returns
*/
getValue(valuePath: string) {
return getValue(this.json, valuePath);
}
/**
* 根据路径设置值
* @param valuePath
* @param visitor
*/
setValue(valuePath: string, visitor: (targetValue: any) => any) {
const target = this.getValue(valuePath);
let next: unknown;
if (typeof visitor === 'function') {
next = visitor?.(target);
} else {
next = visitor;
}
if (next !== undefined) {
setValue(this._object, valuePath, next);
}
return this;
}
/**
* 根据路径删除值
* @param valuePath
* @param visitor
*/
deleteValue(valuePath: string) {
const pathList = valuePath.split('.');
const lastPath = pathList.pop();
const parentPath = pathList.join('.');
let target;
if (parentPath) {
target = this.getValue(parentPath);
} else {
target = this.json;
}
if (!target) {
return this;
}
delete target[lastPath];
if (parentPath) {
this.setValue(parentPath, target);
} else {
this._object = target;
}
return this;
}
}
@@ -0,0 +1,59 @@
import { Dict } from '@music163/tango-helpers';
import { AbstractFile } from './abstract-file';
export interface IViewNodeInitConfig<RawNodeType = unknown, ViewFileType = AbstractFile> {
id: string;
component: string;
rawNode: RawNodeType;
file: ViewFileType;
}
export abstract class AbstractViewNode<RawNodeType = unknown, ViewFileType = AbstractFile> {
/**
* 节点 ID
*/
readonly id: string;
/**
* 节点对应的组件名
*/
readonly component: string;
readonly rawNode: RawNodeType;
/**
* 节点所属的文件对象
*/
file: ViewFileType;
/**
* 节点所属的文件对象
*/
props: Record<string, any>;
/**
* 节点的位置信息
*/
abstract get loc(): unknown;
constructor(props: IViewNodeInitConfig<RawNodeType, ViewFileType>) {
this.id = props.id;
this.component = props.component;
this.rawNode = props.rawNode;
this.file = props.file;
}
/**
* 销毁当前节点,清空文件和节点的关联关系
*/
destroy() {
this.file = undefined;
}
/**
* 返回克隆后的 ast 节点
* @param overrideProps 额外设置给克隆节点的属性
* @returns 返回克隆的原始节点
*/
abstract cloneRawNode(overrideProps?: Dict): RawNodeType;
}
File diff suppressed because it is too large Load Diff
+308
View File
@@ -0,0 +1,308 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { MenuDataType } from '@music163/tango-helpers';
import { AbstractWorkspace } from './abstract-workspace';
export type SimulatorNameType = 'desktop' | 'phone';
export type DesignerViewType = 'design' | 'code' | 'dual';
interface ISimulatorType {
name: SimulatorNameType;
width: number;
height: number;
}
interface IViewportBounding {
width: number;
height: number;
}
interface IDesignerOptions {
workspace: AbstractWorkspace;
simulator?: SimulatorNameType | ISimulatorType;
/**
* 菜单配置
*/
menuData: MenuDataType;
activeSidebarPanel?: string;
/**
* 默认激活的视图模式
*/
activeView?: DesignerViewType;
}
const ISimulatorTypes: Record<string, ISimulatorType> = {
desktop: {
name: 'desktop',
width: 1366,
height: 800,
},
phone: {
name: 'phone',
width: 375,
height: 812,
},
};
export class Designer {
/**
* 当前的沙箱模拟器类型
*/
_simulator: ISimulatorType = ISimulatorTypes.desktop;
/**
* 当前的视图尺寸
*/
_viewport: IViewportBounding = {
width: 1366,
height: 800,
};
/**
* 当前激活的视图
*/
_activeView: DesignerViewType = 'design';
/**
* 当前选中的侧边栏面板
*/
_activeSidebarPanel = '';
/**
* 是否显示智能引导
*/
_showSmartWizard = false;
/**
* 是否显示添加组件面板
*/
_showAddComponentPopover = false;
/**
* 添加组件面板的位置
*/
_addComponentPopoverPosition = { clientX: 0, clientY: 0 };
/**
* 是否显示右键菜单
*/
_showContextMenu = false;
/**
* 右键菜单在 iframe 上的位置
*/
_contextMenuPosition = { clientX: 0, clientY: 0 };
/**
* 是否显示右侧面板
*/
_showRightPanel = true;
/**
* 是否预览模式
*/
_isPreview = false;
/**
* 菜单列表
*/
_menuData?: MenuDataType = null;
private readonly workspace: AbstractWorkspace;
get simulator(): ISimulatorType {
return toJS(this._simulator);
}
get viewport() {
return toJS(this._viewport);
}
get activeView() {
return this._activeView;
}
get isPreview() {
return this._isPreview;
}
get showSmartWizard() {
return this._showSmartWizard;
}
get activeSidebarPanel() {
return this._activeSidebarPanel;
}
get showRightPanel() {
return this._showRightPanel;
}
get showAddComponentPopover() {
return this._showAddComponentPopover;
}
get addComponentPopoverPosition() {
return this._addComponentPopoverPosition;
}
get showContextMenu() {
return this._showContextMenu;
}
get contextMenuPosition() {
return this._contextMenuPosition;
}
get menuData() {
return this._menuData ?? ([] as MenuDataType);
}
constructor(options: IDesignerOptions) {
this.workspace = options.workspace;
const {
simulator,
menuData,
activeSidebarPanel: defaultActiveSidebarPanel,
activeView: defaultActiveView,
} = options;
if (menuData) {
this.setMenuData(menuData);
}
// 默认设计器模式
if (simulator) {
this.setSimulator(simulator);
}
// 默认展开的侧边栏
if (defaultActiveSidebarPanel) {
this.setActiveSidebarPanel(defaultActiveSidebarPanel);
}
// 默认激活的视图
if (defaultActiveView) {
this.setActiveView(defaultActiveView);
}
makeObservable(this, {
_simulator: observable,
_viewport: observable,
_activeView: observable,
_activeSidebarPanel: observable,
_showSmartWizard: observable,
_showRightPanel: observable,
_showAddComponentPopover: observable,
_addComponentPopoverPosition: observable,
_showContextMenu: observable,
_contextMenuPosition: observable,
_menuData: observable,
_isPreview: observable,
simulator: computed,
viewport: computed,
activeView: computed,
activeSidebarPanel: computed,
isPreview: computed,
showRightPanel: computed,
showSmartWizard: computed,
showAddComponentPopover: computed,
addComponentPopoverPosition: computed,
showContextMenu: computed,
contextMenuPosition: computed,
menuData: computed,
setSimulator: action,
setViewport: action,
setActiveView: action,
setActiveSidebarPanel: action,
closeSidebarPanel: action,
toggleRightPanel: action,
toggleSmartWizard: action,
toggleIsPreview: action,
toggleAddComponentPopover: action,
toggleContextMenu: action,
});
}
setSimulator(value: ISimulatorType | SimulatorNameType) {
if (typeof value === 'string') {
this._simulator = ISimulatorTypes[value];
} else {
this._simulator = value;
}
}
setViewport(value: IViewportBounding) {
this._viewport = value;
}
setActiveView(view: DesignerViewType) {
this._activeView = view;
if (view === 'dual') {
this.setActiveSidebarPanel('');
this.toggleRightPanel(false);
this.toggleIsPreview(true);
} else if (view === 'design') {
this.toggleIsPreview(false);
}
}
setActiveSidebarPanel(panel: string) {
if (panel && panel !== this.activeSidebarPanel) {
this._activeSidebarPanel = panel;
} else {
this._activeSidebarPanel = '';
}
}
setMenuData(menuData: MenuDataType) {
this._menuData = menuData;
}
closeSidebarPanel() {
this._activeSidebarPanel = '';
}
toggleSmartWizard(value: boolean) {
this._showSmartWizard = value;
}
toggleRightPanel(value?: boolean) {
this._showRightPanel = value ?? !this._showRightPanel;
}
/**
* 显示添加组件面板
* @param value 是否显示
* @param position 坐标
*/
toggleAddComponentPopover(
value: boolean,
position: {
clientX: number;
clientY: number;
} = this.addComponentPopoverPosition,
) {
this._showAddComponentPopover = value;
this._addComponentPopoverPosition = position;
}
toggleIsPreview(value: boolean) {
this._isPreview = value ?? !this._isPreview;
if (value) {
this.workspace.selectSource.clear();
}
}
toggleContextMenu(
value: boolean,
position: {
clientX: number;
clientY: number;
} = this.contextMenuPosition,
) {
this._showContextMenu = value;
this._contextMenuPosition = position;
}
}
+91
View File
@@ -0,0 +1,91 @@
import { action, computed, makeObservable, observable } from 'mobx';
import { ISelectedItemData } from '@music163/tango-helpers';
import { DropTarget } from './drop-target';
import { AbstractWorkspace } from './abstract-workspace';
/**
* 拖拽来源类,被拖拽的物体
*/
export class DragSource {
/**
* 是否处于拖拽状态
*/
isDragging: boolean;
/**
* 选中的目标元素数据
*/
data: ISelectedItemData;
/**
* 放置目标
*/
dropTarget: DropTarget;
private readonly workspace: AbstractWorkspace;
get node() {
return this.workspace.getNode(this.data?.id, this.data?.filename);
}
/**
* 获取对应的 prototype
*/
get prototype() {
return this.workspace.getPrototype(this.data?.name);
}
get id() {
return this.data?.id;
}
get name() {
return this.data?.name;
}
get bounding() {
return this.data?.bounding;
}
constructor(workspace: AbstractWorkspace) {
this.workspace = workspace;
this.data = null;
this.isDragging = false;
this.dropTarget = new DropTarget(workspace);
makeObservable(this, {
data: observable,
isDragging: observable,
set: action,
clear: action,
node: computed,
prototype: computed,
});
}
/**
* 更新选中数据
* @param props
*/
set(data: ISelectedItemData) {
this.data = data;
this.isDragging = !!data;
}
/**
* 重置
*/
clear() {
this.data = null;
this.isDragging = false;
this.dropTarget.clear();
}
/**
* 获取对应的 node
* @deprecated
*/
getNode() {
return this.node;
}
}
+84
View File
@@ -0,0 +1,84 @@
import { action, computed, makeObservable, observable } from 'mobx';
import { ISelectedItemData } from '@music163/tango-helpers';
import { AbstractWorkspace } from './abstract-workspace';
export enum DropMethod {
ReplaceNode = 'replaceNode', // 替换节点
InsertBefore = 'insertBefore', // 插入节点,放置在前面
InsertAfter = 'insertAfter', // 插入节点,放置在后面
InsertChild = 'insertChild', // 插入子节点,放置在最后
InsertFirstChild = 'insertFirstChild', // 插入子节点,放置在最前
}
/**
* 放置目标类
*/
export class DropTarget {
/**
* 插入方法
*/
method: DropMethod;
/**
* 放置的目标元素数据
*/
data: ISelectedItemData;
private readonly workspace: AbstractWorkspace;
get node() {
return this.workspace.getNode(this.data.id, this.data.filename);
}
/**
* 获取对应的 prototype
*/
get prototype() {
return this.data?.name ? this.workspace.getPrototype(this.data?.name) : null;
}
get id() {
return this.data?.id;
}
get bounding() {
return this.data?.bounding;
}
get display() {
return this.data?.display;
}
constructor(workspace: AbstractWorkspace) {
this.workspace = workspace;
this.method = DropMethod.InsertAfter;
this.data = null;
makeObservable(this, {
method: observable,
data: observable,
set: action,
clear: action,
node: computed,
});
}
set(data: ISelectedItemData, method: DropMethod) {
this.data = data;
this.method = method;
}
/**
* 重置
*/
clear() {
this.data = null;
}
/**
* 获取对应的 node
* @deprecated
*/
getNode() {
return this.node;
}
}
+21
View File
@@ -0,0 +1,21 @@
import { AbstractWorkspace } from './abstract-workspace';
import { Designer } from './designer';
/**
* 设计器引擎
*/
export class Engine {
/**
* 工作区状态
*/
workspace: AbstractWorkspace;
/**
* 设计器状态
*/
designer: Designer;
constructor(options: Pick<Engine, 'workspace' | 'designer'>) {
this.workspace = options.workspace;
this.designer = options.designer;
}
}
+28
View File
@@ -0,0 +1,28 @@
import { action, computed, makeObservable, observable } from 'mobx';
import { isNil } from '@music163/tango-helpers';
import type { IFileConfig } from '../types';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractFile } from './abstract-file';
export class TangoFile extends AbstractFile {
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code);
makeObservable(this, {
_code: observable,
_cleanCode: observable,
code: computed,
cleanCode: computed,
update: action,
});
}
update(code?: string) {
if (!isNil(code)) {
this.lastModified = Date.now();
this._code = code;
this._cleanCode = code;
}
this.workspace.onFilesChange([this.filename]);
}
}
+144
View File
@@ -0,0 +1,144 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { AbstractWorkspace } from './abstract-workspace';
export enum HistoryMessage {
InitView = 'initView',
AddFile = 'addFile',
RemoveFile = 'removeFile',
UpdateDependency = 'updateDependency',
RemoveDependency = 'removeDependency',
RemoveNode = 'removeNode',
ReplaceNode = 'replaceNode',
CloneNode = 'cloneNode',
InsertNode = 'insertNode',
InsertBeforeNode = 'insertBeforeNode',
InsertAfterNode = 'insertAfterNode',
DropNode = 'dropNode',
UpdateAttribute = 'updateAttribute',
UpdateCode = 'updateCode',
}
type HistoryRecordData = {
[filename: string]: string;
};
interface HistoryRecord {
time: number;
message: HistoryMessage;
data: HistoryRecordData;
}
type PushDataType = Pick<HistoryRecord, 'message' | 'data'>;
/**
* 工作区的历史记录记录
*/
export class TangoHistory {
// 历史记录
_records: HistoryRecord[] = [];
// 当前记录指针
_index = 0;
// 最多记录数
_maxSize = 100;
private readonly workspace: AbstractWorkspace;
get index() {
return this._index;
}
get length() {
return this._records.length;
}
get list() {
return toJS(this._records);
}
get couldBack() {
return this._records.length > 0 && this._index > 0;
}
get couldForward() {
return this._records.length > this._index + 1;
}
constructor(workspace: AbstractWorkspace) {
this.workspace = workspace;
makeObservable(this, {
_records: observable,
_index: observable,
back: action,
forward: action,
go: action,
push: action,
couldBack: computed,
couldForward: computed,
});
}
_sync(data: HistoryRecordData) {
if (data) {
Object.keys(data).forEach((filename) => {
this.workspace.getFile(filename).update(data[filename]);
});
}
}
/**
* 上一步
*/
back() {
if (this.couldBack) {
const item = this._records[this._index - 1];
this._sync(item.data);
this._index--;
}
}
/**
* 下一步
*/
forward() {
if (this.couldForward) {
const item = this._records[this._index + 1];
this._sync(item.data);
this._index++;
}
}
/**
* 通过相对位置从历史记录加载记录
*/
go(index: number) {
const item = this._records[index];
if (item) {
this._sync(item.data);
this._index = index;
}
}
/**
* push 数据进入历史记录堆栈
*/
push(data: PushDataType) {
if (this._index < this._records.length - 1) {
this._records = this._records.slice(0, this._index + 1);
}
this._index = this._records.length;
this._records.push({
time: Date.now(),
...data,
});
const overCount = this._records.length - this._maxSize;
if (overCount > 0) {
this._records.splice(0, overCount);
this._index = this._records.length - 1;
}
}
}
+25
View File
@@ -0,0 +1,25 @@
export * from './abstract-workspace';
export * from './abstract-code-workspace';
export * from './abstract-file';
export * from './abstract-js-file';
export * from './abstract-json-file';
export * from './abstract-view-node';
export * from './designer';
export * from './drag-source';
export * from './drop-target';
export * from './engine';
export * from './file';
export * from './history';
export * from './interfaces';
export * from './js-app-entry-file';
export * from './js-file';
export * from './js-local-components-entry-file';
export * from './js-route-config-file';
export * from './js-service-file';
export * from './js-store-entry-file';
export * from './js-store-file';
export * from './js-view-file';
export * from './json-file';
export * from './select-source';
export * from './view-node';
export * from './workspace';
+81
View File
@@ -0,0 +1,81 @@
import { Dict } from '@music163/tango-helpers';
import {
InsertChildPositionType,
IImportSpecifierSourceData,
IImportSpecifierData,
} from '../types';
import { IdGenerator } from '../helpers';
import { AbstractViewNode } from './abstract-view-node';
export interface IViewFile {
/**
* 文件名
*/
filename: string;
/**
* ID 生成器
*/
idGenerator: IdGenerator;
/**
* 通过导入组件名查找组件来自的包
*/
importMap?: Dict<IImportSpecifierSourceData>;
/**
* 判断节点是否存在
* @param codeId 节点 ID
* @returns 存在返回 true,否则返回 false
*/
hasNodeByCodeId?: (codeId: string) => boolean;
listImportSources?: () => string[];
listModals?: () => Array<{ label: string; value: string }>;
listForms?: () => Record<string, string[]>;
update: (code?: string, isFormatCode?: boolean, refreshWorkspace?: boolean) => void;
/**
* 添加新的导入符号
* @param source 导入来源
* @param newSpecifiers 新导入符号列表
* @returns this
*/
addImportSpecifiers: (source: string, newSpecifiers: IImportSpecifierData[]) => IViewFile;
getNode: (targetNodeId: string) => AbstractViewNode;
removeNode: (targetNodeId: string) => this;
insertChild: (targetNodeId: string, newNode: any, position?: InsertChildPositionType) => this;
insertAfter: (targetNodeId: string, newNode: any) => this;
insertBefore: (targetNodeId: string, newNode: any) => this;
replaceNode: (targetNodeId: string, newNode: any) => this;
replaceViewChildren: (rawNodes: any[]) => this;
updateNodeAttribute: (
nodeId: string,
attrName: string,
attrValue?: any,
relatedImports?: string[],
) => this;
updateNodeAttributes: (
nodeId: string,
config: Record<string, any>,
relatedImports?: string[],
) => this;
get nodes(): Map<string, AbstractViewNode>;
get nodesTree(): object[];
get tree(): any;
/**
* 文件中的代码
*/
get code(): string;
}
@@ -0,0 +1,18 @@
import { traverseEntryFile } from '../helpers';
import { IFileConfig } from '../types';
import { AbstractJsFile } from './abstract-js-file';
import { AbstractWorkspace } from './abstract-workspace';
export class JsAppEntryFile extends AbstractJsFile {
routerType: string;
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
}
_analysisAst() {
const config = traverseEntryFile(this.ast);
this.routerType = config?.router?.type;
}
}
+25
View File
@@ -0,0 +1,25 @@
import { action, computed, makeObservable, observable } from 'mobx';
import { IFileConfig } from '../types';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractJsFile } from './abstract-js-file';
/**
* 普通 JS 文件
*/
export class JsFile extends AbstractJsFile {
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
_code: observable,
_cleanCode: observable,
isError: observable,
errorMessage: observable,
code: computed,
cleanCode: computed,
update: action,
updateAst: action,
});
}
}
@@ -0,0 +1,40 @@
import path from 'path';
import { action, computed, makeObservable, observable } from 'mobx';
import { IExportSpecifierData, IFileConfig } from '../types';
import { traverseComponentsEntryFile } from '../helpers';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractJsFile } from './abstract-js-file';
/**
* 本地组件目录的入口文件,例如 '/components/index.js' 或 `/blocks/index.js`
*/
export class JsLocalComponentsEntryFile extends AbstractJsFile {
exportList: Record<string, IExportSpecifierData>;
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
_code: observable,
_cleanCode: observable,
exportList: observable,
code: computed,
cleanCode: computed,
update: action,
});
}
_analysisAst() {
const baseDir = path.dirname(this.filename);
const { exportMap } = traverseComponentsEntryFile(this.ast, baseDir);
this.exportList = exportMap;
Object.keys(this.exportList).forEach((key) => {
this.workspace.componentPrototypes.set(key, {
name: key,
exportType: 'namedExport',
package: baseDir,
type: 'element',
});
});
}
}

Some files were not shown because too many files have changed in this diff Show More