mirror of
https://github.com/NetEase/tango.git
synced 2026-08-31 09:30:28 +08:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c48a49961a | |||
| eae45003c6 | |||
| 1802fc0eb8 | |||
| 8a3ab3c093 | |||
| 55602fc961 | |||
| a3c0e463ca | |||
| 79f9a8c179 | |||
| ea96872261 | |||
| a86e0e049c | |||
| c4e1ed67f5 | |||
| 179de0cc52 | |||
| 6de54884b5 | |||
| 55bf417ddf | |||
| ca56edd9ae | |||
| 65149f1326 | |||
| 939466648d | |||
| 8fe57333a0 | |||
| 269e304d68 | |||
| 8c80255f81 | |||
| 359aa0b148 | |||
| 51ca4a72ce | |||
| b83d965955 | |||
| a1eea02f94 | |||
| 0d0479e3e5 | |||
| 67d602a3ec | |||
| 8dd289cd7d | |||
| 82bc5db4d8 | |||
| 85c053b3db | |||
| 32f5667cae | |||
| 1d0fb4f274 | |||
| 0011cdca0f | |||
| 0f3f0afdfa | |||
| b53a9f7eef | |||
| 5336c2ec28 | |||
| cdbd2351a2 | |||
| 1f03977f7d | |||
| a2cc418a73 |
@@ -9,3 +9,4 @@ coverage/
|
||||
.umi-production/
|
||||
.log
|
||||
.eslintcache
|
||||
*.pem
|
||||
|
||||
@@ -31,23 +31,17 @@ You can view the detailed usage guide through the following links:
|
||||
- Out-of-the-box front-end low-code designer, providing flexible and easy-to-use designer React components
|
||||
- Developed using TypeScript, providing complete type definition files
|
||||
|
||||
## 🎯 Milestone
|
||||
## 💡 Examples
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
dateFormat YYYY-MM-DD
|
||||
title Tango 1.0 Milestones
|
||||
excludes weekends
|
||||
%% (`excludes` accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays".)
|
||||
You can use tango to build lowcode builders, for example:
|
||||
|
||||
section Builder Release
|
||||
1.0 Alpha : m1, 2023-10-01, 2024-01-31
|
||||
1.0 RC : m2, 2024-02-01, 2024-04-30
|
||||
1.0 Stable : m3, 2024-04-30, 2024-12-31
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Alpha versions will continue to optimize some implementation protocols and continuously integrate new features from the internal version of NetEase; RC versions will provide a stable parsing protocol and implementation; The 1.0 official version is planned to be released in Q2 2024. After the release of the 1.0 official version, we will continue to iterate on versions and gradually incorporate core capabilities such as process design, component library, and AIGC from the internal version.
|
||||
| Preview | Description |
|
||||
| ---------------------------------------------------------------------------------------------- | ----------------------- |
|
||||
|  | Internal App Builder |
|
||||
|  | Dashboard App Builder |
|
||||
|  | Mobile App Builder |
|
||||
|  | ReactNative App Builder |
|
||||
|  | Email Builder |
|
||||
|
||||
## 🌐 Compatibility
|
||||
|
||||
@@ -76,6 +70,23 @@ yarn
|
||||
yarn start
|
||||
```
|
||||
|
||||
### Local Https certificate
|
||||
|
||||
If you need to use https in the local development environment, you can use the following command to generate a certificate:
|
||||
|
||||
```bash
|
||||
brew install mkcert
|
||||
|
||||
# install the local certificate
|
||||
mkcert -install
|
||||
|
||||
# enter the playground app directory
|
||||
cd apps/playground
|
||||
|
||||
# generate a certificate
|
||||
mkcert local.netease.com # or change to your own domain name
|
||||
```
|
||||
|
||||
## 💬 Community
|
||||
|
||||
Join NetEase Tango Community to share your ideas, suggestions, or questions and connect with other users and contributors.
|
||||
|
||||
+30
-19
@@ -26,28 +26,22 @@
|
||||
## ✨ 特性
|
||||
|
||||
- 经历网易云音乐内网生产环境的实际检验,可灵活集成应用于低代码平台,本地开发工具等
|
||||
- 基于源码 AST 驱动,无私有 DSL 和协议
|
||||
- 提供实时出码能力,支持源码进,源码出
|
||||
- 开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件
|
||||
- 使用 TypeScript 开发,提供完整的类型定义文件
|
||||
- 提供基于源码 AST 驱动的低代码引擎,无私有 DSL 和协议
|
||||
- 提供实时出码能力,支持源码进,源码出,适配多种源码开发场景
|
||||
- 提供开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件
|
||||
- 使用 TypeScript 开发,提供完整的类型定义文件和完善的文档支持
|
||||
|
||||
## 🎯 里程碑
|
||||
## 💡 例子
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
dateFormat YYYY-MM-DD
|
||||
title Tango 1.0 Milestones
|
||||
excludes weekends
|
||||
%% (`excludes` accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays".)
|
||||
你可以使用 Tango 快速构建多种类型的基于源码的低代码搭建工具,例如:
|
||||
|
||||
section Builder Release
|
||||
1.0 Alpha : m1, 2023-10-01, 2024-01-31
|
||||
1.0 RC : m2, 2024-02-01, 2024-04-30
|
||||
1.0 Stable : m3, 2024-04-30, 2024-12-31
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Alpha 版本会持续对一些实现协议进行优化,并持续融合网易内网版本中的新增特性;RC 版本将会提供基本稳定的解析协议和实现;1.0 正式版本计划在 2024 年Q2发布。1.0 正式版发布后我们将会持续进行版本迭代,并逐步融入内网版本中的流程设计、组件库、AIGC 等核心基础能力。
|
||||
| 预览图 | 说明 |
|
||||
| ---------------------------------------------------------------------------------------------- | -------------------- |
|
||||
|  | 中后台系统搭建 |
|
||||
|  | 仪表盘应用搭建 |
|
||||
|  | H5活动页面搭建 |
|
||||
|  | ReactNative 应用搭建 |
|
||||
|  | 营销类邮件搭建 |
|
||||
|
||||
## 🌐 兼容环境
|
||||
|
||||
@@ -76,6 +70,23 @@ yarn
|
||||
yarn start
|
||||
```
|
||||
|
||||
### 本地 https 证书
|
||||
|
||||
如果需要在本地开发环境中使用 https,可以使用以下命令生成证书:
|
||||
|
||||
```bash
|
||||
brew install mkcert
|
||||
|
||||
# 将 mkcert 添加到本地根 CA,仅在本地生效
|
||||
mkcert -install
|
||||
|
||||
# 进入 playground 应用目录
|
||||
cd apps/playground
|
||||
|
||||
# 为网站生成一个由 mkcert 签名的证书
|
||||
mkcert local.netease.com
|
||||
```
|
||||
|
||||
## 💬 社区讨论
|
||||
|
||||
参与 NetEase Tango 的社区,以分享您的想法、建议或问题,并与其他用户和贡献者建立联系。
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC4sAqDE3nhtSWD
|
||||
YqV4mlglnzIs2AioZr2uxfj/OKrpE1ZML/dflJyc5NGUpOZWV+W1vRc70wWVNFxB
|
||||
Si8nrYrrseoID7Ehxxe6+J1m/JsEbhZWHwV3M1iGW7JD7VhujaDpSLg67wMTMpSW
|
||||
3BUS+0NT+MNfCisp+u/ls4fXPCTgXiWcjMULm95oLlTeYwurPHfxwn9f9hh85I5I
|
||||
K3dI7w8MsQN7EFNrJz3UfrNLpmARMtSUBMwBYqrCitoUxRT0q7KYCru7kHxGPB6A
|
||||
SQrFpSpOWT79vp8GBSp9g3axhRsK+aEgzQ0QGrFPHBwBP/kWgmdB1URc/K7STSC9
|
||||
xyMNZOM7AgMBAAECggEATbyDYDKVbaRymr+tyHqmuYctdmSkGEXOdz8LFFoEzj/+
|
||||
ZekWpVuHJB7H/FoAPb2XiYyCuAKVafz1C+IuwPOx8o0bZbM9KmuDmIZGsm+GzDGO
|
||||
I0fBZC/vhfYYrbC8NSRV43thWCT8VVtH4cvW7vtllnWxvlIoYu4lhbiwZ68AEpO1
|
||||
8PlJsOhnagfkFpG8LehSDs9if5L+Ig0geQEcUY3pGUM9LKYsRoxFv6Vh8wQku1XD
|
||||
YOxLvLWDPqJ75m816t50RARe3e6eDJ+ukzDCoDjmLP8uhf1sk1FcL5aOgUNU3VOi
|
||||
P6sIH5uzuD0MA3FXx2mItYLOc44r4uaFJZiRUMEXGQKBgQDCjUYblE4rh0nKFkqx
|
||||
AhgfcRgpgLbP42OwFtD0u20RXdZAEJDRDQ43NdOgjFp2B50k75LBPIoFXYUZdzKj
|
||||
l7S2FH5O9Cc6hEgZGI4hCiYd+nQVI1mxv820YnzZJN4oooxyL+B5RlnW5cDT7LJN
|
||||
11Q5PRewkM+XpfbE3lKUjTNcDwKBgQDzBS+j8T9GJSzV2xgA+mpGHe1RjDq6yQtk
|
||||
Rl5GWpflW4Nr3/58BbmCWNA8WP0NOWutJhrkltwLCqJC8OZX/WHnmcmq2k+UAfQQ
|
||||
z98p+xcMWOTcl5XIbveuhYoZb2udt9WVJLcpRVxVhEWcBrtHsrpScZXsKYf/TaZ1
|
||||
JtCjPU5KFQKBgGXj/UqmYkYzxXZ8NEP998pHvcLGsXew/F2VpXv2yQlmXrYQtvd1
|
||||
YfOSUjJsL1hPZoKYogBoB1UGBJYwsimxYyhVFU2eWwkvjF7wWEd/fDUJHVCQwgHw
|
||||
lPga+I303BDmCK4o2uRI7vY4P8P92+gelPKhR03mmYIvnky+rvsz9CkLAoGAVerl
|
||||
du6Z0jDecxUsnVvrKrL17jjHorXlYyRFvFXXEe2SvxbIIAzeEylXJZ7IiF5epS6t
|
||||
n1d+oCC4UTZeGYmpjXofhFn+fHNcWH1FhtAQy/q5nwuwltY0luz7cBamU3Jk/n+m
|
||||
id7N0CYdfhYbLDQSD4e822ureyV2zkBUzwGjpikCgYEAoCVbhkxVrQHRmxLC2jDx
|
||||
pM94W1ij9TqblfGg3OMpPXjpTdlknatWjBw7JkoyxYiYJq5Gi2p4QV6HHUvS1osj
|
||||
BB+4A99/PD9A0ICoJglz8SXlGe/895zwHwpKT9f0zjizQrEd2KCPf75URa+t2FGY
|
||||
B2GkXsbChRWVrey5+uMzUD8=
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -1,25 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEQjCCAqqgAwIBAgIRAIeLJ2jwt4MSKycHFmFXL6YwDQYJKoZIhvcNAQELBQAw
|
||||
fTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSkwJwYDVQQLDCB3d3N1
|
||||
bkBzdW4tbWJwLmxvY2FsIChTVU4gV2Vpd2VpKTEwMC4GA1UEAwwnbWtjZXJ0IHd3
|
||||
c3VuQHN1bi1tYnAubG9jYWwgKFNVTiBXZWl3ZWkpMB4XDTIyMDcyODAxNTIxMFoX
|
||||
DTI0MTAyODAxNTIxMFowVDEnMCUGA1UEChMebWtjZXJ0IGRldmVsb3BtZW50IGNl
|
||||
cnRpZmljYXRlMSkwJwYDVQQLDCB3d3N1bkBzdW4tbWJwLmxvY2FsIChTVU4gV2Vp
|
||||
d2VpKTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALiwCoMTeeG1JYNi
|
||||
pXiaWCWfMizYCKhmva7F+P84qukTVkwv91+UnJzk0ZSk5lZX5bW9FzvTBZU0XEFK
|
||||
Lyetiuux6ggPsSHHF7r4nWb8mwRuFlYfBXczWIZbskPtWG6NoOlIuDrvAxMylJbc
|
||||
FRL7Q1P4w18KKyn67+Wzh9c8JOBeJZyMxQub3mguVN5jC6s8d/HCf1/2GHzkjkgr
|
||||
d0jvDwyxA3sQU2snPdR+s0umYBEy1JQEzAFiqsKK2hTFFPSrspgKu7uQfEY8HoBJ
|
||||
CsWlKk5ZPv2+nwYFKn2DdrGFGwr5oSDNDRAasU8cHAE/+RaCZ0HVRFz8rtJNIL3H
|
||||
Iw1k4zsCAwEAAaNmMGQwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUF
|
||||
BwMBMB8GA1UdIwQYMBaAFDILZR+SW32Ug0HkAKrH1KT2a1ttMBwGA1UdEQQVMBOC
|
||||
EWxvY2FsLm5ldGVhc2UuY29tMA0GCSqGSIb3DQEBCwUAA4IBgQCEdeV2Likl4MUt
|
||||
wWeN/X3tAR3L7mkl7e4f0CQ4v2lOs8cekOP7p3ZFk5nSyVbxda3usPB0OtbVTibw
|
||||
qZ1n/TMRb5epHroXIMleqngP82zFZNg+WVyHm/ZAml8gVD0VZjYM1Tb+U9+SrgGw
|
||||
lvZndbuUXa3h/d0irPH90KsdEeAR1SqwzAzvLO2JgvBuimHRceQWcTiiGWDZOyii
|
||||
tux4U+pEzh0Dia40uFozFAoYZPDUpZ+gsiQ3MR3HOrAu2OR0a+CfelDXZNLR3C4z
|
||||
cBKr53ajAbHstnpxKJh9IfeKph44PrbDo6HMe1XPprl7idoEesX3Ma3SBgm4lmId
|
||||
cfEOUWNIYOgtOT5bflXNNyzXOgYVjpLnjwg4EOASO/XIjhvqdjIyCu7HJMIcZ9oo
|
||||
Aob2exeluv5LVwic1UJQDl2McDBIgJ8R32oEN4skCKIBSbWlxWyjnxL5I5WkJF4y
|
||||
vlSx8bKAjVr8map0C3rWISuZdgheJkF96qjwhVSxOv++p6srq6M=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Box, Group } from 'coral-system';
|
||||
import { Box, Group, Text } from 'coral-system';
|
||||
import { Avatar, Space, Switch } from 'antd';
|
||||
import { BranchesOutlined, MenuOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { registerSetter } from '@music163/tango-designer';
|
||||
import type { IVariableTreeNode } from '@music163/tango-helpers';
|
||||
import { FooSetter } from '../components';
|
||||
import { ToggleButton } from '@music163/tango-ui';
|
||||
|
||||
export * from './mock-files';
|
||||
|
||||
@@ -46,8 +47,17 @@ registerSetter({
|
||||
// 平台 Logo
|
||||
export function Logo() {
|
||||
return (
|
||||
<Box width="50px" display="flex" alignItems="center" justifyContent="center" fontSize="20px">
|
||||
<MenuOutlined />
|
||||
<Box width="50px" display="flex" alignItems="center" justifyContent="center" fontSize="14px">
|
||||
<ToggleButton
|
||||
shape="text"
|
||||
items={[
|
||||
{ label: '回到首页', key: 'home' },
|
||||
{ label: '文档', key: 'doc' },
|
||||
{ label: '设置', key: 'setting' },
|
||||
]}
|
||||
>
|
||||
<MenuOutlined />
|
||||
</ToggleButton>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -56,12 +66,12 @@ export function Logo() {
|
||||
export function ProjectDetail() {
|
||||
return (
|
||||
<Box display="flex" alignItems="center" columnGap="l">
|
||||
<Box className="ProjectName" fontSize="18px" fontWeight="bold">
|
||||
<Text className="ProjectName" fontSize="18px" fontWeight="bold" truncated>
|
||||
community-test
|
||||
</Box>
|
||||
<Box className="BranchName" as="code" fontSize="14px">
|
||||
</Text>
|
||||
<Text className="BranchName" as="code" fontSize="14px" truncated>
|
||||
<BranchesOutlined /> feature/list
|
||||
</Box>
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ const packageJson = {
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@music163/antd': '0.2.5',
|
||||
'@music163/tango-boot': '0.2.5',
|
||||
'@music163/tango-boot': '0.3.5',
|
||||
react: '17.0.2',
|
||||
'react-dom': '17.0.2',
|
||||
'prop-types': '15.7.2',
|
||||
@@ -48,7 +48,7 @@ const tangoConfigJson = {
|
||||
},
|
||||
'@music163/tango-boot': {
|
||||
description: '云音乐低代码运行时框架',
|
||||
version: '0.2.5',
|
||||
version: '0.3.5',
|
||||
library: 'TangoBoot',
|
||||
type: 'baseDependency',
|
||||
resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'],
|
||||
@@ -153,6 +153,7 @@ import { definePage } from "@music163/tango-boot";
|
||||
import {
|
||||
Page,
|
||||
Section,
|
||||
Box,
|
||||
Button,
|
||||
Input,
|
||||
FormilyForm,
|
||||
@@ -166,8 +167,10 @@ import { OutButton } from "../components";
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Page title={tango.stores.app.title} subTitle={111}>
|
||||
<Section tid="section0" />
|
||||
<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} />
|
||||
@@ -374,13 +377,16 @@ export default defineServices({
|
||||
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: '删除用户'
|
||||
},
|
||||
});
|
||||
`;
|
||||
|
||||
@@ -48,7 +48,6 @@ const Snippet3ColumnLayout: IComponentPrototype = {
|
||||
icon: 'icon-column-3',
|
||||
type: 'snippet',
|
||||
package: '@music163/antd',
|
||||
// FIXME: Column 组件 需要更新为 defineComponent
|
||||
initChildren: `
|
||||
<Columns columns={12}>
|
||||
<Column colSpan={4}>
|
||||
@@ -132,11 +131,14 @@ export const nativeDomPrototypes = () => {
|
||||
title: '样式',
|
||||
group: 'style',
|
||||
setter: 'expressionSetter',
|
||||
setterProps: {
|
||||
expressionType: 'cssObject',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
title: '类名',
|
||||
setter: 'textSetter',
|
||||
setter: 'classNameSetter',
|
||||
},
|
||||
{
|
||||
name: 'id',
|
||||
@@ -166,6 +168,38 @@ const prototypes: Dict<IComponentPrototype> = {
|
||||
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: '盒子',
|
||||
@@ -197,6 +231,38 @@ const prototypes: Dict<IComponentPrototype> = {
|
||||
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: {
|
||||
|
||||
@@ -20,12 +20,11 @@ import {
|
||||
ApiOutlined,
|
||||
AppstoreAddOutlined,
|
||||
BuildOutlined,
|
||||
ClusterOutlined,
|
||||
FunctionOutlined,
|
||||
PlusOutlined,
|
||||
createFromIconfontCN,
|
||||
} from '@ant-design/icons';
|
||||
import { Action } from '@music163/tango-ui';
|
||||
import { Action, PackageOutlined } from '@music163/tango-ui';
|
||||
import { useState } from 'react';
|
||||
|
||||
const menuData = {
|
||||
@@ -114,8 +113,8 @@ export default function App() {
|
||||
actions={
|
||||
<Box px="l">
|
||||
<Toolbar>
|
||||
<Toolbar.Item key="routeSwitch" placement="left" />
|
||||
<Toolbar.Item key="addPage" placement="left">
|
||||
<Toolbar.Item key="routeSwitch" placement="left" activeViews={['design']} />
|
||||
<Toolbar.Item key="addPage" placement="left" activeViews={['design']}>
|
||||
<Action
|
||||
tooltip="添加页面"
|
||||
shape="outline"
|
||||
@@ -123,10 +122,10 @@ export default function App() {
|
||||
onClick={() => setShowNewPageModal(true)}
|
||||
/>
|
||||
</Toolbar.Item>
|
||||
<Toolbar.Item key="history" placement="left" />
|
||||
<Toolbar.Item key="preview" placement="left" />
|
||||
<Toolbar.Item key="history" placement="left" activeViews={['design']} />
|
||||
<Toolbar.Item key="preview" placement="left" activeViews={['design']} />
|
||||
<Toolbar.Item key="togglePanel" placement="right" activeViews={['design']} />
|
||||
<Toolbar.Item key="modeSwitch" placement="right" />
|
||||
<Toolbar.Item key="togglePanel" placement="right" />
|
||||
<Toolbar.Separator />
|
||||
<Toolbar.Item placement="right">
|
||||
<Space>
|
||||
@@ -182,12 +181,15 @@ export default function App() {
|
||||
<Sidebar.Item
|
||||
key="dependency"
|
||||
label="依赖"
|
||||
icon={<ClusterOutlined />}
|
||||
icon={<PackageOutlined />}
|
||||
isFloat
|
||||
width={800}
|
||||
/>
|
||||
</Sidebar>
|
||||
<WorkspacePanel>
|
||||
<WorkspaceView mode="code">
|
||||
<CodeEditor />
|
||||
</WorkspaceView>
|
||||
<WorkspaceView mode="design">
|
||||
<Sandbox
|
||||
onMessage={(e) => {
|
||||
@@ -209,9 +211,6 @@ export default function App() {
|
||||
navigatorExtra={<Button size="small">hello world</Button>}
|
||||
/>
|
||||
</WorkspaceView>
|
||||
<WorkspaceView mode="code">
|
||||
<CodeEditor />
|
||||
</WorkspaceView>
|
||||
</WorkspacePanel>
|
||||
<SettingPanel />
|
||||
</DesignerPanel>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SystemProvider } from 'coral-system';
|
||||
import 'antd/dist/antd.css';
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
actions: { argTypesRegex: '^on.*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"dependencies": {
|
||||
"@music163/tango-setting-form": "*",
|
||||
"@music163/tango-ui": "*",
|
||||
"mobx": "6.12.3",
|
||||
"mobx": "6.13.2",
|
||||
"mobx-react-lite": "4.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -69,6 +69,14 @@ const prototypeHasBasicProps: IComponentPrototype = {
|
||||
title: 'codeSetter',
|
||||
setter: 'codeSetter',
|
||||
},
|
||||
{
|
||||
name: 'style',
|
||||
title: 'codeSetter(cssObject)',
|
||||
setter: 'codeSetter',
|
||||
setterProps: {
|
||||
expressionType: 'cssObject',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'text',
|
||||
title: 'textSetter',
|
||||
@@ -79,6 +87,11 @@ const prototypeHasBasicProps: IComponentPrototype = {
|
||||
title: 'textAreaSetter',
|
||||
setter: 'textAreaSetter',
|
||||
},
|
||||
{
|
||||
name: 'src',
|
||||
title: 'imageSetter',
|
||||
setter: 'imageSetter',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
title: 'numberSetter',
|
||||
@@ -263,6 +276,9 @@ export function InitValues() {
|
||||
name: 'style',
|
||||
title: 'codeSetter',
|
||||
setter: 'codeSetter',
|
||||
setterProps: {
|
||||
expressionType: 'cssObject',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'object',
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { ActionSelect } from '@music163/tango-ui';
|
||||
|
||||
export default {
|
||||
title: 'UI/ActionSelect',
|
||||
component: ActionSelect,
|
||||
};
|
||||
|
||||
const options = [
|
||||
{ label: 'action1', value: 'action1' },
|
||||
{ label: 'action2', value: 'action2' },
|
||||
{ label: 'action3', value: 'action3' },
|
||||
];
|
||||
|
||||
export const Basic = {
|
||||
args: {
|
||||
defaultText: '选择动作',
|
||||
options,
|
||||
onSelect: console.log,
|
||||
},
|
||||
};
|
||||
|
||||
export const showInput = {
|
||||
args: {
|
||||
text: '选择动作',
|
||||
options,
|
||||
showInput: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,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} />;
|
||||
}
|
||||
@@ -9,15 +9,34 @@ export default {
|
||||
|
||||
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>点击唤起浮层</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,3 +60,23 @@ export function FooterCustom() {
|
||||
</DragPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export function ResizeablePanel() {
|
||||
return (
|
||||
<DragPanel
|
||||
title="弹层标题"
|
||||
resizeable
|
||||
extra="右上角内容"
|
||||
width={350}
|
||||
height={400}
|
||||
body={
|
||||
<Box p="m" textAlign="center" background="#fa8484">
|
||||
内容
|
||||
</Box>
|
||||
}
|
||||
footer={<Text>底部信息</Text>}
|
||||
>
|
||||
<Button>点击唤起浮层</Button>
|
||||
</DragPanel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Action, InputCode } from '@music163/tango-ui';
|
||||
import { Action, InputCode, InputStyleCode } from '@music163/tango-ui';
|
||||
import { BlockOutlined } from '@ant-design/icons';
|
||||
|
||||
export default {
|
||||
@@ -28,6 +28,10 @@ export function Basic() {
|
||||
);
|
||||
}
|
||||
|
||||
export function CSS() {
|
||||
return <InputStyleCode enableESLint suffix={<Action icon={<BlockOutlined />} size="small" />} />;
|
||||
}
|
||||
|
||||
const code = `
|
||||
function foo() {
|
||||
console.log("test");
|
||||
|
||||
+8
-8
@@ -15,17 +15,17 @@
|
||||
"test:watch": "jest --watch",
|
||||
"eslint": "eslint packages/**/src/*.{ts,tsx} --cache",
|
||||
"publish": "lerna publish",
|
||||
"ver": "lerna version --no-private",
|
||||
"release": "yarn eslint && yarn build && lerna version --no-private --conventional-commits && lerna publish from-git",
|
||||
"release:npm": "yarn eslint && yarn build && yarn run ver && lerna publish from-git",
|
||||
"release:beta": "yarn eslint && yarn build && yarn run ver && lerna publish from-package --dist-tag beta",
|
||||
"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.23.6",
|
||||
"@babel/preset-react": "^7.23.3",
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@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",
|
||||
@@ -58,7 +58,7 @@
|
||||
"react": "^17.0.0",
|
||||
"react-dom": "^17.0.0",
|
||||
"styled-components": "^5.3.6",
|
||||
"typedoc": "^0.25.4",
|
||||
"typedoc": "^0.26.7",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -3,6 +3,36 @@
|
||||
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,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-context",
|
||||
"version": "1.1.3",
|
||||
"version": "1.1.10",
|
||||
"description": "react context for tango-apps",
|
||||
"keywords": [
|
||||
"react",
|
||||
@@ -31,8 +31,8 @@
|
||||
"react": ">= 16.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@music163/tango-core": "^1.3.1",
|
||||
"@music163/tango-helpers": "^1.2.0",
|
||||
"@music163/tango-core": "^1.4.4",
|
||||
"@music163/tango-helpers": "^1.2.4",
|
||||
"mobx-react-lite": "4.0.7"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Engine } from '@music163/tango-core';
|
||||
import type { AbstractCodeWorkspace, Engine } from '@music163/tango-core';
|
||||
import { IVariableTreeNode, createContext } from '@music163/tango-helpers';
|
||||
|
||||
export interface ITangoEngineContext {
|
||||
@@ -21,8 +21,12 @@ const [TangoEngineProvider, useTangoEngine] = createContext<ITangoEngineContext>
|
||||
|
||||
export { TangoEngineProvider };
|
||||
|
||||
/**
|
||||
* 获取 CodeWorkspace 实例
|
||||
* @returns
|
||||
*/
|
||||
export const useWorkspace = () => {
|
||||
return useTangoEngine()?.engine.workspace;
|
||||
return useTangoEngine()?.engine.workspace as AbstractCodeWorkspace;
|
||||
};
|
||||
|
||||
export const useDesigner = () => {
|
||||
@@ -90,12 +94,15 @@ export const useWorkspaceData = () => {
|
||||
key: prefix,
|
||||
selectable: false,
|
||||
showAddButton: true,
|
||||
children: Object.keys(file.serviceFunctions || {}).map((key) => ({
|
||||
title: key,
|
||||
key: [prefix, key].join('.'),
|
||||
type: 'function',
|
||||
showRemoveButton: 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,
|
||||
};
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,46 @@
|
||||
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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-core",
|
||||
"version": "1.3.1",
|
||||
"version": "1.4.4",
|
||||
"description": "tango core",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"homepage": "",
|
||||
@@ -24,14 +24,14 @@
|
||||
"prepublishOnly": "yarn build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/generator": "^7.23.5",
|
||||
"@babel/parser": "^7.23.5",
|
||||
"@babel/traverse": "^7.23.5",
|
||||
"@babel/types": "^7.23.5",
|
||||
"@music163/tango-helpers": "^1.2.0",
|
||||
"@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.4",
|
||||
"mobx": "6.12.3",
|
||||
"@types/babel__traverse": "^7.20.6",
|
||||
"mobx": "6.13.2",
|
||||
"path-browserify": "^1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { MenuDataType } from '@music163/tango-helpers';
|
||||
import { Designer, DesignerViewType, Engine, SimulatorNameType } from './models';
|
||||
import { IWorkspace } from './models/interfaces';
|
||||
import { AbstractWorkspace } from './models/abstract-workspace';
|
||||
|
||||
interface ICreateEngineOptions {
|
||||
/**
|
||||
* 自定义工作区
|
||||
*/
|
||||
workspace?: IWorkspace;
|
||||
workspace?: AbstractWorkspace;
|
||||
/**
|
||||
* 菜单信息
|
||||
*/
|
||||
|
||||
@@ -35,7 +35,7 @@ export function isTangoVariable(name: string) {
|
||||
return /^tango\??\.(stores|services)\??\./.test(name) && name.split('.').length > 2;
|
||||
}
|
||||
|
||||
const templatePattern = /^{(.+)}$/s;
|
||||
const templatePattern = /^{(.+)}$/;
|
||||
|
||||
/**
|
||||
* 判断给定字符串是否被表达式容器`{expCode}`包裹
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import generator, { GeneratorOptions } from '@babel/generator';
|
||||
import * as t from '@babel/types';
|
||||
import { logger, wrapCode } from '@music163/tango-helpers';
|
||||
import { Dict, logger, wrapCode } from '@music163/tango-helpers';
|
||||
import { formatCode } from '../string';
|
||||
|
||||
const defaultGeneratorOptions: GeneratorOptions = {
|
||||
@@ -25,19 +25,7 @@ export function ast2code(ast: t.Node, options: GeneratorOptions = defaultGenerat
|
||||
return code;
|
||||
}
|
||||
|
||||
const bracketPattern = /^\(.+\)$/s;
|
||||
|
||||
/**
|
||||
* 是否被 () 包裹
|
||||
*
|
||||
* @example ({ foo: 'foo' }) -> true
|
||||
* @example { foo: 'foo' } -> false
|
||||
*
|
||||
* @param str 目标字符串
|
||||
*/
|
||||
function isWrappingWithBrackets(str: string) {
|
||||
return bracketPattern.test(str);
|
||||
}
|
||||
const bracketPattern = /^\(.+\)$/;
|
||||
|
||||
/**
|
||||
* 将表达式生成为块级代码
|
||||
@@ -54,7 +42,7 @@ export function expression2code(node: t.Expression) {
|
||||
|
||||
const isWrappingExpression = t.isObjectExpression(node) || t.isFunctionExpression(node);
|
||||
|
||||
if (isWrappingExpression && isWrappingWithBrackets(ret)) {
|
||||
if (isWrappingExpression && bracketPattern.test(ret)) {
|
||||
// 如果是对象,输出包含 ({}),则去掉首尾的括号
|
||||
ret = ret.slice(1, -1);
|
||||
}
|
||||
@@ -195,7 +183,7 @@ export function node2value(node: t.Node, isWrapCode = true): any {
|
||||
);
|
||||
if (isSimpleObject) {
|
||||
// simple object: { key1, key2, key3 }
|
||||
ret = node.properties.reduce((prev, propertyNode) => {
|
||||
ret = node.properties.reduce<Dict>((prev, propertyNode) => {
|
||||
if (propertyNode.type === 'ObjectProperty') {
|
||||
const key = keyNode2value(propertyNode.key);
|
||||
const value = node2value(propertyNode.value, isWrapCode);
|
||||
|
||||
@@ -68,11 +68,7 @@ export function isValidExpressionCode(code: string) {
|
||||
* @returns
|
||||
*/
|
||||
export function code2ast(code: string): t.File {
|
||||
try {
|
||||
return parse(code, babelParserConfig);
|
||||
} catch (err) {
|
||||
logger.error('[code2ast failed!]', err);
|
||||
}
|
||||
return parse(code, babelParserConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,7 +171,7 @@ export function value2node(
|
||||
* 将 js 普通对象解析为 t.Node
|
||||
*/
|
||||
export function object2node(
|
||||
obj: object,
|
||||
obj: Dict,
|
||||
getValueNode: (value: any, key?: string) => t.Expression = value2node,
|
||||
) {
|
||||
if (!isPlainObject(obj)) {
|
||||
|
||||
@@ -28,7 +28,7 @@ import { isDefineService, isDefineStore, isTangoVariable } from '../assert';
|
||||
import type {
|
||||
IRouteData,
|
||||
IStorePropertyData,
|
||||
ITangoViewNodeData,
|
||||
IViewNodeData,
|
||||
IImportDeclarationPayload,
|
||||
InsertChildPositionType,
|
||||
IImportSpecifierData,
|
||||
@@ -1009,7 +1009,7 @@ export function serviceConfig2Node(payload: object) {
|
||||
});
|
||||
}
|
||||
|
||||
export function updateServiceConfigToServiceFile(ast: t.File, config: Dict<object>) {
|
||||
export function updateServiceConfigToServiceFile(ast: t.File, config: Dict<Dict>) {
|
||||
traverse(ast, {
|
||||
CallExpression(path) {
|
||||
const calleeName = keyNode2value(path.node.callee) as string;
|
||||
@@ -1017,7 +1017,7 @@ export function updateServiceConfigToServiceFile(ast: t.File, config: Dict<objec
|
||||
const configNode = path.node.arguments[0];
|
||||
|
||||
if (t.isObjectExpression(configNode)) {
|
||||
const newPropertiesNodeMap = Object.keys(config).reduce((properties, key) => {
|
||||
const newPropertiesNodeMap = Object.keys(config).reduce<Dict>((properties, key) => {
|
||||
const serviceConfig = config[key];
|
||||
const property = t.objectProperty(t.identifier(key), serviceConfig2Node(serviceConfig));
|
||||
properties[key] = property;
|
||||
@@ -1234,7 +1234,7 @@ export function cloneJSXElement(node: t.JSXElement, overrideProps?: Dict) {
|
||||
export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
|
||||
const imports: Record<string, IImportSpecifierData[]> = {};
|
||||
const importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]> = {};
|
||||
const nodes: Array<ITangoViewNodeData<t.JSXElement>> = [];
|
||||
const nodes: Array<IViewNodeData<t.JSXElement>> = [];
|
||||
const cloneAst = t.cloneNode(ast, true, true);
|
||||
const cleanAst = clearTrackingData(cloneAst);
|
||||
const variables: string[] = []; // 使用的 tango 变量
|
||||
|
||||
@@ -11,7 +11,7 @@ export function namesToImportDeclarations(
|
||||
names: string[],
|
||||
nameMap: Dict<IImportSpecifierSourceData>,
|
||||
) {
|
||||
const map = {};
|
||||
const map: Dict = {};
|
||||
names.forEach((name) => {
|
||||
const mod = nameMap[name];
|
||||
if (mod) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { parseDndId, uuid } from '@music163/tango-helpers';
|
||||
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 = {};
|
||||
const target: Dict = {};
|
||||
for (const key in obj) {
|
||||
if (Object.hasOwn(obj, key)) {
|
||||
target[key] = deepCloneNode(obj[key], component);
|
||||
|
||||
@@ -8,65 +8,57 @@ import { FileType } from './../types';
|
||||
export function inferFileType(filename: string): FileType {
|
||||
// 增加 tangoConfigJson Module
|
||||
if (/\/tango\.config\.json$/.test(filename)) {
|
||||
return FileType.TangoConfigJson;
|
||||
return FileType.TangoConfigJsonFile;
|
||||
}
|
||||
|
||||
if (/\/appJson\.json$/.test(filename)) {
|
||||
return FileType.AppJson;
|
||||
return FileType.AppJsonFile;
|
||||
}
|
||||
|
||||
if (/\/package\.json$/.test(filename)) {
|
||||
return FileType.PackageJson;
|
||||
return FileType.PackageJsonFile;
|
||||
}
|
||||
|
||||
if (/\/routes\.js$/.test(filename)) {
|
||||
return FileType.RouteModule;
|
||||
return FileType.JsRouteConfigFile;
|
||||
}
|
||||
|
||||
// 所有 pages 下的 js 文件均认为是有效的 viewModule
|
||||
if (/\/pages\/.+\.jsx?$/.test(filename)) {
|
||||
return FileType.JsxViewModule;
|
||||
return FileType.JsViewFile;
|
||||
}
|
||||
|
||||
// 所有 pages 下的 js 文件均认为是有效的 viewModule
|
||||
if (/\/pages\/.+\.schema\.json?$/.test(filename)) {
|
||||
return FileType.JsonViewModule;
|
||||
return FileType.JsonViewFile;
|
||||
}
|
||||
|
||||
if (/\/(blocks|components)\/index\.js/.test(filename)) {
|
||||
return FileType.ComponentsEntryModule;
|
||||
return FileType.JsLocalComponentsEntryFile;
|
||||
}
|
||||
|
||||
if (/\/services\/.+\.js$/.test(filename)) {
|
||||
return FileType.ServiceModule;
|
||||
return FileType.JsServiceFile;
|
||||
}
|
||||
|
||||
if (/service\.js$/.test(filename)) {
|
||||
return FileType.ServiceModule;
|
||||
return FileType.JsServiceFile;
|
||||
}
|
||||
|
||||
if (/\/stores\/index\.js$/.test(filename)) {
|
||||
return FileType.StoreEntryModule;
|
||||
return FileType.JsStoreEntryFile;
|
||||
}
|
||||
|
||||
if (/\/stores\/.+\.js$/.test(filename)) {
|
||||
return FileType.StoreModule;
|
||||
return FileType.JsStoreFile;
|
||||
}
|
||||
|
||||
if (/\.jsx?$/.test(filename)) {
|
||||
return FileType.Module;
|
||||
return FileType.JsFile;
|
||||
}
|
||||
|
||||
if (/\.json$/.test(filename)) {
|
||||
return FileType.Json;
|
||||
}
|
||||
|
||||
if (/\.less$/.test(filename)) {
|
||||
return FileType.Less;
|
||||
}
|
||||
|
||||
if (/\.scss$/.test(filename)) {
|
||||
return FileType.Scss;
|
||||
return FileType.JsonFile;
|
||||
}
|
||||
|
||||
return FileType.File;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { MenuDataType } from '@music163/tango-helpers';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
|
||||
export type SimulatorNameType = 'desktop' | 'phone';
|
||||
|
||||
@@ -18,7 +18,7 @@ interface IViewportBounding {
|
||||
}
|
||||
|
||||
interface IDesignerOptions {
|
||||
workspace: IWorkspace;
|
||||
workspace: AbstractWorkspace;
|
||||
simulator?: SimulatorNameType | ISimulatorType;
|
||||
/**
|
||||
* 菜单配置
|
||||
@@ -108,7 +108,7 @@ export class Designer {
|
||||
*/
|
||||
_menuData?: MenuDataType = null;
|
||||
|
||||
private readonly workspace: IWorkspace;
|
||||
private readonly workspace: AbstractWorkspace;
|
||||
|
||||
get simulator(): ISimulatorType {
|
||||
return toJS(this._simulator);
|
||||
@@ -239,6 +239,13 @@ export class Designer {
|
||||
|
||||
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) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { ISelectedItemData } from '@music163/tango-helpers';
|
||||
import { DropTarget } from './drop-target';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
|
||||
/**
|
||||
* 拖拽来源类,被拖拽的物体
|
||||
@@ -22,7 +22,7 @@ export class DragSource {
|
||||
*/
|
||||
dropTarget: DropTarget;
|
||||
|
||||
private readonly workspace: IWorkspace;
|
||||
private readonly workspace: AbstractWorkspace;
|
||||
|
||||
get node() {
|
||||
return this.workspace.getNode(this.data?.id, this.data?.filename);
|
||||
@@ -47,7 +47,7 @@ export class DragSource {
|
||||
return this.data?.bounding;
|
||||
}
|
||||
|
||||
constructor(workspace: IWorkspace) {
|
||||
constructor(workspace: AbstractWorkspace) {
|
||||
this.workspace = workspace;
|
||||
this.data = null;
|
||||
this.isDragging = false;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { ISelectedItemData } from '@music163/tango-helpers';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
|
||||
export enum DropMethod {
|
||||
ReplaceNode = 'replaceNode', // 替换节点
|
||||
@@ -23,7 +23,7 @@ export class DropTarget {
|
||||
*/
|
||||
data: ISelectedItemData;
|
||||
|
||||
private readonly workspace: IWorkspace;
|
||||
private readonly workspace: AbstractWorkspace;
|
||||
|
||||
get node() {
|
||||
return this.workspace.getNode(this.data.id, this.data.filename);
|
||||
@@ -48,7 +48,7 @@ export class DropTarget {
|
||||
return this.data?.display;
|
||||
}
|
||||
|
||||
constructor(workspace: IWorkspace) {
|
||||
constructor(workspace: AbstractWorkspace) {
|
||||
this.workspace = workspace;
|
||||
this.method = DropMethod.InsertAfter;
|
||||
this.data = null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import { Designer } from './designer';
|
||||
import { IWorkspace } from './interfaces';
|
||||
|
||||
/**
|
||||
* 设计器引擎
|
||||
@@ -8,7 +8,7 @@ export class Engine {
|
||||
/**
|
||||
* 工作区状态
|
||||
*/
|
||||
workspace: IWorkspace;
|
||||
workspace: AbstractWorkspace;
|
||||
/**
|
||||
* 设计器状态
|
||||
*/
|
||||
|
||||
@@ -1,55 +1,22 @@
|
||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||
import { getValue, isNil, logger, setValue } from '@music163/tango-helpers';
|
||||
import type { FileType, IFileConfig } from '../types';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { formatCode } from '../helpers';
|
||||
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';
|
||||
|
||||
/**
|
||||
* 普通文件,不进行 AST 解析
|
||||
*/
|
||||
export class TangoFile {
|
||||
readonly workspace: IWorkspace;
|
||||
/**
|
||||
* 文件名
|
||||
*/
|
||||
readonly filename: string;
|
||||
|
||||
/**
|
||||
* 文件类型
|
||||
*/
|
||||
readonly type: FileType;
|
||||
|
||||
/**
|
||||
* 最近修改的时间戳
|
||||
*/
|
||||
lastModified: number;
|
||||
|
||||
_code: string;
|
||||
_cleanCode: string;
|
||||
|
||||
get code() {
|
||||
return this._code;
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
get cleanCode() {
|
||||
return this._cleanCode;
|
||||
}
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig, isSyncCode = true) {
|
||||
this.workspace = workspace;
|
||||
this.filename = props.filename;
|
||||
this.type = props.type;
|
||||
this.lastModified = Date.now();
|
||||
|
||||
// 这里主要是为了解决 umi ts 编译错误的问题,@see https://github.com/umijs/umi/issues/7594
|
||||
if (isSyncCode) {
|
||||
this.update(props.code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新文件内容
|
||||
*/
|
||||
update(code?: string) {
|
||||
if (!isNil(code)) {
|
||||
this.lastModified = Date.now();
|
||||
@@ -59,137 +26,3 @@ export class TangoFile {
|
||||
this.workspace.onFilesChange([this.filename]);
|
||||
}
|
||||
}
|
||||
|
||||
export class TangoLessFile extends TangoFile {
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code);
|
||||
makeObservable(this, {
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
update: action,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class TangoJsonFile extends TangoFile {
|
||||
_object = {};
|
||||
|
||||
/**
|
||||
* @deprecated 使用 file.json 代替
|
||||
*/
|
||||
get object() {
|
||||
return toJS(this._object);
|
||||
}
|
||||
|
||||
get json() {
|
||||
return toJS(this._object);
|
||||
}
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code);
|
||||
makeObservable(this, {
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
_object: observable,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
object: computed,
|
||||
json: computed,
|
||||
update: action,
|
||||
setValue: action,
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
|
||||
export enum HistoryMessage {
|
||||
InitView = 'initView',
|
||||
@@ -43,7 +43,7 @@ export class TangoHistory {
|
||||
// 最多记录数
|
||||
_maxSize = 100;
|
||||
|
||||
private readonly workspace: IWorkspace;
|
||||
private readonly workspace: AbstractWorkspace;
|
||||
|
||||
get index() {
|
||||
return this._index;
|
||||
@@ -65,7 +65,7 @@ export class TangoHistory {
|
||||
return this._records.length > this._index + 1;
|
||||
}
|
||||
|
||||
constructor(workspace: IWorkspace) {
|
||||
constructor(workspace: AbstractWorkspace) {
|
||||
this.workspace = workspace;
|
||||
|
||||
makeObservable(this, {
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
export * from './engine';
|
||||
export * from './workspace';
|
||||
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 './drop-target';
|
||||
export * from './select-source';
|
||||
export * from './drag-source';
|
||||
export * from './history';
|
||||
export * from './drop-target';
|
||||
export * from './engine';
|
||||
export * from './file';
|
||||
export * from './module';
|
||||
export * from './entry-module';
|
||||
export * from './route-module';
|
||||
export * from './service-module';
|
||||
export * from './store-module';
|
||||
export * from './view-module';
|
||||
export * from './component-module';
|
||||
export * from './node';
|
||||
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';
|
||||
|
||||
@@ -1,27 +1,17 @@
|
||||
import { IComponentPrototype, Dict, ITangoConfigJson } from '@music163/tango-helpers';
|
||||
import { TangoHistory } from './history';
|
||||
import { SelectSource } from './select-source';
|
||||
import { DragSource } from './drag-source';
|
||||
import { Dict } from '@music163/tango-helpers';
|
||||
import {
|
||||
IFileConfig,
|
||||
FileType,
|
||||
InsertChildPositionType,
|
||||
ITangoConfigPackages,
|
||||
IPageConfigData,
|
||||
IImportSpecifierSourceData,
|
||||
IImportSpecifierData,
|
||||
} from '../types';
|
||||
import { TangoFile, TangoJsonFile } from './file';
|
||||
import { TangoRouteModule } from './route-module';
|
||||
import { TangoStoreModule } from './store-module';
|
||||
import { TangoServiceModule } from './service-module';
|
||||
import { IdGenerator } from '../helpers';
|
||||
import { AppEntryModule } from './entry-module';
|
||||
import { AbstractViewNode } from './abstract-view-node';
|
||||
|
||||
export interface IViewFile {
|
||||
readonly workspace: IWorkspace;
|
||||
readonly filename: string;
|
||||
readonly type: FileType;
|
||||
/**
|
||||
* 文件名
|
||||
*/
|
||||
filename: string;
|
||||
|
||||
/**
|
||||
* ID 生成器
|
||||
@@ -54,252 +44,38 @@ export interface IViewFile {
|
||||
*/
|
||||
addImportSpecifiers: (source: string, newSpecifiers: IImportSpecifierData[]) => IViewFile;
|
||||
|
||||
getNode: (targetNodeId: string) => IViewNode;
|
||||
getNode: (targetNodeId: string) => AbstractViewNode;
|
||||
|
||||
removeNode: (targetNodeId: string) => IViewFile;
|
||||
removeNode: (targetNodeId: string) => this;
|
||||
|
||||
insertChild: (
|
||||
targetNodeId: string,
|
||||
newNode: any,
|
||||
position?: InsertChildPositionType,
|
||||
) => IViewFile;
|
||||
insertChild: (targetNodeId: string, newNode: any, position?: InsertChildPositionType) => this;
|
||||
|
||||
insertAfter: (targetNodeId: string, newNode: any) => IViewFile;
|
||||
insertAfter: (targetNodeId: string, newNode: any) => this;
|
||||
|
||||
insertBefore: (targetNodeId: string, newNode: any) => IViewFile;
|
||||
insertBefore: (targetNodeId: string, newNode: any) => this;
|
||||
|
||||
replaceNode: (targetNodeId: string, newNode: any) => IViewFile;
|
||||
replaceNode: (targetNodeId: string, newNode: any) => this;
|
||||
|
||||
replaceViewChildren: (rawNodes: any[]) => IViewFile;
|
||||
replaceViewChildren: (rawNodes: any[]) => this;
|
||||
|
||||
updateNodeAttribute: (
|
||||
nodeId: string,
|
||||
attrName: string,
|
||||
attrValue?: any,
|
||||
relatedImports?: string[],
|
||||
) => IViewFile;
|
||||
) => this;
|
||||
|
||||
updateNodeAttributes: (
|
||||
nodeId: string,
|
||||
config: Record<string, any>,
|
||||
relatedImports?: string[],
|
||||
) => IViewFile;
|
||||
) => this;
|
||||
|
||||
get code(): string;
|
||||
get nodes(): Map<string, IViewNode>;
|
||||
get nodes(): Map<string, AbstractViewNode>;
|
||||
get nodesTree(): object[];
|
||||
get tree(): any;
|
||||
}
|
||||
|
||||
export interface IViewNode {
|
||||
/**
|
||||
* 所属的文件
|
||||
*/
|
||||
file: IViewFile;
|
||||
|
||||
/**
|
||||
* 节点 ID
|
||||
*/
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* 对应的组件
|
||||
*/
|
||||
readonly component: string;
|
||||
|
||||
/**
|
||||
* 原始节点对象
|
||||
*/
|
||||
readonly rawNode: unknown;
|
||||
|
||||
/**
|
||||
* 属性集合
|
||||
*/
|
||||
readonly props: Record<string, any>;
|
||||
|
||||
/**
|
||||
* 克隆原始节点
|
||||
* @param overrideProps 额外设置给克隆节点的属性
|
||||
* @returns
|
||||
*/
|
||||
cloneRawNode: (overrideProps?: Dict) => unknown;
|
||||
|
||||
/**
|
||||
* 销毁节点
|
||||
* @returns
|
||||
*/
|
||||
destroy: () => void;
|
||||
|
||||
/**
|
||||
* 原始节点的位置信息
|
||||
*/
|
||||
get loc(): unknown;
|
||||
}
|
||||
|
||||
export interface IWorkspace {
|
||||
history: TangoHistory;
|
||||
selectSource: SelectSource;
|
||||
dragSource: DragSource;
|
||||
|
||||
files: Map<string, TangoFile>;
|
||||
componentPrototypes: Map<string, IComponentPrototype>;
|
||||
|
||||
entry: string;
|
||||
activeFile: string;
|
||||
activeViewFile: string;
|
||||
activeRoute: string;
|
||||
|
||||
/**
|
||||
* 解析后的 tango.config.json 文件,如果要获取项目配置,推荐使用 projectConfig 获取
|
||||
*/
|
||||
tangoConfigJson: TangoJsonFile;
|
||||
/**
|
||||
* app.js 入口文件解析后的模块
|
||||
*/
|
||||
appEntryModule: AppEntryModule;
|
||||
/**
|
||||
* 解析后的路由模块
|
||||
*/
|
||||
routeModule?: TangoRouteModule;
|
||||
/**
|
||||
* 解析后的状态管理模块 Map
|
||||
*/
|
||||
storeModules?: Record<string, TangoStoreModule>;
|
||||
/**
|
||||
* 解析后的服务模块 Map
|
||||
*/
|
||||
serviceModules?: Record<string, TangoServiceModule>;
|
||||
|
||||
ready: () => void;
|
||||
refresh: (names: string[]) => void;
|
||||
|
||||
setActiveRoute: (path: string) => void;
|
||||
setActiveFile: (filename: string) => void;
|
||||
|
||||
setComponentPrototypes: (prototypes: Record<string, IComponentPrototype>) => void;
|
||||
getPrototype: (name: string | IComponentPrototype) => IComponentPrototype;
|
||||
|
||||
// ----------------- 文件操作 -----------------
|
||||
addFiles: (files: IFileConfig[]) => void;
|
||||
addFile: (filename: string, code: string, fileType?: FileType) => void;
|
||||
|
||||
addServiceFile: (serviceName: string, code: string) => void;
|
||||
addStoreFile: (storeName: string, code: string) => void;
|
||||
addViewFile: (viewName: string, code: string) => void;
|
||||
|
||||
removeFile: (filename: string) => void;
|
||||
|
||||
renameFile: (oldFilename: string, newFilename: string) => void;
|
||||
renameFolder: (oldFoldername: string, newFoldername: string) => void;
|
||||
|
||||
updateFile: (filename: string, code: string, shouldFormatCode?: boolean) => void;
|
||||
|
||||
listFiles: () => Record<string, string>;
|
||||
getFile: (filename: string) => TangoFile;
|
||||
|
||||
/**
|
||||
* 文件变化回调
|
||||
* @param filenames 文件名列表
|
||||
*/
|
||||
onFilesChange: (filenames: string[]) => void;
|
||||
|
||||
// ----------------- 节点操作 -----------------
|
||||
|
||||
removeSelectedNode: () => void;
|
||||
cloneSelectedNode: () => void;
|
||||
copySelectedNode: () => void;
|
||||
pasteSelectedNode: () => void;
|
||||
insertToSelectedNode: (childNameOrPrototype: string | IComponentPrototype) => void;
|
||||
insertBeforeSelectedNode: (sourceNameOrPrototype: string | IComponentPrototype) => void;
|
||||
insertAfterSelectedNode: (sourceNameOrPrototype: string | IComponentPrototype) => void;
|
||||
dropNode: () => void;
|
||||
insertToNode: (targetNodeId: string, sourceNameOrPrototype: string | IComponentPrototype) => void;
|
||||
replaceNode: (targetNodeId: string, sourceNameOrPrototype: string | IComponentPrototype) => void;
|
||||
updateSelectedNodeAttributes: (
|
||||
attributes: Record<string, any>,
|
||||
relatedImports?: string[],
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* 查询节点
|
||||
* @param id 节点 ID
|
||||
* @param module 节点所在的模块名
|
||||
* @returns 返回节点对象
|
||||
*/
|
||||
getNode: (id: string, module?: string) => IViewNode;
|
||||
|
||||
// ----------------- 服务函数文件操作 -----------------
|
||||
|
||||
getServiceFunction?: (serviceKey: string) => {
|
||||
name: string;
|
||||
moduleName: string;
|
||||
config: Dict<object>;
|
||||
};
|
||||
listServiceFunctions?: () => Dict<object>;
|
||||
removeServiceFunction?: (serviceKey: string) => void;
|
||||
addServiceFunction?: (serviceName: string, config: Dict, modName?: string) => void;
|
||||
addServiceFunctions?: (configs: Dict<object>, modName?: string) => void;
|
||||
updateServiceFunction?: (serviceName: string, payload: Dict, modName?: string) => void;
|
||||
updateServiceBaseConfig?: (config: Dict, modName?: string) => void;
|
||||
|
||||
// ----------------- 状态管理文件操作 -----------------
|
||||
addStoreState?: (storeName: string, stateName: string, initValue: string) => void;
|
||||
removeStoreModule?: (storeName: string) => void;
|
||||
removeStoreVariable?: (variablePath: string) => void;
|
||||
updateStoreVariable?: (variablePath: string, code: string) => void;
|
||||
|
||||
// ----------------- 视图文件操作 -----------------
|
||||
|
||||
removeViewModule: (routePath: string) => void;
|
||||
copyViewPage: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
|
||||
|
||||
// ----------------- 路由文件操作 -----------------
|
||||
|
||||
updateRoute: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
|
||||
|
||||
// ----------------- 依赖包操作 -----------------
|
||||
|
||||
addDependency?: (data: any) => void;
|
||||
listDependencies?: () => any;
|
||||
getDependency?: (pkgName: string) => object;
|
||||
|
||||
updateDependency?: (
|
||||
name: string,
|
||||
version: string,
|
||||
options?: {
|
||||
package?: ITangoConfigPackages;
|
||||
[x: string]: any;
|
||||
},
|
||||
) => void;
|
||||
|
||||
removeDependency?: (name: string) => void;
|
||||
|
||||
addBizComp?: (
|
||||
name: string,
|
||||
version: string,
|
||||
options?: {
|
||||
package?: ITangoConfigPackages;
|
||||
[x: string]: any;
|
||||
},
|
||||
) => void;
|
||||
|
||||
removeBizComp?: (name: string) => void;
|
||||
|
||||
// ----------------- getter -----------------
|
||||
/**
|
||||
* 解析后的项目配置信息
|
||||
*/
|
||||
get projectConfig(): ITangoConfigJson;
|
||||
/**
|
||||
* 当前活动的视图文件
|
||||
*/
|
||||
get activeViewModule(): IViewFile;
|
||||
get pages(): any[];
|
||||
get bizComps(): string[];
|
||||
get baseComps(): string[];
|
||||
get localComps(): string[];
|
||||
/**
|
||||
* 是否是合法的项目
|
||||
*/
|
||||
get isValid(): boolean;
|
||||
/**
|
||||
* 文件中的代码
|
||||
*/
|
||||
get code(): string;
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,12 +1,12 @@
|
||||
import { traverseEntryFile } from '../helpers';
|
||||
import { IFileConfig } from '../types';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { TangoModule } from './module';
|
||||
import { AbstractJsFile } from './abstract-js-file';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
|
||||
export class AppEntryModule extends TangoModule {
|
||||
export class JsAppEntryFile extends AbstractJsFile {
|
||||
routerType: string;
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code, true, false);
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -1,19 +1,19 @@
|
||||
import path from 'path';
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { TangoModule } from './module';
|
||||
import { IWorkspace } from './interfaces';
|
||||
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 TangoComponentsEntryModule extends TangoModule {
|
||||
export class JsLocalComponentsEntryFile extends AbstractJsFile {
|
||||
exportList: Record<string, IExportSpecifierData>;
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code, false, false);
|
||||
this.update(props.code, true, false);
|
||||
makeObservable(this, {
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
+9
-5
@@ -1,4 +1,5 @@
|
||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||
import { logger } from '@music163/tango-helpers';
|
||||
import {
|
||||
traverseRouteFile,
|
||||
addRouteToRouteFile,
|
||||
@@ -6,20 +7,20 @@ import {
|
||||
updateRouteToRouteFile,
|
||||
} from '../helpers';
|
||||
import { IRouteData, IFileConfig } from '../types';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { TangoModule } from './module';
|
||||
import { AbstractJsFile } from './abstract-js-file';
|
||||
import { AbstractCodeWorkspace } from './abstract-code-workspace';
|
||||
|
||||
/**
|
||||
* 路由配置模块
|
||||
*/
|
||||
export class TangoRouteModule extends TangoModule {
|
||||
export class JsRouteConfigFile extends AbstractJsFile {
|
||||
_routes: IRouteData[];
|
||||
|
||||
get routes() {
|
||||
return toJS(this._routes);
|
||||
}
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
constructor(workspace: AbstractCodeWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code, true, false);
|
||||
|
||||
@@ -27,10 +28,13 @@ export class TangoRouteModule extends TangoModule {
|
||||
_routes: observable,
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
isError: observable,
|
||||
errorMessage: observable,
|
||||
routes: computed,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
update: action,
|
||||
updateAst: action,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -74,7 +78,7 @@ export class TangoRouteModule extends TangoModule {
|
||||
*/
|
||||
removeRoute(route: string) {
|
||||
if (route === '/') {
|
||||
console.warn('index route should not be removed!');
|
||||
logger.warn('index route should not be removed!');
|
||||
return;
|
||||
}
|
||||
const record = this.getRouteByRoutePath(route);
|
||||
+7
-4
@@ -8,13 +8,13 @@ import {
|
||||
updateBaseConfigToServiceFile,
|
||||
} from '../helpers';
|
||||
import { IFileConfig } from '../types';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { TangoModule } from './module';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import { AbstractJsFile } from './abstract-js-file';
|
||||
|
||||
/**
|
||||
* 数据服务模块
|
||||
*/
|
||||
export class TangoServiceModule extends TangoModule {
|
||||
export class JsServiceFile extends AbstractJsFile {
|
||||
/**
|
||||
* 服务函数的模块名,默认为 index
|
||||
*/
|
||||
@@ -39,7 +39,7 @@ export class TangoServiceModule extends TangoModule {
|
||||
return toJS(this._baseConfig);
|
||||
}
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.name = getModuleNameByFilename(props.filename);
|
||||
this.update(props.code, true, false);
|
||||
@@ -49,11 +49,14 @@ export class TangoServiceModule extends TangoModule {
|
||||
_baseConfig: observable,
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
isError: observable,
|
||||
errorMessage: observable,
|
||||
serviceFunctions: computed,
|
||||
baseConfig: computed,
|
||||
cleanCode: computed,
|
||||
code: computed,
|
||||
update: action,
|
||||
updateAst: action,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||
import { traverseStoreEntryFile, addStoreToEntryFile, removeStoreToEntryFile } from '../helpers';
|
||||
import { IFileConfig } from '../types';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import { AbstractJsFile } from './abstract-js-file';
|
||||
|
||||
/**
|
||||
* stores 入口文件
|
||||
*/
|
||||
export class JsStoreEntryFile extends AbstractJsFile {
|
||||
_stores: string[] = [];
|
||||
|
||||
get stores() {
|
||||
return toJS(this._stores);
|
||||
}
|
||||
|
||||
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code, true, false);
|
||||
|
||||
makeObservable(this, {
|
||||
_stores: observable,
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
isError: observable,
|
||||
errorMessage: observable,
|
||||
stores: computed,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
update: action,
|
||||
updateAst: action,
|
||||
});
|
||||
}
|
||||
|
||||
_analysisAst() {
|
||||
this._stores = traverseStoreEntryFile(this.ast);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建模型
|
||||
* @param name
|
||||
*/
|
||||
addStore(name: string) {
|
||||
this.ast = addStoreToEntryFile(this.ast, name);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除模型
|
||||
* @param name
|
||||
*/
|
||||
removeStore(name: string) {
|
||||
this.ast = removeStoreToEntryFile(this.ast, name);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
+5
-56
@@ -1,70 +1,19 @@
|
||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import {
|
||||
traverseStoreFile,
|
||||
traverseStoreEntryFile,
|
||||
addStoreToEntryFile,
|
||||
getModuleNameByFilename,
|
||||
addStoreState,
|
||||
updateStoreState,
|
||||
removeStoreState,
|
||||
removeStoreToEntryFile,
|
||||
} from '../helpers';
|
||||
import { IFileConfig, IStorePropertyData } from '../types';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { TangoModule } from './module';
|
||||
|
||||
/**
|
||||
* 入口配置模块
|
||||
*/
|
||||
export class TangoStoreEntryModule extends TangoModule {
|
||||
_stores: string[] = [];
|
||||
|
||||
get stores() {
|
||||
return toJS(this._stores);
|
||||
}
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code, true, false);
|
||||
|
||||
makeObservable(this, {
|
||||
_stores: observable,
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
stores: computed,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
update: action,
|
||||
});
|
||||
}
|
||||
|
||||
_analysisAst() {
|
||||
this._stores = traverseStoreEntryFile(this.ast);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建模型
|
||||
* @param name
|
||||
*/
|
||||
addStore(name: string) {
|
||||
this.ast = addStoreToEntryFile(this.ast, name);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除模型
|
||||
* @param name
|
||||
*/
|
||||
removeStore(name: string) {
|
||||
this.ast = removeStoreToEntryFile(this.ast, name);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import { AbstractJsFile } from './abstract-js-file';
|
||||
|
||||
/**
|
||||
* 状态模型模块
|
||||
*/
|
||||
export class TangoStoreModule extends TangoModule {
|
||||
export class JsStoreFile extends AbstractJsFile {
|
||||
/**
|
||||
* 模块名
|
||||
*/
|
||||
@@ -76,7 +25,7 @@ export class TangoStoreModule extends TangoModule {
|
||||
|
||||
actions: IStorePropertyData[];
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.name = getModuleNameByFilename(props.filename);
|
||||
this.update(props.code, true, false);
|
||||
+20
-13
@@ -20,18 +20,19 @@ import {
|
||||
addImportDeclarationLegacy,
|
||||
updateImportDeclarationLegacy,
|
||||
} from '../helpers';
|
||||
import { TangoNode } from './node';
|
||||
import { JsxViewNode } from './view-node';
|
||||
import {
|
||||
IFileConfig,
|
||||
ITangoViewNodeData,
|
||||
IViewNodeData,
|
||||
IImportDeclarationPayload,
|
||||
InsertChildPositionType,
|
||||
IImportSpecifierSourceData,
|
||||
ImportDeclarationDataType,
|
||||
IImportSpecifierData,
|
||||
} from '../types';
|
||||
import { IViewFile, IWorkspace } from './interfaces';
|
||||
import { TangoModule } from './module';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import { IViewFile } from './interfaces';
|
||||
import { AbstractJsFile } from './abstract-js-file';
|
||||
|
||||
/**
|
||||
* 导入信息转为 变量名->来源 的 map 结构
|
||||
@@ -56,8 +57,8 @@ function buildImportMap(importedModules: ImportDeclarationDataType) {
|
||||
* 将节点列表转换为 tree data 嵌套数组
|
||||
* @param list
|
||||
*/
|
||||
function nodeListToTreeData(list: ITangoViewNodeData[]) {
|
||||
const map: Record<string, ITangoViewNodeData> = {};
|
||||
function nodeListToTreeData(list: IViewNodeData[]) {
|
||||
const map: Record<string, IViewNodeData> = {};
|
||||
|
||||
list.forEach((item) => {
|
||||
// 如果不存在,则初始化
|
||||
@@ -82,9 +83,9 @@ function nodeListToTreeData(list: ITangoViewNodeData[]) {
|
||||
/**
|
||||
* 视图模块
|
||||
*/
|
||||
export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
export class JsViewFile extends AbstractJsFile implements IViewFile {
|
||||
// 解析为树结构的 jsxNodes 数组
|
||||
_nodesTree: ITangoViewNodeData[];
|
||||
_nodesTree: IViewNodeData[] = [];
|
||||
/**
|
||||
* 通过导入组件名查找组件来自的包
|
||||
*/
|
||||
@@ -108,7 +109,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
/**
|
||||
* 节点列表 <id, Node>
|
||||
*/
|
||||
private _nodes: Map<string, TangoNode>;
|
||||
private _nodes: Map<string, JsxViewNode>;
|
||||
/**
|
||||
* 导入的模块
|
||||
* @deprecated
|
||||
@@ -127,7 +128,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
return this.ast;
|
||||
}
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this._nodes = new Map();
|
||||
this.idGenerator = new IdGenerator({ prefix: props.filename });
|
||||
@@ -138,10 +139,14 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
|
||||
isError: observable,
|
||||
errorMessage: observable,
|
||||
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
|
||||
update: action,
|
||||
updateAst: action,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -172,8 +177,10 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
this._codeIdList = [];
|
||||
|
||||
nodes.forEach((cur) => {
|
||||
const node = new TangoNode({
|
||||
...cur,
|
||||
const node = new JsxViewNode({
|
||||
id: cur.id,
|
||||
component: cur.component,
|
||||
rawNode: cur.rawNode,
|
||||
file: this,
|
||||
});
|
||||
this._nodes.set(cur.id, node);
|
||||
@@ -320,7 +327,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
updateNodeAttributes(nodeId: string, config: Record<string, any>, relatedImports?: string[]) {
|
||||
if (relatedImports && relatedImports.length) {
|
||||
// 导入依赖的组件
|
||||
const newImportData = relatedImports.reduce((prev, name) => {
|
||||
const newImportData = relatedImports.reduce<Dict<IImportSpecifierData[]>>((prev, name) => {
|
||||
const proto = this.workspace.getPrototype(name);
|
||||
const { source, specifiers } = prototype2importDeclarationData(proto, this.filename);
|
||||
const existSpecifiers: IImportSpecifierData[] = prev[source];
|
||||
@@ -0,0 +1,24 @@
|
||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||
import type { IFileConfig } from '../types';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import { AbstractJsonFile } from './abstract-json-file';
|
||||
|
||||
export class JsonFile extends AbstractJsonFile {
|
||||
get json(): object {
|
||||
return toJS(this._object);
|
||||
}
|
||||
|
||||
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||
super(workspace, props);
|
||||
makeObservable(this, {
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
_object: observable,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
json: computed,
|
||||
update: action,
|
||||
setValue: action,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import * as t from '@babel/types';
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { isNil } from '@music163/tango-helpers';
|
||||
import {
|
||||
code2ast,
|
||||
ast2code,
|
||||
formatCode,
|
||||
traverseFile,
|
||||
addImportDeclaration,
|
||||
updateImportDeclaration,
|
||||
} from '../helpers';
|
||||
import { TangoFile } from './file';
|
||||
import { IFileConfig, IImportSpecifierData, ImportDeclarationDataType } from '../types';
|
||||
import { IWorkspace } from './interfaces';
|
||||
|
||||
/**
|
||||
* JS 模块实现规范
|
||||
* - ast 操纵类方法,统一返回 this,支持外层链式调用
|
||||
* - observable state 统一用 _foo 格式,并提供 getter 方法
|
||||
*/
|
||||
export class TangoModule extends TangoFile {
|
||||
ast: t.File;
|
||||
|
||||
/**
|
||||
* 导入的依赖列表
|
||||
*/
|
||||
importList: ImportDeclarationDataType;
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig, isSyncCode = true) {
|
||||
super(workspace, props, isSyncCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于最新的 ast 进行同步
|
||||
* @param code 如果传入 code,则基于 code 进行同步
|
||||
* @param isFormatCode 是否格式化代码
|
||||
* @param refreshWorkspace 是否刷新 workspace
|
||||
*/
|
||||
update(code?: string, isFormatCode = true, refreshWorkspace = true) {
|
||||
this.lastModified = Date.now();
|
||||
if (isNil(code)) {
|
||||
this._syncByAst();
|
||||
} else {
|
||||
this._syncByCode(code, isFormatCode);
|
||||
}
|
||||
|
||||
this._analysisAst();
|
||||
|
||||
this.workspace.onFilesChange([this.filename]);
|
||||
|
||||
if (refreshWorkspace) {
|
||||
this.workspace.refresh([this.filename]);
|
||||
}
|
||||
}
|
||||
|
||||
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 isFormatCode 是否格式化代码
|
||||
* @returns
|
||||
*/
|
||||
_syncByCode(code: string, isFormatCode = true) {
|
||||
if (code === this._code) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 提前格式化代码
|
||||
if (isFormatCode) {
|
||||
code = formatCode(code);
|
||||
}
|
||||
|
||||
this._code = code;
|
||||
this._cleanCode = code;
|
||||
this.ast = code2ast(code);
|
||||
}
|
||||
|
||||
_analysisAst() {
|
||||
const { imports } = traverseFile(this.ast);
|
||||
this.importList = imports;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 普通 JS 文件
|
||||
*/
|
||||
export class TangoJsModule extends TangoModule {
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code, false, false);
|
||||
|
||||
makeObservable(this, {
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
update: action,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import { JSXElement, SourceLocation } from '@babel/types';
|
||||
import { Dict } from '@music163/tango-helpers';
|
||||
import { cloneJSXElement, getJSXElementAttributes } from '../helpers';
|
||||
import { ITangoViewNodeData } from '../types';
|
||||
import { TangoViewModule } from './view-module';
|
||||
import { IViewNode } from './interfaces';
|
||||
|
||||
type TangoNodeConstructorPropsType = ITangoViewNodeData & {
|
||||
file: TangoViewModule;
|
||||
};
|
||||
|
||||
/**
|
||||
* 视图节点类
|
||||
*/
|
||||
export class TangoNode implements IViewNode {
|
||||
/**
|
||||
* 节点 ID
|
||||
*/
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* 节点对应的组件名
|
||||
*/
|
||||
readonly component: string;
|
||||
|
||||
readonly rawNode: JSXElement;
|
||||
|
||||
/**
|
||||
* 节点所属的文件对象
|
||||
*/
|
||||
file: TangoViewModule;
|
||||
|
||||
props: Record<string, any>;
|
||||
|
||||
get loc(): SourceLocation {
|
||||
return this.rawNode?.loc;
|
||||
}
|
||||
|
||||
constructor(props: TangoNodeConstructorPropsType) {
|
||||
this.file = props.file;
|
||||
this.id = props.id;
|
||||
this.component = props.component;
|
||||
this.rawNode = props.rawNode;
|
||||
this.props = getJSXElementAttributes(cloneJSXElement(props.rawNode));
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回克隆后的 ast 节点
|
||||
* @param overrideProps 额外设置给克隆节点的属性
|
||||
* @returns
|
||||
*/
|
||||
cloneRawNode(overrideProps?: Dict) {
|
||||
return cloneJSXElement(this.rawNode, overrideProps);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空节点的指向,交给 GC 去回收
|
||||
*/
|
||||
destroy() {
|
||||
this.file = null;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ISelectedItemData, MousePoint } from '@music163/tango-helpers';
|
||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||
import { IViewFile, IWorkspace } from './interfaces';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import { IViewFile } from './interfaces';
|
||||
|
||||
type StartDataType = {
|
||||
point: MousePoint;
|
||||
@@ -24,7 +25,7 @@ export class SelectSource {
|
||||
element: null,
|
||||
};
|
||||
|
||||
private readonly workspace: IWorkspace;
|
||||
private readonly workspace: AbstractWorkspace;
|
||||
|
||||
get start() {
|
||||
return toJS(this._start);
|
||||
@@ -74,7 +75,7 @@ export class SelectSource {
|
||||
.filter((node) => !!node);
|
||||
}
|
||||
|
||||
constructor(workspace: IWorkspace) {
|
||||
constructor(workspace: AbstractWorkspace) {
|
||||
this.workspace = workspace;
|
||||
makeObservable(this, {
|
||||
_items: observable,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { JSXElement } from '@babel/types';
|
||||
import { Dict } from '@music163/tango-helpers';
|
||||
import { cloneJSXElement, getJSXElementAttributes } from '../helpers';
|
||||
import { JsViewFile } from './js-view-file';
|
||||
import { AbstractViewNode, IViewNodeInitConfig } from './abstract-view-node';
|
||||
|
||||
/**
|
||||
* 视图节点类
|
||||
*/
|
||||
export class JsxViewNode extends AbstractViewNode<JSXElement, JsViewFile> {
|
||||
get loc() {
|
||||
return this.rawNode?.loc;
|
||||
}
|
||||
|
||||
constructor(props: IViewNodeInitConfig<JSXElement, JsViewFile>) {
|
||||
super(props);
|
||||
this.props = getJSXElementAttributes(cloneJSXElement(props.rawNode));
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回克隆后的 ast 节点
|
||||
* @param overrideProps 额外设置给克隆节点的属性
|
||||
* @returns
|
||||
*/
|
||||
cloneRawNode(overrideProps?: Dict) {
|
||||
return cloneJSXElement(this.rawNode, overrideProps);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+24
-31
@@ -6,40 +6,28 @@ export type SimulatorMode = 'desktop' | 'tablet' | 'phone';
|
||||
* 文件类型枚举
|
||||
*/
|
||||
export enum FileType {
|
||||
// js 文件
|
||||
Module = 'module',
|
||||
AppEntryModule = 'appEntryModule',
|
||||
StoreEntryModule = 'storeEntryModule',
|
||||
RouteModule = 'routeModule',
|
||||
ServiceModule = 'serviceModule',
|
||||
StoreModule = 'storeModule',
|
||||
File = 'file',
|
||||
|
||||
JsFile = 'jsFile',
|
||||
JsAppEntryFile = 'jsAppEntryFile',
|
||||
JsRouteConfigFile = 'jsRouteConfigFile',
|
||||
JsStoreEntryFile = 'jsStoreEntryFile',
|
||||
JsStoreFile = 'jsStoreFile',
|
||||
JsServiceFile = 'jsServiceFile',
|
||||
JsLocalComponentsEntryFile = 'jsLocalComponentsEntryFile',
|
||||
|
||||
JsViewFile = 'jsViewFile',
|
||||
JsonViewFile = 'jsonViewFile',
|
||||
|
||||
JsonFile = 'jsonFile',
|
||||
PackageJsonFile = 'packageJsonFile',
|
||||
TangoConfigJsonFile = 'tangoConfigJsonFile',
|
||||
AppJsonFile = 'appJsonFile',
|
||||
|
||||
// 组件配置文件
|
||||
ComponentPrototypeModule = 'componentPrototypeModule',
|
||||
// 组件运行调试入口文件,一般为 `/app.js`
|
||||
ComponentDemoEntryModule = 'componentDemoEntryModule',
|
||||
/**
|
||||
* 本地组件目录的入口文件
|
||||
*/
|
||||
ComponentsEntryModule = 'componentsEntryModule',
|
||||
/**
|
||||
* @deprecated 已废弃
|
||||
*/
|
||||
BlockEntryModule = 'blockEntryModule',
|
||||
|
||||
// jsx 类型视图文件
|
||||
JsxViewModule = 'jsxViewModule',
|
||||
// json 类型视图文件
|
||||
JsonViewModule = 'jsonViewModule',
|
||||
|
||||
// 非 js 文件
|
||||
PackageJson = 'packageJson',
|
||||
TangoConfigJson = 'tangoConfigJson',
|
||||
AppJson = 'appJson',
|
||||
File = 'file',
|
||||
Json = 'json',
|
||||
Less = 'less',
|
||||
Scss = 'scss',
|
||||
}
|
||||
|
||||
export interface IFileConfig {
|
||||
@@ -57,10 +45,15 @@ export interface IFileConfig {
|
||||
type?: FileType;
|
||||
}
|
||||
|
||||
export interface IFileError {
|
||||
filename: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 视图节点数据类型
|
||||
*/
|
||||
export interface ITangoViewNodeData<T = JSXElement> {
|
||||
export interface IViewNodeData<T = JSXElement> {
|
||||
/**
|
||||
* 节点 ID
|
||||
*/
|
||||
@@ -92,7 +85,7 @@ export interface ITangoViewNodeData<T = JSXElement> {
|
||||
/**
|
||||
* 子节点列表
|
||||
*/
|
||||
children?: Array<ITangoViewNodeData<T>>;
|
||||
children?: Array<IViewNodeData<T>>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -179,12 +179,12 @@ describe('string helpers', () => {
|
||||
});
|
||||
|
||||
it('inferFileType', () => {
|
||||
expect(inferFileType('/src/pages/template.js')).toBe(FileType.JsxViewModule);
|
||||
expect(inferFileType('/src/pages/template.jsx')).toBe(FileType.JsxViewModule);
|
||||
expect(inferFileType('/src/pages/template.js')).toBe(FileType.JsViewFile);
|
||||
expect(inferFileType('/src/pages/template.jsx')).toBe(FileType.JsViewFile);
|
||||
expect(inferFileType('/src/pages/template.ejs')).toBe(FileType.File);
|
||||
expect(inferFileType('/src/index.scss')).toBe(FileType.Scss);
|
||||
expect(inferFileType('/src/index.less')).toBe(FileType.Less);
|
||||
expect(inferFileType('/src/index.json')).toBe(FileType.Json);
|
||||
expect(inferFileType('/src/index.scss')).toBe(FileType.File);
|
||||
expect(inferFileType('/src/index.less')).toBe(FileType.File);
|
||||
expect(inferFileType('/src/index.json')).toBe(FileType.JsonFile);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,70 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.4.6](https://github.com/netease/tango/compare/@music163/tango-designer@1.4.5...@music163/tango-designer@1.4.6) (2024-09-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add classNameSetter and classNameInput ([#207](https://github.com/netease/tango/issues/207)) ([55602fc](https://github.com/netease/tango/commit/55602fc961b5c20d923891ce13d7a51588bd047f))
|
||||
- renderSetter with template ([#202](https://github.com/netease/tango/issues/202)) ([ea96872](https://github.com/netease/tango/commit/ea9687226123c839f7a9e001b4c01aa8315a135d))
|
||||
- service support set description field ([#206](https://github.com/netease/tango/issues/206)) ([79f9a8c](https://github.com/netease/tango/commit/79f9a8c1791424f35f194ab2a9aa7c263ef39f40))
|
||||
|
||||
## [1.4.5](https://github.com/netease/tango/compare/@music163/tango-designer@1.4.4...@music163/tango-designer@1.4.5) (2024-09-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add prop value template & fix components popover ([#201](https://github.com/netease/tango/issues/201)) ([c4e1ed6](https://github.com/netease/tango/commit/c4e1ed67f55deb17dae740559602cd0adcfc8eb4))
|
||||
|
||||
## [1.4.4](https://github.com/netease/tango/compare/@music163/tango-designer@1.4.3...@music163/tango-designer@1.4.4) (2024-08-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- CodeInput 支持 jsx css 代码补全 ([#198](https://github.com/netease/tango/issues/198)) ([6de5488](https://github.com/netease/tango/commit/6de54884b50d683c74d695a8c13184dc0ce8d3c4))
|
||||
|
||||
## [1.4.3](https://github.com/netease/tango/compare/@music163/tango-designer@1.4.2...@music163/tango-designer@1.4.3) (2024-08-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- file errors overlay support click to error file & optimize ui ([#197](https://github.com/netease/tango/issues/197)) ([ca56edd](https://github.com/netease/tango/commit/ca56edd9ae201dbeb84b03ad390a09085433a257))
|
||||
- route matching without query string & toggle button wrapping ([#195](https://github.com/netease/tango/issues/195)) ([9394666](https://github.com/netease/tango/commit/939466648d40331800f15e886256a0789b7e7623))
|
||||
- update mobile simulator ui ([#196](https://github.com/netease/tango/issues/196)) ([65149f1](https://github.com/netease/tango/commit/65149f1326eaf78e8a27222993c3cacc674cd378))
|
||||
|
||||
## [1.4.2](https://github.com/netease/tango/compare/@music163/tango-designer@1.4.1...@music163/tango-designer@1.4.2) (2024-08-06)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-designer
|
||||
|
||||
## [1.4.1](https://github.com/netease/tango/compare/@music163/tango-designer@1.4.0...@music163/tango-designer@1.4.1) (2024-08-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- parse template code & update maxRow of TextAreaSetter ([#192](https://github.com/netease/tango/issues/192)) ([a1eea02](https://github.com/netease/tango/commit/a1eea02f9409db73f042c74d7903ad84258f25c2))
|
||||
- show right panel when design view select node & optimize editor css ([#193](https://github.com/netease/tango/issues/193)) ([b83d965](https://github.com/netease/tango/commit/b83d965955c8d33ed8d37063482c7abe079e62b1))
|
||||
|
||||
# [1.4.0](https://github.com/netease/tango/compare/@music163/tango-designer@1.3.3...@music163/tango-designer@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-designer@1.3.2...@music163/tango-designer@1.3.3) (2024-07-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- selection bug without prototype & update ToggleButton style ([#180](https://github.com/netease/tango/issues/180)) ([1d0fb4f](https://github.com/netease/tango/commit/1d0fb4f274a5b767962c54583ab5863329d5bcc5))
|
||||
- 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-designer@1.3.1...@music163/tango-designer@1.3.2) (2024-06-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- drag-panel add resizeable & height prop ([#175](https://github.com/netease/tango/issues/175)) ([b53a9f7](https://github.com/netease/tango/commit/b53a9f7eefc748ba8a7c94270c3a374b9d6ed211))
|
||||
- fix global hotkey triggering on content editable elements ([#174](https://github.com/netease/tango/issues/174)) ([a2cc418](https://github.com/netease/tango/commit/a2cc418a73edfd9b7fe8aad456ae775a49df2166))
|
||||
- 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-designer@1.3.0...@music163/tango-designer@1.3.1) (2024-06-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-designer",
|
||||
"version": "1.3.1",
|
||||
"version": "1.4.6",
|
||||
"description": "lowcode designer",
|
||||
"keywords": [
|
||||
"react"
|
||||
@@ -33,12 +33,12 @@
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/request": "^0.2.0",
|
||||
"@music163/tango-context": "^1.1.3",
|
||||
"@music163/tango-core": "^1.3.1",
|
||||
"@music163/tango-helpers": "^1.2.0",
|
||||
"@music163/tango-sandbox": "^1.0.6",
|
||||
"@music163/tango-setting-form": "^1.2.6",
|
||||
"@music163/tango-ui": "^1.3.1",
|
||||
"@music163/tango-context": "^1.1.10",
|
||||
"@music163/tango-core": "^1.4.4",
|
||||
"@music163/tango-helpers": "^1.2.4",
|
||||
"@music163/tango-sandbox": "^1.0.13",
|
||||
"@music163/tango-setting-form": "^1.2.15",
|
||||
"@music163/tango-ui": "^1.4.5",
|
||||
"antd": "^4.24.2",
|
||||
"cash-dom": "^8.1.2",
|
||||
"classnames": "^2.5.1",
|
||||
|
||||
@@ -34,17 +34,17 @@ export const ComponentsPopover = observer(
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
workspace.componentPrototypes.get(selectedNode?.name) ?? ({} as IComponentPrototype);
|
||||
|
||||
// 推荐使用的子组件
|
||||
const insertedList = useMemo(
|
||||
() =>
|
||||
Array.isArray(prototype?.childrenName)
|
||||
? prototype?.childrenName
|
||||
: [prototype?.childrenName].filter(Boolean),
|
||||
[prototype?.childrenName],
|
||||
);
|
||||
|
||||
// 推荐使用的代码片段
|
||||
const siblingList = useMemo(() => prototype?.siblingNames ?? [], [prototype.siblingNames]);
|
||||
const recommendedList = useMemo(() => {
|
||||
if (type === 'inner') {
|
||||
return prototype?.childrenName
|
||||
? Array.isArray(prototype?.childrenName)
|
||||
? prototype.childrenName
|
||||
: [prototype.childrenName]
|
||||
: [];
|
||||
}
|
||||
// 默认推荐使用相同类型的组件作为兄弟节点
|
||||
return prototype.siblingNames || [prototype.name];
|
||||
}, [prototype.childrenName, prototype.siblingNames, prototype.name, type]);
|
||||
|
||||
const tipsTextMap = useMemo(
|
||||
() => ({
|
||||
@@ -82,22 +82,15 @@ export const ComponentsPopover = observer(
|
||||
const menuList = JSON.parse(JSON.stringify(designer.menuData));
|
||||
|
||||
const commonList = menuList['common'] ?? [];
|
||||
if (commonList?.length && siblingList?.length) {
|
||||
commonList.unshift({
|
||||
title: '代码片段',
|
||||
items: siblingList,
|
||||
});
|
||||
}
|
||||
|
||||
if (commonList?.length && insertedList?.length) {
|
||||
if (commonList?.length && recommendedList.length) {
|
||||
commonList.unshift({
|
||||
title: '推荐使用',
|
||||
items: insertedList,
|
||||
items: recommendedList,
|
||||
});
|
||||
}
|
||||
|
||||
return menuList;
|
||||
}, [insertedList, siblingList, designer.menuData]);
|
||||
}, [recommendedList, designer.menuData]);
|
||||
|
||||
const innerTypeProps =
|
||||
// 手动触发 适用于 点击添加组件
|
||||
@@ -125,6 +118,8 @@ export const ComponentsPopover = observer(
|
||||
}
|
||||
footer={tipsTextMap[type]}
|
||||
width="330px"
|
||||
height="450px"
|
||||
resizeable
|
||||
maskClosable
|
||||
body={
|
||||
<ComponentsPanel
|
||||
@@ -133,10 +128,6 @@ export const ComponentsPopover = observer(
|
||||
menuData={menuData}
|
||||
layout={layout}
|
||||
onItemSelect={handleSelect}
|
||||
style={{
|
||||
maxHeight: '400px',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
{...popoverProps}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, css, Text } from 'coral-system';
|
||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
|
||||
const errorMessageStyle = css`
|
||||
padding: 35px;
|
||||
position: relative;
|
||||
white-space: pre-wrap;
|
||||
color: #f45755;
|
||||
font-weight: bold;
|
||||
height: 100%;
|
||||
background-color: #171717;
|
||||
border-radius: 8px;
|
||||
.top-bar {
|
||||
border-radius: 8px 8px 0 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 10px;
|
||||
background-color: #f45755;
|
||||
}
|
||||
.WorkspaceErrorOverlayItem {
|
||||
font-size: 18px;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
.WorkspaceErrorOverlayItemFilename {
|
||||
color: #48cacb;
|
||||
text-decoration: underline;
|
||||
letter-spacing: 1px;
|
||||
&:hover {
|
||||
color: #51b1ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* 文件错误提示浮层
|
||||
*/
|
||||
export const FileErrorsOverlay = observer(() => {
|
||||
const [isVisible, setIsVisible] = useState<boolean>(true);
|
||||
const workspace = useWorkspace();
|
||||
const designer = useDesigner();
|
||||
|
||||
const handleClose = () => {
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
const handleOpenErrorFile = (fileName: string) => {
|
||||
workspace.setActiveFile(fileName);
|
||||
designer.setActiveView('code');
|
||||
};
|
||||
|
||||
if (workspace.fileErrors.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="WorkspaceErrorOverlay"
|
||||
position="absolute"
|
||||
top="0"
|
||||
left="0"
|
||||
right="0"
|
||||
height="100%"
|
||||
bg="rgb(84, 84, 84)"
|
||||
padding="30px"
|
||||
color="red"
|
||||
overflow="auto"
|
||||
zIndex={1999}
|
||||
>
|
||||
<Box position="relative" p="4" boxShadow="0 0 10px rgba(0, 0, 0, 0.5)">
|
||||
<CloseOutlined
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 24,
|
||||
right: 12,
|
||||
color: '#fff',
|
||||
zIndex: 2,
|
||||
}}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
<Box css={errorMessageStyle}>
|
||||
<Box className="top-bar" />
|
||||
<Box fontSize="18px" mb="l">
|
||||
<Text letterSpacing=".5px" color="#c29ef5">
|
||||
[plugin: @babel/parser]
|
||||
</Text>
|
||||
<Text marginLeft={'8px'} color="#f45755">
|
||||
SyntaxError in {workspace.fileErrors.length} files,Click file to modify the error
|
||||
code
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
{workspace.fileErrors.map((fileError, index) => (
|
||||
<Box
|
||||
className="WorkspaceErrorOverlayItem"
|
||||
key={fileError.filename}
|
||||
borderBottom={index === workspace.fileErrors.length - 1 ? 'none' : 'dashed'}
|
||||
borderBottomColor="line2"
|
||||
py="l"
|
||||
>
|
||||
<Box
|
||||
className="WorkspaceErrorOverlayItemFilename"
|
||||
fontWeight="bold"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => handleOpenErrorFile(fileError.filename)}
|
||||
>
|
||||
{fileError.filename}
|
||||
</Box>
|
||||
<Box
|
||||
as="code"
|
||||
display="block"
|
||||
color="#daa15e"
|
||||
fontSize="16px"
|
||||
fontWeight={'normal'}
|
||||
>
|
||||
{fileError.message}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
@@ -4,3 +4,4 @@ export * from './input-kv';
|
||||
export * from './variable-tree';
|
||||
export * from './variable-tree-modal';
|
||||
export * from './components-popover';
|
||||
export * from './file-errors-overlay';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { isValidFunctionCode, url2serviceName } from '@music163/tango-helpers';
|
||||
import { Dict, isValidFunctionCode, url2serviceName } from '@music163/tango-helpers';
|
||||
import { InputCode } from '@music163/tango-ui';
|
||||
import { Button, Dropdown, Form, FormProps, Input, Select, Space } from 'antd';
|
||||
import { Box } from 'coral-system';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface AddServiceFormProps extends FormProps {
|
||||
initialValues?: object;
|
||||
initialValues?: Dict;
|
||||
onSubmit: (values: object) => void;
|
||||
onCancel: () => void;
|
||||
serviceModules: object[];
|
||||
@@ -88,6 +88,10 @@ export function AddServiceForm({
|
||||
>
|
||||
<Input placeholder="请输入数据服务调用名称" disabled={disabled || isModifyMode} />
|
||||
</Form.Item>
|
||||
{/* 备注 */}
|
||||
<Form.Item label="备注" name="description">
|
||||
<Input placeholder="请输入备注信息" />
|
||||
</Form.Item>
|
||||
<Form.Item label="路径" name="url" rules={[{ required: true, type: 'url' }]}>
|
||||
<Input placeholder="请输入数据服务调用名称" disabled={disabled || isModifyMode} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Dict } from '@music163/tango-helpers';
|
||||
|
||||
/**
|
||||
* 快捷键
|
||||
*/
|
||||
export class Hotkey {
|
||||
private readonly hotkeyMap = {};
|
||||
private readonly hotkeyMap: Dict = {};
|
||||
|
||||
constructor(hotkeys: Record<string, Function>) {
|
||||
Object.keys(hotkeys).forEach((hotkey) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { DropMethod, FileType, Designer, IWorkspace } from '@music163/tango-core';
|
||||
import { DropMethod, FileType, Designer, AbstractCodeWorkspace } from '@music163/tango-core';
|
||||
import { ISelectedItemData, events, getHotkey } from '@music163/tango-helpers';
|
||||
import {
|
||||
setElementStyle,
|
||||
@@ -12,7 +12,7 @@ import { Hotkey } from './hotkey';
|
||||
import { SelectModeType } from '../types';
|
||||
|
||||
interface UseDndProps {
|
||||
workspace: IWorkspace;
|
||||
workspace: AbstractCodeWorkspace;
|
||||
designer: Designer;
|
||||
/**
|
||||
* 沙箱内的 DOM 查询操作
|
||||
@@ -137,6 +137,8 @@ export function useDnd({
|
||||
const data = sandboxQuery.getDraggableParentsData(e.target as HTMLElement, true);
|
||||
if (data && data.id) {
|
||||
selectSource.select(data);
|
||||
// 画布选中节点时,自动唤起 right-panel
|
||||
designer.toggleRightPanel(true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -343,7 +345,7 @@ export function useDnd({
|
||||
// 区块不能拖拽到区块中
|
||||
if (
|
||||
workspace.dragSource.prototype.type === 'block' &&
|
||||
closetDropTargetNode.file.type === FileType.BlockEntryModule
|
||||
closetDropTargetNode.file.type === FileType.JsLocalComponentsEntryFile
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -386,7 +388,8 @@ export function useDnd({
|
||||
};
|
||||
|
||||
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||
const isInputElement = isInputElements((e.target as HTMLElement).nodeName);
|
||||
const target = e.target as HTMLElement;
|
||||
const isInputElement = target.isContentEditable || isInputElements(target.nodeName);
|
||||
// 禁用输入事件的触发的快捷键逻辑
|
||||
if (!isInputElement) {
|
||||
const key = getHotkey(e);
|
||||
|
||||
@@ -2,8 +2,6 @@ import React, { useRef, useEffect, useCallback } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { MultiEditor, MultiEditorProps } from '@music163/tango-ui';
|
||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
import { isValidCode } from '@music163/tango-core';
|
||||
import { Modal } from 'antd';
|
||||
|
||||
const ideConfig = {
|
||||
// disableFileOps: {
|
||||
@@ -21,116 +19,89 @@ const ideConfig = {
|
||||
// disableSetting: true,
|
||||
};
|
||||
|
||||
export interface CodeEditorProps extends Partial<MultiEditorProps> {
|
||||
/**
|
||||
* 是否自动清楚未使用的导入
|
||||
*/
|
||||
autoRemoveUnusedImports?: boolean;
|
||||
}
|
||||
export type CodeEditorProps = Partial<MultiEditorProps>;
|
||||
|
||||
export const CodeEditor = observer(
|
||||
({ autoRemoveUnusedImports = true, ...rest }: CodeEditorProps) => {
|
||||
const editorRef = useRef(null);
|
||||
const workspace = useWorkspace();
|
||||
const designer = useDesigner();
|
||||
const files = workspace.listFiles();
|
||||
const activeFile = workspace.activeFile;
|
||||
export const CodeEditor = observer((props: CodeEditorProps) => {
|
||||
const editorRef = useRef(null);
|
||||
const workspace = useWorkspace();
|
||||
const designer = useDesigner();
|
||||
const files = workspace.listFiles();
|
||||
const activeFile = workspace.activeFile;
|
||||
|
||||
let loc: any; // 记录视图代码的选中位置
|
||||
const selectNode = workspace.selectSource.firstNode;
|
||||
if (selectNode && activeFile === workspace.activeViewFile) {
|
||||
loc = selectNode.loc;
|
||||
}
|
||||
let loc: any; // 记录视图代码的选中位置
|
||||
const selectNode = workspace.selectSource.firstNode;
|
||||
if (selectNode && activeFile === workspace.activeViewFile) {
|
||||
loc = selectNode.loc;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
editorRef.current?.refresh(files, activeFile, loc);
|
||||
}, [files, activeFile, loc]);
|
||||
useEffect(() => {
|
||||
editorRef.current?.refresh(files, activeFile, loc);
|
||||
}, [files, activeFile, loc]);
|
||||
|
||||
const fileSave = useCallback(
|
||||
(path: string, value: string) => {
|
||||
if (!isJsFile(path)) {
|
||||
// 非 js 文件直接保存
|
||||
workspace.updateFile(path, value, autoRemoveUnusedImports);
|
||||
return;
|
||||
const fileSave = useCallback(
|
||||
(path: string, value: string) => {
|
||||
workspace.updateFile(path, value, false);
|
||||
},
|
||||
[workspace],
|
||||
);
|
||||
|
||||
const handleFileChange = useCallback(
|
||||
(type: string, info: any) => {
|
||||
switch (type) {
|
||||
case 'addFile':
|
||||
workspace.addFile(info.path, info.value);
|
||||
break;
|
||||
case 'deleteFile':
|
||||
case 'deleteFolder':
|
||||
workspace.removeFile(info.path);
|
||||
break;
|
||||
case 'renameFile':
|
||||
workspace.renameFile(info.path, info.newpath);
|
||||
workspace.setActiveFile(info.newpath);
|
||||
break;
|
||||
case 'renameFolder':
|
||||
workspace.renameFolder(info.path, info.newpath);
|
||||
break;
|
||||
case 'addFolder':
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[workspace],
|
||||
);
|
||||
|
||||
const handlePathChange = useCallback(
|
||||
(path: string) => {
|
||||
workspace.setActiveFile(path);
|
||||
},
|
||||
[workspace],
|
||||
);
|
||||
|
||||
const borderStyle =
|
||||
designer.activeView === 'dual'
|
||||
? {
|
||||
borderLeft: 'solid 1px var(--tango-colors-line2)',
|
||||
borderRight: 'solid 1px var(--tango-colors-line2)',
|
||||
}
|
||||
: {};
|
||||
|
||||
// js 文件需要先检查语法,只有语法正确才会保存
|
||||
if (isValidCode(value)) {
|
||||
workspace.updateFile(path, value, autoRemoveUnusedImports);
|
||||
} else {
|
||||
Modal.confirm({
|
||||
title: '检测到代码中存在语法错误,暂时无法将代码同步给设计器,是否回退到安全代码?',
|
||||
onOk: () => {
|
||||
editorRef.current?.refresh(files, activeFile);
|
||||
},
|
||||
onCancel: () => {},
|
||||
});
|
||||
}
|
||||
},
|
||||
[workspace, autoRemoveUnusedImports, activeFile, files],
|
||||
);
|
||||
|
||||
const handleFileChange = useCallback(
|
||||
(type: string, info: any) => {
|
||||
switch (type) {
|
||||
case 'addFile':
|
||||
workspace.addFile(info.path, info.value);
|
||||
break;
|
||||
case 'deleteFile':
|
||||
case 'deleteFolder':
|
||||
workspace.removeFile(info.path);
|
||||
break;
|
||||
case 'renameFile':
|
||||
workspace.renameFile(info.path, info.newpath);
|
||||
workspace.setActiveFile(info.newpath);
|
||||
break;
|
||||
case 'renameFolder':
|
||||
workspace.renameFolder(info.path, info.newpath);
|
||||
break;
|
||||
case 'addFolder':
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[workspace],
|
||||
);
|
||||
|
||||
const handlePathChange = useCallback(
|
||||
(path: string) => {
|
||||
workspace.setActiveFile(path);
|
||||
},
|
||||
[workspace],
|
||||
);
|
||||
|
||||
const borderStyle =
|
||||
designer.activeView === 'dual'
|
||||
? {
|
||||
borderLeft: 'solid 1px var(--tango-colors-line2)',
|
||||
}
|
||||
: {};
|
||||
|
||||
return (
|
||||
<Box display="flex" flexDirection="row" height="100%" bg="white" {...borderStyle}>
|
||||
<MultiEditor
|
||||
ref={editorRef}
|
||||
options={{
|
||||
fontSize: 14,
|
||||
automaticLayout: true,
|
||||
}}
|
||||
ideConfig={ideConfig}
|
||||
onFileSave={fileSave}
|
||||
onPathChange={handlePathChange}
|
||||
onFileChange={handleFileChange}
|
||||
defaultPath={activeFile}
|
||||
defaultTheme="GithubLightDefault"
|
||||
defaultFiles={files}
|
||||
{...rest}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
function isJsFile(path: string) {
|
||||
return /.jsx?$/.test(path);
|
||||
}
|
||||
return (
|
||||
<Box display="flex" flexDirection="row" height="100%" bg="white" {...borderStyle}>
|
||||
<MultiEditor
|
||||
ref={editorRef}
|
||||
options={{
|
||||
fontSize: 14,
|
||||
automaticLayout: true,
|
||||
}}
|
||||
ideConfig={ideConfig}
|
||||
onFileSave={fileSave}
|
||||
onPathChange={handlePathChange}
|
||||
onFileChange={handleFileChange}
|
||||
defaultPath={activeFile}
|
||||
defaultTheme="GithubLightDefault"
|
||||
defaultFiles={files}
|
||||
{...props}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useImperativeHandle, ForwardedRef, useRef, useState, useEffect } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { CodeSandbox, CodeSandboxProps } from '@music163/tango-sandbox';
|
||||
import { getValue, isFunction, logger, pick, setValue } from '@music163/tango-helpers';
|
||||
import { Dict, getValue, isFunction, logger, pick, setValue } from '@music163/tango-helpers';
|
||||
import { observer, useWorkspace, useDesigner } from '@music163/tango-context';
|
||||
import { Simulator, Viewport } from '../simulator';
|
||||
import { useSandboxQuery } from '../context';
|
||||
@@ -88,7 +88,7 @@ function useSandbox({
|
||||
onViewChange: (data) => onViewChange && onViewChange(data, getSandboxConfig()),
|
||||
});
|
||||
|
||||
let files = Array.from(workspace.files.keys()).reduce((prev, filename) => {
|
||||
let files = Array.from(workspace.files.keys()).reduce<Dict>((prev, filename) => {
|
||||
let code = workspace.getFile(filename).code;
|
||||
if (filename === '/tango.config.json') {
|
||||
code = mergeTangoConfigJson(code, { isPreview, injectScript, formatter: configFormatter });
|
||||
@@ -109,7 +109,7 @@ function useSandbox({
|
||||
// 根据当前 workspace 状态与组件传入的状态是否一致,控制是否需要切换到空白路由
|
||||
const display = isActive ? 'block' : 'none';
|
||||
const routePath = isActive ? startRoute || workspace.activeRoute : LANDING_PAGE_PATH;
|
||||
const routerMode = fixRouterMode(workspace.appEntryModule?.routerType);
|
||||
const routerMode = fixRouterMode(workspace.jsAppEntryFile?.routerType);
|
||||
|
||||
const sandboxProps = isPreview
|
||||
? {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Radio, RadioProps, Tooltip } from 'antd';
|
||||
import type { OptionType } from '@music163/tango-helpers';
|
||||
import type { IOptionItem } from '@music163/tango-helpers';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { IconFont } from '@music163/tango-ui';
|
||||
|
||||
interface ChoiceSetterProps {
|
||||
options?: OptionType[];
|
||||
options?: IOptionItem[];
|
||||
}
|
||||
|
||||
export function ChoiceSetter({
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { ClassNameInput } from '@music163/tango-ui';
|
||||
import React from 'react';
|
||||
|
||||
export function ClassNameSetter({ value, onChange }: FormItemComponentProps<string>) {
|
||||
return <ClassNameInput value={value} onChange={onChange} />;
|
||||
}
|
||||
+94
-35
@@ -2,9 +2,17 @@ import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Box, Text, css } from 'coral-system';
|
||||
import { Dropdown, Button } from 'antd';
|
||||
import { isValidExpressionCode } from '@music163/tango-core';
|
||||
import { getValue, IVariableTreeNode, noop } from '@music163/tango-helpers';
|
||||
import { CloseCircleFilled, ExpandAltOutlined, MenuOutlined } from '@ant-design/icons';
|
||||
import { Panel, InputCode, Action, DragPanel } from '@music163/tango-ui';
|
||||
import { getValue, interpolate, IVariableTreeNode, noop } from '@music163/tango-helpers';
|
||||
import { CloseCircleFilled, MenuOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Panel,
|
||||
InputCode,
|
||||
Action,
|
||||
DragPanel,
|
||||
PopOutOutlined,
|
||||
InputCodeProps,
|
||||
InputStyleCode,
|
||||
} from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||
import { VariableTree } from '../components';
|
||||
@@ -26,6 +34,19 @@ export const jsonValueValidate = (value: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 拼装回调函数
|
||||
* @param value 回调函数体
|
||||
* @param template 回调函数模板
|
||||
* @returns
|
||||
*/
|
||||
export function getCallbackValue(value: string, template?: string) {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
return template ? interpolate(template, { content: value }) : `() => {\n ${value}\n}`;
|
||||
}
|
||||
|
||||
const suffixStyle = css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -37,26 +58,32 @@ const suffixStyle = css`
|
||||
}
|
||||
`;
|
||||
|
||||
export interface ExpressionSetterProps extends FormItemComponentProps<string> {
|
||||
export interface CodeSetterProps extends FormItemComponentProps<string> {
|
||||
modalTitle?: string;
|
||||
modalTip?: string;
|
||||
autoCompleteOptions?: string[];
|
||||
allowClear?: boolean;
|
||||
showOptionsDropDown?: boolean;
|
||||
/**
|
||||
* 表达式类型指定为 css object
|
||||
*/
|
||||
expressionType?: 'cssObject';
|
||||
}
|
||||
|
||||
export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
export function CodeSetter(props: CodeSetterProps) {
|
||||
const {
|
||||
onChange,
|
||||
modalTitle,
|
||||
modalTip,
|
||||
autoCompleteOptions,
|
||||
placeholder = '在这里输入JS代码',
|
||||
template,
|
||||
placeholder = '在这里输入代码',
|
||||
value: valueProp,
|
||||
status,
|
||||
allowClear = true,
|
||||
newStoreTemplate,
|
||||
showOptionsDropDown = true,
|
||||
expressionType,
|
||||
} = props;
|
||||
// const codeValue = getCodeOfWrappedCode(valueProp);
|
||||
const [inputValue, setInputValue] = useState(valueProp);
|
||||
@@ -79,10 +106,12 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
const sandbox = useSandboxQuery();
|
||||
const evaluateContext = sandbox.window;
|
||||
|
||||
const InputCodeComponent = expressionType === 'cssObject' ? InputStyleCode : InputCode;
|
||||
|
||||
return (
|
||||
<Box className="ExpressionSetter">
|
||||
{/* 同时支持下拉框展示 */}
|
||||
<InputCode
|
||||
<InputCodeComponent
|
||||
placeholder={placeholder}
|
||||
suffix={
|
||||
<Box css={suffixStyle}>
|
||||
@@ -90,7 +119,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
<CloseCircleFilled
|
||||
title="清空"
|
||||
onClick={() => {
|
||||
change('');
|
||||
change(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -119,8 +148,10 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
subTitle={modalTip}
|
||||
placeholder={placeholder}
|
||||
autoCompleteOptions={autoCompleteOptions}
|
||||
template={template}
|
||||
newStoreTemplate={newStoreTemplate}
|
||||
value={inputValue}
|
||||
expressionType={expressionType}
|
||||
onOk={(value) => {
|
||||
change(value);
|
||||
}}
|
||||
@@ -141,7 +172,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export interface ExpressionPopoverProps {
|
||||
export interface ExpressionPopoverProps extends InputCodeProps {
|
||||
title?: string;
|
||||
subTitle?: string;
|
||||
placeholder?: string;
|
||||
@@ -150,10 +181,18 @@ export interface ExpressionPopoverProps {
|
||||
onOk?: (value: string) => void;
|
||||
dataSource?: IVariableTreeNode[];
|
||||
autoCompleteOptions?: string[];
|
||||
/**
|
||||
* 值的模板,一般用于定义函数模板
|
||||
*/
|
||||
template?: string;
|
||||
/**
|
||||
* 新建 store 的模板代码
|
||||
*/
|
||||
newStoreTemplate?: string;
|
||||
/**
|
||||
* 表达式类型指定为 css object
|
||||
*/
|
||||
expressionType?: 'cssObject';
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -166,12 +205,15 @@ export function ExpressionPopover({
|
||||
value,
|
||||
dataSource,
|
||||
autoCompleteOptions,
|
||||
template,
|
||||
newStoreTemplate = CODE_TEMPLATES.newStoreTemplate,
|
||||
children,
|
||||
expressionType,
|
||||
}: ExpressionPopoverProps) {
|
||||
const [exp, setExp] = useState(value ?? defaultValue);
|
||||
const [error, setError] = useState('');
|
||||
const workspace = useWorkspace();
|
||||
const InputCodeComponent = expressionType === 'cssObject' ? InputStyleCode : InputCode;
|
||||
|
||||
const selectNodePath = workspace.selectSource.selected[0]?.codeId;
|
||||
|
||||
@@ -182,7 +224,7 @@ export function ExpressionPopover({
|
||||
}));
|
||||
|
||||
const sandbox = useSandboxQuery();
|
||||
const evaluateContext = sandbox.window;
|
||||
const evaluateContext: any = sandbox.window;
|
||||
|
||||
const handleExpInputChange = (val: string) => {
|
||||
setExp(val?.trim());
|
||||
@@ -195,56 +237,64 @@ export function ExpressionPopover({
|
||||
return (
|
||||
<DragPanel
|
||||
width={700}
|
||||
title={`将 ${selectNodePath}/${title} 设置为引用变量或自定义表达式`}
|
||||
extra={subTitle}
|
||||
height={615}
|
||||
resizeable
|
||||
title={`使用代码设置 ${selectNodePath} 组件的${title}`}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setExp(undefined);
|
||||
setError('');
|
||||
}
|
||||
}}
|
||||
popoverStyle={{
|
||||
height: '615px',
|
||||
}}
|
||||
body={
|
||||
<>
|
||||
<Panel shape="solid">
|
||||
<InputCode
|
||||
<Box display="flex" flexDirection="column">
|
||||
<Box p="m">
|
||||
<InputCodeComponent
|
||||
shape="inset"
|
||||
minHeight="56px"
|
||||
maxHeight="200px"
|
||||
maxHeight="160px"
|
||||
value={exp}
|
||||
placeholder={placeholder}
|
||||
onChange={handleExpInputChange}
|
||||
onBlur={() => {
|
||||
setError(expressionValueValidate(exp));
|
||||
if (exp) {
|
||||
setError(expressionValueValidate(exp));
|
||||
}
|
||||
}}
|
||||
autoCompleteContext={evaluateContext}
|
||||
autoCompleteOptions={autoCompleteOptions}
|
||||
/>
|
||||
{error ? (
|
||||
<Text color="red" fontSize="12px">
|
||||
<Text color="red" fontSize="12px" as="div">
|
||||
出错了!输入的表达式存在语法错误,请修改后再提交!
|
||||
</Text>
|
||||
) : (
|
||||
<Text fontSize="12px" color="text3">
|
||||
说明:你可以在上面的代码输入框里输入常规的 javascript 代码,还可以直接使用 jsx
|
||||
) : null}
|
||||
<Box fontSize="12px" color="text2">
|
||||
<Text display="block">说明:</Text>
|
||||
{subTitle && <Text display="block">{subTitle}</Text>}
|
||||
<Text display="block">
|
||||
你可以在上面的代码输入框里输入常规的 javascript 代码,还可以直接使用 jsx
|
||||
代码,但需要符合该属性的接受值定义。
|
||||
</Text>
|
||||
)}
|
||||
</Panel>
|
||||
</Box>
|
||||
</Box>
|
||||
<Panel
|
||||
title="从变量列表中选中"
|
||||
shape="solid"
|
||||
borderTop="0"
|
||||
overflow="hidden"
|
||||
bodyProps={{ overflow: 'hidden' }}
|
||||
borderLeft="0"
|
||||
borderRight="0"
|
||||
flex={1}
|
||||
bodyProps={{
|
||||
style: {
|
||||
overflow: 'hidden',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<VariableTree
|
||||
height={380}
|
||||
height="100%"
|
||||
showViewButton
|
||||
dataSource={dataSource || expressionVariables}
|
||||
appContext={sandbox?.window['tango']}
|
||||
appContext={evaluateContext?.['tango']}
|
||||
getStoreNames={() => Object.keys(workspace.storeModules)}
|
||||
serviceModules={serviceModules}
|
||||
getServiceData={(serviceKey) => {
|
||||
@@ -265,7 +315,11 @@ export function ExpressionPopover({
|
||||
}
|
||||
let str;
|
||||
if (/^(stores|services)\./.test(node.key)) {
|
||||
str = `tango.${node.key.replaceAll('.', '?.')}`;
|
||||
// 从匹配到的第二个点开始替换为 ?.,因为第一个点是 stores 或 services
|
||||
str = `tango.${node.key.replace(/(?<=\..*?)\./g, '?.')}`;
|
||||
if (node.type === 'function') {
|
||||
str = getCallbackValue(`${str}();`, template);
|
||||
}
|
||||
} else {
|
||||
str = `${node.key}`;
|
||||
}
|
||||
@@ -307,10 +361,10 @@ export function ExpressionPopover({
|
||||
}}
|
||||
/>
|
||||
</Panel>
|
||||
</>
|
||||
</Box>
|
||||
}
|
||||
footer={(close) => (
|
||||
<Box display="flex" justifyContent="flex-end" gap="5px">
|
||||
<Box display="flex" width="100%" justifyContent="flex-end" gap="5px">
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
@@ -330,7 +384,12 @@ export function ExpressionPopover({
|
||||
)}
|
||||
>
|
||||
{children || (
|
||||
<Action tooltip="打开表达式变量选择面板" icon={<ExpandAltOutlined />} size="small" />
|
||||
<Action
|
||||
tooltip="弹出代码编辑和变量选择面板"
|
||||
tooltipProps={{ placement: 'topLeft' }}
|
||||
icon={<PopOutOutlined />}
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
</DragPanel>
|
||||
);
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { css, Box, Text } from 'coral-system';
|
||||
import { AutoComplete } from 'antd';
|
||||
import { AutoComplete, Input } from 'antd';
|
||||
import { ActionSelect } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||
import { wrapCode } from '@music163/tango-helpers';
|
||||
import { ExpressionPopover } from './expression-setter';
|
||||
import { Dict, wrapCode } from '@music163/tango-helpers';
|
||||
import { ExpressionPopover, getCallbackValue } from './code-setter';
|
||||
import { value2code } from '@music163/tango-core';
|
||||
|
||||
enum EventAction {
|
||||
@@ -31,7 +31,7 @@ export type EventSetterProps = FormItemComponentProps<string>;
|
||||
* 事件监听函数绑定器
|
||||
*/
|
||||
export function EventSetter(props: EventSetterProps) {
|
||||
const { value, onChange, modalTitle } = props;
|
||||
const { value, onChange, modalTitle, modalTip, template } = props;
|
||||
const [type, setType] = useState<EventAction>(); // 事件类型
|
||||
const [temp, setTemp] = useState(''); // 二级暂存值
|
||||
const { actionVariables, routeOptions } = useWorkspaceData();
|
||||
@@ -59,7 +59,9 @@ export function EventSetter(props: EventSetterProps) {
|
||||
label: (
|
||||
<ExpressionPopover
|
||||
title={modalTitle}
|
||||
subTitle={modalTip}
|
||||
value={value}
|
||||
template={template}
|
||||
onOk={(nextValue) => {
|
||||
handleChange(nextValue);
|
||||
}}
|
||||
@@ -74,30 +76,34 @@ export function EventSetter(props: EventSetterProps) {
|
||||
{ label: '打开弹窗', value: EventAction.OpenModal },
|
||||
{ label: '关闭弹窗', value: EventAction.CloseModal },
|
||||
],
|
||||
[modalTitle, value, actionVariables, handleChange],
|
||||
[modalTitle, value, actionVariables, template, handleChange],
|
||||
);
|
||||
|
||||
const onAction = (key: string) => {
|
||||
setType(key as EventAction); // 记录事件类型
|
||||
setTemp(''); // 重置二级选项值
|
||||
|
||||
switch (key) {
|
||||
case EventAction.ConsoleLog:
|
||||
handleChange('(...args) => console.log(...args)');
|
||||
break;
|
||||
case EventAction.NoAction:
|
||||
handleChange(undefined);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
if (key === EventAction.NoAction) {
|
||||
handleChange(undefined);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const actionText = getActionText(type, temp, code);
|
||||
|
||||
return (
|
||||
<Box css={wrapperStyle}>
|
||||
<ActionSelect options={options} onSelect={onAction} text={actionText} />
|
||||
<ActionSelect options={options} onSelect={onAction} defaultText="请选择动作类型" />
|
||||
{type === EventAction.ConsoleLog && (
|
||||
<Input
|
||||
placeholder="输入 Console.log 日志内容"
|
||||
value={temp}
|
||||
onChange={(e) => setTemp(e.target.value)}
|
||||
onBlur={() => {
|
||||
if (temp) {
|
||||
handleChange(getExpressionValue(type, temp));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{type === EventAction.NavigateTo && (
|
||||
<AutoComplete
|
||||
placeholder="选择或输入页面路由"
|
||||
@@ -141,26 +147,16 @@ export function EventSetter(props: EventSetterProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const handlerMap = {
|
||||
[EventAction.OpenModal]: 'openModal',
|
||||
[EventAction.CloseModal]: 'closeModal',
|
||||
[EventAction.NavigateTo]: 'navigateTo',
|
||||
const handlerMap: Dict = {
|
||||
[EventAction.ConsoleLog]: 'console.log',
|
||||
[EventAction.OpenModal]: 'tango.openModal',
|
||||
[EventAction.CloseModal]: 'tango.closeModal',
|
||||
[EventAction.NavigateTo]: 'tango.navigateTo',
|
||||
};
|
||||
|
||||
function getActionText(type: EventAction, temp: string, fallbackCode: string) {
|
||||
let text;
|
||||
if (handlerMap[type]) {
|
||||
text = getExpressionValue(type, temp);
|
||||
} else if (fallbackCode) {
|
||||
text = fallbackCode;
|
||||
}
|
||||
text = text || '请选择';
|
||||
return text;
|
||||
}
|
||||
|
||||
function getExpressionValue(type: EventAction, value = '') {
|
||||
const handler = handlerMap[type];
|
||||
if (handler) {
|
||||
return `() => tango.${handler}("${value}")`;
|
||||
return getCallbackValue(`${handler}("${value}");`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@ import { CssSetter } from './css-setter';
|
||||
import { DateRangeSetter, DateSetter, TimeRangeSetter } from './date-setter';
|
||||
import { EnumSetter } from './enum-setter';
|
||||
import { EventSetter } from './event-setter';
|
||||
import { ExpressionSetter } from './expression-setter';
|
||||
import { CodeSetter } from './code-setter';
|
||||
import { JSONSetter } from './json-setter';
|
||||
import { JsxSetter } from './jsx-setter';
|
||||
import { ListSetter } from './list-setter';
|
||||
import { ModelSetter } from './model-setter';
|
||||
import { OptionSetter } from './option-setter';
|
||||
import { PickerSetter } from './picker-setter';
|
||||
import { RenderSetter, TableCellSetter, TableExpandableSetter } from './render-setter';
|
||||
import { RenderSetter } from './render-setter';
|
||||
import { RouterSetter } from './router-setter';
|
||||
import {
|
||||
SpacingSetter,
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
FlexDirectionSetter,
|
||||
} from './style-setter';
|
||||
import { ChoiceSetter } from './choice-setter';
|
||||
import { ClassNameSetter } from './classname-setter';
|
||||
import { isValidExpressionCode } from '@music163/tango-core';
|
||||
|
||||
const codeValidate: IFormItemCreateOptions['validate'] = (value, field) => {
|
||||
@@ -49,10 +50,14 @@ export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
|
||||
{
|
||||
name: 'codeSetter',
|
||||
alias: ['expSetter', 'expressionSetter'],
|
||||
component: ExpressionSetter,
|
||||
component: CodeSetter,
|
||||
type: 'code',
|
||||
validate: codeValidate,
|
||||
},
|
||||
{
|
||||
name: 'classNameSetter',
|
||||
component: ClassNameSetter,
|
||||
},
|
||||
{
|
||||
name: 'radioGroupSetter',
|
||||
alias: ['choiceSetter'],
|
||||
@@ -126,22 +131,12 @@ export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
|
||||
component: PickerSetter,
|
||||
},
|
||||
{
|
||||
name: 'renderPropsSetter',
|
||||
name: 'renderSetter',
|
||||
alias: ['renderPropsSetter'],
|
||||
component: RenderSetter,
|
||||
type: 'code',
|
||||
validate: codeValidate,
|
||||
},
|
||||
{
|
||||
name: 'tableCellSetter',
|
||||
component: TableCellSetter,
|
||||
type: 'code',
|
||||
},
|
||||
{
|
||||
name: 'tableExpandableSetter',
|
||||
component: TableExpandableSetter,
|
||||
type: 'code',
|
||||
validate: codeValidate,
|
||||
},
|
||||
{
|
||||
name: 'routerSetter',
|
||||
component: RouterSetter,
|
||||
|
||||
@@ -3,10 +3,11 @@ import { css, Box } from 'coral-system';
|
||||
import { Form, Input, Button, Popover } from 'antd';
|
||||
import { DeleteOutlined, EditOutlined, HolderOutlined } from '@ant-design/icons';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { Dict } from '@music163/tango-helpers';
|
||||
import { DragBox } from '../components';
|
||||
|
||||
function copyWithoutUndefined(source: Record<string, any>) {
|
||||
const ret = {};
|
||||
function copyWithoutUndefined(source: Dict) {
|
||||
const ret: Dict = {};
|
||||
for (const key in source) {
|
||||
if (source[key] !== undefined) {
|
||||
ret[key] = source[key];
|
||||
|
||||
@@ -60,7 +60,7 @@ export function ModelSetter({
|
||||
}: FormItemComponentProps) {
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
const { modelVariables } = useWorkspaceData();
|
||||
const evaluateContext = useSandboxQuery().window || {};
|
||||
const evaluateContext: any = useSandboxQuery().window || {};
|
||||
const workspace = useWorkspace();
|
||||
const definedVariables = useMemo(() => {
|
||||
const list: string[] = [];
|
||||
|
||||
@@ -2,21 +2,16 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { ActionSelect, InputCode } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { Box } from 'coral-system';
|
||||
|
||||
interface IRenderOption {
|
||||
label: string;
|
||||
value: string;
|
||||
render?: string;
|
||||
relatedImports?: string[];
|
||||
}
|
||||
import { Dict, IOptionItem } from '@music163/tango-helpers';
|
||||
import { getCallbackValue } from './code-setter';
|
||||
|
||||
export interface RenderSetterProps {
|
||||
text?: string;
|
||||
options?: IRenderOption[];
|
||||
fallbackOption?: IRenderOption;
|
||||
options?: IOptionItem[];
|
||||
fallbackOption?: IOptionItem;
|
||||
}
|
||||
|
||||
const defaultOptions: IRenderOption[] = [
|
||||
const defaultOptions: IOptionItem[] = [
|
||||
{ label: '取消自定义', value: '' },
|
||||
{ label: '自定义渲染', value: 'Box', render: '() => <Box></Box>' },
|
||||
];
|
||||
@@ -29,6 +24,7 @@ export function RenderSetter({
|
||||
onChange,
|
||||
text = '自定义渲染为',
|
||||
options = defaultOptions,
|
||||
template = `() => {{content}}`,
|
||||
fallbackOption,
|
||||
}: FormItemComponentProps & RenderSetterProps) {
|
||||
const [inputValue, setInputValue] = useState(value || '');
|
||||
@@ -37,7 +33,7 @@ export function RenderSetter({
|
||||
}, [value]);
|
||||
|
||||
const optionsMap = useMemo(() => {
|
||||
return options.reduce((prev, cur) => {
|
||||
return options.reduce<Dict>((prev, cur) => {
|
||||
prev[cur.value] = cur;
|
||||
return prev;
|
||||
}, {});
|
||||
@@ -46,13 +42,14 @@ export function RenderSetter({
|
||||
const onSelect = useCallback(
|
||||
(key: string) => {
|
||||
const option = optionsMap[key] || fallbackOption;
|
||||
if (option?.render) {
|
||||
onChange(option.render, { relatedImports: option.relatedImports });
|
||||
const next = option?.render || getCallbackValue(option.renderBody, template);
|
||||
if (next) {
|
||||
onChange(next, { relatedImports: option.relatedImports });
|
||||
} else {
|
||||
onChange(undefined);
|
||||
}
|
||||
},
|
||||
[optionsMap, fallbackOption, onChange],
|
||||
[optionsMap, fallbackOption, onChange, template],
|
||||
);
|
||||
return (
|
||||
<Box>
|
||||
@@ -67,69 +64,3 @@ export function RenderSetter({
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const getRender = (content: string, type?: 'tableCell' | 'tableExpandable') => {
|
||||
let code;
|
||||
switch (type) {
|
||||
case 'tableCell':
|
||||
code = `(value, record, index) => ${content}`;
|
||||
break;
|
||||
case 'tableExpandable':
|
||||
code = `{
|
||||
expandedRowRender: (record) => ${content},
|
||||
rowExpandable: (record) => true
|
||||
}`;
|
||||
break;
|
||||
default:
|
||||
code = `() => ${content}`;
|
||||
break;
|
||||
}
|
||||
return code;
|
||||
};
|
||||
|
||||
const tableCellOptions: RenderSetterProps['options'] = [
|
||||
{ label: '取消自定义', value: '' },
|
||||
{
|
||||
label: '自定义区域',
|
||||
value: 'Box',
|
||||
render: getRender('<Box></Box>', 'tableCell'),
|
||||
relatedImports: ['Box'],
|
||||
},
|
||||
{
|
||||
label: '标签',
|
||||
value: 'Tag',
|
||||
render: getRender('<Tag>tag</Tag>', 'tableCell'),
|
||||
relatedImports: ['Tag'],
|
||||
},
|
||||
{
|
||||
label: '按钮',
|
||||
value: 'Button',
|
||||
render: getRender('<Button>button</Button>', 'tableCell'),
|
||||
relatedImports: ['Button'],
|
||||
},
|
||||
{
|
||||
label: '图片',
|
||||
value: 'Image',
|
||||
render: getRender('<Image width={150} src="https://picsum.photos/100" />', 'tableCell'),
|
||||
relatedImports: ['Image'],
|
||||
},
|
||||
];
|
||||
|
||||
const tableExpandableOptions: RenderSetterProps['options'] = [
|
||||
{
|
||||
label: '设置可展开行',
|
||||
value: 'Box',
|
||||
render: getRender('<Box></Box>', 'tableExpandable'),
|
||||
relatedImports: ['Box'],
|
||||
},
|
||||
{ label: '取消可展开行', value: '' },
|
||||
];
|
||||
|
||||
export function TableCellSetter(props: FormItemComponentProps) {
|
||||
return <RenderSetter options={tableCellOptions} {...props} />;
|
||||
}
|
||||
|
||||
// FIXME: 应该直接用 props 嵌套的模式
|
||||
export function TableExpandableSetter(props: FormItemComponentProps) {
|
||||
return <RenderSetter options={tableExpandableOptions} text="配置表格可展开行" {...props} />;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { Input, Radio } from 'antd';
|
||||
import { isValidUrl } from '@music163/tango-helpers';
|
||||
import { Dict, isValidUrl } from '@music163/tango-helpers';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { useWorkspaceData } from '@music163/tango-context';
|
||||
import { PickerSetter } from './picker-setter';
|
||||
|
||||
const routerModeMap: Dict = {
|
||||
link: '链接',
|
||||
router: '路由',
|
||||
both: '',
|
||||
};
|
||||
|
||||
export function RouterSetter(props: FormItemComponentProps) {
|
||||
const [type, setType] = useState<'input' | 'select'>(() => {
|
||||
return isValidUrl(props.value) ? 'input' : 'select';
|
||||
@@ -37,7 +43,7 @@ export function RouterSetter(props: FormItemComponentProps) {
|
||||
)}
|
||||
{type === 'input' && (
|
||||
<Input
|
||||
placeholder={`请输入任意${{ link: '链接', router: '路由', both: '' }[displayType]}地址`}
|
||||
placeholder={`请输入任意${routerModeMap[displayType]}地址`}
|
||||
{...props}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { SettingForm, FormModel, SettingFormProps } from '@music163/tango-setting-form';
|
||||
import { Panel } from '@music163/tango-ui';
|
||||
import { clone, parseDndId } from '@music163/tango-helpers';
|
||||
import { clone, Dict, parseDndId } from '@music163/tango-helpers';
|
||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
import { registerBuiltinSetters } from './setters';
|
||||
|
||||
@@ -34,7 +34,7 @@ export const SettingPanel = observer((props: SettingPanelProps) => {
|
||||
}
|
||||
|
||||
const getSettingValue = (attributes: Record<string, string>) => {
|
||||
const ret = {};
|
||||
const ret: Dict = {};
|
||||
const keys = Object.keys(attributes);
|
||||
for (const key of keys) {
|
||||
if (/^data-/.test(key)) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useMemo, useState } from 'react';
|
||||
import { Box, Grid, Text } from 'coral-system';
|
||||
import styled, { css } from 'styled-components';
|
||||
import {
|
||||
Dict,
|
||||
IComponentPrototype,
|
||||
MenuDataType,
|
||||
MenuValueType,
|
||||
@@ -66,7 +67,7 @@ export interface ComponentsPanelProps {
|
||||
layout?: 'grid' | 'line';
|
||||
}
|
||||
|
||||
const localeMap = {
|
||||
const localeMap: Dict = {
|
||||
common: '基础组件',
|
||||
atom: '原子组件',
|
||||
snippet: '组合',
|
||||
@@ -124,7 +125,7 @@ export const ComponentsPanel = observer(
|
||||
const tabs = Object.keys(menuData).map((key) => ({
|
||||
key,
|
||||
label: localeMap[key],
|
||||
children: <MaterialList data={menuData[key]} />,
|
||||
children: <MaterialList data={(menuData as any)[key]} />,
|
||||
}));
|
||||
|
||||
if (showBizComps) {
|
||||
@@ -364,7 +365,7 @@ function MaterialGrid({ data }: MaterialProps) {
|
||||
<IconFont className="material-icon" type={data.icon || 'icon-placeholder'} />
|
||||
) : (
|
||||
<Box className="material-icon">
|
||||
<img src={icon} alt={data.name} />
|
||||
<img src={icon} alt={data.name} height="32" />
|
||||
</Box>
|
||||
);
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ const DataSourceView = observer(() => {
|
||||
<Box className="ServiceFunctionList" p="m">
|
||||
<VariableTree
|
||||
dataSource={serviceVariables}
|
||||
appContext={sandbox?.window['tango']}
|
||||
appContext={(sandbox?.window as any)['tango']}
|
||||
serviceModules={serviceModules}
|
||||
onRemoveService={(variableKey) => {
|
||||
workspace.removeServiceFunction(variableKey);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ConfigGroup, ConfigItem } from '@music163/tango-ui';
|
||||
import { Box, css } from 'coral-system';
|
||||
import { Input, Switch, Form, Button, message, FormProps } from 'antd';
|
||||
import { CloseCircleOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
import { Dict } from '@music163/tango-helpers';
|
||||
|
||||
export default observer(() => {
|
||||
const workspace = useWorkspace();
|
||||
@@ -63,7 +64,7 @@ function ProxyManage({
|
||||
initialValues: initialValuesProp = {},
|
||||
onSave,
|
||||
}: {
|
||||
initialValues?: object;
|
||||
initialValues?: Dict;
|
||||
onSave?: (values: any) => void;
|
||||
}) {
|
||||
const initialValues = useMemo(() => {
|
||||
@@ -84,7 +85,7 @@ function ProxyManage({
|
||||
{...proxyManageFormStyle}
|
||||
initialValues={initialValues}
|
||||
onFinish={(values) => {
|
||||
const proxy = values.proxy.reduce((prev: object, { path, ...rest }: any) => {
|
||||
const proxy = values.proxy.reduce((prev: any, { path, ...rest }: any) => {
|
||||
prev[path] = rest;
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
@@ -21,7 +21,6 @@ import { ColorTag, ConfigGroup, ConfigItem } from '@music163/tango-ui';
|
||||
import { MinusCircleOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { useBoolean } from '@music163/tango-helpers';
|
||||
import { isUndefined } from 'lodash-es';
|
||||
import { Workspace } from '@music163/tango-core';
|
||||
import { useSandboxQuery } from '../context';
|
||||
|
||||
enum DependencyItemType {
|
||||
@@ -68,17 +67,6 @@ export const DependencyPanel = observer(
|
||||
[dependencies, baseDependencies, bizDependencies, packages, packageNames],
|
||||
);
|
||||
|
||||
// 业务组件依赖
|
||||
const bizDeps = useMemo(
|
||||
() =>
|
||||
bizDependencies?.map((name: string) => ({
|
||||
name,
|
||||
version: dependencies[name] ?? packages?.[name]?.version,
|
||||
package: packages?.[name],
|
||||
})) || [],
|
||||
[bizDependencies, dependencies, packages],
|
||||
);
|
||||
|
||||
// 基础包
|
||||
const baseDeps = useMemo(
|
||||
() =>
|
||||
@@ -180,7 +168,7 @@ function RenderItem({
|
||||
}: RenderItemProps) {
|
||||
const [open, { on, off }] = useBoolean(false);
|
||||
|
||||
const workspace = useWorkspace() as Workspace;
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const basePackage = useMemo(() => {
|
||||
if (type !== DependencyItemType.基础包 || !templateBaseDependencies) return undefined;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Box, css } from 'coral-system';
|
||||
import { IconFont } from '@music163/tango-ui';
|
||||
import { EyeOutlined, EyeInvisibleOutlined, EllipsisOutlined } from '@ant-design/icons';
|
||||
import { observer, useWorkspace } from '@music163/tango-context';
|
||||
import { DropMethod, ITangoViewNodeData } from '@music163/tango-core';
|
||||
import { DropMethod, IViewNodeData } from '@music163/tango-core';
|
||||
import { noop, parseDndId } from '@music163/tango-helpers';
|
||||
import { useSandboxQuery } from '../../context';
|
||||
import { buildQueryBySlotId } from '../../helpers';
|
||||
@@ -65,7 +65,7 @@ const filedNames = {
|
||||
children: 'children',
|
||||
};
|
||||
|
||||
const getNodeKeys = (data: ITangoViewNodeData[]) => {
|
||||
const getNodeKeys = (data: IViewNodeData[]) => {
|
||||
const ids: string[] = [];
|
||||
data?.forEach((node) => {
|
||||
ids.push(node.id);
|
||||
@@ -76,7 +76,7 @@ const getNodeKeys = (data: ITangoViewNodeData[]) => {
|
||||
return ids;
|
||||
};
|
||||
|
||||
const OutlineTreeNode: React.FC<{ node: ITangoViewNodeData } & ComponentsTreeProps> = observer(
|
||||
const OutlineTreeNode: React.FC<{ node: IViewNodeData } & ComponentsTreeProps> = observer(
|
||||
({ node, showToggleVisibleIcon, actionItems }) => {
|
||||
const workspace = useWorkspace();
|
||||
const sandboxQuery = useSandboxQuery();
|
||||
@@ -142,7 +142,7 @@ export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
|
||||
workspace.selectSource.selected.map((item) => item.id),
|
||||
);
|
||||
const file = workspace.activeViewModule;
|
||||
const nodesTree = (file?.nodesTree ?? []) as ITangoViewNodeData[];
|
||||
const nodesTree = (file?.nodesTree ?? []) as IViewNodeData[];
|
||||
const [expandedKeys, setExpandedKeys] = useState(getNodeKeys(nodesTree));
|
||||
const [contextMenuOpen, setContextMenuOpen] = useState(false);
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useEffect, useReducer } from 'react';
|
||||
import { observer } from '@music163/tango-context';
|
||||
import { isFunction, isObject, pick } from '@music163/tango-helpers';
|
||||
import { Dict, isFunction, isObject, pick } from '@music163/tango-helpers';
|
||||
import { CollapsePanel, IconButton, JsonView } from '@music163/tango-ui';
|
||||
import { ReloadOutlined } from '@ant-design/icons';
|
||||
import { useSandboxQuery } from '../../context';
|
||||
|
||||
function processTango(obj: any = {}, index = 0) {
|
||||
const ret = {};
|
||||
function processTango(obj: Dict = {}, index = 0) {
|
||||
const ret: Dict = {};
|
||||
Object.keys(obj).forEach((key) => {
|
||||
const val = obj[key];
|
||||
if (index > 2) {
|
||||
@@ -25,11 +25,12 @@ function processTango(obj: any = {}, index = 0) {
|
||||
export const StateTree = observer(() => {
|
||||
const sandboxQuery = useSandboxQuery();
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
const tangoContext = pick(sandboxQuery.window['tango'] || {}, [
|
||||
const tangoContext = pick((sandboxQuery.window as any)['tango'] || {}, [
|
||||
'stores',
|
||||
'pageStore',
|
||||
'services',
|
||||
'config',
|
||||
'routeData',
|
||||
]);
|
||||
const callback = () => {
|
||||
forceUpdate();
|
||||
|
||||
@@ -19,7 +19,7 @@ const resizeHandleStyle = (axis: ResizableBoxProps['axis'], barStyle: HTMLCoralP
|
||||
cursor: ${axis === 'x' ? 'col-resize' : 'row-resize'};
|
||||
${barStyle &&
|
||||
Object.keys(barStyle)
|
||||
.map((key) => `${key}: ${barStyle[key]};`)
|
||||
.map((key) => `${key}: ${(barStyle as any)[key]};`)
|
||||
.join('')}
|
||||
&:hover,
|
||||
&:active {
|
||||
|
||||
@@ -3,7 +3,7 @@ import cx from 'classnames';
|
||||
import { Box, Text, css, HTMLCoralProps } from 'coral-system';
|
||||
import { Badge } from 'antd';
|
||||
import { ReactComponentProps } from '@music163/tango-helpers';
|
||||
import { observer, useDesigner } from '@music163/tango-context';
|
||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
import { ResizableBox } from './resizable-box';
|
||||
import { getWidget } from '../widgets';
|
||||
|
||||
@@ -19,15 +19,22 @@ const sidebarStyle = css`
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.SidebarPanelBarListItem {
|
||||
color: var(--tango-colors-text2);
|
||||
|
||||
&.active {
|
||||
background-color: var(--tango-colors-custom-sidebarItemActiveBg);
|
||||
color: var(--tango-colors-brand);
|
||||
background-color: var(--tango-colors-custom-sidebarItemActiveBg, --tango-colors-fill2);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--tango-colors-custom-sidebarItemHoverBg);
|
||||
background-color: var(--tango-colors-custom-sidebarItemHoverBg, --tango-colors-fill2);
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -76,6 +83,7 @@ export interface SidebarPanelItemProps
|
||||
|
||||
function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 266, footer, children }: SidebarProps) {
|
||||
const designer = useDesigner();
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const items = useMemo(() => {
|
||||
const ret: Record<string, SidebarPanelItemProps> = {};
|
||||
@@ -92,10 +100,23 @@ function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 266, footer, childre
|
||||
return ret;
|
||||
}, [children]);
|
||||
|
||||
if (designer.activeView !== 'design') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box display="flex" flexShrink={0} css={sidebarStyle} className="SidebarPanel">
|
||||
<Box className="SidebarPanelBar" display="flex" flexDirection="column" position="relative">
|
||||
<Box as="ul" flex="0" p="0" m="0" textAlign="center" className="SidebarPanelBarList">
|
||||
<Box
|
||||
as="ul"
|
||||
flex="0"
|
||||
p="0"
|
||||
m="0"
|
||||
textAlign="center"
|
||||
className={cx('SidebarPanelBarList', {
|
||||
disabled: workspace.fileErrors.length > 0,
|
||||
})}
|
||||
>
|
||||
{Object.keys(items).map((key) => {
|
||||
const item = items[key];
|
||||
const isActive = key === designer.activeSidebarPanel;
|
||||
@@ -112,7 +133,6 @@ function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 266, footer, childre
|
||||
icon={item.icon}
|
||||
label={item.label}
|
||||
showBadge={item.showBadge}
|
||||
isActive={isActive}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
@@ -183,10 +203,6 @@ BaseSidebarPanel.Item = function ({
|
||||
export const Sidebar = observer(BaseSidebarPanel);
|
||||
|
||||
interface SidebarPanelBarItemProps {
|
||||
/**
|
||||
* 是否选中
|
||||
*/
|
||||
isActive?: boolean;
|
||||
/**
|
||||
* 侧边栏图标
|
||||
*/
|
||||
@@ -212,15 +228,9 @@ interface SidebarPanelBarItemProps {
|
||||
};
|
||||
}
|
||||
|
||||
function SidebarPanelBarItem({
|
||||
isActive,
|
||||
icon: iconProp,
|
||||
label,
|
||||
showBadge,
|
||||
}: SidebarPanelBarItemProps) {
|
||||
const color = isActive ? 'brand' : 'text.body';
|
||||
function SidebarPanelBarItem({ icon: iconProp, label, showBadge }: SidebarPanelBarItemProps) {
|
||||
let icon = (
|
||||
<Text fontSize="24px" lineHeight={1} color={color}>
|
||||
<Text fontSize="24px" lineHeight={1}>
|
||||
{iconProp}
|
||||
</Text>
|
||||
);
|
||||
@@ -241,7 +251,7 @@ function SidebarPanelBarItem({
|
||||
py="12px"
|
||||
>
|
||||
{icon}
|
||||
<Text fontSize="12px" mt="s" color={color}>
|
||||
<Text fontSize="12px" mt="s">
|
||||
{label}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -155,18 +155,20 @@ function SelectionBox({ showActions, actions, data }: SelectionBoxProps) {
|
||||
css={selectionBoxStyle}
|
||||
style={style}
|
||||
>
|
||||
<>
|
||||
<ComponentsPopover type="before">
|
||||
<Tooltip title={`在 ${selectedNodeName} 的前方添加兄弟节点`}>
|
||||
<SelectionHelper icon={<PlusOutlined />} css={topAddSiblingBtnStyle} />
|
||||
</Tooltip>
|
||||
</ComponentsPopover>
|
||||
<ComponentsPopover type="after">
|
||||
<Tooltip title={`在 ${selectedNodeName} 的后方添加兄弟节点`}>
|
||||
<SelectionHelper icon={<PlusOutlined />} css={bottomAddSiblingBtnStyle} />
|
||||
</Tooltip>
|
||||
</ComponentsPopover>
|
||||
</>
|
||||
{!isPage ? (
|
||||
<>
|
||||
<ComponentsPopover type="before">
|
||||
<Tooltip title={`在 ${selectedNodeName} 的前方添加兄弟节点`}>
|
||||
<SelectionHelper icon={<PlusOutlined />} css={topAddSiblingBtnStyle} />
|
||||
</Tooltip>
|
||||
</ComponentsPopover>
|
||||
<ComponentsPopover type="after">
|
||||
<Tooltip title={`在 ${selectedNodeName} 的后方添加兄弟节点`}>
|
||||
<SelectionHelper icon={<PlusOutlined />} css={bottomAddSiblingBtnStyle} />
|
||||
</Tooltip>
|
||||
</ComponentsPopover>
|
||||
</>
|
||||
) : null}
|
||||
{showActions && (
|
||||
<SelectionHelpers align={selectionHelpersAlign}>
|
||||
<SelectionHelper
|
||||
@@ -204,7 +206,7 @@ function SelectionBox({ showActions, actions, data }: SelectionBoxProps) {
|
||||
}
|
||||
/>
|
||||
<SelectionToolSet>{!isPage && actions}</SelectionToolSet>
|
||||
{isFromCurrentFile && prototype?.hasChildren !== false && (
|
||||
{isFromCurrentFile && prototype?.hasChildren && (
|
||||
<ComponentsPopover>
|
||||
<Tooltip title="快捷添加子元素">
|
||||
<SelectionHelper icon={<PlusOutlined />} />
|
||||
|
||||
@@ -25,22 +25,45 @@ function DesktopSimulator({ children }: HTMLCoralProps<'div'>) {
|
||||
}
|
||||
|
||||
const mobileStyle = css`
|
||||
--device-viewport-width: 375px;
|
||||
--device-viewport-height: 812px;
|
||||
--device-frame-width: 13px;
|
||||
background-image: url(https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/21897838692/b1c5/2665/56db/10558c1bb2030d55ceabbcc74431de99.png);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
box-shadow: 0 2px 8px rgb(2 8 20 / 10%), 0 8px 16px rgb(2 8 20 / 8%);
|
||||
border-radius: 62px;
|
||||
overflow: hidden;
|
||||
--device-viewport-width: 360px;
|
||||
--device-viewport-height: 640px;
|
||||
position: relative;
|
||||
margin: auto;
|
||||
border: 16px black solid;
|
||||
border-top-width: 60px;
|
||||
border-bottom-width: 60px;
|
||||
border-radius: 36px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 60px;
|
||||
height: 5px;
|
||||
position: absolute;
|
||||
top: -30px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: #333;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: -65px;
|
||||
transform: translate(-50%, -50%);
|
||||
background: #333;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.MobileSimulatorDeviceFrame {
|
||||
border-radius: 50px;
|
||||
width: var(--device-viewport-width);
|
||||
height: var(--device-viewport-height);
|
||||
margin: var(--device-frame-width);
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -5,11 +5,12 @@ import { observer, useWorkspace } from '@music163/tango-context';
|
||||
|
||||
export const HistoryTool = observer(() => {
|
||||
const workspace = useWorkspace();
|
||||
const disabled = !workspace.isValid;
|
||||
return (
|
||||
<Box display="flex" columnGap="m">
|
||||
<ToggleButton
|
||||
shape="ghost"
|
||||
disabled={!workspace.history.couldBack || workspace.history.index === 0}
|
||||
disabled={disabled || !workspace.history.couldBack || workspace.history.index === 0}
|
||||
tooltip="撤销"
|
||||
onClick={() => {
|
||||
workspace.history.back();
|
||||
@@ -20,7 +21,7 @@ export const HistoryTool = observer(() => {
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
shape="ghost"
|
||||
disabled={!workspace.history.couldForward}
|
||||
disabled={disabled || !workspace.history.couldForward}
|
||||
tooltip="重做"
|
||||
onClick={() => {
|
||||
workspace.history.forward();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React from 'react';
|
||||
import { Group } from 'coral-system';
|
||||
import { Modal } from 'antd';
|
||||
import { ToggleButton, CodeOutlined, DualOutlined } from '@music163/tango-ui';
|
||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
import { BorderOutlined } from '@ant-design/icons';
|
||||
@@ -9,23 +8,13 @@ export const ModeSwitchTool = observer(() => {
|
||||
const workspace = useWorkspace();
|
||||
const designer = useDesigner();
|
||||
|
||||
const activeFileCheck = useCallback(() => {
|
||||
if (workspace.activeFile !== workspace.activeViewFile) {
|
||||
Modal.confirm({
|
||||
title: '当前打开的文件与视图不匹配,是否切换到当前视图对应的文件?',
|
||||
onOk: () => {
|
||||
workspace.setActiveFile(workspace.activeViewFile);
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [workspace]);
|
||||
|
||||
return (
|
||||
<Group attached>
|
||||
<Group attached whiteSpace="nowrap">
|
||||
<ToggleButton
|
||||
shape="ghost"
|
||||
selected={designer.activeView === 'design'}
|
||||
onClick={() => {
|
||||
workspace.syncFiles(); // 保证 ast 与 code 是同步的
|
||||
designer.setActiveView('design');
|
||||
}}
|
||||
tooltip="设计视图"
|
||||
@@ -38,7 +27,6 @@ export const ModeSwitchTool = observer(() => {
|
||||
onClick={() => {
|
||||
designer.setActiveView('code'); // 切换到源码视图
|
||||
designer.setActiveSidebarPanel(''); // 关闭左侧面板
|
||||
activeFileCheck();
|
||||
}}
|
||||
tooltip="源码视图"
|
||||
>
|
||||
@@ -50,7 +38,6 @@ export const ModeSwitchTool = observer(() => {
|
||||
onClick={() => {
|
||||
designer.setActiveView('dual'); // 切换到双屏视图
|
||||
designer.setActiveSidebarPanel(''); // 关闭左侧面板
|
||||
activeFileCheck();
|
||||
}}
|
||||
tooltip="双屏视图"
|
||||
>
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import React from 'react';
|
||||
import { ToggleButton } from '@music163/tango-ui';
|
||||
import { EyeOutlined } from '@ant-design/icons';
|
||||
import { observer, useDesigner } from '@music163/tango-context';
|
||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
|
||||
export const PreviewTool = observer(() => {
|
||||
const designer = useDesigner();
|
||||
const workspace = useWorkspace();
|
||||
|
||||
return (
|
||||
<ToggleButton
|
||||
disabled={!workspace.isValid}
|
||||
shape="ghost"
|
||||
selected={designer.isPreview}
|
||||
onClick={() => {
|
||||
|
||||
@@ -47,11 +47,14 @@ function PageSelect({ value, onSelect, onRemove, onUpdate, onCopy, options }: Pa
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const current = optionMap[value];
|
||||
const pathname = value?.split?.('?')[0];
|
||||
const current = optionMap[pathname];
|
||||
return (
|
||||
<ToggleButton
|
||||
shape="ghost"
|
||||
minWidth={100}
|
||||
maxWidth={200}
|
||||
textAlign="left"
|
||||
dropdownProps={{
|
||||
open: visible,
|
||||
onOpenChange: setVisible,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, Group } from 'coral-system';
|
||||
import { ReactComponentProps } from '@music163/tango-helpers';
|
||||
import { DesignerViewType } from '@music163/tango-core';
|
||||
import { observer, useDesigner } from '@music163/tango-context';
|
||||
import { getWidget } from '../widgets';
|
||||
|
||||
export interface ToolbarProps {
|
||||
@@ -15,25 +17,23 @@ export function Toolbar({ children }: ToolbarProps) {
|
||||
|
||||
let prevPlacement: string;
|
||||
React.Children.forEach(children, (child: React.ReactElement, index) => {
|
||||
let fallbackNode;
|
||||
const childProps = child.props;
|
||||
|
||||
let fallbackChildren;
|
||||
if (child.key) {
|
||||
const Widget = getWidget(['toolbar', child.key].join('.'));
|
||||
if (Widget) {
|
||||
fallbackNode = React.createElement(Widget, child.props.widgetProps);
|
||||
fallbackChildren = <Widget {...childProps.widgetProps} />;
|
||||
}
|
||||
}
|
||||
|
||||
let node = child.props.children ?? fallbackNode ?? null;
|
||||
if (!node) {
|
||||
node = child; // separator
|
||||
}
|
||||
const key = child.key || index;
|
||||
node = (
|
||||
<div key={key} className="ToolbarPanelItem" data-key={key}>
|
||||
{node}
|
||||
</div>
|
||||
);
|
||||
const placement = child.props.placement || prevPlacement || 'center';
|
||||
const node = React.cloneElement(child, {
|
||||
key,
|
||||
'data-key': key,
|
||||
children: childProps.children || fallbackChildren,
|
||||
});
|
||||
|
||||
const placement = childProps.placement || prevPlacement || 'center';
|
||||
switch (placement) {
|
||||
case 'left':
|
||||
left.push(node);
|
||||
@@ -68,17 +68,47 @@ export function Toolbar({ children }: ToolbarProps) {
|
||||
}
|
||||
|
||||
export interface ToolbarItemProps extends ReactComponentProps {
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
type?: 'divider';
|
||||
placement?: 'left' | 'center' | 'right';
|
||||
children?: React.ReactElement;
|
||||
/**
|
||||
* 如果 key 匹配到内置组件的话,传递给子节点的属性
|
||||
*/
|
||||
widgetProps?: object;
|
||||
/**
|
||||
* 工具箱展示的视图
|
||||
*/
|
||||
activeViews?: DesignerViewType[];
|
||||
}
|
||||
|
||||
function ToolbarItem({ placement, widgetProps, children }: ToolbarItemProps) {
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
const ToolbarItem = observer(
|
||||
({
|
||||
activeViews = ['code', 'design', 'dual'],
|
||||
type,
|
||||
placement,
|
||||
widgetProps,
|
||||
children,
|
||||
...rest
|
||||
}: ToolbarItemProps) => {
|
||||
const designer = useDesigner();
|
||||
|
||||
if (!activeViews.includes(designer.activeView)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (type === 'divider') {
|
||||
return <Separator />;
|
||||
}
|
||||
return (
|
||||
<div className="ToolbarPanelItem" {...rest}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
function Separator() {
|
||||
return <Box className="Separator" width={1} height={16} bg="colors.custom.toolbarDividerColor" />;
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
const widgets = {};
|
||||
|
||||
export function registerWidget(key: string, component: React.ComponentType<any>) {
|
||||
if (!/^(toolbar|sidebar|selectionMenu|contextMenu)\.[a-zA-z]+$/.test(key)) {
|
||||
if (!/^(toolbar|sidebar|selectionMenu|contextMenu)\.[a-zA-Z]+$/.test(key)) {
|
||||
throw new Error(
|
||||
`Invalid widget key(${key}), should start with toolbar, sidebar, contextMenu or selectionMenu`,
|
||||
);
|
||||
|
||||
@@ -1,35 +1,37 @@
|
||||
import React from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { Box, HTMLCoralProps } from 'coral-system';
|
||||
import { observer, useDesigner } from '@music163/tango-context';
|
||||
import { DesignerViewType } from '@music163/tango-core';
|
||||
import { ComponentsPopover } from './components';
|
||||
import cx from 'classnames';
|
||||
import { ComponentsPopover, FileErrorsOverlay } from './components';
|
||||
|
||||
export interface WorkspaceViewProps {
|
||||
export interface WorkspaceViewProps extends HTMLCoralProps<'div'> {
|
||||
/**
|
||||
* 视图面板模式,对应 Workspace 的模式
|
||||
*/
|
||||
mode?: DesignerViewType;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const WorkspaceView = observer((props: WorkspaceViewProps) => {
|
||||
const { mode = 'design', children } = props;
|
||||
const { mode = 'design', children, className, ...rest } = props;
|
||||
const designer = useDesigner();
|
||||
const display = mode !== designer.activeView ? 'none' : 'block';
|
||||
// 云音乐移动端模式小屏幕适配,可能会溢出屏幕
|
||||
// 移动端模式小屏幕适配,可能会溢出屏幕
|
||||
const overflow =
|
||||
designer.simulator.name === 'phone' ? 'auto' : designer.isPreview ? 'auto' : 'hidden';
|
||||
return (
|
||||
<Box
|
||||
className={`ViewPanel ${mode}`}
|
||||
className={cx('ViewPanel', mode, className)}
|
||||
display={designer.activeView === 'dual' ? 'block' : display}
|
||||
flex="1"
|
||||
overflow={overflow}
|
||||
position="relative"
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
{/* 添加组件弹层 */}
|
||||
{display === 'block' && <ComponentsPopover type="inner" isControlled />}
|
||||
{designer.activeView === 'design' && <FileErrorsOverlay />}
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.2.4](https://github.com/netease/tango/compare/@music163/tango-helpers@1.2.3...@music163/tango-helpers@1.2.4) (2024-09-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- renderSetter with template ([#202](https://github.com/netease/tango/issues/202)) ([ea96872](https://github.com/netease/tango/commit/ea9687226123c839f7a9e001b4c01aa8315a135d))
|
||||
|
||||
## [1.2.3](https://github.com/netease/tango/compare/@music163/tango-helpers@1.2.2...@music163/tango-helpers@1.2.3) (2024-09-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add prop value template & fix components popover ([#201](https://github.com/netease/tango/issues/201)) ([c4e1ed6](https://github.com/netease/tango/commit/c4e1ed67f55deb17dae740559602cd0adcfc8eb4))
|
||||
|
||||
## [1.2.2](https://github.com/netease/tango/compare/@music163/tango-helpers@1.2.1...@music163/tango-helpers@1.2.2) (2024-08-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- parse template code & update maxRow of TextAreaSetter ([#192](https://github.com/netease/tango/issues/192)) ([a1eea02](https://github.com/netease/tango/commit/a1eea02f9409db73f042c74d7903ad84258f25c2))
|
||||
|
||||
## [1.2.1](https://github.com/netease/tango/compare/@music163/tango-helpers@1.2.0...@music163/tango-helpers@1.2.1) (2024-08-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update types ([67d602a](https://github.com/netease/tango/commit/67d602a3ec6b7f74156bb78fda6f3b4bc2676e55))
|
||||
|
||||
# [1.2.0](https://github.com/netease/tango/compare/@music163/tango-helpers@1.1.1...@music163/tango-helpers@1.2.0) (2024-06-03)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-helpers",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.4",
|
||||
"description": "Shared types, helpers, and hooks of tango-apps",
|
||||
"keywords": [
|
||||
"shared",
|
||||
|
||||
@@ -43,7 +43,7 @@ export function filterTreeData<T = unknown>(
|
||||
childrenProp = 'children',
|
||||
onlyLeaf = false,
|
||||
) {
|
||||
const reducer = (result: T[], current: T) => {
|
||||
const reducer = (result: T[], current: any) => {
|
||||
if (current[childrenProp]) {
|
||||
const newChildren = current[childrenProp].reduce(reducer, []);
|
||||
if (newChildren.length) {
|
||||
@@ -70,7 +70,7 @@ export function mapTreeData<T = unknown>(
|
||||
mapper: (node: T) => any,
|
||||
childrenProp = 'children',
|
||||
) {
|
||||
return treeData.map((node) => {
|
||||
return treeData.map((node: any) => {
|
||||
const newNode = mapper(node);
|
||||
if (node[childrenProp]) {
|
||||
newNode[childrenProp] = mapTreeData(node[childrenProp], mapper, childrenProp);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user