mirror of
https://github.com/NetEase/tango.git
synced 2026-08-31 09:30:28 +08:00
Compare commits
30 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 |
@@ -9,3 +9,4 @@ coverage/
|
|||||||
.umi-production/
|
.umi-production/
|
||||||
.log
|
.log
|
||||||
.eslintcache
|
.eslintcache
|
||||||
|
*.pem
|
||||||
|
|||||||
@@ -35,13 +35,13 @@ You can view the detailed usage guide through the following links:
|
|||||||
|
|
||||||
You can use tango to build lowcode builders, for example:
|
You can use tango to build lowcode builders, for example:
|
||||||
|
|
||||||
| Preview | Description |
|
| Preview | Description |
|
||||||
|---------|-------------------------|
|
| ---------------------------------------------------------------------------------------------- | ----------------------- |
|
||||||
|  | Internal App Builder |
|
|  | Internal App Builder |
|
||||||
|  | Dashboard App Builder |
|
|  | Dashboard App Builder |
|
||||||
|  | Mobile App Builder |
|
|  | Mobile App Builder |
|
||||||
|  | ReactNative App Builder |
|
|  | ReactNative App Builder |
|
||||||
|  | Email Builder |
|
|  | Email Builder |
|
||||||
|
|
||||||
## 🌐 Compatibility
|
## 🌐 Compatibility
|
||||||
|
|
||||||
@@ -70,6 +70,23 @@ yarn
|
|||||||
yarn start
|
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
|
## 💬 Community
|
||||||
|
|
||||||
Join NetEase Tango Community to share your ideas, suggestions, or questions and connect with other users and contributors.
|
Join NetEase Tango Community to share your ideas, suggestions, or questions and connect with other users and contributors.
|
||||||
|
|||||||
+24
-8
@@ -35,14 +35,13 @@
|
|||||||
|
|
||||||
你可以使用 Tango 快速构建多种类型的基于源码的低代码搭建工具,例如:
|
你可以使用 Tango 快速构建多种类型的基于源码的低代码搭建工具,例如:
|
||||||
|
|
||||||
| 预览图 | 说明 |
|
| 预览图 | 说明 |
|
||||||
|---------|-------------------------|
|
| ---------------------------------------------------------------------------------------------- | -------------------- |
|
||||||
|  | 中后台系统搭建 |
|
|  | 中后台系统搭建 |
|
||||||
|  | 仪表盘应用搭建 |
|
|  | 仪表盘应用搭建 |
|
||||||
|  | H5活动页面搭建 |
|
|  | H5活动页面搭建 |
|
||||||
|  | ReactNative 应用搭建 |
|
|  | ReactNative 应用搭建 |
|
||||||
|  | 营销类邮件搭建 |
|
|  | 营销类邮件搭建 |
|
||||||
|
|
||||||
|
|
||||||
## 🌐 兼容环境
|
## 🌐 兼容环境
|
||||||
|
|
||||||
@@ -71,6 +70,23 @@ yarn
|
|||||||
yarn start
|
yarn start
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 本地 https 证书
|
||||||
|
|
||||||
|
如果需要在本地开发环境中使用 https,可以使用以下命令生成证书:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install mkcert
|
||||||
|
|
||||||
|
# 将 mkcert 添加到本地根 CA,仅在本地生效
|
||||||
|
mkcert -install
|
||||||
|
|
||||||
|
# 进入 playground 应用目录
|
||||||
|
cd apps/playground
|
||||||
|
|
||||||
|
# 为网站生成一个由 mkcert 签名的证书
|
||||||
|
mkcert local.netease.com
|
||||||
|
```
|
||||||
|
|
||||||
## 💬 社区讨论
|
## 💬 社区讨论
|
||||||
|
|
||||||
参与 NetEase Tango 的社区,以分享您的想法、建议或问题,并与其他用户和贡献者建立联系。
|
参与 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,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, Group } from 'coral-system';
|
import { Box, Group, Text } from 'coral-system';
|
||||||
import { Avatar, Space, Switch } from 'antd';
|
import { Avatar, Space, Switch } from 'antd';
|
||||||
import { BranchesOutlined, MenuOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
import { BranchesOutlined, MenuOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
||||||
import { registerSetter } from '@music163/tango-designer';
|
import { registerSetter } from '@music163/tango-designer';
|
||||||
@@ -66,12 +66,12 @@ export function Logo() {
|
|||||||
export function ProjectDetail() {
|
export function ProjectDetail() {
|
||||||
return (
|
return (
|
||||||
<Box display="flex" alignItems="center" columnGap="l">
|
<Box display="flex" alignItems="center" columnGap="l">
|
||||||
<Box className="ProjectName" fontSize="18px" fontWeight="bold">
|
<Text className="ProjectName" fontSize="18px" fontWeight="bold" truncated>
|
||||||
community-test
|
community-test
|
||||||
</Box>
|
</Text>
|
||||||
<Box className="BranchName" as="code" fontSize="14px">
|
<Text className="BranchName" as="code" fontSize="14px" truncated>
|
||||||
<BranchesOutlined /> feature/list
|
<BranchesOutlined /> feature/list
|
||||||
</Box>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const packageJson = {
|
|||||||
private: true,
|
private: true,
|
||||||
dependencies: {
|
dependencies: {
|
||||||
'@music163/antd': '0.2.5',
|
'@music163/antd': '0.2.5',
|
||||||
'@music163/tango-boot': '0.2.5',
|
'@music163/tango-boot': '0.3.5',
|
||||||
react: '17.0.2',
|
react: '17.0.2',
|
||||||
'react-dom': '17.0.2',
|
'react-dom': '17.0.2',
|
||||||
'prop-types': '15.7.2',
|
'prop-types': '15.7.2',
|
||||||
@@ -48,7 +48,7 @@ const tangoConfigJson = {
|
|||||||
},
|
},
|
||||||
'@music163/tango-boot': {
|
'@music163/tango-boot': {
|
||||||
description: '云音乐低代码运行时框架',
|
description: '云音乐低代码运行时框架',
|
||||||
version: '0.2.5',
|
version: '0.3.5',
|
||||||
library: 'TangoBoot',
|
library: 'TangoBoot',
|
||||||
type: 'baseDependency',
|
type: 'baseDependency',
|
||||||
resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'],
|
resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'],
|
||||||
@@ -153,6 +153,7 @@ import { definePage } from "@music163/tango-boot";
|
|||||||
import {
|
import {
|
||||||
Page,
|
Page,
|
||||||
Section,
|
Section,
|
||||||
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Input,
|
Input,
|
||||||
FormilyForm,
|
FormilyForm,
|
||||||
@@ -166,8 +167,10 @@ import { OutButton } from "../components";
|
|||||||
class App extends React.Component {
|
class App extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Page title={tango.stores.app.title} subTitle={111}>
|
<Page title={tango.stores.app.title} subTitle={<><Button>hello</Button></>}>
|
||||||
<Section tid="section0" />
|
<Section tid="section0">
|
||||||
|
<Box></Box>
|
||||||
|
</Section>
|
||||||
<Section tid="section1" title="Section Title">
|
<Section tid="section1" title="Section Title">
|
||||||
your input: <Input tid="input1" defaultValue="hello" />
|
your input: <Input tid="input1" defaultValue="hello" />
|
||||||
copy input: <Input value={tango.page.input1?.value} />
|
copy input: <Input value={tango.page.input1?.value} />
|
||||||
@@ -374,13 +377,16 @@ export default defineServices({
|
|||||||
add: {
|
add: {
|
||||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
|
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
|
description: '新增用户'
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
|
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
|
description: '更新用户'
|
||||||
},
|
},
|
||||||
delete: {
|
delete: {
|
||||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users?id=1',
|
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',
|
icon: 'icon-column-3',
|
||||||
type: 'snippet',
|
type: 'snippet',
|
||||||
package: '@music163/antd',
|
package: '@music163/antd',
|
||||||
// FIXME: Column 组件 需要更新为 defineComponent
|
|
||||||
initChildren: `
|
initChildren: `
|
||||||
<Columns columns={12}>
|
<Columns columns={12}>
|
||||||
<Column colSpan={4}>
|
<Column colSpan={4}>
|
||||||
@@ -132,11 +131,14 @@ export const nativeDomPrototypes = () => {
|
|||||||
title: '样式',
|
title: '样式',
|
||||||
group: 'style',
|
group: 'style',
|
||||||
setter: 'expressionSetter',
|
setter: 'expressionSetter',
|
||||||
|
setterProps: {
|
||||||
|
expressionType: 'cssObject',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'className',
|
name: 'className',
|
||||||
title: '类名',
|
title: '类名',
|
||||||
setter: 'textSetter',
|
setter: 'classNameSetter',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'id',
|
name: 'id',
|
||||||
@@ -166,6 +168,38 @@ const prototypes: Dict<IComponentPrototype> = {
|
|||||||
Snippet2ColumnLayout,
|
Snippet2ColumnLayout,
|
||||||
Snippet3ColumnLayout,
|
Snippet3ColumnLayout,
|
||||||
SnippetButtonGroup,
|
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: {
|
Box: {
|
||||||
name: 'Box',
|
name: 'Box',
|
||||||
title: '盒子',
|
title: '盒子',
|
||||||
@@ -197,6 +231,38 @@ const prototypes: Dict<IComponentPrototype> = {
|
|||||||
title: 'd',
|
title: 'd',
|
||||||
setter: 'textSetter',
|
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: {
|
Columns: {
|
||||||
|
|||||||
@@ -113,8 +113,8 @@ export default function App() {
|
|||||||
actions={
|
actions={
|
||||||
<Box px="l">
|
<Box px="l">
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<Toolbar.Item key="routeSwitch" placement="left" />
|
<Toolbar.Item key="routeSwitch" placement="left" activeViews={['design']} />
|
||||||
<Toolbar.Item key="addPage" placement="left">
|
<Toolbar.Item key="addPage" placement="left" activeViews={['design']}>
|
||||||
<Action
|
<Action
|
||||||
tooltip="添加页面"
|
tooltip="添加页面"
|
||||||
shape="outline"
|
shape="outline"
|
||||||
@@ -122,10 +122,10 @@ export default function App() {
|
|||||||
onClick={() => setShowNewPageModal(true)}
|
onClick={() => setShowNewPageModal(true)}
|
||||||
/>
|
/>
|
||||||
</Toolbar.Item>
|
</Toolbar.Item>
|
||||||
<Toolbar.Item key="history" placement="left" />
|
<Toolbar.Item key="history" placement="left" activeViews={['design']} />
|
||||||
<Toolbar.Item key="preview" placement="left" />
|
<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="modeSwitch" placement="right" />
|
||||||
<Toolbar.Item key="togglePanel" placement="right" />
|
|
||||||
<Toolbar.Separator />
|
<Toolbar.Separator />
|
||||||
<Toolbar.Item placement="right">
|
<Toolbar.Item placement="right">
|
||||||
<Space>
|
<Space>
|
||||||
@@ -187,6 +187,9 @@ export default function App() {
|
|||||||
/>
|
/>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
<WorkspacePanel>
|
<WorkspacePanel>
|
||||||
|
<WorkspaceView mode="code">
|
||||||
|
<CodeEditor />
|
||||||
|
</WorkspaceView>
|
||||||
<WorkspaceView mode="design">
|
<WorkspaceView mode="design">
|
||||||
<Sandbox
|
<Sandbox
|
||||||
onMessage={(e) => {
|
onMessage={(e) => {
|
||||||
@@ -208,9 +211,6 @@ export default function App() {
|
|||||||
navigatorExtra={<Button size="small">hello world</Button>}
|
navigatorExtra={<Button size="small">hello world</Button>}
|
||||||
/>
|
/>
|
||||||
</WorkspaceView>
|
</WorkspaceView>
|
||||||
<WorkspaceView mode="code">
|
|
||||||
<CodeEditor />
|
|
||||||
</WorkspaceView>
|
|
||||||
</WorkspacePanel>
|
</WorkspacePanel>
|
||||||
<SettingPanel />
|
<SettingPanel />
|
||||||
</DesignerPanel>
|
</DesignerPanel>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { SystemProvider } from 'coral-system';
|
|||||||
import 'antd/dist/antd.css';
|
import 'antd/dist/antd.css';
|
||||||
|
|
||||||
export const parameters = {
|
export const parameters = {
|
||||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
actions: { argTypesRegex: '^on.*' },
|
||||||
controls: {
|
controls: {
|
||||||
matchers: {
|
matchers: {
|
||||||
color: /(background|color)$/i,
|
color: /(background|color)$/i,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@music163/tango-setting-form": "*",
|
"@music163/tango-setting-form": "*",
|
||||||
"@music163/tango-ui": "*",
|
"@music163/tango-ui": "*",
|
||||||
"mobx": "6.12.3",
|
"mobx": "6.13.2",
|
||||||
"mobx-react-lite": "4.0.7"
|
"mobx-react-lite": "4.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -69,6 +69,14 @@ const prototypeHasBasicProps: IComponentPrototype = {
|
|||||||
title: 'codeSetter',
|
title: 'codeSetter',
|
||||||
setter: 'codeSetter',
|
setter: 'codeSetter',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'style',
|
||||||
|
title: 'codeSetter(cssObject)',
|
||||||
|
setter: 'codeSetter',
|
||||||
|
setterProps: {
|
||||||
|
expressionType: 'cssObject',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'text',
|
name: 'text',
|
||||||
title: 'textSetter',
|
title: 'textSetter',
|
||||||
@@ -268,6 +276,9 @@ export function InitValues() {
|
|||||||
name: 'style',
|
name: 'style',
|
||||||
title: 'codeSetter',
|
title: 'codeSetter',
|
||||||
setter: 'codeSetter',
|
setter: 'codeSetter',
|
||||||
|
setterProps: {
|
||||||
|
expressionType: 'cssObject',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'object',
|
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} />;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
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';
|
import { BlockOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -28,6 +28,10 @@ export function Basic() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function CSS() {
|
||||||
|
return <InputStyleCode enableESLint suffix={<Action icon={<BlockOutlined />} size="small" />} />;
|
||||||
|
}
|
||||||
|
|
||||||
const code = `
|
const code = `
|
||||||
function foo() {
|
function foo() {
|
||||||
console.log("test");
|
console.log("test");
|
||||||
|
|||||||
+8
-8
@@ -15,17 +15,17 @@
|
|||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"eslint": "eslint packages/**/src/*.{ts,tsx} --cache",
|
"eslint": "eslint packages/**/src/*.{ts,tsx} --cache",
|
||||||
"publish": "lerna publish",
|
"publish": "lerna publish",
|
||||||
"ver": "lerna version --no-private",
|
"ver": "lerna version --no-private --conventional-commits",
|
||||||
"release": "yarn eslint && yarn build && lerna version --no-private --conventional-commits && lerna publish from-git",
|
"release": "yarn eslint && yarn build && yarn ver && lerna publish from-git",
|
||||||
"release:npm": "yarn eslint && yarn build && yarn run ver && lerna publish from-git",
|
"release:npm": "yarn eslint && yarn build && yarn ver && lerna publish from-package",
|
||||||
"release:beta": "yarn eslint && yarn build && yarn run ver && lerna publish from-package --dist-tag beta",
|
"release:beta": "yarn eslint && yarn build && yarn ver && lerna publish from-package --dist-tag beta",
|
||||||
"up": "yarn upgrade-interactive --latest",
|
"up": "yarn upgrade-interactive --latest",
|
||||||
"prepare": "husky install"
|
"prepare": "husky install"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-env": "^7.23.6",
|
"@babel/preset-env": "^7.25.4",
|
||||||
"@babel/preset-react": "^7.23.3",
|
"@babel/preset-react": "^7.24.7",
|
||||||
"@babel/preset-typescript": "^7.23.3",
|
"@babel/preset-typescript": "^7.24.7",
|
||||||
"@commitlint/cli": "^18.4.3",
|
"@commitlint/cli": "^18.4.3",
|
||||||
"@commitlint/config-conventional": "^18.4.3",
|
"@commitlint/config-conventional": "^18.4.3",
|
||||||
"@types/jest": "^29.5.7",
|
"@types/jest": "^29.5.7",
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
"react": "^17.0.0",
|
"react": "^17.0.0",
|
||||||
"react-dom": "^17.0.0",
|
"react-dom": "^17.0.0",
|
||||||
"styled-components": "^5.3.6",
|
"styled-components": "^5.3.6",
|
||||||
"typedoc": "^0.25.4",
|
"typedoc": "^0.26.7",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -3,6 +3,32 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
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)
|
## [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
|
**Note:** Version bump only for package @music163/tango-context
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@music163/tango-context",
|
"name": "@music163/tango-context",
|
||||||
"version": "1.1.4",
|
"version": "1.1.10",
|
||||||
"description": "react context for tango-apps",
|
"description": "react context for tango-apps",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react",
|
"react",
|
||||||
@@ -31,8 +31,8 @@
|
|||||||
"react": ">= 16.8"
|
"react": ">= 16.8"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@music163/tango-core": "^1.3.2",
|
"@music163/tango-core": "^1.4.4",
|
||||||
"@music163/tango-helpers": "^1.2.0",
|
"@music163/tango-helpers": "^1.2.4",
|
||||||
"mobx-react-lite": "4.0.7"
|
"mobx-react-lite": "4.0.7"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"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';
|
import { IVariableTreeNode, createContext } from '@music163/tango-helpers';
|
||||||
|
|
||||||
export interface ITangoEngineContext {
|
export interface ITangoEngineContext {
|
||||||
@@ -21,8 +21,12 @@ const [TangoEngineProvider, useTangoEngine] = createContext<ITangoEngineContext>
|
|||||||
|
|
||||||
export { TangoEngineProvider };
|
export { TangoEngineProvider };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 CodeWorkspace 实例
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export const useWorkspace = () => {
|
export const useWorkspace = () => {
|
||||||
return useTangoEngine()?.engine.workspace;
|
return useTangoEngine()?.engine.workspace as AbstractCodeWorkspace;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useDesigner = () => {
|
export const useDesigner = () => {
|
||||||
@@ -90,12 +94,15 @@ export const useWorkspaceData = () => {
|
|||||||
key: prefix,
|
key: prefix,
|
||||||
selectable: false,
|
selectable: false,
|
||||||
showAddButton: true,
|
showAddButton: true,
|
||||||
children: Object.keys(file.serviceFunctions || {}).map((key) => ({
|
children: Object.entries(file.serviceFunctions || {}).map(([key, value]) => {
|
||||||
title: key,
|
const title = value.description ? `${key}(${value.description})` : key;
|
||||||
key: [prefix, key].join('.'),
|
return {
|
||||||
type: 'function',
|
title,
|
||||||
showRemoveButton: true,
|
key: [prefix, key].join('.'),
|
||||||
})),
|
type: 'function',
|
||||||
|
showRemoveButton: true,
|
||||||
|
};
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,40 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
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)
|
## [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
|
### Bug Fixes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@music163/tango-core",
|
"name": "@music163/tango-core",
|
||||||
"version": "1.3.2",
|
"version": "1.4.4",
|
||||||
"description": "tango core",
|
"description": "tango core",
|
||||||
"author": "wwsun <ww.sun@outlook.com>",
|
"author": "wwsun <ww.sun@outlook.com>",
|
||||||
"homepage": "",
|
"homepage": "",
|
||||||
@@ -24,14 +24,14 @@
|
|||||||
"prepublishOnly": "yarn build"
|
"prepublishOnly": "yarn build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/generator": "^7.23.5",
|
"@babel/generator": "^7.25.6",
|
||||||
"@babel/parser": "^7.23.5",
|
"@babel/parser": "^7.25.6",
|
||||||
"@babel/traverse": "^7.23.5",
|
"@babel/traverse": "^7.25.6",
|
||||||
"@babel/types": "^7.23.5",
|
"@babel/types": "^7.25.6",
|
||||||
"@music163/tango-helpers": "^1.2.0",
|
"@music163/tango-helpers": "^1.2.4",
|
||||||
"@types/babel__generator": "^7.6.7",
|
"@types/babel__generator": "^7.6.7",
|
||||||
"@types/babel__traverse": "^7.20.4",
|
"@types/babel__traverse": "^7.20.6",
|
||||||
"mobx": "6.12.3",
|
"mobx": "6.13.2",
|
||||||
"path-browserify": "^1.0.1"
|
"path-browserify": "^1.0.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { MenuDataType } from '@music163/tango-helpers';
|
import { MenuDataType } from '@music163/tango-helpers';
|
||||||
import { Designer, DesignerViewType, Engine, SimulatorNameType } from './models';
|
import { Designer, DesignerViewType, Engine, SimulatorNameType } from './models';
|
||||||
import { IWorkspace } from './models/interfaces';
|
import { AbstractWorkspace } from './models/abstract-workspace';
|
||||||
|
|
||||||
interface ICreateEngineOptions {
|
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;
|
return /^tango\??\.(stores|services)\??\./.test(name) && name.split('.').length > 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
const templatePattern = /^{(.+)}$/s;
|
const templatePattern = /^{(.+)}$/;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断给定字符串是否被表达式容器`{expCode}`包裹
|
* 判断给定字符串是否被表达式容器`{expCode}`包裹
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
import generator, { GeneratorOptions } from '@babel/generator';
|
import generator, { GeneratorOptions } from '@babel/generator';
|
||||||
import * as t from '@babel/types';
|
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';
|
import { formatCode } from '../string';
|
||||||
|
|
||||||
const defaultGeneratorOptions: GeneratorOptions = {
|
const defaultGeneratorOptions: GeneratorOptions = {
|
||||||
@@ -25,19 +25,7 @@ export function ast2code(ast: t.Node, options: GeneratorOptions = defaultGenerat
|
|||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bracketPattern = /^\(.+\)$/s;
|
const bracketPattern = /^\(.+\)$/;
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否被 () 包裹
|
|
||||||
*
|
|
||||||
* @example ({ foo: 'foo' }) -> true
|
|
||||||
* @example { foo: 'foo' } -> false
|
|
||||||
*
|
|
||||||
* @param str 目标字符串
|
|
||||||
*/
|
|
||||||
function isWrappingWithBrackets(str: string) {
|
|
||||||
return bracketPattern.test(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将表达式生成为块级代码
|
* 将表达式生成为块级代码
|
||||||
@@ -54,7 +42,7 @@ export function expression2code(node: t.Expression) {
|
|||||||
|
|
||||||
const isWrappingExpression = t.isObjectExpression(node) || t.isFunctionExpression(node);
|
const isWrappingExpression = t.isObjectExpression(node) || t.isFunctionExpression(node);
|
||||||
|
|
||||||
if (isWrappingExpression && isWrappingWithBrackets(ret)) {
|
if (isWrappingExpression && bracketPattern.test(ret)) {
|
||||||
// 如果是对象,输出包含 ({}),则去掉首尾的括号
|
// 如果是对象,输出包含 ({}),则去掉首尾的括号
|
||||||
ret = ret.slice(1, -1);
|
ret = ret.slice(1, -1);
|
||||||
}
|
}
|
||||||
@@ -195,7 +183,7 @@ export function node2value(node: t.Node, isWrapCode = true): any {
|
|||||||
);
|
);
|
||||||
if (isSimpleObject) {
|
if (isSimpleObject) {
|
||||||
// simple object: { key1, key2, key3 }
|
// simple object: { key1, key2, key3 }
|
||||||
ret = node.properties.reduce((prev, propertyNode) => {
|
ret = node.properties.reduce<Dict>((prev, propertyNode) => {
|
||||||
if (propertyNode.type === 'ObjectProperty') {
|
if (propertyNode.type === 'ObjectProperty') {
|
||||||
const key = keyNode2value(propertyNode.key);
|
const key = keyNode2value(propertyNode.key);
|
||||||
const value = node2value(propertyNode.value, isWrapCode);
|
const value = node2value(propertyNode.value, isWrapCode);
|
||||||
|
|||||||
@@ -68,11 +68,7 @@ export function isValidExpressionCode(code: string) {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function code2ast(code: string): t.File {
|
export function code2ast(code: string): t.File {
|
||||||
try {
|
return parse(code, babelParserConfig);
|
||||||
return parse(code, babelParserConfig);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('[code2ast failed!]', err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -175,7 +171,7 @@ export function value2node(
|
|||||||
* 将 js 普通对象解析为 t.Node
|
* 将 js 普通对象解析为 t.Node
|
||||||
*/
|
*/
|
||||||
export function object2node(
|
export function object2node(
|
||||||
obj: object,
|
obj: Dict,
|
||||||
getValueNode: (value: any, key?: string) => t.Expression = value2node,
|
getValueNode: (value: any, key?: string) => t.Expression = value2node,
|
||||||
) {
|
) {
|
||||||
if (!isPlainObject(obj)) {
|
if (!isPlainObject(obj)) {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { isDefineService, isDefineStore, isTangoVariable } from '../assert';
|
|||||||
import type {
|
import type {
|
||||||
IRouteData,
|
IRouteData,
|
||||||
IStorePropertyData,
|
IStorePropertyData,
|
||||||
ITangoViewNodeData,
|
IViewNodeData,
|
||||||
IImportDeclarationPayload,
|
IImportDeclarationPayload,
|
||||||
InsertChildPositionType,
|
InsertChildPositionType,
|
||||||
IImportSpecifierData,
|
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, {
|
traverse(ast, {
|
||||||
CallExpression(path) {
|
CallExpression(path) {
|
||||||
const calleeName = keyNode2value(path.node.callee) as string;
|
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];
|
const configNode = path.node.arguments[0];
|
||||||
|
|
||||||
if (t.isObjectExpression(configNode)) {
|
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 serviceConfig = config[key];
|
||||||
const property = t.objectProperty(t.identifier(key), serviceConfig2Node(serviceConfig));
|
const property = t.objectProperty(t.identifier(key), serviceConfig2Node(serviceConfig));
|
||||||
properties[key] = property;
|
properties[key] = property;
|
||||||
@@ -1234,7 +1234,7 @@ export function cloneJSXElement(node: t.JSXElement, overrideProps?: Dict) {
|
|||||||
export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
|
export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
|
||||||
const imports: Record<string, IImportSpecifierData[]> = {};
|
const imports: Record<string, IImportSpecifierData[]> = {};
|
||||||
const importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]> = {};
|
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 cloneAst = t.cloneNode(ast, true, true);
|
||||||
const cleanAst = clearTrackingData(cloneAst);
|
const cleanAst = clearTrackingData(cloneAst);
|
||||||
const variables: string[] = []; // 使用的 tango 变量
|
const variables: string[] = []; // 使用的 tango 变量
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export function namesToImportDeclarations(
|
|||||||
names: string[],
|
names: string[],
|
||||||
nameMap: Dict<IImportSpecifierSourceData>,
|
nameMap: Dict<IImportSpecifierSourceData>,
|
||||||
) {
|
) {
|
||||||
const map = {};
|
const map: Dict = {};
|
||||||
names.forEach((name) => {
|
names.forEach((name) => {
|
||||||
const mod = nameMap[name];
|
const mod = nameMap[name];
|
||||||
if (mod) {
|
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 } ] }
|
// { id: 1, component, props: { id: 1 }, children: [ { id: 2, component } ] }
|
||||||
export function deepCloneNode(obj: any, component?: string): any {
|
export function deepCloneNode(obj: any, component?: string): any {
|
||||||
function deepCloneObject() {
|
function deepCloneObject() {
|
||||||
const target = {};
|
const target: Dict = {};
|
||||||
for (const key in obj) {
|
for (const key in obj) {
|
||||||
if (Object.hasOwn(obj, key)) {
|
if (Object.hasOwn(obj, key)) {
|
||||||
target[key] = deepCloneNode(obj[key], component);
|
target[key] = deepCloneNode(obj[key], component);
|
||||||
|
|||||||
@@ -8,65 +8,57 @@ import { FileType } from './../types';
|
|||||||
export function inferFileType(filename: string): FileType {
|
export function inferFileType(filename: string): FileType {
|
||||||
// 增加 tangoConfigJson Module
|
// 增加 tangoConfigJson Module
|
||||||
if (/\/tango\.config\.json$/.test(filename)) {
|
if (/\/tango\.config\.json$/.test(filename)) {
|
||||||
return FileType.TangoConfigJson;
|
return FileType.TangoConfigJsonFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/\/appJson\.json$/.test(filename)) {
|
if (/\/appJson\.json$/.test(filename)) {
|
||||||
return FileType.AppJson;
|
return FileType.AppJsonFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/\/package\.json$/.test(filename)) {
|
if (/\/package\.json$/.test(filename)) {
|
||||||
return FileType.PackageJson;
|
return FileType.PackageJsonFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/\/routes\.js$/.test(filename)) {
|
if (/\/routes\.js$/.test(filename)) {
|
||||||
return FileType.RouteModule;
|
return FileType.JsRouteConfigFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 所有 pages 下的 js 文件均认为是有效的 viewModule
|
// 所有 pages 下的 js 文件均认为是有效的 viewModule
|
||||||
if (/\/pages\/.+\.jsx?$/.test(filename)) {
|
if (/\/pages\/.+\.jsx?$/.test(filename)) {
|
||||||
return FileType.JsxViewModule;
|
return FileType.JsViewFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 所有 pages 下的 js 文件均认为是有效的 viewModule
|
// 所有 pages 下的 js 文件均认为是有效的 viewModule
|
||||||
if (/\/pages\/.+\.schema\.json?$/.test(filename)) {
|
if (/\/pages\/.+\.schema\.json?$/.test(filename)) {
|
||||||
return FileType.JsonViewModule;
|
return FileType.JsonViewFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/\/(blocks|components)\/index\.js/.test(filename)) {
|
if (/\/(blocks|components)\/index\.js/.test(filename)) {
|
||||||
return FileType.ComponentsEntryModule;
|
return FileType.JsLocalComponentsEntryFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/\/services\/.+\.js$/.test(filename)) {
|
if (/\/services\/.+\.js$/.test(filename)) {
|
||||||
return FileType.ServiceModule;
|
return FileType.JsServiceFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/service\.js$/.test(filename)) {
|
if (/service\.js$/.test(filename)) {
|
||||||
return FileType.ServiceModule;
|
return FileType.JsServiceFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/\/stores\/index\.js$/.test(filename)) {
|
if (/\/stores\/index\.js$/.test(filename)) {
|
||||||
return FileType.StoreEntryModule;
|
return FileType.JsStoreEntryFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/\/stores\/.+\.js$/.test(filename)) {
|
if (/\/stores\/.+\.js$/.test(filename)) {
|
||||||
return FileType.StoreModule;
|
return FileType.JsStoreFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/\.jsx?$/.test(filename)) {
|
if (/\.jsx?$/.test(filename)) {
|
||||||
return FileType.Module;
|
return FileType.JsFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/\.json$/.test(filename)) {
|
if (/\.json$/.test(filename)) {
|
||||||
return FileType.Json;
|
return FileType.JsonFile;
|
||||||
}
|
|
||||||
|
|
||||||
if (/\.less$/.test(filename)) {
|
|
||||||
return FileType.Less;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/\.scss$/.test(filename)) {
|
|
||||||
return FileType.Scss;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return FileType.File;
|
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 { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||||
import { IWorkspace } from './interfaces';
|
|
||||||
import { MenuDataType } from '@music163/tango-helpers';
|
import { MenuDataType } from '@music163/tango-helpers';
|
||||||
|
import { AbstractWorkspace } from './abstract-workspace';
|
||||||
|
|
||||||
export type SimulatorNameType = 'desktop' | 'phone';
|
export type SimulatorNameType = 'desktop' | 'phone';
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ interface IViewportBounding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IDesignerOptions {
|
interface IDesignerOptions {
|
||||||
workspace: IWorkspace;
|
workspace: AbstractWorkspace;
|
||||||
simulator?: SimulatorNameType | ISimulatorType;
|
simulator?: SimulatorNameType | ISimulatorType;
|
||||||
/**
|
/**
|
||||||
* 菜单配置
|
* 菜单配置
|
||||||
@@ -108,7 +108,7 @@ export class Designer {
|
|||||||
*/
|
*/
|
||||||
_menuData?: MenuDataType = null;
|
_menuData?: MenuDataType = null;
|
||||||
|
|
||||||
private readonly workspace: IWorkspace;
|
private readonly workspace: AbstractWorkspace;
|
||||||
|
|
||||||
get simulator(): ISimulatorType {
|
get simulator(): ISimulatorType {
|
||||||
return toJS(this._simulator);
|
return toJS(this._simulator);
|
||||||
@@ -239,6 +239,13 @@ export class Designer {
|
|||||||
|
|
||||||
setActiveView(view: DesignerViewType) {
|
setActiveView(view: DesignerViewType) {
|
||||||
this._activeView = view;
|
this._activeView = view;
|
||||||
|
if (view === 'dual') {
|
||||||
|
this.setActiveSidebarPanel('');
|
||||||
|
this.toggleRightPanel(false);
|
||||||
|
this.toggleIsPreview(true);
|
||||||
|
} else if (view === 'design') {
|
||||||
|
this.toggleIsPreview(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setActiveSidebarPanel(panel: string) {
|
setActiveSidebarPanel(panel: string) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { action, computed, makeObservable, observable } from 'mobx';
|
import { action, computed, makeObservable, observable } from 'mobx';
|
||||||
import { ISelectedItemData } from '@music163/tango-helpers';
|
import { ISelectedItemData } from '@music163/tango-helpers';
|
||||||
import { DropTarget } from './drop-target';
|
import { DropTarget } from './drop-target';
|
||||||
import { IWorkspace } from './interfaces';
|
import { AbstractWorkspace } from './abstract-workspace';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 拖拽来源类,被拖拽的物体
|
* 拖拽来源类,被拖拽的物体
|
||||||
@@ -22,7 +22,7 @@ export class DragSource {
|
|||||||
*/
|
*/
|
||||||
dropTarget: DropTarget;
|
dropTarget: DropTarget;
|
||||||
|
|
||||||
private readonly workspace: IWorkspace;
|
private readonly workspace: AbstractWorkspace;
|
||||||
|
|
||||||
get node() {
|
get node() {
|
||||||
return this.workspace.getNode(this.data?.id, this.data?.filename);
|
return this.workspace.getNode(this.data?.id, this.data?.filename);
|
||||||
@@ -47,7 +47,7 @@ export class DragSource {
|
|||||||
return this.data?.bounding;
|
return this.data?.bounding;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(workspace: IWorkspace) {
|
constructor(workspace: AbstractWorkspace) {
|
||||||
this.workspace = workspace;
|
this.workspace = workspace;
|
||||||
this.data = null;
|
this.data = null;
|
||||||
this.isDragging = false;
|
this.isDragging = false;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { action, computed, makeObservable, observable } from 'mobx';
|
import { action, computed, makeObservable, observable } from 'mobx';
|
||||||
import { ISelectedItemData } from '@music163/tango-helpers';
|
import { ISelectedItemData } from '@music163/tango-helpers';
|
||||||
import { IWorkspace } from './interfaces';
|
import { AbstractWorkspace } from './abstract-workspace';
|
||||||
|
|
||||||
export enum DropMethod {
|
export enum DropMethod {
|
||||||
ReplaceNode = 'replaceNode', // 替换节点
|
ReplaceNode = 'replaceNode', // 替换节点
|
||||||
@@ -23,7 +23,7 @@ export class DropTarget {
|
|||||||
*/
|
*/
|
||||||
data: ISelectedItemData;
|
data: ISelectedItemData;
|
||||||
|
|
||||||
private readonly workspace: IWorkspace;
|
private readonly workspace: AbstractWorkspace;
|
||||||
|
|
||||||
get node() {
|
get node() {
|
||||||
return this.workspace.getNode(this.data.id, this.data.filename);
|
return this.workspace.getNode(this.data.id, this.data.filename);
|
||||||
@@ -48,7 +48,7 @@ export class DropTarget {
|
|||||||
return this.data?.display;
|
return this.data?.display;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(workspace: IWorkspace) {
|
constructor(workspace: AbstractWorkspace) {
|
||||||
this.workspace = workspace;
|
this.workspace = workspace;
|
||||||
this.method = DropMethod.InsertAfter;
|
this.method = DropMethod.InsertAfter;
|
||||||
this.data = null;
|
this.data = null;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { AbstractWorkspace } from './abstract-workspace';
|
||||||
import { Designer } from './designer';
|
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 { action, computed, makeObservable, observable } from 'mobx';
|
||||||
import { getValue, isNil, logger, setValue } from '@music163/tango-helpers';
|
import { isNil } from '@music163/tango-helpers';
|
||||||
import type { FileType, IFileConfig } from '../types';
|
import type { IFileConfig } from '../types';
|
||||||
import { IWorkspace } from './interfaces';
|
import { AbstractWorkspace } from './abstract-workspace';
|
||||||
import { formatCode } from '../helpers';
|
import { AbstractFile } from './abstract-file';
|
||||||
|
|
||||||
/**
|
export class TangoFile extends AbstractFile {
|
||||||
* 普通文件,不进行 AST 解析
|
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||||
*/
|
super(workspace, props, false);
|
||||||
export class TangoFile {
|
this.update(props.code);
|
||||||
readonly workspace: IWorkspace;
|
makeObservable(this, {
|
||||||
/**
|
_code: observable,
|
||||||
* 文件名
|
_cleanCode: observable,
|
||||||
*/
|
code: computed,
|
||||||
readonly filename: string;
|
cleanCode: computed,
|
||||||
|
update: action,
|
||||||
/**
|
});
|
||||||
* 文件类型
|
|
||||||
*/
|
|
||||||
readonly type: FileType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 最近修改的时间戳
|
|
||||||
*/
|
|
||||||
lastModified: number;
|
|
||||||
|
|
||||||
_code: string;
|
|
||||||
_cleanCode: string;
|
|
||||||
|
|
||||||
get code() {
|
|
||||||
return this._code;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
update(code?: string) {
|
||||||
if (!isNil(code)) {
|
if (!isNil(code)) {
|
||||||
this.lastModified = Date.now();
|
this.lastModified = Date.now();
|
||||||
@@ -59,137 +26,3 @@ export class TangoFile {
|
|||||||
this.workspace.onFilesChange([this.filename]);
|
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 { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||||
import { IWorkspace } from './interfaces';
|
import { AbstractWorkspace } from './abstract-workspace';
|
||||||
|
|
||||||
export enum HistoryMessage {
|
export enum HistoryMessage {
|
||||||
InitView = 'initView',
|
InitView = 'initView',
|
||||||
@@ -43,7 +43,7 @@ export class TangoHistory {
|
|||||||
// 最多记录数
|
// 最多记录数
|
||||||
_maxSize = 100;
|
_maxSize = 100;
|
||||||
|
|
||||||
private readonly workspace: IWorkspace;
|
private readonly workspace: AbstractWorkspace;
|
||||||
|
|
||||||
get index() {
|
get index() {
|
||||||
return this._index;
|
return this._index;
|
||||||
@@ -65,7 +65,7 @@ export class TangoHistory {
|
|||||||
return this._records.length > this._index + 1;
|
return this._records.length > this._index + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(workspace: IWorkspace) {
|
constructor(workspace: AbstractWorkspace) {
|
||||||
this.workspace = workspace;
|
this.workspace = workspace;
|
||||||
|
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
|
|||||||
@@ -1,17 +1,25 @@
|
|||||||
export * from './engine';
|
export * from './abstract-workspace';
|
||||||
export * from './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 './designer';
|
||||||
export * from './drop-target';
|
|
||||||
export * from './select-source';
|
|
||||||
export * from './drag-source';
|
export * from './drag-source';
|
||||||
export * from './history';
|
export * from './drop-target';
|
||||||
|
export * from './engine';
|
||||||
export * from './file';
|
export * from './file';
|
||||||
export * from './module';
|
export * from './history';
|
||||||
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 './interfaces';
|
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 { Dict } from '@music163/tango-helpers';
|
||||||
import { TangoHistory } from './history';
|
|
||||||
import { SelectSource } from './select-source';
|
|
||||||
import { DragSource } from './drag-source';
|
|
||||||
import {
|
import {
|
||||||
IFileConfig,
|
|
||||||
FileType,
|
|
||||||
InsertChildPositionType,
|
InsertChildPositionType,
|
||||||
ITangoConfigPackages,
|
|
||||||
IPageConfigData,
|
|
||||||
IImportSpecifierSourceData,
|
IImportSpecifierSourceData,
|
||||||
IImportSpecifierData,
|
IImportSpecifierData,
|
||||||
} from '../types';
|
} 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 { IdGenerator } from '../helpers';
|
||||||
import { AppEntryModule } from './entry-module';
|
import { AbstractViewNode } from './abstract-view-node';
|
||||||
|
|
||||||
export interface IViewFile {
|
export interface IViewFile {
|
||||||
readonly workspace: IWorkspace;
|
/**
|
||||||
readonly filename: string;
|
* 文件名
|
||||||
readonly type: FileType;
|
*/
|
||||||
|
filename: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ID 生成器
|
* ID 生成器
|
||||||
@@ -54,252 +44,38 @@ export interface IViewFile {
|
|||||||
*/
|
*/
|
||||||
addImportSpecifiers: (source: string, newSpecifiers: IImportSpecifierData[]) => IViewFile;
|
addImportSpecifiers: (source: string, newSpecifiers: IImportSpecifierData[]) => IViewFile;
|
||||||
|
|
||||||
getNode: (targetNodeId: string) => IViewNode;
|
getNode: (targetNodeId: string) => AbstractViewNode;
|
||||||
|
|
||||||
removeNode: (targetNodeId: string) => IViewFile;
|
removeNode: (targetNodeId: string) => this;
|
||||||
|
|
||||||
insertChild: (
|
insertChild: (targetNodeId: string, newNode: any, position?: InsertChildPositionType) => this;
|
||||||
targetNodeId: string,
|
|
||||||
newNode: any,
|
|
||||||
position?: InsertChildPositionType,
|
|
||||||
) => IViewFile;
|
|
||||||
|
|
||||||
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: (
|
updateNodeAttribute: (
|
||||||
nodeId: string,
|
nodeId: string,
|
||||||
attrName: string,
|
attrName: string,
|
||||||
attrValue?: any,
|
attrValue?: any,
|
||||||
relatedImports?: string[],
|
relatedImports?: string[],
|
||||||
) => IViewFile;
|
) => this;
|
||||||
|
|
||||||
updateNodeAttributes: (
|
updateNodeAttributes: (
|
||||||
nodeId: string,
|
nodeId: string,
|
||||||
config: Record<string, any>,
|
config: Record<string, any>,
|
||||||
relatedImports?: string[],
|
relatedImports?: string[],
|
||||||
) => IViewFile;
|
) => this;
|
||||||
|
|
||||||
get code(): string;
|
get nodes(): Map<string, AbstractViewNode>;
|
||||||
get nodes(): Map<string, IViewNode>;
|
|
||||||
get nodesTree(): object[];
|
get nodesTree(): object[];
|
||||||
get tree(): any;
|
get tree(): any;
|
||||||
}
|
/**
|
||||||
|
* 文件中的代码
|
||||||
export interface IViewNode {
|
*/
|
||||||
/**
|
get code(): string;
|
||||||
* 所属的文件
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -1,12 +1,12 @@
|
|||||||
import { traverseEntryFile } from '../helpers';
|
import { traverseEntryFile } from '../helpers';
|
||||||
import { IFileConfig } from '../types';
|
import { IFileConfig } from '../types';
|
||||||
import { IWorkspace } from './interfaces';
|
import { AbstractJsFile } from './abstract-js-file';
|
||||||
import { TangoModule } from './module';
|
import { AbstractWorkspace } from './abstract-workspace';
|
||||||
|
|
||||||
export class AppEntryModule extends TangoModule {
|
export class JsAppEntryFile extends AbstractJsFile {
|
||||||
routerType: string;
|
routerType: string;
|
||||||
|
|
||||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||||
super(workspace, props, false);
|
super(workspace, props, false);
|
||||||
this.update(props.code, true, 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 path from 'path';
|
||||||
import { action, computed, makeObservable, observable } from 'mobx';
|
import { action, computed, makeObservable, observable } from 'mobx';
|
||||||
import { TangoModule } from './module';
|
|
||||||
import { IWorkspace } from './interfaces';
|
|
||||||
import { IExportSpecifierData, IFileConfig } from '../types';
|
import { IExportSpecifierData, IFileConfig } from '../types';
|
||||||
import { traverseComponentsEntryFile } from '../helpers';
|
import { traverseComponentsEntryFile } from '../helpers';
|
||||||
|
import { AbstractWorkspace } from './abstract-workspace';
|
||||||
|
import { AbstractJsFile } from './abstract-js-file';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 本地组件目录的入口文件,例如 '/components/index.js' 或 `/blocks/index.js`
|
* 本地组件目录的入口文件,例如 '/components/index.js' 或 `/blocks/index.js`
|
||||||
*/
|
*/
|
||||||
export class TangoComponentsEntryModule extends TangoModule {
|
export class JsLocalComponentsEntryFile extends AbstractJsFile {
|
||||||
exportList: Record<string, IExportSpecifierData>;
|
exportList: Record<string, IExportSpecifierData>;
|
||||||
|
|
||||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||||
super(workspace, props, false);
|
super(workspace, props, false);
|
||||||
this.update(props.code, false, false);
|
this.update(props.code, true, false);
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
_code: observable,
|
_code: observable,
|
||||||
_cleanCode: observable,
|
_cleanCode: observable,
|
||||||
+9
-5
@@ -1,4 +1,5 @@
|
|||||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||||
|
import { logger } from '@music163/tango-helpers';
|
||||||
import {
|
import {
|
||||||
traverseRouteFile,
|
traverseRouteFile,
|
||||||
addRouteToRouteFile,
|
addRouteToRouteFile,
|
||||||
@@ -6,20 +7,20 @@ import {
|
|||||||
updateRouteToRouteFile,
|
updateRouteToRouteFile,
|
||||||
} from '../helpers';
|
} from '../helpers';
|
||||||
import { IRouteData, IFileConfig } from '../types';
|
import { IRouteData, IFileConfig } from '../types';
|
||||||
import { IWorkspace } from './interfaces';
|
import { AbstractJsFile } from './abstract-js-file';
|
||||||
import { TangoModule } from './module';
|
import { AbstractCodeWorkspace } from './abstract-code-workspace';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 路由配置模块
|
* 路由配置模块
|
||||||
*/
|
*/
|
||||||
export class TangoRouteModule extends TangoModule {
|
export class JsRouteConfigFile extends AbstractJsFile {
|
||||||
_routes: IRouteData[];
|
_routes: IRouteData[];
|
||||||
|
|
||||||
get routes() {
|
get routes() {
|
||||||
return toJS(this._routes);
|
return toJS(this._routes);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
constructor(workspace: AbstractCodeWorkspace, props: IFileConfig) {
|
||||||
super(workspace, props, false);
|
super(workspace, props, false);
|
||||||
this.update(props.code, true, false);
|
this.update(props.code, true, false);
|
||||||
|
|
||||||
@@ -27,10 +28,13 @@ export class TangoRouteModule extends TangoModule {
|
|||||||
_routes: observable,
|
_routes: observable,
|
||||||
_code: observable,
|
_code: observable,
|
||||||
_cleanCode: observable,
|
_cleanCode: observable,
|
||||||
|
isError: observable,
|
||||||
|
errorMessage: observable,
|
||||||
routes: computed,
|
routes: computed,
|
||||||
code: computed,
|
code: computed,
|
||||||
cleanCode: computed,
|
cleanCode: computed,
|
||||||
update: action,
|
update: action,
|
||||||
|
updateAst: action,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +78,7 @@ export class TangoRouteModule extends TangoModule {
|
|||||||
*/
|
*/
|
||||||
removeRoute(route: string) {
|
removeRoute(route: string) {
|
||||||
if (route === '/') {
|
if (route === '/') {
|
||||||
console.warn('index route should not be removed!');
|
logger.warn('index route should not be removed!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const record = this.getRouteByRoutePath(route);
|
const record = this.getRouteByRoutePath(route);
|
||||||
+7
-4
@@ -8,13 +8,13 @@ import {
|
|||||||
updateBaseConfigToServiceFile,
|
updateBaseConfigToServiceFile,
|
||||||
} from '../helpers';
|
} from '../helpers';
|
||||||
import { IFileConfig } from '../types';
|
import { IFileConfig } from '../types';
|
||||||
import { IWorkspace } from './interfaces';
|
import { AbstractWorkspace } from './abstract-workspace';
|
||||||
import { TangoModule } from './module';
|
import { AbstractJsFile } from './abstract-js-file';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数据服务模块
|
* 数据服务模块
|
||||||
*/
|
*/
|
||||||
export class TangoServiceModule extends TangoModule {
|
export class JsServiceFile extends AbstractJsFile {
|
||||||
/**
|
/**
|
||||||
* 服务函数的模块名,默认为 index
|
* 服务函数的模块名,默认为 index
|
||||||
*/
|
*/
|
||||||
@@ -39,7 +39,7 @@ export class TangoServiceModule extends TangoModule {
|
|||||||
return toJS(this._baseConfig);
|
return toJS(this._baseConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||||
super(workspace, props, false);
|
super(workspace, props, false);
|
||||||
this.name = getModuleNameByFilename(props.filename);
|
this.name = getModuleNameByFilename(props.filename);
|
||||||
this.update(props.code, true, false);
|
this.update(props.code, true, false);
|
||||||
@@ -49,11 +49,14 @@ export class TangoServiceModule extends TangoModule {
|
|||||||
_baseConfig: observable,
|
_baseConfig: observable,
|
||||||
_code: observable,
|
_code: observable,
|
||||||
_cleanCode: observable,
|
_cleanCode: observable,
|
||||||
|
isError: observable,
|
||||||
|
errorMessage: observable,
|
||||||
serviceFunctions: computed,
|
serviceFunctions: computed,
|
||||||
baseConfig: computed,
|
baseConfig: computed,
|
||||||
cleanCode: computed,
|
cleanCode: computed,
|
||||||
code: computed,
|
code: computed,
|
||||||
update: action,
|
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 {
|
import {
|
||||||
traverseStoreFile,
|
traverseStoreFile,
|
||||||
traverseStoreEntryFile,
|
|
||||||
addStoreToEntryFile,
|
|
||||||
getModuleNameByFilename,
|
getModuleNameByFilename,
|
||||||
addStoreState,
|
addStoreState,
|
||||||
updateStoreState,
|
updateStoreState,
|
||||||
removeStoreState,
|
removeStoreState,
|
||||||
removeStoreToEntryFile,
|
|
||||||
} from '../helpers';
|
} from '../helpers';
|
||||||
import { IFileConfig, IStorePropertyData } from '../types';
|
import { IFileConfig, IStorePropertyData } from '../types';
|
||||||
import { IWorkspace } from './interfaces';
|
import { AbstractWorkspace } from './abstract-workspace';
|
||||||
import { TangoModule } from './module';
|
import { AbstractJsFile } from './abstract-js-file';
|
||||||
|
|
||||||
/**
|
|
||||||
* 入口配置模块
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 状态模型模块
|
* 状态模型模块
|
||||||
*/
|
*/
|
||||||
export class TangoStoreModule extends TangoModule {
|
export class JsStoreFile extends AbstractJsFile {
|
||||||
/**
|
/**
|
||||||
* 模块名
|
* 模块名
|
||||||
*/
|
*/
|
||||||
@@ -76,7 +25,7 @@ export class TangoStoreModule extends TangoModule {
|
|||||||
|
|
||||||
actions: IStorePropertyData[];
|
actions: IStorePropertyData[];
|
||||||
|
|
||||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||||
super(workspace, props, false);
|
super(workspace, props, false);
|
||||||
this.name = getModuleNameByFilename(props.filename);
|
this.name = getModuleNameByFilename(props.filename);
|
||||||
this.update(props.code, true, false);
|
this.update(props.code, true, false);
|
||||||
+20
-13
@@ -20,18 +20,19 @@ import {
|
|||||||
addImportDeclarationLegacy,
|
addImportDeclarationLegacy,
|
||||||
updateImportDeclarationLegacy,
|
updateImportDeclarationLegacy,
|
||||||
} from '../helpers';
|
} from '../helpers';
|
||||||
import { TangoNode } from './node';
|
import { JsxViewNode } from './view-node';
|
||||||
import {
|
import {
|
||||||
IFileConfig,
|
IFileConfig,
|
||||||
ITangoViewNodeData,
|
IViewNodeData,
|
||||||
IImportDeclarationPayload,
|
IImportDeclarationPayload,
|
||||||
InsertChildPositionType,
|
InsertChildPositionType,
|
||||||
IImportSpecifierSourceData,
|
IImportSpecifierSourceData,
|
||||||
ImportDeclarationDataType,
|
ImportDeclarationDataType,
|
||||||
IImportSpecifierData,
|
IImportSpecifierData,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { IViewFile, IWorkspace } from './interfaces';
|
import { AbstractWorkspace } from './abstract-workspace';
|
||||||
import { TangoModule } from './module';
|
import { IViewFile } from './interfaces';
|
||||||
|
import { AbstractJsFile } from './abstract-js-file';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导入信息转为 变量名->来源 的 map 结构
|
* 导入信息转为 变量名->来源 的 map 结构
|
||||||
@@ -56,8 +57,8 @@ function buildImportMap(importedModules: ImportDeclarationDataType) {
|
|||||||
* 将节点列表转换为 tree data 嵌套数组
|
* 将节点列表转换为 tree data 嵌套数组
|
||||||
* @param list
|
* @param list
|
||||||
*/
|
*/
|
||||||
function nodeListToTreeData(list: ITangoViewNodeData[]) {
|
function nodeListToTreeData(list: IViewNodeData[]) {
|
||||||
const map: Record<string, ITangoViewNodeData> = {};
|
const map: Record<string, IViewNodeData> = {};
|
||||||
|
|
||||||
list.forEach((item) => {
|
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 数组
|
// 解析为树结构的 jsxNodes 数组
|
||||||
_nodesTree: ITangoViewNodeData[];
|
_nodesTree: IViewNodeData[] = [];
|
||||||
/**
|
/**
|
||||||
* 通过导入组件名查找组件来自的包
|
* 通过导入组件名查找组件来自的包
|
||||||
*/
|
*/
|
||||||
@@ -108,7 +109,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
|||||||
/**
|
/**
|
||||||
* 节点列表 <id, Node>
|
* 节点列表 <id, Node>
|
||||||
*/
|
*/
|
||||||
private _nodes: Map<string, TangoNode>;
|
private _nodes: Map<string, JsxViewNode>;
|
||||||
/**
|
/**
|
||||||
* 导入的模块
|
* 导入的模块
|
||||||
* @deprecated
|
* @deprecated
|
||||||
@@ -127,7 +128,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
|||||||
return this.ast;
|
return this.ast;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||||
super(workspace, props, false);
|
super(workspace, props, false);
|
||||||
this._nodes = new Map();
|
this._nodes = new Map();
|
||||||
this.idGenerator = new IdGenerator({ prefix: props.filename });
|
this.idGenerator = new IdGenerator({ prefix: props.filename });
|
||||||
@@ -138,10 +139,14 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
|||||||
_code: observable,
|
_code: observable,
|
||||||
_cleanCode: observable,
|
_cleanCode: observable,
|
||||||
|
|
||||||
|
isError: observable,
|
||||||
|
errorMessage: observable,
|
||||||
|
|
||||||
code: computed,
|
code: computed,
|
||||||
cleanCode: computed,
|
cleanCode: computed,
|
||||||
|
|
||||||
update: action,
|
update: action,
|
||||||
|
updateAst: action,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,8 +177,10 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
|||||||
this._codeIdList = [];
|
this._codeIdList = [];
|
||||||
|
|
||||||
nodes.forEach((cur) => {
|
nodes.forEach((cur) => {
|
||||||
const node = new TangoNode({
|
const node = new JsxViewNode({
|
||||||
...cur,
|
id: cur.id,
|
||||||
|
component: cur.component,
|
||||||
|
rawNode: cur.rawNode,
|
||||||
file: this,
|
file: this,
|
||||||
});
|
});
|
||||||
this._nodes.set(cur.id, node);
|
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[]) {
|
updateNodeAttributes(nodeId: string, config: Record<string, any>, relatedImports?: string[]) {
|
||||||
if (relatedImports && relatedImports.length) {
|
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 proto = this.workspace.getPrototype(name);
|
||||||
const { source, specifiers } = prototype2importDeclarationData(proto, this.filename);
|
const { source, specifiers } = prototype2importDeclarationData(proto, this.filename);
|
||||||
const existSpecifiers: IImportSpecifierData[] = prev[source];
|
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 { ISelectedItemData, MousePoint } from '@music163/tango-helpers';
|
||||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||||
import { IViewFile, IWorkspace } from './interfaces';
|
import { AbstractWorkspace } from './abstract-workspace';
|
||||||
|
import { IViewFile } from './interfaces';
|
||||||
|
|
||||||
type StartDataType = {
|
type StartDataType = {
|
||||||
point: MousePoint;
|
point: MousePoint;
|
||||||
@@ -24,7 +25,7 @@ export class SelectSource {
|
|||||||
element: null,
|
element: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly workspace: IWorkspace;
|
private readonly workspace: AbstractWorkspace;
|
||||||
|
|
||||||
get start() {
|
get start() {
|
||||||
return toJS(this._start);
|
return toJS(this._start);
|
||||||
@@ -74,7 +75,7 @@ export class SelectSource {
|
|||||||
.filter((node) => !!node);
|
.filter((node) => !!node);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(workspace: IWorkspace) {
|
constructor(workspace: AbstractWorkspace) {
|
||||||
this.workspace = workspace;
|
this.workspace = workspace;
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
_items: observable,
|
_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 {
|
export enum FileType {
|
||||||
// js 文件
|
File = 'file',
|
||||||
Module = 'module',
|
|
||||||
AppEntryModule = 'appEntryModule',
|
JsFile = 'jsFile',
|
||||||
StoreEntryModule = 'storeEntryModule',
|
JsAppEntryFile = 'jsAppEntryFile',
|
||||||
RouteModule = 'routeModule',
|
JsRouteConfigFile = 'jsRouteConfigFile',
|
||||||
ServiceModule = 'serviceModule',
|
JsStoreEntryFile = 'jsStoreEntryFile',
|
||||||
StoreModule = 'storeModule',
|
JsStoreFile = 'jsStoreFile',
|
||||||
|
JsServiceFile = 'jsServiceFile',
|
||||||
|
JsLocalComponentsEntryFile = 'jsLocalComponentsEntryFile',
|
||||||
|
|
||||||
|
JsViewFile = 'jsViewFile',
|
||||||
|
JsonViewFile = 'jsonViewFile',
|
||||||
|
|
||||||
|
JsonFile = 'jsonFile',
|
||||||
|
PackageJsonFile = 'packageJsonFile',
|
||||||
|
TangoConfigJsonFile = 'tangoConfigJsonFile',
|
||||||
|
AppJsonFile = 'appJsonFile',
|
||||||
|
|
||||||
// 组件配置文件
|
// 组件配置文件
|
||||||
ComponentPrototypeModule = 'componentPrototypeModule',
|
ComponentPrototypeModule = 'componentPrototypeModule',
|
||||||
// 组件运行调试入口文件,一般为 `/app.js`
|
// 组件运行调试入口文件,一般为 `/app.js`
|
||||||
ComponentDemoEntryModule = 'componentDemoEntryModule',
|
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 {
|
export interface IFileConfig {
|
||||||
@@ -57,10 +45,15 @@ export interface IFileConfig {
|
|||||||
type?: FileType;
|
type?: FileType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IFileError {
|
||||||
|
filename: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 视图节点数据类型
|
* 视图节点数据类型
|
||||||
*/
|
*/
|
||||||
export interface ITangoViewNodeData<T = JSXElement> {
|
export interface IViewNodeData<T = JSXElement> {
|
||||||
/**
|
/**
|
||||||
* 节点 ID
|
* 节点 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', () => {
|
it('inferFileType', () => {
|
||||||
expect(inferFileType('/src/pages/template.js')).toBe(FileType.JsxViewModule);
|
expect(inferFileType('/src/pages/template.js')).toBe(FileType.JsViewFile);
|
||||||
expect(inferFileType('/src/pages/template.jsx')).toBe(FileType.JsxViewModule);
|
expect(inferFileType('/src/pages/template.jsx')).toBe(FileType.JsViewFile);
|
||||||
expect(inferFileType('/src/pages/template.ejs')).toBe(FileType.File);
|
expect(inferFileType('/src/pages/template.ejs')).toBe(FileType.File);
|
||||||
expect(inferFileType('/src/index.scss')).toBe(FileType.Scss);
|
expect(inferFileType('/src/index.scss')).toBe(FileType.File);
|
||||||
expect(inferFileType('/src/index.less')).toBe(FileType.Less);
|
expect(inferFileType('/src/index.less')).toBe(FileType.File);
|
||||||
expect(inferFileType('/src/index.json')).toBe(FileType.Json);
|
expect(inferFileType('/src/index.json')).toBe(FileType.JsonFile);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,62 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
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)
|
## [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
|
### Bug Fixes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@music163/tango-designer",
|
"name": "@music163/tango-designer",
|
||||||
"version": "1.3.2",
|
"version": "1.4.6",
|
||||||
"description": "lowcode designer",
|
"description": "lowcode designer",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react"
|
"react"
|
||||||
@@ -33,12 +33,12 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^4.8.0",
|
"@ant-design/icons": "^4.8.0",
|
||||||
"@music163/request": "^0.2.0",
|
"@music163/request": "^0.2.0",
|
||||||
"@music163/tango-context": "^1.1.4",
|
"@music163/tango-context": "^1.1.10",
|
||||||
"@music163/tango-core": "^1.3.2",
|
"@music163/tango-core": "^1.4.4",
|
||||||
"@music163/tango-helpers": "^1.2.0",
|
"@music163/tango-helpers": "^1.2.4",
|
||||||
"@music163/tango-sandbox": "^1.0.7",
|
"@music163/tango-sandbox": "^1.0.13",
|
||||||
"@music163/tango-setting-form": "^1.2.7",
|
"@music163/tango-setting-form": "^1.2.15",
|
||||||
"@music163/tango-ui": "^1.3.2",
|
"@music163/tango-ui": "^1.4.5",
|
||||||
"antd": "^4.24.2",
|
"antd": "^4.24.2",
|
||||||
"cash-dom": "^8.1.2",
|
"cash-dom": "^8.1.2",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
|
|||||||
@@ -34,17 +34,17 @@ export const ComponentsPopover = observer(
|
|||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
workspace.componentPrototypes.get(selectedNode?.name) ?? ({} as IComponentPrototype);
|
workspace.componentPrototypes.get(selectedNode?.name) ?? ({} as IComponentPrototype);
|
||||||
|
|
||||||
// 推荐使用的子组件
|
const recommendedList = useMemo(() => {
|
||||||
const insertedList = useMemo(
|
if (type === 'inner') {
|
||||||
() =>
|
return prototype?.childrenName
|
||||||
Array.isArray(prototype?.childrenName)
|
? Array.isArray(prototype?.childrenName)
|
||||||
? prototype?.childrenName
|
? prototype.childrenName
|
||||||
: [prototype?.childrenName].filter(Boolean),
|
: [prototype.childrenName]
|
||||||
[prototype?.childrenName],
|
: [];
|
||||||
);
|
}
|
||||||
|
// 默认推荐使用相同类型的组件作为兄弟节点
|
||||||
// 推荐使用的代码片段
|
return prototype.siblingNames || [prototype.name];
|
||||||
const siblingList = useMemo(() => prototype?.siblingNames ?? [], [prototype.siblingNames]);
|
}, [prototype.childrenName, prototype.siblingNames, prototype.name, type]);
|
||||||
|
|
||||||
const tipsTextMap = useMemo(
|
const tipsTextMap = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -82,22 +82,15 @@ export const ComponentsPopover = observer(
|
|||||||
const menuList = JSON.parse(JSON.stringify(designer.menuData));
|
const menuList = JSON.parse(JSON.stringify(designer.menuData));
|
||||||
|
|
||||||
const commonList = menuList['common'] ?? [];
|
const commonList = menuList['common'] ?? [];
|
||||||
if (commonList?.length && siblingList?.length) {
|
if (commonList?.length && recommendedList.length) {
|
||||||
commonList.unshift({
|
|
||||||
title: '代码片段',
|
|
||||||
items: siblingList,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (commonList?.length && insertedList?.length) {
|
|
||||||
commonList.unshift({
|
commonList.unshift({
|
||||||
title: '推荐使用',
|
title: '推荐使用',
|
||||||
items: insertedList,
|
items: recommendedList,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return menuList;
|
return menuList;
|
||||||
}, [insertedList, siblingList, designer.menuData]);
|
}, [recommendedList, designer.menuData]);
|
||||||
|
|
||||||
const innerTypeProps =
|
const innerTypeProps =
|
||||||
// 手动触发 适用于 点击添加组件
|
// 手动触发 适用于 点击添加组件
|
||||||
|
|||||||
@@ -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';
|
||||||
export * from './variable-tree-modal';
|
export * from './variable-tree-modal';
|
||||||
export * from './components-popover';
|
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 { InputCode } from '@music163/tango-ui';
|
||||||
import { Button, Dropdown, Form, FormProps, Input, Select, Space } from 'antd';
|
import { Button, Dropdown, Form, FormProps, Input, Select, Space } from 'antd';
|
||||||
import { Box } from 'coral-system';
|
import { Box } from 'coral-system';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
interface AddServiceFormProps extends FormProps {
|
interface AddServiceFormProps extends FormProps {
|
||||||
initialValues?: object;
|
initialValues?: Dict;
|
||||||
onSubmit: (values: object) => void;
|
onSubmit: (values: object) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
serviceModules: object[];
|
serviceModules: object[];
|
||||||
@@ -88,6 +88,10 @@ export function AddServiceForm({
|
|||||||
>
|
>
|
||||||
<Input placeholder="请输入数据服务调用名称" disabled={disabled || isModifyMode} />
|
<Input placeholder="请输入数据服务调用名称" disabled={disabled || isModifyMode} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
{/* 备注 */}
|
||||||
|
<Form.Item label="备注" name="description">
|
||||||
|
<Input placeholder="请输入备注信息" />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item label="路径" name="url" rules={[{ required: true, type: 'url' }]}>
|
<Form.Item label="路径" name="url" rules={[{ required: true, type: 'url' }]}>
|
||||||
<Input placeholder="请输入数据服务调用名称" disabled={disabled || isModifyMode} />
|
<Input placeholder="请输入数据服务调用名称" disabled={disabled || isModifyMode} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import { Dict } from '@music163/tango-helpers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 快捷键
|
* 快捷键
|
||||||
*/
|
*/
|
||||||
export class Hotkey {
|
export class Hotkey {
|
||||||
private readonly hotkeyMap = {};
|
private readonly hotkeyMap: Dict = {};
|
||||||
|
|
||||||
constructor(hotkeys: Record<string, Function>) {
|
constructor(hotkeys: Record<string, Function>) {
|
||||||
Object.keys(hotkeys).forEach((hotkey) => {
|
Object.keys(hotkeys).forEach((hotkey) => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useMemo } from 'react';
|
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 { ISelectedItemData, events, getHotkey } from '@music163/tango-helpers';
|
||||||
import {
|
import {
|
||||||
setElementStyle,
|
setElementStyle,
|
||||||
@@ -12,7 +12,7 @@ import { Hotkey } from './hotkey';
|
|||||||
import { SelectModeType } from '../types';
|
import { SelectModeType } from '../types';
|
||||||
|
|
||||||
interface UseDndProps {
|
interface UseDndProps {
|
||||||
workspace: IWorkspace;
|
workspace: AbstractCodeWorkspace;
|
||||||
designer: Designer;
|
designer: Designer;
|
||||||
/**
|
/**
|
||||||
* 沙箱内的 DOM 查询操作
|
* 沙箱内的 DOM 查询操作
|
||||||
@@ -137,6 +137,8 @@ export function useDnd({
|
|||||||
const data = sandboxQuery.getDraggableParentsData(e.target as HTMLElement, true);
|
const data = sandboxQuery.getDraggableParentsData(e.target as HTMLElement, true);
|
||||||
if (data && data.id) {
|
if (data && data.id) {
|
||||||
selectSource.select(data);
|
selectSource.select(data);
|
||||||
|
// 画布选中节点时,自动唤起 right-panel
|
||||||
|
designer.toggleRightPanel(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -343,7 +345,7 @@ export function useDnd({
|
|||||||
// 区块不能拖拽到区块中
|
// 区块不能拖拽到区块中
|
||||||
if (
|
if (
|
||||||
workspace.dragSource.prototype.type === 'block' &&
|
workspace.dragSource.prototype.type === 'block' &&
|
||||||
closetDropTargetNode.file.type === FileType.BlockEntryModule
|
closetDropTargetNode.file.type === FileType.JsLocalComponentsEntryFile
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import React, { useRef, useEffect, useCallback } from 'react';
|
|||||||
import { Box } from 'coral-system';
|
import { Box } from 'coral-system';
|
||||||
import { MultiEditor, MultiEditorProps } from '@music163/tango-ui';
|
import { MultiEditor, MultiEditorProps } from '@music163/tango-ui';
|
||||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||||
import { isValidCode } from '@music163/tango-core';
|
|
||||||
import { Modal } from 'antd';
|
|
||||||
|
|
||||||
const ideConfig = {
|
const ideConfig = {
|
||||||
// disableFileOps: {
|
// disableFileOps: {
|
||||||
@@ -21,116 +19,89 @@ const ideConfig = {
|
|||||||
// disableSetting: true,
|
// disableSetting: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface CodeEditorProps extends Partial<MultiEditorProps> {
|
export type CodeEditorProps = Partial<MultiEditorProps>;
|
||||||
/**
|
|
||||||
* 是否自动清楚未使用的导入
|
|
||||||
*/
|
|
||||||
autoRemoveUnusedImports?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CodeEditor = observer(
|
export const CodeEditor = observer((props: CodeEditorProps) => {
|
||||||
({ autoRemoveUnusedImports = true, ...rest }: CodeEditorProps) => {
|
const editorRef = useRef(null);
|
||||||
const editorRef = useRef(null);
|
const workspace = useWorkspace();
|
||||||
const workspace = useWorkspace();
|
const designer = useDesigner();
|
||||||
const designer = useDesigner();
|
const files = workspace.listFiles();
|
||||||
const files = workspace.listFiles();
|
const activeFile = workspace.activeFile;
|
||||||
const activeFile = workspace.activeFile;
|
|
||||||
|
|
||||||
let loc: any; // 记录视图代码的选中位置
|
let loc: any; // 记录视图代码的选中位置
|
||||||
const selectNode = workspace.selectSource.firstNode;
|
const selectNode = workspace.selectSource.firstNode;
|
||||||
if (selectNode && activeFile === workspace.activeViewFile) {
|
if (selectNode && activeFile === workspace.activeViewFile) {
|
||||||
loc = selectNode.loc;
|
loc = selectNode.loc;
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
editorRef.current?.refresh(files, activeFile, loc);
|
editorRef.current?.refresh(files, activeFile, loc);
|
||||||
}, [files, activeFile, loc]);
|
}, [files, activeFile, loc]);
|
||||||
|
|
||||||
const fileSave = useCallback(
|
const fileSave = useCallback(
|
||||||
(path: string, value: string) => {
|
(path: string, value: string) => {
|
||||||
if (!isJsFile(path)) {
|
workspace.updateFile(path, value, false);
|
||||||
// 非 js 文件直接保存
|
},
|
||||||
workspace.updateFile(path, value, autoRemoveUnusedImports);
|
[workspace],
|
||||||
return;
|
);
|
||||||
|
|
||||||
|
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 文件需要先检查语法,只有语法正确才会保存
|
return (
|
||||||
if (isValidCode(value)) {
|
<Box display="flex" flexDirection="row" height="100%" bg="white" {...borderStyle}>
|
||||||
workspace.updateFile(path, value, autoRemoveUnusedImports);
|
<MultiEditor
|
||||||
} else {
|
ref={editorRef}
|
||||||
Modal.confirm({
|
options={{
|
||||||
title: '检测到代码中存在语法错误,暂时无法将代码同步给设计器,是否回退到安全代码?',
|
fontSize: 14,
|
||||||
onOk: () => {
|
automaticLayout: true,
|
||||||
editorRef.current?.refresh(files, activeFile);
|
}}
|
||||||
},
|
ideConfig={ideConfig}
|
||||||
onCancel: () => {},
|
onFileSave={fileSave}
|
||||||
});
|
onPathChange={handlePathChange}
|
||||||
}
|
onFileChange={handleFileChange}
|
||||||
},
|
defaultPath={activeFile}
|
||||||
[workspace, autoRemoveUnusedImports, activeFile, files],
|
defaultTheme="GithubLightDefault"
|
||||||
);
|
defaultFiles={files}
|
||||||
|
{...props}
|
||||||
const handleFileChange = useCallback(
|
/>
|
||||||
(type: string, info: any) => {
|
</Box>
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useImperativeHandle, ForwardedRef, useRef, useState, useEffect } from 'react';
|
import React, { useImperativeHandle, ForwardedRef, useRef, useState, useEffect } from 'react';
|
||||||
import { Box } from 'coral-system';
|
import { Box } from 'coral-system';
|
||||||
import { CodeSandbox, CodeSandboxProps } from '@music163/tango-sandbox';
|
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 { observer, useWorkspace, useDesigner } from '@music163/tango-context';
|
||||||
import { Simulator, Viewport } from '../simulator';
|
import { Simulator, Viewport } from '../simulator';
|
||||||
import { useSandboxQuery } from '../context';
|
import { useSandboxQuery } from '../context';
|
||||||
@@ -88,7 +88,7 @@ function useSandbox({
|
|||||||
onViewChange: (data) => onViewChange && onViewChange(data, getSandboxConfig()),
|
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;
|
let code = workspace.getFile(filename).code;
|
||||||
if (filename === '/tango.config.json') {
|
if (filename === '/tango.config.json') {
|
||||||
code = mergeTangoConfigJson(code, { isPreview, injectScript, formatter: configFormatter });
|
code = mergeTangoConfigJson(code, { isPreview, injectScript, formatter: configFormatter });
|
||||||
@@ -109,7 +109,7 @@ function useSandbox({
|
|||||||
// 根据当前 workspace 状态与组件传入的状态是否一致,控制是否需要切换到空白路由
|
// 根据当前 workspace 状态与组件传入的状态是否一致,控制是否需要切换到空白路由
|
||||||
const display = isActive ? 'block' : 'none';
|
const display = isActive ? 'block' : 'none';
|
||||||
const routePath = isActive ? startRoute || workspace.activeRoute : LANDING_PAGE_PATH;
|
const routePath = isActive ? startRoute || workspace.activeRoute : LANDING_PAGE_PATH;
|
||||||
const routerMode = fixRouterMode(workspace.appEntryModule?.routerType);
|
const routerMode = fixRouterMode(workspace.jsAppEntryFile?.routerType);
|
||||||
|
|
||||||
const sandboxProps = isPreview
|
const sandboxProps = isPreview
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Radio, RadioProps, Tooltip } from 'antd';
|
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 { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||||
import { IconFont } from '@music163/tango-ui';
|
import { IconFont } from '@music163/tango-ui';
|
||||||
|
|
||||||
interface ChoiceSetterProps {
|
interface ChoiceSetterProps {
|
||||||
options?: OptionType[];
|
options?: IOptionItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChoiceSetter({
|
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} />;
|
||||||
|
}
|
||||||
+66
-21
@@ -2,9 +2,17 @@ import React, { useState, useEffect, useCallback } from 'react';
|
|||||||
import { Box, Text, css } from 'coral-system';
|
import { Box, Text, css } from 'coral-system';
|
||||||
import { Dropdown, Button } from 'antd';
|
import { Dropdown, Button } from 'antd';
|
||||||
import { isValidExpressionCode } from '@music163/tango-core';
|
import { isValidExpressionCode } from '@music163/tango-core';
|
||||||
import { getValue, IVariableTreeNode, noop } from '@music163/tango-helpers';
|
import { getValue, interpolate, IVariableTreeNode, noop } from '@music163/tango-helpers';
|
||||||
import { CloseCircleFilled, MenuOutlined } from '@ant-design/icons';
|
import { CloseCircleFilled, MenuOutlined } from '@ant-design/icons';
|
||||||
import { Panel, InputCode, Action, DragPanel, PopOutOutlined } from '@music163/tango-ui';
|
import {
|
||||||
|
Panel,
|
||||||
|
InputCode,
|
||||||
|
Action,
|
||||||
|
DragPanel,
|
||||||
|
PopOutOutlined,
|
||||||
|
InputCodeProps,
|
||||||
|
InputStyleCode,
|
||||||
|
} from '@music163/tango-ui';
|
||||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||||
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||||
import { VariableTree } from '../components';
|
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`
|
const suffixStyle = css`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -37,26 +58,32 @@ const suffixStyle = css`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export interface ExpressionSetterProps extends FormItemComponentProps<string> {
|
export interface CodeSetterProps extends FormItemComponentProps<string> {
|
||||||
modalTitle?: string;
|
modalTitle?: string;
|
||||||
modalTip?: string;
|
modalTip?: string;
|
||||||
autoCompleteOptions?: string[];
|
autoCompleteOptions?: string[];
|
||||||
allowClear?: boolean;
|
allowClear?: boolean;
|
||||||
showOptionsDropDown?: boolean;
|
showOptionsDropDown?: boolean;
|
||||||
|
/**
|
||||||
|
* 表达式类型指定为 css object
|
||||||
|
*/
|
||||||
|
expressionType?: 'cssObject';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ExpressionSetter(props: ExpressionSetterProps) {
|
export function CodeSetter(props: CodeSetterProps) {
|
||||||
const {
|
const {
|
||||||
onChange,
|
onChange,
|
||||||
modalTitle,
|
modalTitle,
|
||||||
modalTip,
|
modalTip,
|
||||||
autoCompleteOptions,
|
autoCompleteOptions,
|
||||||
placeholder = '在这里输入JS代码',
|
template,
|
||||||
|
placeholder = '在这里输入代码',
|
||||||
value: valueProp,
|
value: valueProp,
|
||||||
status,
|
status,
|
||||||
allowClear = true,
|
allowClear = true,
|
||||||
newStoreTemplate,
|
newStoreTemplate,
|
||||||
showOptionsDropDown = true,
|
showOptionsDropDown = true,
|
||||||
|
expressionType,
|
||||||
} = props;
|
} = props;
|
||||||
// const codeValue = getCodeOfWrappedCode(valueProp);
|
// const codeValue = getCodeOfWrappedCode(valueProp);
|
||||||
const [inputValue, setInputValue] = useState(valueProp);
|
const [inputValue, setInputValue] = useState(valueProp);
|
||||||
@@ -79,10 +106,12 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
|||||||
const sandbox = useSandboxQuery();
|
const sandbox = useSandboxQuery();
|
||||||
const evaluateContext = sandbox.window;
|
const evaluateContext = sandbox.window;
|
||||||
|
|
||||||
|
const InputCodeComponent = expressionType === 'cssObject' ? InputStyleCode : InputCode;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="ExpressionSetter">
|
<Box className="ExpressionSetter">
|
||||||
{/* 同时支持下拉框展示 */}
|
{/* 同时支持下拉框展示 */}
|
||||||
<InputCode
|
<InputCodeComponent
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
suffix={
|
suffix={
|
||||||
<Box css={suffixStyle}>
|
<Box css={suffixStyle}>
|
||||||
@@ -90,7 +119,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
|||||||
<CloseCircleFilled
|
<CloseCircleFilled
|
||||||
title="清空"
|
title="清空"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
change('');
|
change(undefined);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -119,8 +148,10 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
|||||||
subTitle={modalTip}
|
subTitle={modalTip}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
autoCompleteOptions={autoCompleteOptions}
|
autoCompleteOptions={autoCompleteOptions}
|
||||||
|
template={template}
|
||||||
newStoreTemplate={newStoreTemplate}
|
newStoreTemplate={newStoreTemplate}
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
|
expressionType={expressionType}
|
||||||
onOk={(value) => {
|
onOk={(value) => {
|
||||||
change(value);
|
change(value);
|
||||||
}}
|
}}
|
||||||
@@ -141,7 +172,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExpressionPopoverProps {
|
export interface ExpressionPopoverProps extends InputCodeProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
subTitle?: string;
|
subTitle?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
@@ -150,10 +181,18 @@ export interface ExpressionPopoverProps {
|
|||||||
onOk?: (value: string) => void;
|
onOk?: (value: string) => void;
|
||||||
dataSource?: IVariableTreeNode[];
|
dataSource?: IVariableTreeNode[];
|
||||||
autoCompleteOptions?: string[];
|
autoCompleteOptions?: string[];
|
||||||
|
/**
|
||||||
|
* 值的模板,一般用于定义函数模板
|
||||||
|
*/
|
||||||
|
template?: string;
|
||||||
/**
|
/**
|
||||||
* 新建 store 的模板代码
|
* 新建 store 的模板代码
|
||||||
*/
|
*/
|
||||||
newStoreTemplate?: string;
|
newStoreTemplate?: string;
|
||||||
|
/**
|
||||||
|
* 表达式类型指定为 css object
|
||||||
|
*/
|
||||||
|
expressionType?: 'cssObject';
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,12 +205,15 @@ export function ExpressionPopover({
|
|||||||
value,
|
value,
|
||||||
dataSource,
|
dataSource,
|
||||||
autoCompleteOptions,
|
autoCompleteOptions,
|
||||||
|
template,
|
||||||
newStoreTemplate = CODE_TEMPLATES.newStoreTemplate,
|
newStoreTemplate = CODE_TEMPLATES.newStoreTemplate,
|
||||||
children,
|
children,
|
||||||
|
expressionType,
|
||||||
}: ExpressionPopoverProps) {
|
}: ExpressionPopoverProps) {
|
||||||
const [exp, setExp] = useState(value ?? defaultValue);
|
const [exp, setExp] = useState(value ?? defaultValue);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
|
const InputCodeComponent = expressionType === 'cssObject' ? InputStyleCode : InputCode;
|
||||||
|
|
||||||
const selectNodePath = workspace.selectSource.selected[0]?.codeId;
|
const selectNodePath = workspace.selectSource.selected[0]?.codeId;
|
||||||
|
|
||||||
@@ -182,7 +224,7 @@ export function ExpressionPopover({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const sandbox = useSandboxQuery();
|
const sandbox = useSandboxQuery();
|
||||||
const evaluateContext = sandbox.window;
|
const evaluateContext: any = sandbox.window;
|
||||||
|
|
||||||
const handleExpInputChange = (val: string) => {
|
const handleExpInputChange = (val: string) => {
|
||||||
setExp(val?.trim());
|
setExp(val?.trim());
|
||||||
@@ -207,7 +249,7 @@ export function ExpressionPopover({
|
|||||||
body={
|
body={
|
||||||
<Box display="flex" flexDirection="column">
|
<Box display="flex" flexDirection="column">
|
||||||
<Box p="m">
|
<Box p="m">
|
||||||
<InputCode
|
<InputCodeComponent
|
||||||
shape="inset"
|
shape="inset"
|
||||||
minHeight="56px"
|
minHeight="56px"
|
||||||
maxHeight="160px"
|
maxHeight="160px"
|
||||||
@@ -223,19 +265,18 @@ export function ExpressionPopover({
|
|||||||
autoCompleteOptions={autoCompleteOptions}
|
autoCompleteOptions={autoCompleteOptions}
|
||||||
/>
|
/>
|
||||||
{error ? (
|
{error ? (
|
||||||
<Text color="red" fontSize="12px" as="p">
|
<Text color="red" fontSize="12px" as="div">
|
||||||
出错了!输入的表达式存在语法错误,请修改后再提交!
|
出错了!输入的表达式存在语法错误,请修改后再提交!
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
{subTitle && (
|
<Box fontSize="12px" color="text2">
|
||||||
<Text fontSize="12px" color="text3" as="p">
|
<Text display="block">说明:</Text>
|
||||||
{subTitle}
|
{subTitle && <Text display="block">{subTitle}</Text>}
|
||||||
|
<Text display="block">
|
||||||
|
你可以在上面的代码输入框里输入常规的 javascript 代码,还可以直接使用 jsx
|
||||||
|
代码,但需要符合该属性的接受值定义。
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
</Box>
|
||||||
<Text fontSize="12px" color="text3" as="p">
|
|
||||||
说明:你可以在上面的代码输入框里输入常规的 javascript 代码,还可以直接使用 jsx
|
|
||||||
代码,但需要符合该属性的接受值定义。
|
|
||||||
</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Panel
|
<Panel
|
||||||
title="从变量列表中选中"
|
title="从变量列表中选中"
|
||||||
@@ -253,7 +294,7 @@ export function ExpressionPopover({
|
|||||||
height="100%"
|
height="100%"
|
||||||
showViewButton
|
showViewButton
|
||||||
dataSource={dataSource || expressionVariables}
|
dataSource={dataSource || expressionVariables}
|
||||||
appContext={sandbox?.window['tango']}
|
appContext={evaluateContext?.['tango']}
|
||||||
getStoreNames={() => Object.keys(workspace.storeModules)}
|
getStoreNames={() => Object.keys(workspace.storeModules)}
|
||||||
serviceModules={serviceModules}
|
serviceModules={serviceModules}
|
||||||
getServiceData={(serviceKey) => {
|
getServiceData={(serviceKey) => {
|
||||||
@@ -274,7 +315,11 @@ export function ExpressionPopover({
|
|||||||
}
|
}
|
||||||
let str;
|
let str;
|
||||||
if (/^(stores|services)\./.test(node.key)) {
|
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 {
|
} else {
|
||||||
str = `${node.key}`;
|
str = `${node.key}`;
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { css, Box, Text } from 'coral-system';
|
import { css, Box, Text } from 'coral-system';
|
||||||
import { AutoComplete } from 'antd';
|
import { AutoComplete, Input } from 'antd';
|
||||||
import { ActionSelect } from '@music163/tango-ui';
|
import { ActionSelect } from '@music163/tango-ui';
|
||||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||||
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||||
import { wrapCode } from '@music163/tango-helpers';
|
import { Dict, wrapCode } from '@music163/tango-helpers';
|
||||||
import { ExpressionPopover } from './expression-setter';
|
import { ExpressionPopover, getCallbackValue } from './code-setter';
|
||||||
import { value2code } from '@music163/tango-core';
|
import { value2code } from '@music163/tango-core';
|
||||||
|
|
||||||
enum EventAction {
|
enum EventAction {
|
||||||
@@ -31,7 +31,7 @@ export type EventSetterProps = FormItemComponentProps<string>;
|
|||||||
* 事件监听函数绑定器
|
* 事件监听函数绑定器
|
||||||
*/
|
*/
|
||||||
export function EventSetter(props: EventSetterProps) {
|
export function EventSetter(props: EventSetterProps) {
|
||||||
const { value, onChange, modalTitle } = props;
|
const { value, onChange, modalTitle, modalTip, template } = props;
|
||||||
const [type, setType] = useState<EventAction>(); // 事件类型
|
const [type, setType] = useState<EventAction>(); // 事件类型
|
||||||
const [temp, setTemp] = useState(''); // 二级暂存值
|
const [temp, setTemp] = useState(''); // 二级暂存值
|
||||||
const { actionVariables, routeOptions } = useWorkspaceData();
|
const { actionVariables, routeOptions } = useWorkspaceData();
|
||||||
@@ -59,7 +59,9 @@ export function EventSetter(props: EventSetterProps) {
|
|||||||
label: (
|
label: (
|
||||||
<ExpressionPopover
|
<ExpressionPopover
|
||||||
title={modalTitle}
|
title={modalTitle}
|
||||||
|
subTitle={modalTip}
|
||||||
value={value}
|
value={value}
|
||||||
|
template={template}
|
||||||
onOk={(nextValue) => {
|
onOk={(nextValue) => {
|
||||||
handleChange(nextValue);
|
handleChange(nextValue);
|
||||||
}}
|
}}
|
||||||
@@ -74,30 +76,34 @@ export function EventSetter(props: EventSetterProps) {
|
|||||||
{ label: '打开弹窗', value: EventAction.OpenModal },
|
{ label: '打开弹窗', value: EventAction.OpenModal },
|
||||||
{ label: '关闭弹窗', value: EventAction.CloseModal },
|
{ label: '关闭弹窗', value: EventAction.CloseModal },
|
||||||
],
|
],
|
||||||
[modalTitle, value, actionVariables, handleChange],
|
[modalTitle, value, actionVariables, template, handleChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onAction = (key: string) => {
|
const onAction = (key: string) => {
|
||||||
setType(key as EventAction); // 记录事件类型
|
setType(key as EventAction); // 记录事件类型
|
||||||
setTemp(''); // 重置二级选项值
|
setTemp(''); // 重置二级选项值
|
||||||
|
|
||||||
switch (key) {
|
if (key === EventAction.NoAction) {
|
||||||
case EventAction.ConsoleLog:
|
handleChange(undefined);
|
||||||
handleChange('(...args) => console.log(...args)');
|
return;
|
||||||
break;
|
|
||||||
case EventAction.NoAction:
|
|
||||||
handleChange(undefined);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionText = getActionText(type, temp, code);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box css={wrapperStyle}>
|
<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 && (
|
{type === EventAction.NavigateTo && (
|
||||||
<AutoComplete
|
<AutoComplete
|
||||||
placeholder="选择或输入页面路由"
|
placeholder="选择或输入页面路由"
|
||||||
@@ -141,26 +147,16 @@ export function EventSetter(props: EventSetterProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlerMap = {
|
const handlerMap: Dict = {
|
||||||
[EventAction.OpenModal]: 'openModal',
|
[EventAction.ConsoleLog]: 'console.log',
|
||||||
[EventAction.CloseModal]: 'closeModal',
|
[EventAction.OpenModal]: 'tango.openModal',
|
||||||
[EventAction.NavigateTo]: 'navigateTo',
|
[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 = '') {
|
function getExpressionValue(type: EventAction, value = '') {
|
||||||
const handler = handlerMap[type];
|
const handler = handlerMap[type];
|
||||||
if (handler) {
|
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 { DateRangeSetter, DateSetter, TimeRangeSetter } from './date-setter';
|
||||||
import { EnumSetter } from './enum-setter';
|
import { EnumSetter } from './enum-setter';
|
||||||
import { EventSetter } from './event-setter';
|
import { EventSetter } from './event-setter';
|
||||||
import { ExpressionSetter } from './expression-setter';
|
import { CodeSetter } from './code-setter';
|
||||||
import { JSONSetter } from './json-setter';
|
import { JSONSetter } from './json-setter';
|
||||||
import { JsxSetter } from './jsx-setter';
|
import { JsxSetter } from './jsx-setter';
|
||||||
import { ListSetter } from './list-setter';
|
import { ListSetter } from './list-setter';
|
||||||
import { ModelSetter } from './model-setter';
|
import { ModelSetter } from './model-setter';
|
||||||
import { OptionSetter } from './option-setter';
|
import { OptionSetter } from './option-setter';
|
||||||
import { PickerSetter } from './picker-setter';
|
import { PickerSetter } from './picker-setter';
|
||||||
import { RenderSetter, TableCellSetter, TableExpandableSetter } from './render-setter';
|
import { RenderSetter } from './render-setter';
|
||||||
import { RouterSetter } from './router-setter';
|
import { RouterSetter } from './router-setter';
|
||||||
import {
|
import {
|
||||||
SpacingSetter,
|
SpacingSetter,
|
||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
FlexDirectionSetter,
|
FlexDirectionSetter,
|
||||||
} from './style-setter';
|
} from './style-setter';
|
||||||
import { ChoiceSetter } from './choice-setter';
|
import { ChoiceSetter } from './choice-setter';
|
||||||
|
import { ClassNameSetter } from './classname-setter';
|
||||||
import { isValidExpressionCode } from '@music163/tango-core';
|
import { isValidExpressionCode } from '@music163/tango-core';
|
||||||
|
|
||||||
const codeValidate: IFormItemCreateOptions['validate'] = (value, field) => {
|
const codeValidate: IFormItemCreateOptions['validate'] = (value, field) => {
|
||||||
@@ -49,10 +50,14 @@ export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
|
|||||||
{
|
{
|
||||||
name: 'codeSetter',
|
name: 'codeSetter',
|
||||||
alias: ['expSetter', 'expressionSetter'],
|
alias: ['expSetter', 'expressionSetter'],
|
||||||
component: ExpressionSetter,
|
component: CodeSetter,
|
||||||
type: 'code',
|
type: 'code',
|
||||||
validate: codeValidate,
|
validate: codeValidate,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'classNameSetter',
|
||||||
|
component: ClassNameSetter,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'radioGroupSetter',
|
name: 'radioGroupSetter',
|
||||||
alias: ['choiceSetter'],
|
alias: ['choiceSetter'],
|
||||||
@@ -126,22 +131,12 @@ export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
|
|||||||
component: PickerSetter,
|
component: PickerSetter,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'renderPropsSetter',
|
name: 'renderSetter',
|
||||||
|
alias: ['renderPropsSetter'],
|
||||||
component: RenderSetter,
|
component: RenderSetter,
|
||||||
type: 'code',
|
type: 'code',
|
||||||
validate: codeValidate,
|
validate: codeValidate,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'tableCellSetter',
|
|
||||||
component: TableCellSetter,
|
|
||||||
type: 'code',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'tableExpandableSetter',
|
|
||||||
component: TableExpandableSetter,
|
|
||||||
type: 'code',
|
|
||||||
validate: codeValidate,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'routerSetter',
|
name: 'routerSetter',
|
||||||
component: RouterSetter,
|
component: RouterSetter,
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import { css, Box } from 'coral-system';
|
|||||||
import { Form, Input, Button, Popover } from 'antd';
|
import { Form, Input, Button, Popover } from 'antd';
|
||||||
import { DeleteOutlined, EditOutlined, HolderOutlined } from '@ant-design/icons';
|
import { DeleteOutlined, EditOutlined, HolderOutlined } from '@ant-design/icons';
|
||||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||||
|
import { Dict } from '@music163/tango-helpers';
|
||||||
import { DragBox } from '../components';
|
import { DragBox } from '../components';
|
||||||
|
|
||||||
function copyWithoutUndefined(source: Record<string, any>) {
|
function copyWithoutUndefined(source: Dict) {
|
||||||
const ret = {};
|
const ret: Dict = {};
|
||||||
for (const key in source) {
|
for (const key in source) {
|
||||||
if (source[key] !== undefined) {
|
if (source[key] !== undefined) {
|
||||||
ret[key] = source[key];
|
ret[key] = source[key];
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export function ModelSetter({
|
|||||||
}: FormItemComponentProps) {
|
}: FormItemComponentProps) {
|
||||||
const [inputValue, setInputValue] = useState(value);
|
const [inputValue, setInputValue] = useState(value);
|
||||||
const { modelVariables } = useWorkspaceData();
|
const { modelVariables } = useWorkspaceData();
|
||||||
const evaluateContext = useSandboxQuery().window || {};
|
const evaluateContext: any = useSandboxQuery().window || {};
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
const definedVariables = useMemo(() => {
|
const definedVariables = useMemo(() => {
|
||||||
const list: string[] = [];
|
const list: string[] = [];
|
||||||
|
|||||||
@@ -2,21 +2,16 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|||||||
import { ActionSelect, InputCode } from '@music163/tango-ui';
|
import { ActionSelect, InputCode } from '@music163/tango-ui';
|
||||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||||
import { Box } from 'coral-system';
|
import { Box } from 'coral-system';
|
||||||
|
import { Dict, IOptionItem } from '@music163/tango-helpers';
|
||||||
interface IRenderOption {
|
import { getCallbackValue } from './code-setter';
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
render?: string;
|
|
||||||
relatedImports?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RenderSetterProps {
|
export interface RenderSetterProps {
|
||||||
text?: string;
|
text?: string;
|
||||||
options?: IRenderOption[];
|
options?: IOptionItem[];
|
||||||
fallbackOption?: IRenderOption;
|
fallbackOption?: IOptionItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions: IRenderOption[] = [
|
const defaultOptions: IOptionItem[] = [
|
||||||
{ label: '取消自定义', value: '' },
|
{ label: '取消自定义', value: '' },
|
||||||
{ label: '自定义渲染', value: 'Box', render: '() => <Box></Box>' },
|
{ label: '自定义渲染', value: 'Box', render: '() => <Box></Box>' },
|
||||||
];
|
];
|
||||||
@@ -29,6 +24,7 @@ export function RenderSetter({
|
|||||||
onChange,
|
onChange,
|
||||||
text = '自定义渲染为',
|
text = '自定义渲染为',
|
||||||
options = defaultOptions,
|
options = defaultOptions,
|
||||||
|
template = `() => {{content}}`,
|
||||||
fallbackOption,
|
fallbackOption,
|
||||||
}: FormItemComponentProps & RenderSetterProps) {
|
}: FormItemComponentProps & RenderSetterProps) {
|
||||||
const [inputValue, setInputValue] = useState(value || '');
|
const [inputValue, setInputValue] = useState(value || '');
|
||||||
@@ -37,7 +33,7 @@ export function RenderSetter({
|
|||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
const optionsMap = useMemo(() => {
|
const optionsMap = useMemo(() => {
|
||||||
return options.reduce((prev, cur) => {
|
return options.reduce<Dict>((prev, cur) => {
|
||||||
prev[cur.value] = cur;
|
prev[cur.value] = cur;
|
||||||
return prev;
|
return prev;
|
||||||
}, {});
|
}, {});
|
||||||
@@ -46,13 +42,14 @@ export function RenderSetter({
|
|||||||
const onSelect = useCallback(
|
const onSelect = useCallback(
|
||||||
(key: string) => {
|
(key: string) => {
|
||||||
const option = optionsMap[key] || fallbackOption;
|
const option = optionsMap[key] || fallbackOption;
|
||||||
if (option?.render) {
|
const next = option?.render || getCallbackValue(option.renderBody, template);
|
||||||
onChange(option.render, { relatedImports: option.relatedImports });
|
if (next) {
|
||||||
|
onChange(next, { relatedImports: option.relatedImports });
|
||||||
} else {
|
} else {
|
||||||
onChange(undefined);
|
onChange(undefined);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[optionsMap, fallbackOption, onChange],
|
[optionsMap, fallbackOption, onChange, template],
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
@@ -67,69 +64,3 @@ export function RenderSetter({
|
|||||||
</Box>
|
</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 React, { useEffect, useState } from 'react';
|
||||||
import { Box } from 'coral-system';
|
import { Box } from 'coral-system';
|
||||||
import { Input, Radio } from 'antd';
|
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 { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||||
import { useWorkspaceData } from '@music163/tango-context';
|
import { useWorkspaceData } from '@music163/tango-context';
|
||||||
import { PickerSetter } from './picker-setter';
|
import { PickerSetter } from './picker-setter';
|
||||||
|
|
||||||
|
const routerModeMap: Dict = {
|
||||||
|
link: '链接',
|
||||||
|
router: '路由',
|
||||||
|
both: '',
|
||||||
|
};
|
||||||
|
|
||||||
export function RouterSetter(props: FormItemComponentProps) {
|
export function RouterSetter(props: FormItemComponentProps) {
|
||||||
const [type, setType] = useState<'input' | 'select'>(() => {
|
const [type, setType] = useState<'input' | 'select'>(() => {
|
||||||
return isValidUrl(props.value) ? 'input' : 'select';
|
return isValidUrl(props.value) ? 'input' : 'select';
|
||||||
@@ -37,7 +43,7 @@ export function RouterSetter(props: FormItemComponentProps) {
|
|||||||
)}
|
)}
|
||||||
{type === 'input' && (
|
{type === 'input' && (
|
||||||
<Input
|
<Input
|
||||||
placeholder={`请输入任意${{ link: '链接', router: '路由', both: '' }[displayType]}地址`}
|
placeholder={`请输入任意${routerModeMap[displayType]}地址`}
|
||||||
{...props}
|
{...props}
|
||||||
value={input}
|
value={input}
|
||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { Box } from 'coral-system';
|
import { Box } from 'coral-system';
|
||||||
import { SettingForm, FormModel, SettingFormProps } from '@music163/tango-setting-form';
|
import { SettingForm, FormModel, SettingFormProps } from '@music163/tango-setting-form';
|
||||||
import { Panel } from '@music163/tango-ui';
|
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 { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||||
import { registerBuiltinSetters } from './setters';
|
import { registerBuiltinSetters } from './setters';
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ export const SettingPanel = observer((props: SettingPanelProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getSettingValue = (attributes: Record<string, string>) => {
|
const getSettingValue = (attributes: Record<string, string>) => {
|
||||||
const ret = {};
|
const ret: Dict = {};
|
||||||
const keys = Object.keys(attributes);
|
const keys = Object.keys(attributes);
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
if (/^data-/.test(key)) {
|
if (/^data-/.test(key)) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useMemo, useState } from 'react';
|
|||||||
import { Box, Grid, Text } from 'coral-system';
|
import { Box, Grid, Text } from 'coral-system';
|
||||||
import styled, { css } from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
import {
|
import {
|
||||||
|
Dict,
|
||||||
IComponentPrototype,
|
IComponentPrototype,
|
||||||
MenuDataType,
|
MenuDataType,
|
||||||
MenuValueType,
|
MenuValueType,
|
||||||
@@ -66,7 +67,7 @@ export interface ComponentsPanelProps {
|
|||||||
layout?: 'grid' | 'line';
|
layout?: 'grid' | 'line';
|
||||||
}
|
}
|
||||||
|
|
||||||
const localeMap = {
|
const localeMap: Dict = {
|
||||||
common: '基础组件',
|
common: '基础组件',
|
||||||
atom: '原子组件',
|
atom: '原子组件',
|
||||||
snippet: '组合',
|
snippet: '组合',
|
||||||
@@ -124,7 +125,7 @@ export const ComponentsPanel = observer(
|
|||||||
const tabs = Object.keys(menuData).map((key) => ({
|
const tabs = Object.keys(menuData).map((key) => ({
|
||||||
key,
|
key,
|
||||||
label: localeMap[key],
|
label: localeMap[key],
|
||||||
children: <MaterialList data={menuData[key]} />,
|
children: <MaterialList data={(menuData as any)[key]} />,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (showBizComps) {
|
if (showBizComps) {
|
||||||
@@ -364,7 +365,7 @@ function MaterialGrid({ data }: MaterialProps) {
|
|||||||
<IconFont className="material-icon" type={data.icon || 'icon-placeholder'} />
|
<IconFont className="material-icon" type={data.icon || 'icon-placeholder'} />
|
||||||
) : (
|
) : (
|
||||||
<Box className="material-icon">
|
<Box className="material-icon">
|
||||||
<img src={icon} alt={data.name} />
|
<img src={icon} alt={data.name} height="32" />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const DataSourceView = observer(() => {
|
|||||||
<Box className="ServiceFunctionList" p="m">
|
<Box className="ServiceFunctionList" p="m">
|
||||||
<VariableTree
|
<VariableTree
|
||||||
dataSource={serviceVariables}
|
dataSource={serviceVariables}
|
||||||
appContext={sandbox?.window['tango']}
|
appContext={(sandbox?.window as any)['tango']}
|
||||||
serviceModules={serviceModules}
|
serviceModules={serviceModules}
|
||||||
onRemoveService={(variableKey) => {
|
onRemoveService={(variableKey) => {
|
||||||
workspace.removeServiceFunction(variableKey);
|
workspace.removeServiceFunction(variableKey);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ConfigGroup, ConfigItem } from '@music163/tango-ui';
|
|||||||
import { Box, css } from 'coral-system';
|
import { Box, css } from 'coral-system';
|
||||||
import { Input, Switch, Form, Button, message, FormProps } from 'antd';
|
import { Input, Switch, Form, Button, message, FormProps } from 'antd';
|
||||||
import { CloseCircleOutlined, CloseOutlined } from '@ant-design/icons';
|
import { CloseCircleOutlined, CloseOutlined } from '@ant-design/icons';
|
||||||
|
import { Dict } from '@music163/tango-helpers';
|
||||||
|
|
||||||
export default observer(() => {
|
export default observer(() => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
@@ -63,7 +64,7 @@ function ProxyManage({
|
|||||||
initialValues: initialValuesProp = {},
|
initialValues: initialValuesProp = {},
|
||||||
onSave,
|
onSave,
|
||||||
}: {
|
}: {
|
||||||
initialValues?: object;
|
initialValues?: Dict;
|
||||||
onSave?: (values: any) => void;
|
onSave?: (values: any) => void;
|
||||||
}) {
|
}) {
|
||||||
const initialValues = useMemo(() => {
|
const initialValues = useMemo(() => {
|
||||||
@@ -84,7 +85,7 @@ function ProxyManage({
|
|||||||
{...proxyManageFormStyle}
|
{...proxyManageFormStyle}
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
onFinish={(values) => {
|
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;
|
prev[path] = rest;
|
||||||
return prev;
|
return prev;
|
||||||
}, {});
|
}, {});
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import { ColorTag, ConfigGroup, ConfigItem } from '@music163/tango-ui';
|
|||||||
import { MinusCircleOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
import { MinusCircleOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
||||||
import { useBoolean } from '@music163/tango-helpers';
|
import { useBoolean } from '@music163/tango-helpers';
|
||||||
import { isUndefined } from 'lodash-es';
|
import { isUndefined } from 'lodash-es';
|
||||||
import { Workspace } from '@music163/tango-core';
|
|
||||||
import { useSandboxQuery } from '../context';
|
import { useSandboxQuery } from '../context';
|
||||||
|
|
||||||
enum DependencyItemType {
|
enum DependencyItemType {
|
||||||
@@ -169,7 +168,7 @@ function RenderItem({
|
|||||||
}: RenderItemProps) {
|
}: RenderItemProps) {
|
||||||
const [open, { on, off }] = useBoolean(false);
|
const [open, { on, off }] = useBoolean(false);
|
||||||
|
|
||||||
const workspace = useWorkspace() as Workspace;
|
const workspace = useWorkspace();
|
||||||
|
|
||||||
const basePackage = useMemo(() => {
|
const basePackage = useMemo(() => {
|
||||||
if (type !== DependencyItemType.基础包 || !templateBaseDependencies) return undefined;
|
if (type !== DependencyItemType.基础包 || !templateBaseDependencies) return undefined;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Box, css } from 'coral-system';
|
|||||||
import { IconFont } from '@music163/tango-ui';
|
import { IconFont } from '@music163/tango-ui';
|
||||||
import { EyeOutlined, EyeInvisibleOutlined, EllipsisOutlined } from '@ant-design/icons';
|
import { EyeOutlined, EyeInvisibleOutlined, EllipsisOutlined } from '@ant-design/icons';
|
||||||
import { observer, useWorkspace } from '@music163/tango-context';
|
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 { noop, parseDndId } from '@music163/tango-helpers';
|
||||||
import { useSandboxQuery } from '../../context';
|
import { useSandboxQuery } from '../../context';
|
||||||
import { buildQueryBySlotId } from '../../helpers';
|
import { buildQueryBySlotId } from '../../helpers';
|
||||||
@@ -65,7 +65,7 @@ const filedNames = {
|
|||||||
children: 'children',
|
children: 'children',
|
||||||
};
|
};
|
||||||
|
|
||||||
const getNodeKeys = (data: ITangoViewNodeData[]) => {
|
const getNodeKeys = (data: IViewNodeData[]) => {
|
||||||
const ids: string[] = [];
|
const ids: string[] = [];
|
||||||
data?.forEach((node) => {
|
data?.forEach((node) => {
|
||||||
ids.push(node.id);
|
ids.push(node.id);
|
||||||
@@ -76,7 +76,7 @@ const getNodeKeys = (data: ITangoViewNodeData[]) => {
|
|||||||
return ids;
|
return ids;
|
||||||
};
|
};
|
||||||
|
|
||||||
const OutlineTreeNode: React.FC<{ node: ITangoViewNodeData } & ComponentsTreeProps> = observer(
|
const OutlineTreeNode: React.FC<{ node: IViewNodeData } & ComponentsTreeProps> = observer(
|
||||||
({ node, showToggleVisibleIcon, actionItems }) => {
|
({ node, showToggleVisibleIcon, actionItems }) => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
const sandboxQuery = useSandboxQuery();
|
const sandboxQuery = useSandboxQuery();
|
||||||
@@ -142,7 +142,7 @@ export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
|
|||||||
workspace.selectSource.selected.map((item) => item.id),
|
workspace.selectSource.selected.map((item) => item.id),
|
||||||
);
|
);
|
||||||
const file = workspace.activeViewModule;
|
const file = workspace.activeViewModule;
|
||||||
const nodesTree = (file?.nodesTree ?? []) as ITangoViewNodeData[];
|
const nodesTree = (file?.nodesTree ?? []) as IViewNodeData[];
|
||||||
const [expandedKeys, setExpandedKeys] = useState(getNodeKeys(nodesTree));
|
const [expandedKeys, setExpandedKeys] = useState(getNodeKeys(nodesTree));
|
||||||
const [contextMenuOpen, setContextMenuOpen] = useState(false);
|
const [contextMenuOpen, setContextMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React, { useEffect, useReducer } from 'react';
|
import React, { useEffect, useReducer } from 'react';
|
||||||
import { observer } from '@music163/tango-context';
|
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 { CollapsePanel, IconButton, JsonView } from '@music163/tango-ui';
|
||||||
import { ReloadOutlined } from '@ant-design/icons';
|
import { ReloadOutlined } from '@ant-design/icons';
|
||||||
import { useSandboxQuery } from '../../context';
|
import { useSandboxQuery } from '../../context';
|
||||||
|
|
||||||
function processTango(obj: any = {}, index = 0) {
|
function processTango(obj: Dict = {}, index = 0) {
|
||||||
const ret = {};
|
const ret: Dict = {};
|
||||||
Object.keys(obj).forEach((key) => {
|
Object.keys(obj).forEach((key) => {
|
||||||
const val = obj[key];
|
const val = obj[key];
|
||||||
if (index > 2) {
|
if (index > 2) {
|
||||||
@@ -25,11 +25,12 @@ function processTango(obj: any = {}, index = 0) {
|
|||||||
export const StateTree = observer(() => {
|
export const StateTree = observer(() => {
|
||||||
const sandboxQuery = useSandboxQuery();
|
const sandboxQuery = useSandboxQuery();
|
||||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||||
const tangoContext = pick(sandboxQuery.window['tango'] || {}, [
|
const tangoContext = pick((sandboxQuery.window as any)['tango'] || {}, [
|
||||||
'stores',
|
'stores',
|
||||||
'pageStore',
|
'pageStore',
|
||||||
'services',
|
'services',
|
||||||
'config',
|
'config',
|
||||||
|
'routeData',
|
||||||
]);
|
]);
|
||||||
const callback = () => {
|
const callback = () => {
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const resizeHandleStyle = (axis: ResizableBoxProps['axis'], barStyle: HTMLCoralP
|
|||||||
cursor: ${axis === 'x' ? 'col-resize' : 'row-resize'};
|
cursor: ${axis === 'x' ? 'col-resize' : 'row-resize'};
|
||||||
${barStyle &&
|
${barStyle &&
|
||||||
Object.keys(barStyle)
|
Object.keys(barStyle)
|
||||||
.map((key) => `${key}: ${barStyle[key]};`)
|
.map((key) => `${key}: ${(barStyle as any)[key]};`)
|
||||||
.join('')}
|
.join('')}
|
||||||
&:hover,
|
&:hover,
|
||||||
&:active {
|
&:active {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import cx from 'classnames';
|
|||||||
import { Box, Text, css, HTMLCoralProps } from 'coral-system';
|
import { Box, Text, css, HTMLCoralProps } from 'coral-system';
|
||||||
import { Badge } from 'antd';
|
import { Badge } from 'antd';
|
||||||
import { ReactComponentProps } from '@music163/tango-helpers';
|
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 { ResizableBox } from './resizable-box';
|
||||||
import { getWidget } from '../widgets';
|
import { getWidget } from '../widgets';
|
||||||
|
|
||||||
@@ -19,15 +19,22 @@ const sidebarStyle = css`
|
|||||||
list-style: none;
|
list-style: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.SidebarPanelBarListItem {
|
.SidebarPanelBarListItem {
|
||||||
|
color: var(--tango-colors-text2);
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background-color: var(--tango-colors-custom-sidebarItemActiveBg);
|
color: var(--tango-colors-brand);
|
||||||
|
background-color: var(--tango-colors-custom-sidebarItemActiveBg, --tango-colors-fill2);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&: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) {
|
function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 266, footer, children }: SidebarProps) {
|
||||||
const designer = useDesigner();
|
const designer = useDesigner();
|
||||||
|
const workspace = useWorkspace();
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
const ret: Record<string, SidebarPanelItemProps> = {};
|
const ret: Record<string, SidebarPanelItemProps> = {};
|
||||||
@@ -92,10 +100,23 @@ function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 266, footer, childre
|
|||||||
return ret;
|
return ret;
|
||||||
}, [children]);
|
}, [children]);
|
||||||
|
|
||||||
|
if (designer.activeView !== 'design') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box display="flex" flexShrink={0} css={sidebarStyle} className="SidebarPanel">
|
<Box display="flex" flexShrink={0} css={sidebarStyle} className="SidebarPanel">
|
||||||
<Box className="SidebarPanelBar" display="flex" flexDirection="column" position="relative">
|
<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) => {
|
{Object.keys(items).map((key) => {
|
||||||
const item = items[key];
|
const item = items[key];
|
||||||
const isActive = key === designer.activeSidebarPanel;
|
const isActive = key === designer.activeSidebarPanel;
|
||||||
@@ -112,7 +133,6 @@ function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 266, footer, childre
|
|||||||
icon={item.icon}
|
icon={item.icon}
|
||||||
label={item.label}
|
label={item.label}
|
||||||
showBadge={item.showBadge}
|
showBadge={item.showBadge}
|
||||||
isActive={isActive}
|
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@@ -183,10 +203,6 @@ BaseSidebarPanel.Item = function ({
|
|||||||
export const Sidebar = observer(BaseSidebarPanel);
|
export const Sidebar = observer(BaseSidebarPanel);
|
||||||
|
|
||||||
interface SidebarPanelBarItemProps {
|
interface SidebarPanelBarItemProps {
|
||||||
/**
|
|
||||||
* 是否选中
|
|
||||||
*/
|
|
||||||
isActive?: boolean;
|
|
||||||
/**
|
/**
|
||||||
* 侧边栏图标
|
* 侧边栏图标
|
||||||
*/
|
*/
|
||||||
@@ -212,15 +228,9 @@ interface SidebarPanelBarItemProps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function SidebarPanelBarItem({
|
function SidebarPanelBarItem({ icon: iconProp, label, showBadge }: SidebarPanelBarItemProps) {
|
||||||
isActive,
|
|
||||||
icon: iconProp,
|
|
||||||
label,
|
|
||||||
showBadge,
|
|
||||||
}: SidebarPanelBarItemProps) {
|
|
||||||
const color = isActive ? 'brand' : 'text.body';
|
|
||||||
let icon = (
|
let icon = (
|
||||||
<Text fontSize="24px" lineHeight={1} color={color}>
|
<Text fontSize="24px" lineHeight={1}>
|
||||||
{iconProp}
|
{iconProp}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
@@ -241,7 +251,7 @@ function SidebarPanelBarItem({
|
|||||||
py="12px"
|
py="12px"
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
<Text fontSize="12px" mt="s" color={color}>
|
<Text fontSize="12px" mt="s">
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ function SelectionBox({ showActions, actions, data }: SelectionBoxProps) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<SelectionToolSet>{!isPage && actions}</SelectionToolSet>
|
<SelectionToolSet>{!isPage && actions}</SelectionToolSet>
|
||||||
{isFromCurrentFile && prototype.hasChildren && (
|
{isFromCurrentFile && prototype?.hasChildren && (
|
||||||
<ComponentsPopover>
|
<ComponentsPopover>
|
||||||
<Tooltip title="快捷添加子元素">
|
<Tooltip title="快捷添加子元素">
|
||||||
<SelectionHelper icon={<PlusOutlined />} />
|
<SelectionHelper icon={<PlusOutlined />} />
|
||||||
|
|||||||
@@ -25,22 +25,45 @@ function DesktopSimulator({ children }: HTMLCoralProps<'div'>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mobileStyle = css`
|
const mobileStyle = css`
|
||||||
--device-viewport-width: 375px;
|
--device-viewport-width: 360px;
|
||||||
--device-viewport-height: 812px;
|
--device-viewport-height: 640px;
|
||||||
--device-frame-width: 13px;
|
position: relative;
|
||||||
background-image: url(https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/21897838692/b1c5/2665/56db/10558c1bb2030d55ceabbcc74431de99.png);
|
margin: auto;
|
||||||
background-repeat: no-repeat;
|
border: 16px black solid;
|
||||||
background-size: 100% 100%;
|
border-top-width: 60px;
|
||||||
box-shadow: 0 2px 8px rgb(2 8 20 / 10%), 0 8px 16px rgb(2 8 20 / 8%);
|
border-bottom-width: 60px;
|
||||||
border-radius: 62px;
|
border-radius: 36px;
|
||||||
overflow: hidden;
|
|
||||||
|
&::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 {
|
.MobileSimulatorDeviceFrame {
|
||||||
border-radius: 50px;
|
|
||||||
width: var(--device-viewport-width);
|
width: var(--device-viewport-width);
|
||||||
height: var(--device-viewport-height);
|
height: var(--device-viewport-height);
|
||||||
margin: var(--device-frame-width);
|
background: white;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import { observer, useWorkspace } from '@music163/tango-context';
|
|||||||
|
|
||||||
export const HistoryTool = observer(() => {
|
export const HistoryTool = observer(() => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
|
const disabled = !workspace.isValid;
|
||||||
return (
|
return (
|
||||||
<Box display="flex" columnGap="m">
|
<Box display="flex" columnGap="m">
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
shape="ghost"
|
shape="ghost"
|
||||||
disabled={!workspace.history.couldBack || workspace.history.index === 0}
|
disabled={disabled || !workspace.history.couldBack || workspace.history.index === 0}
|
||||||
tooltip="撤销"
|
tooltip="撤销"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
workspace.history.back();
|
workspace.history.back();
|
||||||
@@ -20,7 +21,7 @@ export const HistoryTool = observer(() => {
|
|||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
shape="ghost"
|
shape="ghost"
|
||||||
disabled={!workspace.history.couldForward}
|
disabled={disabled || !workspace.history.couldForward}
|
||||||
tooltip="重做"
|
tooltip="重做"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
workspace.history.forward();
|
workspace.history.forward();
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React from 'react';
|
||||||
import { Group } from 'coral-system';
|
import { Group } from 'coral-system';
|
||||||
import { Modal } from 'antd';
|
|
||||||
import { ToggleButton, CodeOutlined, DualOutlined } from '@music163/tango-ui';
|
import { ToggleButton, CodeOutlined, DualOutlined } from '@music163/tango-ui';
|
||||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||||
import { BorderOutlined } from '@ant-design/icons';
|
import { BorderOutlined } from '@ant-design/icons';
|
||||||
@@ -9,23 +8,13 @@ export const ModeSwitchTool = observer(() => {
|
|||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
const designer = useDesigner();
|
const designer = useDesigner();
|
||||||
|
|
||||||
const activeFileCheck = useCallback(() => {
|
|
||||||
if (workspace.activeFile !== workspace.activeViewFile) {
|
|
||||||
Modal.confirm({
|
|
||||||
title: '当前打开的文件与视图不匹配,是否切换到当前视图对应的文件?',
|
|
||||||
onOk: () => {
|
|
||||||
workspace.setActiveFile(workspace.activeViewFile);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [workspace]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group attached>
|
<Group attached whiteSpace="nowrap">
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
shape="ghost"
|
shape="ghost"
|
||||||
selected={designer.activeView === 'design'}
|
selected={designer.activeView === 'design'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
workspace.syncFiles(); // 保证 ast 与 code 是同步的
|
||||||
designer.setActiveView('design');
|
designer.setActiveView('design');
|
||||||
}}
|
}}
|
||||||
tooltip="设计视图"
|
tooltip="设计视图"
|
||||||
@@ -38,7 +27,6 @@ export const ModeSwitchTool = observer(() => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
designer.setActiveView('code'); // 切换到源码视图
|
designer.setActiveView('code'); // 切换到源码视图
|
||||||
designer.setActiveSidebarPanel(''); // 关闭左侧面板
|
designer.setActiveSidebarPanel(''); // 关闭左侧面板
|
||||||
activeFileCheck();
|
|
||||||
}}
|
}}
|
||||||
tooltip="源码视图"
|
tooltip="源码视图"
|
||||||
>
|
>
|
||||||
@@ -50,7 +38,6 @@ export const ModeSwitchTool = observer(() => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
designer.setActiveView('dual'); // 切换到双屏视图
|
designer.setActiveView('dual'); // 切换到双屏视图
|
||||||
designer.setActiveSidebarPanel(''); // 关闭左侧面板
|
designer.setActiveSidebarPanel(''); // 关闭左侧面板
|
||||||
activeFileCheck();
|
|
||||||
}}
|
}}
|
||||||
tooltip="双屏视图"
|
tooltip="双屏视图"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ToggleButton } from '@music163/tango-ui';
|
import { ToggleButton } from '@music163/tango-ui';
|
||||||
import { EyeOutlined } from '@ant-design/icons';
|
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(() => {
|
export const PreviewTool = observer(() => {
|
||||||
const designer = useDesigner();
|
const designer = useDesigner();
|
||||||
|
const workspace = useWorkspace();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
|
disabled={!workspace.isValid}
|
||||||
shape="ghost"
|
shape="ghost"
|
||||||
selected={designer.isPreview}
|
selected={designer.isPreview}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -47,11 +47,14 @@ function PageSelect({ value, onSelect, onRemove, onUpdate, onCopy, options }: Pa
|
|||||||
setVisible(false);
|
setVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const current = optionMap[value];
|
const pathname = value?.split?.('?')[0];
|
||||||
|
const current = optionMap[pathname];
|
||||||
return (
|
return (
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
shape="ghost"
|
shape="ghost"
|
||||||
minWidth={100}
|
minWidth={100}
|
||||||
|
maxWidth={200}
|
||||||
|
textAlign="left"
|
||||||
dropdownProps={{
|
dropdownProps={{
|
||||||
open: visible,
|
open: visible,
|
||||||
onOpenChange: setVisible,
|
onOpenChange: setVisible,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Box, Group } from 'coral-system';
|
import { Box, Group } from 'coral-system';
|
||||||
import { ReactComponentProps } from '@music163/tango-helpers';
|
import { ReactComponentProps } from '@music163/tango-helpers';
|
||||||
|
import { DesignerViewType } from '@music163/tango-core';
|
||||||
|
import { observer, useDesigner } from '@music163/tango-context';
|
||||||
import { getWidget } from '../widgets';
|
import { getWidget } from '../widgets';
|
||||||
|
|
||||||
export interface ToolbarProps {
|
export interface ToolbarProps {
|
||||||
@@ -15,25 +17,23 @@ export function Toolbar({ children }: ToolbarProps) {
|
|||||||
|
|
||||||
let prevPlacement: string;
|
let prevPlacement: string;
|
||||||
React.Children.forEach(children, (child: React.ReactElement, index) => {
|
React.Children.forEach(children, (child: React.ReactElement, index) => {
|
||||||
let fallbackNode;
|
const childProps = child.props;
|
||||||
|
|
||||||
|
let fallbackChildren;
|
||||||
if (child.key) {
|
if (child.key) {
|
||||||
const Widget = getWidget(['toolbar', child.key].join('.'));
|
const Widget = getWidget(['toolbar', child.key].join('.'));
|
||||||
if (Widget) {
|
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;
|
const key = child.key || index;
|
||||||
node = (
|
const node = React.cloneElement(child, {
|
||||||
<div key={key} className="ToolbarPanelItem" data-key={key}>
|
key,
|
||||||
{node}
|
'data-key': key,
|
||||||
</div>
|
children: childProps.children || fallbackChildren,
|
||||||
);
|
});
|
||||||
const placement = child.props.placement || prevPlacement || 'center';
|
|
||||||
|
const placement = childProps.placement || prevPlacement || 'center';
|
||||||
switch (placement) {
|
switch (placement) {
|
||||||
case 'left':
|
case 'left':
|
||||||
left.push(node);
|
left.push(node);
|
||||||
@@ -68,17 +68,47 @@ export function Toolbar({ children }: ToolbarProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ToolbarItemProps extends ReactComponentProps {
|
export interface ToolbarItemProps extends ReactComponentProps {
|
||||||
|
/**
|
||||||
|
* 类型
|
||||||
|
*/
|
||||||
|
type?: 'divider';
|
||||||
placement?: 'left' | 'center' | 'right';
|
placement?: 'left' | 'center' | 'right';
|
||||||
children?: React.ReactElement;
|
children?: React.ReactElement;
|
||||||
/**
|
/**
|
||||||
* 如果 key 匹配到内置组件的话,传递给子节点的属性
|
* 如果 key 匹配到内置组件的话,传递给子节点的属性
|
||||||
*/
|
*/
|
||||||
widgetProps?: object;
|
widgetProps?: object;
|
||||||
|
/**
|
||||||
|
* 工具箱展示的视图
|
||||||
|
*/
|
||||||
|
activeViews?: DesignerViewType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolbarItem({ placement, widgetProps, children }: ToolbarItemProps) {
|
const ToolbarItem = observer(
|
||||||
return <div>{children}</div>;
|
({
|
||||||
}
|
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() {
|
function Separator() {
|
||||||
return <Box className="Separator" width={1} height={16} bg="colors.custom.toolbarDividerColor" />;
|
return <Box className="Separator" width={1} height={16} bg="colors.custom.toolbarDividerColor" />;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
const widgets = {};
|
const widgets = {};
|
||||||
|
|
||||||
export function registerWidget(key: string, component: React.ComponentType<any>) {
|
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(
|
throw new Error(
|
||||||
`Invalid widget key(${key}), should start with toolbar, sidebar, contextMenu or selectionMenu`,
|
`Invalid widget key(${key}), should start with toolbar, sidebar, contextMenu or selectionMenu`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,35 +1,37 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box } from 'coral-system';
|
import { Box, HTMLCoralProps } from 'coral-system';
|
||||||
import { observer, useDesigner } from '@music163/tango-context';
|
import { observer, useDesigner } from '@music163/tango-context';
|
||||||
import { DesignerViewType } from '@music163/tango-core';
|
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 的模式
|
* 视图面板模式,对应 Workspace 的模式
|
||||||
*/
|
*/
|
||||||
mode?: DesignerViewType;
|
mode?: DesignerViewType;
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WorkspaceView = observer((props: WorkspaceViewProps) => {
|
export const WorkspaceView = observer((props: WorkspaceViewProps) => {
|
||||||
const { mode = 'design', children } = props;
|
const { mode = 'design', children, className, ...rest } = props;
|
||||||
const designer = useDesigner();
|
const designer = useDesigner();
|
||||||
const display = mode !== designer.activeView ? 'none' : 'block';
|
const display = mode !== designer.activeView ? 'none' : 'block';
|
||||||
// 云音乐移动端模式小屏幕适配,可能会溢出屏幕
|
// 移动端模式小屏幕适配,可能会溢出屏幕
|
||||||
const overflow =
|
const overflow =
|
||||||
designer.simulator.name === 'phone' ? 'auto' : designer.isPreview ? 'auto' : 'hidden';
|
designer.simulator.name === 'phone' ? 'auto' : designer.isPreview ? 'auto' : 'hidden';
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
className={`ViewPanel ${mode}`}
|
className={cx('ViewPanel', mode, className)}
|
||||||
display={designer.activeView === 'dual' ? 'block' : display}
|
display={designer.activeView === 'dual' ? 'block' : display}
|
||||||
flex="1"
|
flex="1"
|
||||||
overflow={overflow}
|
overflow={overflow}
|
||||||
position="relative"
|
position="relative"
|
||||||
|
{...rest}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{/* 添加组件弹层 */}
|
{/* 添加组件弹层 */}
|
||||||
{display === 'block' && <ComponentsPopover type="inner" isControlled />}
|
{display === 'block' && <ComponentsPopover type="inner" isControlled />}
|
||||||
|
{designer.activeView === 'design' && <FileErrorsOverlay />}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,30 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
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)
|
# [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
|
### Features
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@music163/tango-helpers",
|
"name": "@music163/tango-helpers",
|
||||||
"version": "1.2.0",
|
"version": "1.2.4",
|
||||||
"description": "Shared types, helpers, and hooks of tango-apps",
|
"description": "Shared types, helpers, and hooks of tango-apps",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"shared",
|
"shared",
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function filterTreeData<T = unknown>(
|
|||||||
childrenProp = 'children',
|
childrenProp = 'children',
|
||||||
onlyLeaf = false,
|
onlyLeaf = false,
|
||||||
) {
|
) {
|
||||||
const reducer = (result: T[], current: T) => {
|
const reducer = (result: T[], current: any) => {
|
||||||
if (current[childrenProp]) {
|
if (current[childrenProp]) {
|
||||||
const newChildren = current[childrenProp].reduce(reducer, []);
|
const newChildren = current[childrenProp].reduce(reducer, []);
|
||||||
if (newChildren.length) {
|
if (newChildren.length) {
|
||||||
@@ -70,7 +70,7 @@ export function mapTreeData<T = unknown>(
|
|||||||
mapper: (node: T) => any,
|
mapper: (node: T) => any,
|
||||||
childrenProp = 'children',
|
childrenProp = 'children',
|
||||||
) {
|
) {
|
||||||
return treeData.map((node) => {
|
return treeData.map((node: any) => {
|
||||||
const newNode = mapper(node);
|
const newNode = mapper(node);
|
||||||
if (node[childrenProp]) {
|
if (node[childrenProp]) {
|
||||||
newNode[childrenProp] = mapTreeData(node[childrenProp], mapper, childrenProp);
|
newNode[childrenProp] = mapTreeData(node[childrenProp], mapper, childrenProp);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function isValidFunctionCode(str: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const templatePattern = /^{{(.+)}}$/s;
|
const templatePattern = /^{{([\s\S]+)}}$/;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断给定代码是否被双花括号包裹
|
* 判断给定代码是否被双花括号包裹
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user