mirror of
https://github.com/NetEase/tango.git
synced 2026-09-01 04:19:05 +08:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 82bc5db4d8 | |||
| 85c053b3db | |||
| 32f5667cae | |||
| 1d0fb4f274 | |||
| 0011cdca0f | |||
| 0f3f0afdfa | |||
| b53a9f7eef | |||
| 5336c2ec28 | |||
| cdbd2351a2 | |||
| 1f03977f7d | |||
| a2cc418a73 | |||
| 239c0b0a28 | |||
| ae04850bb2 | |||
| ca740c83ae | |||
| 00bf52a411 | |||
| c1306e24e0 | |||
| 28040fc006 | |||
| 3f3981bb9d | |||
| 253ceca969 | |||
| a0ed57ff4f | |||
| f83c6bc0f6 | |||
| bb92210594 | |||
| 163ecedff3 | |||
| 22a2e9bdd1 | |||
| 696f82bd26 | |||
| a0484e8f64 | |||
| cf31ef19f4 | |||
| fe31246483 | |||
| 9daf3872e7 | |||
| f17ccbb7f6 | |||
| f854f75b8a | |||
| df3021f75e | |||
| 8bf53a76f8 | |||
| 791fbb162a |
@@ -9,3 +9,4 @@ coverage/
|
||||
.umi-production/
|
||||
.log
|
||||
.eslintcache
|
||||
*.pem
|
||||
|
||||
@@ -31,23 +31,17 @@ You can view the detailed usage guide through the following links:
|
||||
- Out-of-the-box front-end low-code designer, providing flexible and easy-to-use designer React components
|
||||
- Developed using TypeScript, providing complete type definition files
|
||||
|
||||
## 🎯 Milestone
|
||||
## 💡 Examples
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
dateFormat YYYY-MM-DD
|
||||
title Tango 1.0 Milestones
|
||||
excludes weekends
|
||||
%% (`excludes` accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays".)
|
||||
You can use tango to build lowcode builders, for example:
|
||||
|
||||
section Builder Release
|
||||
1.0 Alpha : m1, 2023-10-01, 2024-01-31
|
||||
1.0 RC : m2, 2024-02-01, 2024-04-30
|
||||
1.0 Stable : m3, 2024-04-30, 2024-12-31
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Alpha versions will continue to optimize some implementation protocols and continuously integrate new features from the internal version of NetEase; RC versions will provide a stable parsing protocol and implementation; The 1.0 official version is planned to be released in Q2 2024. After the release of the 1.0 official version, we will continue to iterate on versions and gradually incorporate core capabilities such as process design, component library, and AIGC from the internal version.
|
||||
| Preview | Description |
|
||||
| ---------------------------------------------------------------------------------------------- | ----------------------- |
|
||||
|  | Internal App Builder |
|
||||
|  | Dashboard App Builder |
|
||||
|  | Mobile App Builder |
|
||||
|  | ReactNative App Builder |
|
||||
|  | Email Builder |
|
||||
|
||||
## 🌐 Compatibility
|
||||
|
||||
@@ -76,6 +70,23 @@ yarn
|
||||
yarn start
|
||||
```
|
||||
|
||||
### Local Https certificate
|
||||
|
||||
If you need to use https in the local development environment, you can use the following command to generate a certificate:
|
||||
|
||||
```bash
|
||||
brew install mkcert
|
||||
|
||||
# install the local certificate
|
||||
mkcert -install
|
||||
|
||||
# enter the playground app directory
|
||||
cd apps/playground
|
||||
|
||||
# generate a certificate
|
||||
mkcert local.netease.com # or change to your own domain name
|
||||
```
|
||||
|
||||
## 💬 Community
|
||||
|
||||
Join NetEase Tango Community to share your ideas, suggestions, or questions and connect with other users and contributors.
|
||||
|
||||
+30
-19
@@ -26,28 +26,22 @@
|
||||
## ✨ 特性
|
||||
|
||||
- 经历网易云音乐内网生产环境的实际检验,可灵活集成应用于低代码平台,本地开发工具等
|
||||
- 基于源码 AST 驱动,无私有 DSL 和协议
|
||||
- 提供实时出码能力,支持源码进,源码出
|
||||
- 开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件
|
||||
- 使用 TypeScript 开发,提供完整的类型定义文件
|
||||
- 提供基于源码 AST 驱动的低代码引擎,无私有 DSL 和协议
|
||||
- 提供实时出码能力,支持源码进,源码出,适配多种源码开发场景
|
||||
- 提供开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件
|
||||
- 使用 TypeScript 开发,提供完整的类型定义文件和完善的文档支持
|
||||
|
||||
## 🎯 里程碑
|
||||
## 💡 例子
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
dateFormat YYYY-MM-DD
|
||||
title Tango 1.0 Milestones
|
||||
excludes weekends
|
||||
%% (`excludes` accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays".)
|
||||
你可以使用 Tango 快速构建多种类型的基于源码的低代码搭建工具,例如:
|
||||
|
||||
section Builder Release
|
||||
1.0 Alpha : m1, 2023-10-01, 2024-01-31
|
||||
1.0 RC : m2, 2024-02-01, 2024-04-30
|
||||
1.0 Stable : m3, 2024-04-30, 2024-12-31
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Alpha 版本会持续对一些实现协议进行优化,并持续融合网易内网版本中的新增特性;RC 版本将会提供基本稳定的解析协议和实现;1.0 正式版本计划在 2024 年Q2发布。1.0 正式版发布后我们将会持续进行版本迭代,并逐步融入内网版本中的流程设计、组件库、AIGC 等核心基础能力。
|
||||
| 预览图 | 说明 |
|
||||
| ---------------------------------------------------------------------------------------------- | -------------------- |
|
||||
|  | 中后台系统搭建 |
|
||||
|  | 仪表盘应用搭建 |
|
||||
|  | H5活动页面搭建 |
|
||||
|  | ReactNative 应用搭建 |
|
||||
|  | 营销类邮件搭建 |
|
||||
|
||||
## 🌐 兼容环境
|
||||
|
||||
@@ -76,6 +70,23 @@ yarn
|
||||
yarn start
|
||||
```
|
||||
|
||||
### 本地 https 证书
|
||||
|
||||
如果需要在本地开发环境中使用 https,可以使用以下命令生成证书:
|
||||
|
||||
```bash
|
||||
brew install mkcert
|
||||
|
||||
# 将 mkcert 添加到本地根 CA,仅在本地生效
|
||||
mkcert -install
|
||||
|
||||
# 进入 playground 应用目录
|
||||
cd apps/playground
|
||||
|
||||
# 为网站生成一个由 mkcert 签名的证书
|
||||
mkcert local.netease.com
|
||||
```
|
||||
|
||||
## 💬 社区讨论
|
||||
|
||||
参与 NetEase Tango 的社区,以分享您的想法、建议或问题,并与其他用户和贡献者建立联系。
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC4sAqDE3nhtSWD
|
||||
YqV4mlglnzIs2AioZr2uxfj/OKrpE1ZML/dflJyc5NGUpOZWV+W1vRc70wWVNFxB
|
||||
Si8nrYrrseoID7Ehxxe6+J1m/JsEbhZWHwV3M1iGW7JD7VhujaDpSLg67wMTMpSW
|
||||
3BUS+0NT+MNfCisp+u/ls4fXPCTgXiWcjMULm95oLlTeYwurPHfxwn9f9hh85I5I
|
||||
K3dI7w8MsQN7EFNrJz3UfrNLpmARMtSUBMwBYqrCitoUxRT0q7KYCru7kHxGPB6A
|
||||
SQrFpSpOWT79vp8GBSp9g3axhRsK+aEgzQ0QGrFPHBwBP/kWgmdB1URc/K7STSC9
|
||||
xyMNZOM7AgMBAAECggEATbyDYDKVbaRymr+tyHqmuYctdmSkGEXOdz8LFFoEzj/+
|
||||
ZekWpVuHJB7H/FoAPb2XiYyCuAKVafz1C+IuwPOx8o0bZbM9KmuDmIZGsm+GzDGO
|
||||
I0fBZC/vhfYYrbC8NSRV43thWCT8VVtH4cvW7vtllnWxvlIoYu4lhbiwZ68AEpO1
|
||||
8PlJsOhnagfkFpG8LehSDs9if5L+Ig0geQEcUY3pGUM9LKYsRoxFv6Vh8wQku1XD
|
||||
YOxLvLWDPqJ75m816t50RARe3e6eDJ+ukzDCoDjmLP8uhf1sk1FcL5aOgUNU3VOi
|
||||
P6sIH5uzuD0MA3FXx2mItYLOc44r4uaFJZiRUMEXGQKBgQDCjUYblE4rh0nKFkqx
|
||||
AhgfcRgpgLbP42OwFtD0u20RXdZAEJDRDQ43NdOgjFp2B50k75LBPIoFXYUZdzKj
|
||||
l7S2FH5O9Cc6hEgZGI4hCiYd+nQVI1mxv820YnzZJN4oooxyL+B5RlnW5cDT7LJN
|
||||
11Q5PRewkM+XpfbE3lKUjTNcDwKBgQDzBS+j8T9GJSzV2xgA+mpGHe1RjDq6yQtk
|
||||
Rl5GWpflW4Nr3/58BbmCWNA8WP0NOWutJhrkltwLCqJC8OZX/WHnmcmq2k+UAfQQ
|
||||
z98p+xcMWOTcl5XIbveuhYoZb2udt9WVJLcpRVxVhEWcBrtHsrpScZXsKYf/TaZ1
|
||||
JtCjPU5KFQKBgGXj/UqmYkYzxXZ8NEP998pHvcLGsXew/F2VpXv2yQlmXrYQtvd1
|
||||
YfOSUjJsL1hPZoKYogBoB1UGBJYwsimxYyhVFU2eWwkvjF7wWEd/fDUJHVCQwgHw
|
||||
lPga+I303BDmCK4o2uRI7vY4P8P92+gelPKhR03mmYIvnky+rvsz9CkLAoGAVerl
|
||||
du6Z0jDecxUsnVvrKrL17jjHorXlYyRFvFXXEe2SvxbIIAzeEylXJZ7IiF5epS6t
|
||||
n1d+oCC4UTZeGYmpjXofhFn+fHNcWH1FhtAQy/q5nwuwltY0luz7cBamU3Jk/n+m
|
||||
id7N0CYdfhYbLDQSD4e822ureyV2zkBUzwGjpikCgYEAoCVbhkxVrQHRmxLC2jDx
|
||||
pM94W1ij9TqblfGg3OMpPXjpTdlknatWjBw7JkoyxYiYJq5Gi2p4QV6HHUvS1osj
|
||||
BB+4A99/PD9A0ICoJglz8SXlGe/895zwHwpKT9f0zjizQrEd2KCPf75URa+t2FGY
|
||||
B2GkXsbChRWVrey5+uMzUD8=
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -1,25 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEQjCCAqqgAwIBAgIRAIeLJ2jwt4MSKycHFmFXL6YwDQYJKoZIhvcNAQELBQAw
|
||||
fTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSkwJwYDVQQLDCB3d3N1
|
||||
bkBzdW4tbWJwLmxvY2FsIChTVU4gV2Vpd2VpKTEwMC4GA1UEAwwnbWtjZXJ0IHd3
|
||||
c3VuQHN1bi1tYnAubG9jYWwgKFNVTiBXZWl3ZWkpMB4XDTIyMDcyODAxNTIxMFoX
|
||||
DTI0MTAyODAxNTIxMFowVDEnMCUGA1UEChMebWtjZXJ0IGRldmVsb3BtZW50IGNl
|
||||
cnRpZmljYXRlMSkwJwYDVQQLDCB3d3N1bkBzdW4tbWJwLmxvY2FsIChTVU4gV2Vp
|
||||
d2VpKTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALiwCoMTeeG1JYNi
|
||||
pXiaWCWfMizYCKhmva7F+P84qukTVkwv91+UnJzk0ZSk5lZX5bW9FzvTBZU0XEFK
|
||||
Lyetiuux6ggPsSHHF7r4nWb8mwRuFlYfBXczWIZbskPtWG6NoOlIuDrvAxMylJbc
|
||||
FRL7Q1P4w18KKyn67+Wzh9c8JOBeJZyMxQub3mguVN5jC6s8d/HCf1/2GHzkjkgr
|
||||
d0jvDwyxA3sQU2snPdR+s0umYBEy1JQEzAFiqsKK2hTFFPSrspgKu7uQfEY8HoBJ
|
||||
CsWlKk5ZPv2+nwYFKn2DdrGFGwr5oSDNDRAasU8cHAE/+RaCZ0HVRFz8rtJNIL3H
|
||||
Iw1k4zsCAwEAAaNmMGQwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUF
|
||||
BwMBMB8GA1UdIwQYMBaAFDILZR+SW32Ug0HkAKrH1KT2a1ttMBwGA1UdEQQVMBOC
|
||||
EWxvY2FsLm5ldGVhc2UuY29tMA0GCSqGSIb3DQEBCwUAA4IBgQCEdeV2Likl4MUt
|
||||
wWeN/X3tAR3L7mkl7e4f0CQ4v2lOs8cekOP7p3ZFk5nSyVbxda3usPB0OtbVTibw
|
||||
qZ1n/TMRb5epHroXIMleqngP82zFZNg+WVyHm/ZAml8gVD0VZjYM1Tb+U9+SrgGw
|
||||
lvZndbuUXa3h/d0irPH90KsdEeAR1SqwzAzvLO2JgvBuimHRceQWcTiiGWDZOyii
|
||||
tux4U+pEzh0Dia40uFozFAoYZPDUpZ+gsiQ3MR3HOrAu2OR0a+CfelDXZNLR3C4z
|
||||
cBKr53ajAbHstnpxKJh9IfeKph44PrbDo6HMe1XPprl7idoEesX3Ma3SBgm4lmId
|
||||
cfEOUWNIYOgtOT5bflXNNyzXOgYVjpLnjwg4EOASO/XIjhvqdjIyCu7HJMIcZ9oo
|
||||
Aob2exeluv5LVwic1UJQDl2McDBIgJ8R32oEN4skCKIBSbWlxWyjnxL5I5WkJF4y
|
||||
vlSx8bKAjVr8map0C3rWISuZdgheJkF96qjwhVSxOv++p6srq6M=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -5,6 +5,7 @@ import { BranchesOutlined, MenuOutlined, QuestionCircleOutlined } from '@ant-des
|
||||
import { registerSetter } from '@music163/tango-designer';
|
||||
import type { IVariableTreeNode } from '@music163/tango-helpers';
|
||||
import { FooSetter } from '../components';
|
||||
import { ToggleButton } from '@music163/tango-ui';
|
||||
|
||||
export * from './mock-files';
|
||||
|
||||
@@ -46,8 +47,17 @@ registerSetter({
|
||||
// 平台 Logo
|
||||
export function Logo() {
|
||||
return (
|
||||
<Box width="50px" display="flex" alignItems="center" justifyContent="center" fontSize="20px">
|
||||
<MenuOutlined />
|
||||
<Box width="50px" display="flex" alignItems="center" justifyContent="center" fontSize="14px">
|
||||
<ToggleButton
|
||||
shape="text"
|
||||
items={[
|
||||
{ label: '回到首页', key: 'home' },
|
||||
{ label: '文档', key: 'doc' },
|
||||
{ label: '设置', key: 'setting' },
|
||||
]}
|
||||
>
|
||||
<MenuOutlined />
|
||||
</ToggleButton>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ const packageJson = {
|
||||
name: 'demo',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@music163/tango-mail': '0.1.1',
|
||||
'@music163/tango-mail': '0.2.4',
|
||||
'@music163/tango-boot': '0.2.5',
|
||||
react: '17.0.2',
|
||||
'react-dom': '17.0.2',
|
||||
@@ -44,7 +44,7 @@ const tangoConfigJson = {
|
||||
},
|
||||
'@music163/tango-mail': {
|
||||
description: 'TangoMail 基础物料',
|
||||
version: '0.1.1',
|
||||
version: '0.2.4',
|
||||
library: 'TangoMail',
|
||||
type: 'baseDependency',
|
||||
resources: ['https://unpkg.com/@music163/tango-mail@{{version}}/dist/index.js'],
|
||||
@@ -116,7 +116,6 @@ const WelcomeEmail = () => (
|
||||
</Section>
|
||||
<Text style={paragraph}>
|
||||
Best,
|
||||
<br />
|
||||
The Koala team
|
||||
</Text>
|
||||
<Hr style={hr} />
|
||||
|
||||
@@ -3,7 +3,7 @@ const packageJson = {
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@music163/antd': '0.2.5',
|
||||
'@music163/tango-boot': '0.2.5',
|
||||
'@music163/tango-boot': '0.3.5',
|
||||
react: '17.0.2',
|
||||
'react-dom': '17.0.2',
|
||||
'prop-types': '15.7.2',
|
||||
@@ -48,7 +48,7 @@ const tangoConfigJson = {
|
||||
},
|
||||
'@music163/tango-boot': {
|
||||
description: '云音乐低代码运行时框架',
|
||||
version: '0.2.5',
|
||||
version: '0.3.5',
|
||||
library: 'TangoBoot',
|
||||
type: 'baseDependency',
|
||||
resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'],
|
||||
@@ -56,7 +56,7 @@ const tangoConfigJson = {
|
||||
},
|
||||
'@music163/antd': {
|
||||
description: '云音乐低代码中后台应用基础物料',
|
||||
version: '0.2.5',
|
||||
version: '0.2.6',
|
||||
library: 'TangoAntd',
|
||||
type: 'baseDependency',
|
||||
resources: [
|
||||
@@ -161,10 +161,13 @@ import {
|
||||
} from "@music163/antd";
|
||||
import { Space } from "@music163/antd";
|
||||
import { LocalButton } from "../components";
|
||||
import { OutButton } from "../components";
|
||||
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Page title={tango.stores.app.title} subTitle={111}>
|
||||
<Section tid="section0" />
|
||||
<Section tid="section1" title="Section Title">
|
||||
your input: <Input tid="input1" defaultValue="hello" />
|
||||
copy input: <Input value={tango.page.input1?.value} />
|
||||
@@ -180,6 +183,7 @@ class App extends React.Component {
|
||||
<Section tid="section2">
|
||||
<Space tid="space1">
|
||||
<LocalButton />
|
||||
<OutButton />
|
||||
<Button tid="button1">button</Button>
|
||||
</Space>
|
||||
</Section>
|
||||
@@ -191,7 +195,7 @@ class App extends React.Component {
|
||||
</Section>
|
||||
<Section title="原生 DOM" tid="section4">
|
||||
<h1 style={{ ...{ color: "red" }, fontSize: 64 }}>
|
||||
hello world
|
||||
hello world
|
||||
</h1>
|
||||
<div
|
||||
style={{
|
||||
@@ -289,6 +293,14 @@ registerComponentPrototype({
|
||||
});
|
||||
`;
|
||||
|
||||
const outButtonCode = `
|
||||
import React from 'react';
|
||||
|
||||
export default function OutButton(props) {
|
||||
return <button {...props}>Out button (from other file)</button>
|
||||
}
|
||||
`;
|
||||
|
||||
const componentsInputCode = `
|
||||
import React from 'react';
|
||||
import { registerComponentPrototype } from '../utils';
|
||||
@@ -311,6 +323,7 @@ registerComponentPrototype({
|
||||
const componentsEntryCode = `
|
||||
export { default as LocalButton } from './button';
|
||||
export { default as LocalInput } from './input';
|
||||
export { default as OutButton } from './out-button';
|
||||
`;
|
||||
|
||||
const storeApp = `
|
||||
@@ -411,6 +424,7 @@ export const sampleFiles = [
|
||||
{ filename: '/src/pages/list.js', code: viewHomeCode },
|
||||
{ filename: '/src/pages/detail.js', code: emptyPageCode },
|
||||
{ filename: '/src/components/button.js', code: componentsButtonCode },
|
||||
{ filename: '/src/components/out-button.js', code: outButtonCode },
|
||||
{ filename: '/src/components/input.js', code: componentsInputCode },
|
||||
{ filename: '/src/components/index.js', code: componentsEntryCode },
|
||||
{ filename: '/src/routes.js', code: routesCode },
|
||||
|
||||
@@ -45,14 +45,18 @@ const Snippet2ColumnLayout: IComponentPrototype = {
|
||||
const Snippet3ColumnLayout: IComponentPrototype = {
|
||||
name: 'Snippet3ColumnLayout',
|
||||
title: '三列布局',
|
||||
icon: 'icon-column3',
|
||||
icon: 'icon-column-3',
|
||||
type: 'snippet',
|
||||
package: '@music163/antd',
|
||||
// FIXME: Column 组件 需要更新为 defineComponent
|
||||
initChildren: `
|
||||
<Columns columns={12}>
|
||||
<Column colSpan={4}></Column>
|
||||
<Column colSpan={4}></Column>
|
||||
<Column colSpan={4}></Column>
|
||||
<Column colSpan={4}>
|
||||
</Column>
|
||||
<Column colSpan={4}>
|
||||
</Column>
|
||||
<Column colSpan={4}>
|
||||
</Column>
|
||||
</Columns>
|
||||
`,
|
||||
relatedImports: ['Columns', 'Column'],
|
||||
@@ -76,13 +80,9 @@ const SnippetButtonGroup: IComponentPrototype = {
|
||||
// hack some prototypes
|
||||
basePrototypes['Section'].siblingNames = [
|
||||
'SnippetButtonGroup',
|
||||
'Section',
|
||||
'Section',
|
||||
'Section',
|
||||
'Section',
|
||||
'Section',
|
||||
'Section',
|
||||
'Section',
|
||||
'Snippet2ColumnLayout',
|
||||
'Snippet3ColumnLayout',
|
||||
'SnippetSuccessResult',
|
||||
];
|
||||
|
||||
export const nativeDomPrototypes = () => {
|
||||
|
||||
@@ -20,42 +20,13 @@ import {
|
||||
ApiOutlined,
|
||||
AppstoreAddOutlined,
|
||||
BuildOutlined,
|
||||
ClusterOutlined,
|
||||
FunctionOutlined,
|
||||
PlusOutlined,
|
||||
createFromIconfontCN,
|
||||
} from '@ant-design/icons';
|
||||
import { Action } from '@music163/tango-ui';
|
||||
import { Action, PackageOutlined } from '@music163/tango-ui';
|
||||
import { useState } from 'react';
|
||||
|
||||
// 1. 实例化工作区
|
||||
const workspace = new Workspace({
|
||||
entry: '/src/index.js',
|
||||
files: sampleFiles,
|
||||
prototypes,
|
||||
});
|
||||
|
||||
// inject workspace to window for debug
|
||||
(window as any).__workspace__ = workspace;
|
||||
|
||||
// 2. 引擎初始化
|
||||
const engine = createEngine({
|
||||
workspace,
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
window.__workspace__ = workspace;
|
||||
|
||||
// 3. 沙箱初始化
|
||||
const sandboxQuery = new DndQuery({
|
||||
context: 'iframe',
|
||||
});
|
||||
|
||||
// 4. 图标库初始化(物料面板和组件树使用了 iconfont 里的图标)
|
||||
createFromIconfontCN({
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_2891794_6d4hj5u0bjx.js',
|
||||
});
|
||||
|
||||
const menuData = {
|
||||
common: [
|
||||
{
|
||||
@@ -90,6 +61,36 @@ const menuData = {
|
||||
],
|
||||
};
|
||||
|
||||
// 1. 实例化工作区
|
||||
const workspace = new Workspace({
|
||||
entry: '/src/index.js',
|
||||
files: sampleFiles,
|
||||
prototypes,
|
||||
});
|
||||
|
||||
// inject workspace to window for debug
|
||||
(window as any).__workspace__ = workspace;
|
||||
|
||||
// 2. 引擎初始化
|
||||
const engine = createEngine({
|
||||
workspace,
|
||||
menuData,
|
||||
defaultActiveView: 'design', // dual code design
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
window.__workspace__ = workspace;
|
||||
|
||||
// 3. 沙箱初始化
|
||||
const sandboxQuery = new DndQuery({
|
||||
context: 'iframe',
|
||||
});
|
||||
|
||||
// 4. 图标库初始化(物料面板和组件树使用了 iconfont 里的图标)
|
||||
createFromIconfontCN({
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_2891794_151xsllxqd7.js',
|
||||
});
|
||||
|
||||
/**
|
||||
* 5. 平台初始化,访问 https://local.netease.com:6006/
|
||||
*/
|
||||
@@ -180,12 +181,15 @@ export default function App() {
|
||||
<Sidebar.Item
|
||||
key="dependency"
|
||||
label="依赖"
|
||||
icon={<ClusterOutlined />}
|
||||
icon={<PackageOutlined />}
|
||||
isFloat
|
||||
width={800}
|
||||
/>
|
||||
</Sidebar>
|
||||
<WorkspacePanel>
|
||||
<WorkspaceView mode="code">
|
||||
<CodeEditor />
|
||||
</WorkspaceView>
|
||||
<WorkspaceView mode="design">
|
||||
<Sandbox
|
||||
onMessage={(e) => {
|
||||
@@ -207,9 +211,6 @@ export default function App() {
|
||||
navigatorExtra={<Button size="small">hello world</Button>}
|
||||
/>
|
||||
</WorkspaceView>
|
||||
<WorkspaceView mode="code">
|
||||
<CodeEditor />
|
||||
</WorkspaceView>
|
||||
</WorkspacePanel>
|
||||
<SettingPanel />
|
||||
</DesignerPanel>
|
||||
|
||||
@@ -45,7 +45,7 @@ const sandboxQuery = new DndQuery({
|
||||
|
||||
// 4. 图标库初始化(物料面板和组件树使用了 iconfont 里的图标)
|
||||
createFromIconfontCN({
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_2891794_cou9i7556tl.js',
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_2891794_151xsllxqd7.js',
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -112,6 +112,7 @@ export default function App() {
|
||||
if (sandboxWindow.TangoMail) {
|
||||
if (sandboxWindow.TangoMail.menuData) {
|
||||
setMenuData(sandboxWindow.TangoMail.menuData);
|
||||
engine.designer.setMenuData(sandboxWindow.TangoMail.menuData);
|
||||
}
|
||||
if (sandboxWindow.TangoMail.prototypes) {
|
||||
workspace.setComponentPrototypes(sandboxWindow.TangoMail.prototypes);
|
||||
|
||||
@@ -14,7 +14,7 @@ const BLACK_LIST = ['codeSetter', 'eventSetter', 'modelSetter', 'routerSetter'];
|
||||
BUILT_IN_SETTERS.filter((setter) => !BLACK_LIST.includes(setter.name)).forEach(register);
|
||||
|
||||
createFromIconfontCN({
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_2891794_cou9i7556tl.js',
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_2891794_151xsllxqd7.js',
|
||||
});
|
||||
|
||||
export default {
|
||||
@@ -38,7 +38,7 @@ function SettingFormDemo({ initValues, prototype }: SettingFormDemoProps) {
|
||||
const model = new FormModel(initValues, { onChange: console.log });
|
||||
return (
|
||||
<Box display="flex">
|
||||
<Box flex="0 0 400px" overflow="hidden">
|
||||
<Box flex="0 0 320px" overflow="hidden">
|
||||
<SettingForm
|
||||
model={model}
|
||||
prototype={prototype}
|
||||
@@ -59,8 +59,10 @@ function SettingFormDemo({ initValues, prototype }: SettingFormDemoProps) {
|
||||
|
||||
const prototypeHasBasicProps: IComponentPrototype = {
|
||||
name: 'Sample',
|
||||
title: '演示组件',
|
||||
package: 'sample-pkg',
|
||||
type: 'element',
|
||||
docs: 'https://4x-ant-design.antgroup.com/components/slider-cn',
|
||||
props: [
|
||||
{
|
||||
name: 'code',
|
||||
@@ -77,6 +79,11 @@ const prototypeHasBasicProps: IComponentPrototype = {
|
||||
title: 'textAreaSetter',
|
||||
setter: 'textAreaSetter',
|
||||
},
|
||||
{
|
||||
name: 'src',
|
||||
title: 'imageSetter',
|
||||
setter: 'imageSetter',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
title: 'numberSetter',
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import { DragPanel } from '@music163/tango-ui';
|
||||
import { Box, Text } from 'coral-system';
|
||||
import { Button } from 'antd';
|
||||
|
||||
export default {
|
||||
title: 'UI/DragPanel',
|
||||
};
|
||||
|
||||
export function Basic() {
|
||||
return (
|
||||
<>
|
||||
<DragPanel
|
||||
title="弹层标题"
|
||||
extra="右上角内容"
|
||||
width={350}
|
||||
body={<Box p="m">内容</Box>}
|
||||
footer={<Text>底部信息</Text>}
|
||||
>
|
||||
<Button>点击唤起浮层</Button>
|
||||
</DragPanel>
|
||||
<DragPanel
|
||||
title="弹层标题"
|
||||
extra="右上角内容"
|
||||
width={350}
|
||||
body={<Box p="m">内容</Box>}
|
||||
footer={<Text>底部信息</Text>}
|
||||
>
|
||||
<Button
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
right: 20,
|
||||
}}
|
||||
>
|
||||
底部浮层自动修正弹出区域
|
||||
</Button>
|
||||
</DragPanel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function FooterCustom() {
|
||||
return (
|
||||
<DragPanel
|
||||
title="弹层标题"
|
||||
extra="右上角内容"
|
||||
width={350}
|
||||
body={<Box p="m">内容</Box>}
|
||||
footer={(close) => (
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Text>底部信息</Text>
|
||||
<Button size="small" onClick={() => close()}>
|
||||
点此关闭
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
>
|
||||
<Button>点击唤起浮层</Button>
|
||||
</DragPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export function ResizeablePanel() {
|
||||
return (
|
||||
<DragPanel
|
||||
title="弹层标题"
|
||||
resizeable
|
||||
extra="右上角内容"
|
||||
width={350}
|
||||
height={400}
|
||||
body={
|
||||
<Box p="m" textAlign="center" background="#fa8484">
|
||||
内容
|
||||
</Box>
|
||||
}
|
||||
footer={<Text>底部信息</Text>}
|
||||
>
|
||||
<Button>点击唤起浮层</Button>
|
||||
</DragPanel>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,26 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.1.5](https://github.com/netease/tango/compare/@music163/tango-context@1.1.4...@music163/tango-context@1.1.5) (2024-07-12)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.4](https://github.com/netease/tango/compare/@music163/tango-context@1.1.3...@music163/tango-context@1.1.4) (2024-06-20)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.3](https://github.com/netease/tango/compare/@music163/tango-context@1.1.2...@music163/tango-context@1.1.3) (2024-06-05)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.2](https://github.com/netease/tango/compare/@music163/tango-context@1.1.1...@music163/tango-context@1.1.2) (2024-06-03)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.1](https://github.com/netease/tango/compare/@music163/tango-context@1.1.0...@music163/tango-context@1.1.1) (2024-05-21)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
# [1.1.0](https://github.com/netease/tango/compare/@music163/tango-context@1.0.2...@music163/tango-context@1.1.0) (2024-05-17)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-context",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.5",
|
||||
"description": "react context for tango-apps",
|
||||
"keywords": [
|
||||
"react",
|
||||
@@ -31,8 +31,8 @@
|
||||
"react": ">= 16.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@music163/tango-core": "^1.1.0",
|
||||
"@music163/tango-helpers": "^1.1.0",
|
||||
"@music163/tango-core": "^1.3.3",
|
||||
"@music163/tango-helpers": "^1.2.0",
|
||||
"mobx-react-lite": "4.0.7"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -3,6 +3,47 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.3.3](https://github.com/netease/tango/compare/@music163/tango-core@1.3.2...@music163/tango-core@1.3.3) (2024-07-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update designer config ([#186](https://github.com/netease/tango/issues/186)) ([85c053b](https://github.com/netease/tango/commit/85c053b3db6d652b41c9873dba1366315174833f))
|
||||
|
||||
## [1.3.2](https://github.com/netease/tango/compare/@music163/tango-core@1.3.1...@music163/tango-core@1.3.2) (2024-06-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update designer style ([#176](https://github.com/netease/tango/issues/176)) ([0f3f0af](https://github.com/netease/tango/commit/0f3f0afdfa8aee2532a97c5c2e92ef4230397d86))
|
||||
|
||||
## [1.3.1](https://github.com/netease/tango/compare/@music163/tango-core@1.3.0...@music163/tango-core@1.3.1) (2024-06-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- set value as jsxElement children ([#173](https://github.com/netease/tango/issues/173)) ([ae04850](https://github.com/netease/tango/commit/ae04850bb251f392575a72ca5fd960249ea9d06b))
|
||||
|
||||
# [1.3.0](https://github.com/netease/tango/compare/@music163/tango-core@1.2.0...@music163/tango-core@1.3.0) (2024-06-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- parse app entry file & pass correct routerType to sandbox ([#168](https://github.com/netease/tango/issues/168)) ([3f3981b](https://github.com/netease/tango/commit/3f3981bb9db874b738a67fd449c579c35c58d08b))
|
||||
|
||||
### Features
|
||||
|
||||
- add context menu ([#161](https://github.com/netease/tango/issues/161)) ([28040fc](https://github.com/netease/tango/commit/28040fc00604339a40ad3216b76baf7de93a13e0))
|
||||
|
||||
# [1.2.0](https://github.com/netease/tango/compare/@music163/tango-core@1.1.0...@music163/tango-core@1.2.0) (2024-05-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- enhance code value validate in SettingForm ([#152](https://github.com/netease/tango/issues/152)) ([791fbb1](https://github.com/netease/tango/commit/791fbb162a7147243924e01f54e9c0b586f14438))
|
||||
- prototype2code & go back history error ([#156](https://github.com/netease/tango/issues/156)) ([8bf53a7](https://github.com/netease/tango/commit/8bf53a76f8a71eaf261ea68b9ee44e5bf19893aa))
|
||||
- support add components with popover ([#155](https://github.com/netease/tango/issues/155)) ([f17ccbb](https://github.com/netease/tango/commit/f17ccbb7f645f8047ecd96d9f3f2185048a3b726))
|
||||
|
||||
### Features
|
||||
|
||||
- add select parent node of selected node ([#158](https://github.com/netease/tango/issues/158)) ([fe31246](https://github.com/netease/tango/commit/fe3124648325e72abfc58da8b2f8ff83301d40b8))
|
||||
- supoort set default active view ([#157](https://github.com/netease/tango/issues/157)) ([f854f75](https://github.com/netease/tango/commit/f854f75b8a8c25384d290bd76d6b8bb6fb69f22d))
|
||||
|
||||
# [1.1.0](https://github.com/netease/tango/compare/@music163/tango-core@1.0.2...@music163/tango-core@1.1.0) (2024-05-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-core",
|
||||
"version": "1.1.0",
|
||||
"version": "1.3.3",
|
||||
"description": "tango core",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"homepage": "",
|
||||
@@ -28,7 +28,7 @@
|
||||
"@babel/parser": "^7.23.5",
|
||||
"@babel/traverse": "^7.23.5",
|
||||
"@babel/types": "^7.23.5",
|
||||
"@music163/tango-helpers": "^1.1.0",
|
||||
"@music163/tango-helpers": "^1.2.0",
|
||||
"@types/babel__generator": "^7.6.7",
|
||||
"@types/babel__traverse": "^7.20.4",
|
||||
"mobx": "6.12.3",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Designer, Engine, SimulatorNameType } from './models';
|
||||
import { MenuDataType } from '@music163/tango-helpers';
|
||||
import { Designer, DesignerViewType, Engine, SimulatorNameType } from './models';
|
||||
import { IWorkspace } from './models/interfaces';
|
||||
|
||||
interface ICreateEngineOptions {
|
||||
@@ -6,6 +7,10 @@ interface ICreateEngineOptions {
|
||||
* 自定义工作区
|
||||
*/
|
||||
workspace?: IWorkspace;
|
||||
/**
|
||||
* 菜单信息
|
||||
*/
|
||||
menuData?: MenuDataType;
|
||||
/**
|
||||
* 默认的模拟器模式
|
||||
*/
|
||||
@@ -14,6 +19,10 @@ interface ICreateEngineOptions {
|
||||
* 默认激活的侧边栏
|
||||
*/
|
||||
defaultActiveSidebarPanel?: string;
|
||||
/**
|
||||
* 默认激活的视图
|
||||
*/
|
||||
defaultActiveView?: DesignerViewType;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,15 +32,19 @@ interface ICreateEngineOptions {
|
||||
*/
|
||||
export function createEngine({
|
||||
workspace,
|
||||
defaultActiveView = 'design',
|
||||
defaultSimulatorMode = 'desktop',
|
||||
defaultActiveSidebarPanel = '',
|
||||
menuData,
|
||||
}: ICreateEngineOptions) {
|
||||
const engine = new Engine({
|
||||
workspace,
|
||||
designer: new Designer({
|
||||
workspace,
|
||||
simulator: defaultSimulatorMode,
|
||||
activeView: defaultActiveView,
|
||||
activeSidebarPanel: defaultActiveSidebarPanel,
|
||||
menuData,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -97,8 +97,7 @@ export function code2expression(code: string) {
|
||||
try {
|
||||
expNode = t.cloneNode(parseExpression(code, babelParserConfig), false, true);
|
||||
} catch (err) {
|
||||
logger.error('invalid code', err);
|
||||
// expNode = t.identifier('undefined');
|
||||
logger.error('code2expression failed, invalid code:', code);
|
||||
}
|
||||
return expNode;
|
||||
}
|
||||
@@ -194,7 +193,12 @@ export function code2jsxAttributeValueNode(code: string) {
|
||||
return t.jsxExpressionContainer(code2expression(code));
|
||||
}
|
||||
|
||||
// FIXME: 统一处理为 code2jsxAttributeValueNode
|
||||
/**
|
||||
* FIXME: 统一处理为 code2jsxAttributeValueNode
|
||||
* 将 js value 转为 JSXAttributeValueNode
|
||||
* @param value js value, or wrapped code
|
||||
* @returns 返回 JSXAttributeValueNode,转换失败返回Node为 {undefined}
|
||||
*/
|
||||
export function value2jsxAttributeValueNode(value: any) {
|
||||
let ret;
|
||||
switch (typeof value) {
|
||||
@@ -204,7 +208,8 @@ export function value2jsxAttributeValueNode(value: any) {
|
||||
}
|
||||
if (isWrappedCode(value)) {
|
||||
const innerCode = getCodeOfWrappedCode(value);
|
||||
ret = t.jsxExpressionContainer(code2expression(innerCode));
|
||||
const node = code2expression(innerCode);
|
||||
ret = t.jsxExpressionContainer(node || t.identifier('undefined'));
|
||||
} else {
|
||||
ret = t.stringLiteral(value);
|
||||
}
|
||||
@@ -221,10 +226,12 @@ export function value2jsxChildrenValueNode(value: any) {
|
||||
let ret: t.JSXElement | t.JSXFragment | t.JSXExpressionContainer | t.JSXSpreadChild | t.JSXText;
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
if (isValidExpressionCode(value)) {
|
||||
if (isWrappedCode(value)) {
|
||||
// 代码模式
|
||||
const innerString = getVariableContent(value);
|
||||
ret = t.jsxExpressionContainer(code2expression(innerString));
|
||||
} else {
|
||||
// 普通的文本
|
||||
ret = t.jsxText(value);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -391,8 +391,10 @@ export function updateJSXElementAttribute(node: t.JSXElement, attrName: string,
|
||||
isExist = true;
|
||||
// @ts-ignore
|
||||
if (t.isJSXAttribute(jsxAttributeNode)) {
|
||||
// 更新 jsx 属性值 <Foo bar="1" /> 的模式
|
||||
jsxAttributeNode.value = value2jsxAttributeValueNode(attrValue);
|
||||
} else if (name === 'children') {
|
||||
// 更新 jsx children 的情况 <Foo>children</Foo>
|
||||
node.children = value2jsxChildrenValueNode(attrValue);
|
||||
}
|
||||
}
|
||||
@@ -1425,3 +1427,22 @@ export function traverseFile(ast: t.File) {
|
||||
imports,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* app.js 入口文件解析
|
||||
* @param ast
|
||||
*/
|
||||
export function traverseEntryFile(ast: t.File) {
|
||||
let appConfig: any;
|
||||
traverse(ast, {
|
||||
CallExpression(path) {
|
||||
const { node } = path;
|
||||
const calleeName = keyNode2value(node.callee) as string;
|
||||
if (calleeName === 'runApp') {
|
||||
appConfig = node2value(node.arguments[0]);
|
||||
path.stop();
|
||||
}
|
||||
},
|
||||
});
|
||||
return appConfig;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export function prototype2importDeclarationData(
|
||||
relativeFilepath?: string,
|
||||
): { source: string; specifiers: IImportSpecifierData[] } {
|
||||
let source = prototype.package;
|
||||
const isSnippet = prototype.type === 'snippet';
|
||||
if (relativeFilepath && isFilepath(source)) {
|
||||
source = getRelativePath(relativeFilepath, source);
|
||||
}
|
||||
@@ -35,12 +36,15 @@ export function prototype2importDeclarationData(
|
||||
type: 'ImportDefaultSpecifier',
|
||||
});
|
||||
} else {
|
||||
[prototype.name, ...(prototype.relatedImports || [])].forEach((item) => {
|
||||
specifiers.push({
|
||||
localName: item,
|
||||
type: 'ImportSpecifier',
|
||||
});
|
||||
});
|
||||
// 忽略代码片段 name
|
||||
[...(isSnippet ? [] : [prototype.name]), ...(prototype.relatedImports || [])].forEach(
|
||||
(item) => {
|
||||
specifiers.push({
|
||||
localName: item,
|
||||
type: 'ImportSpecifier',
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { MenuDataType } from '@music163/tango-helpers';
|
||||
|
||||
export type SimulatorNameType = 'desktop' | 'phone';
|
||||
|
||||
@@ -19,7 +20,15 @@ interface IViewportBounding {
|
||||
interface IDesignerOptions {
|
||||
workspace: IWorkspace;
|
||||
simulator?: SimulatorNameType | ISimulatorType;
|
||||
/**
|
||||
* 菜单配置
|
||||
*/
|
||||
menuData: MenuDataType;
|
||||
activeSidebarPanel?: string;
|
||||
/**
|
||||
* 默认激活的视图模式
|
||||
*/
|
||||
activeView?: DesignerViewType;
|
||||
}
|
||||
|
||||
const ISimulatorTypes: Record<string, ISimulatorType> = {
|
||||
@@ -64,6 +73,26 @@ export class Designer {
|
||||
*/
|
||||
_showSmartWizard = false;
|
||||
|
||||
/**
|
||||
* 是否显示添加组件面板
|
||||
*/
|
||||
_showAddComponentPopover = false;
|
||||
|
||||
/**
|
||||
* 添加组件面板的位置
|
||||
*/
|
||||
_addComponentPopoverPosition = { clientX: 0, clientY: 0 };
|
||||
|
||||
/**
|
||||
* 是否显示右键菜单
|
||||
*/
|
||||
_showContextMenu = false;
|
||||
|
||||
/**
|
||||
* 右键菜单在 iframe 上的位置
|
||||
*/
|
||||
_contextMenuPosition = { clientX: 0, clientY: 0 };
|
||||
|
||||
/**
|
||||
* 是否显示右侧面板
|
||||
*/
|
||||
@@ -75,9 +104,9 @@ export class Designer {
|
||||
_isPreview = false;
|
||||
|
||||
/**
|
||||
* 默认展开的侧边栏
|
||||
* 菜单列表
|
||||
*/
|
||||
defaultActiveSidebarPanel?: string;
|
||||
_menuData?: MenuDataType = null;
|
||||
|
||||
private readonly workspace: IWorkspace;
|
||||
|
||||
@@ -109,10 +138,39 @@ export class Designer {
|
||||
return this._showRightPanel;
|
||||
}
|
||||
|
||||
get showAddComponentPopover() {
|
||||
return this._showAddComponentPopover;
|
||||
}
|
||||
|
||||
get addComponentPopoverPosition() {
|
||||
return this._addComponentPopoverPosition;
|
||||
}
|
||||
|
||||
get showContextMenu() {
|
||||
return this._showContextMenu;
|
||||
}
|
||||
|
||||
get contextMenuPosition() {
|
||||
return this._contextMenuPosition;
|
||||
}
|
||||
|
||||
get menuData() {
|
||||
return this._menuData ?? ([] as MenuDataType);
|
||||
}
|
||||
|
||||
constructor(options: IDesignerOptions) {
|
||||
this.workspace = options.workspace;
|
||||
|
||||
const { simulator, activeSidebarPanel: defaultActiveSidebarPanel } = options;
|
||||
const {
|
||||
simulator,
|
||||
menuData,
|
||||
activeSidebarPanel: defaultActiveSidebarPanel,
|
||||
activeView: defaultActiveView,
|
||||
} = options;
|
||||
|
||||
if (menuData) {
|
||||
this.setMenuData(menuData);
|
||||
}
|
||||
|
||||
// 默认设计器模式
|
||||
if (simulator) {
|
||||
@@ -124,6 +182,11 @@ export class Designer {
|
||||
this.setActiveSidebarPanel(defaultActiveSidebarPanel);
|
||||
}
|
||||
|
||||
// 默认激活的视图
|
||||
if (defaultActiveView) {
|
||||
this.setActiveView(defaultActiveView);
|
||||
}
|
||||
|
||||
makeObservable(this, {
|
||||
_simulator: observable,
|
||||
_viewport: observable,
|
||||
@@ -131,6 +194,11 @@ export class Designer {
|
||||
_activeSidebarPanel: observable,
|
||||
_showSmartWizard: observable,
|
||||
_showRightPanel: observable,
|
||||
_showAddComponentPopover: observable,
|
||||
_addComponentPopoverPosition: observable,
|
||||
_showContextMenu: observable,
|
||||
_contextMenuPosition: observable,
|
||||
_menuData: observable,
|
||||
_isPreview: observable,
|
||||
simulator: computed,
|
||||
viewport: computed,
|
||||
@@ -139,6 +207,11 @@ export class Designer {
|
||||
isPreview: computed,
|
||||
showRightPanel: computed,
|
||||
showSmartWizard: computed,
|
||||
showAddComponentPopover: computed,
|
||||
addComponentPopoverPosition: computed,
|
||||
showContextMenu: computed,
|
||||
contextMenuPosition: computed,
|
||||
menuData: computed,
|
||||
setSimulator: action,
|
||||
setViewport: action,
|
||||
setActiveView: action,
|
||||
@@ -147,6 +220,8 @@ export class Designer {
|
||||
toggleRightPanel: action,
|
||||
toggleSmartWizard: action,
|
||||
toggleIsPreview: action,
|
||||
toggleAddComponentPopover: action,
|
||||
toggleContextMenu: action,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -164,6 +239,13 @@ export class Designer {
|
||||
|
||||
setActiveView(view: DesignerViewType) {
|
||||
this._activeView = view;
|
||||
if (view === 'dual') {
|
||||
this.setActiveSidebarPanel('');
|
||||
this.toggleRightPanel(false);
|
||||
this.toggleIsPreview(true);
|
||||
} else if (view === 'design') {
|
||||
this.toggleIsPreview(false);
|
||||
}
|
||||
}
|
||||
|
||||
setActiveSidebarPanel(panel: string) {
|
||||
@@ -173,6 +255,11 @@ export class Designer {
|
||||
this._activeSidebarPanel = '';
|
||||
}
|
||||
}
|
||||
|
||||
setMenuData(menuData: MenuDataType) {
|
||||
this._menuData = menuData;
|
||||
}
|
||||
|
||||
closeSidebarPanel() {
|
||||
this._activeSidebarPanel = '';
|
||||
}
|
||||
@@ -185,10 +272,37 @@ export class Designer {
|
||||
this._showRightPanel = value ?? !this._showRightPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示添加组件面板
|
||||
* @param value 是否显示
|
||||
* @param position 坐标
|
||||
*/
|
||||
toggleAddComponentPopover(
|
||||
value: boolean,
|
||||
position: {
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
} = this.addComponentPopoverPosition,
|
||||
) {
|
||||
this._showAddComponentPopover = value;
|
||||
this._addComponentPopoverPosition = position;
|
||||
}
|
||||
|
||||
toggleIsPreview(value: boolean) {
|
||||
this._isPreview = value ?? !this._isPreview;
|
||||
if (value) {
|
||||
this.workspace.selectSource.clear();
|
||||
}
|
||||
}
|
||||
|
||||
toggleContextMenu(
|
||||
value: boolean,
|
||||
position: {
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
} = this.contextMenuPosition,
|
||||
) {
|
||||
this._showContextMenu = value;
|
||||
this._contextMenuPosition = position;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { traverseEntryFile } from '../helpers';
|
||||
import { IFileConfig } from '../types';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { TangoModule } from './module';
|
||||
|
||||
export class AppEntryModule extends TangoModule {
|
||||
routerType: string;
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code, true, false);
|
||||
}
|
||||
|
||||
_analysisAst() {
|
||||
const config = traverseEntryFile(this.ast);
|
||||
this.routerType = config?.router?.type;
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ export class TangoHistory {
|
||||
}
|
||||
|
||||
get couldBack() {
|
||||
return this._records.length > 0 && this._index > -1;
|
||||
return this._records.length > 0 && this._index > 0;
|
||||
}
|
||||
|
||||
get couldForward() {
|
||||
|
||||
@@ -7,6 +7,7 @@ export * from './drag-source';
|
||||
export * from './history';
|
||||
export * from './file';
|
||||
export * from './module';
|
||||
export * from './entry-module';
|
||||
export * from './route-module';
|
||||
export * from './service-module';
|
||||
export * from './store-module';
|
||||
|
||||
@@ -16,6 +16,7 @@ import { TangoRouteModule } from './route-module';
|
||||
import { TangoStoreModule } from './store-module';
|
||||
import { TangoServiceModule } from './service-module';
|
||||
import { IdGenerator } from '../helpers';
|
||||
import { AppEntryModule } from './entry-module';
|
||||
|
||||
export interface IViewFile {
|
||||
readonly workspace: IWorkspace;
|
||||
@@ -152,6 +153,10 @@ export interface IWorkspace {
|
||||
* 解析后的 tango.config.json 文件,如果要获取项目配置,推荐使用 projectConfig 获取
|
||||
*/
|
||||
tangoConfigJson: TangoJsonFile;
|
||||
/**
|
||||
* app.js 入口文件解析后的模块
|
||||
*/
|
||||
appEntryModule: AppEntryModule;
|
||||
/**
|
||||
* 解析后的路由模块
|
||||
*/
|
||||
|
||||
@@ -109,6 +109,20 @@ export class SelectSource {
|
||||
this._start = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 选中当前选中节点的父节点
|
||||
*/
|
||||
selectParent() {
|
||||
const parents = this.first?.parents || [];
|
||||
if (parents.length) {
|
||||
const [parent, ...rest] = parents;
|
||||
this.select({
|
||||
...parent,
|
||||
parents: rest,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setStart(data: StartDataType) {
|
||||
this._start = data;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
parseServiceVariablePath,
|
||||
parseStoreVariablePath,
|
||||
uniq,
|
||||
setValue,
|
||||
} from '@music163/tango-helpers';
|
||||
import {
|
||||
prototype2jsxElement,
|
||||
@@ -35,6 +36,7 @@ import { TangoStoreEntryModule, TangoStoreModule } from './store-module';
|
||||
import { TangoServiceModule } from './service-module';
|
||||
import { TangoViewModule } from './view-module';
|
||||
import { TangoComponentsEntryModule } from './component-module';
|
||||
import { AppEntryModule } from './entry-module';
|
||||
|
||||
export interface IWorkspaceOptions {
|
||||
/**
|
||||
@@ -97,6 +99,11 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
*/
|
||||
activeViewFile: string;
|
||||
|
||||
/**
|
||||
* 应用入口模块
|
||||
*/
|
||||
appEntryModule: AppEntryModule;
|
||||
|
||||
/**
|
||||
* 路由配置模块
|
||||
*/
|
||||
@@ -329,6 +336,9 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
* @param fileType 模块类型
|
||||
*/
|
||||
addFile(filename: string, code: string, fileType?: FileType) {
|
||||
if (!fileType && filename === this.entry) {
|
||||
fileType = FileType.AppEntryModule;
|
||||
}
|
||||
const moduleType = fileType || inferFileType(filename);
|
||||
const props = {
|
||||
filename,
|
||||
@@ -338,6 +348,10 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
|
||||
let module;
|
||||
switch (moduleType) {
|
||||
case FileType.AppEntryModule:
|
||||
module = new AppEntryModule(this, props);
|
||||
this.appEntryModule = module;
|
||||
break;
|
||||
case FileType.StoreEntryModule:
|
||||
module = new TangoStoreEntryModule(this, props);
|
||||
this.storeEntryModule = module;
|
||||
@@ -428,7 +442,9 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
updateFile(filename: string, code: string, shouldFormatCode = false) {
|
||||
const file = this.getFile(filename);
|
||||
file.update(code);
|
||||
if (shouldFormatCode && file instanceof TangoViewModule) {
|
||||
|
||||
const shouldFormat = shouldFormatCode ?? this.projectConfig?.designerConfig?.autoFormatCode;
|
||||
if (shouldFormat && file instanceof TangoViewModule) {
|
||||
file.removeUnusedImportSpecifiers().update();
|
||||
}
|
||||
this.history.push({
|
||||
@@ -728,6 +744,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
* 更新依赖,没有就添加
|
||||
* @param name
|
||||
* @param version
|
||||
* FIXME: 参数3的设置需要重新考虑下
|
||||
*/
|
||||
updateDependency(
|
||||
name: string,
|
||||
@@ -750,15 +767,12 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
if (!packages) {
|
||||
return undefined;
|
||||
}
|
||||
if (options?.package || packages[name]) {
|
||||
packages[name] = {
|
||||
type: packages[name]?.type,
|
||||
// 如果没有传入 package 信息,则沿用 tango.config.json 中已记录的数据
|
||||
// 如果传入了,则全量使用传入的信息,适配可能在某个版本中如 resources 等字段被删除的情况
|
||||
...(options?.package || packages[name]),
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
setValue(packages, name, {
|
||||
...packages[name], // 保留原有的配置
|
||||
version, // 更新版本号
|
||||
...options?.package, // 更新 package 配置
|
||||
});
|
||||
|
||||
return packages;
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ export type SimulatorMode = 'desktop' | 'tablet' | 'phone';
|
||||
export enum FileType {
|
||||
// js 文件
|
||||
Module = 'module',
|
||||
AppEntryModule = 'appEntryModule',
|
||||
StoreEntryModule = 'storeEntryModule',
|
||||
RouteModule = 'routeModule',
|
||||
ServiceModule = 'serviceModule',
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import { Identifier } from '@babel/types';
|
||||
import {
|
||||
object2node,
|
||||
serviceConfig2Node,
|
||||
isValidCode,
|
||||
isValidExpressionCode,
|
||||
code2expression,
|
||||
value2jsxAttributeValueNode,
|
||||
} from '../src/helpers';
|
||||
|
||||
describe('ast helpers', () => {
|
||||
it('isValidCode', () => {
|
||||
expect(isValidCode('')).toBeTruthy();
|
||||
expect(isValidCode('1')).toBeTruthy();
|
||||
expect(isValidCode('"hello"')).toBeTruthy();
|
||||
expect(isValidCode('<div>hello</div>')).toBeTruthy();
|
||||
expect(isValidCode('function fn() {}')).toBeTruthy();
|
||||
|
||||
// invalid function body
|
||||
expect(isValidCode('() => { hello world }')).toBeFalsy();
|
||||
// invalid function
|
||||
expect(isValidCode('function() {}')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('isValidExpression', () => {
|
||||
expect(isValidExpressionCode('() => { }')).toBeTruthy();
|
||||
it('isValidExpressionCode', () => {
|
||||
expect(isValidExpressionCode('1')).toBeTruthy();
|
||||
expect(isValidExpressionCode('a = 1')).toBeTruthy();
|
||||
expect(isValidExpressionCode('1 + 1')).toBeTruthy();
|
||||
@@ -22,8 +31,11 @@ describe('ast helpers', () => {
|
||||
expect(isValidExpressionCode('{ bizId: "vip", type: "category" }')).toBeTruthy();
|
||||
expect(isValidExpressionCode('[1,2,3]')).toBeTruthy();
|
||||
expect(isValidExpressionCode('<div>hello</div>')).toBeTruthy();
|
||||
expect(isValidExpressionCode('<div>hello</div>')).toBeTruthy();
|
||||
expect(isValidExpressionCode('tango.stores.app.title')).toBeTruthy();
|
||||
expect(isValidExpressionCode('"hello" + window.location.path')).toBeTruthy();
|
||||
expect(isValidExpressionCode('() => { }')).toBeTruthy();
|
||||
|
||||
expect(isValidExpressionCode('')).toBeFalsy();
|
||||
expect(isValidExpressionCode('{1}')).toBeFalsy();
|
||||
expect(isValidExpressionCode('{"1"}')).toBeFalsy();
|
||||
expect(isValidExpressionCode('{ 1+1 }')).toBeFalsy();
|
||||
@@ -68,4 +80,16 @@ describe('ast helpers', () => {
|
||||
`;
|
||||
expect(code2expression(arrayCode).type).toEqual('ArrayExpression');
|
||||
});
|
||||
|
||||
it('value2jsxAttributeValueNode', () => {
|
||||
expect(value2jsxAttributeValueNode('hello').type).toBe('StringLiteral');
|
||||
expect(value2jsxAttributeValueNode(true).type).toBe('JSXExpressionContainer');
|
||||
expect(value2jsxAttributeValueNode(1).type).toBe('JSXExpressionContainer');
|
||||
expect(value2jsxAttributeValueNode('{{{ foo: "foo"}}}').type).toBe('JSXExpressionContainer');
|
||||
|
||||
// invalid code will return undefined
|
||||
const node: any = value2jsxAttributeValueNode('{{tango.xx+}}');
|
||||
expect(node.type).toBe('JSXExpressionContainer');
|
||||
expect((node.expression as Identifier).name).toBe('undefined');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,69 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.3.3](https://github.com/netease/tango/compare/@music163/tango-designer@1.3.2...@music163/tango-designer@1.3.3) (2024-07-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- selection bug without prototype & update ToggleButton style ([#180](https://github.com/netease/tango/issues/180)) ([1d0fb4f](https://github.com/netease/tango/commit/1d0fb4f274a5b767962c54583ab5863329d5bcc5))
|
||||
- update designer config ([#186](https://github.com/netease/tango/issues/186)) ([85c053b](https://github.com/netease/tango/commit/85c053b3db6d652b41c9873dba1366315174833f))
|
||||
|
||||
## [1.3.2](https://github.com/netease/tango/compare/@music163/tango-designer@1.3.1...@music163/tango-designer@1.3.2) (2024-06-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- drag-panel add resizeable & height prop ([#175](https://github.com/netease/tango/issues/175)) ([b53a9f7](https://github.com/netease/tango/commit/b53a9f7eefc748ba8a7c94270c3a374b9d6ed211))
|
||||
- fix global hotkey triggering on content editable elements ([#174](https://github.com/netease/tango/issues/174)) ([a2cc418](https://github.com/netease/tango/commit/a2cc418a73edfd9b7fe8aad456ae775a49df2166))
|
||||
- update designer style ([#176](https://github.com/netease/tango/issues/176)) ([0f3f0af](https://github.com/netease/tango/commit/0f3f0afdfa8aee2532a97c5c2e92ef4230397d86))
|
||||
|
||||
## [1.3.1](https://github.com/netease/tango/compare/@music163/tango-designer@1.3.0...@music163/tango-designer@1.3.1) (2024-06-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix context menu error on macOS ([#170](https://github.com/netease/tango/issues/170)) ([ca740c8](https://github.com/netease/tango/commit/ca740c83ae7f72a75c7db90d2e137a7e9a032326))
|
||||
|
||||
# [1.3.0](https://github.com/netease/tango/compare/@music163/tango-designer@1.2.3...@music163/tango-designer@1.3.0) (2024-06-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- parse app entry file & pass correct routerType to sandbox ([#168](https://github.com/netease/tango/issues/168)) ([3f3981b](https://github.com/netease/tango/commit/3f3981bb9db874b738a67fd449c579c35c58d08b))
|
||||
|
||||
### Features
|
||||
|
||||
- add context menu ([#161](https://github.com/netease/tango/issues/161)) ([28040fc](https://github.com/netease/tango/commit/28040fc00604339a40ad3216b76baf7de93a13e0))
|
||||
|
||||
## [1.2.3](https://github.com/netease/tango/compare/@music163/tango-designer@1.2.2...@music163/tango-designer@1.2.3) (2024-05-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- optimize variable panel scroll ui & onCancel bug ([#166](https://github.com/netease/tango/issues/166)) ([a0ed57f](https://github.com/netease/tango/commit/a0ed57ff4f332bd88749b8b1925bdb29319e4d13))
|
||||
|
||||
## [1.2.2](https://github.com/netease/tango/compare/@music163/tango-designer@1.2.1...@music163/tango-designer@1.2.2) (2024-05-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- export usePreviewSandboxQuery & add builtin sandboxQuery ([#163](https://github.com/netease/tango/issues/163)) ([163eced](https://github.com/netease/tango/commit/163ecedff3ed7baee59e200a8cc60dcc63a24e48))
|
||||
|
||||
## [1.2.1](https://github.com/netease/tango/compare/@music163/tango-designer@1.2.0...@music163/tango-designer@1.2.1) (2024-05-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- check local component prototypes ([#160](https://github.com/netease/tango/issues/160)) ([a0484e8](https://github.com/netease/tango/commit/a0484e8f64f20f67c30c25c3d5ce65de549b3e04))
|
||||
|
||||
# [1.2.0](https://github.com/netease/tango/compare/@music163/tango-designer@1.1.0...@music163/tango-designer@1.2.0) (2024-05-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- enhance code value validate in SettingForm ([#152](https://github.com/netease/tango/issues/152)) ([791fbb1](https://github.com/netease/tango/commit/791fbb162a7147243924e01f54e9c0b586f14438))
|
||||
- prototype2code & go back history error ([#156](https://github.com/netease/tango/issues/156)) ([8bf53a7](https://github.com/netease/tango/commit/8bf53a76f8a71eaf261ea68b9ee44e5bf19893aa))
|
||||
- support add components with popover ([#155](https://github.com/netease/tango/issues/155)) ([f17ccbb](https://github.com/netease/tango/commit/f17ccbb7f645f8047ecd96d9f3f2185048a3b726))
|
||||
|
||||
### Features
|
||||
|
||||
- add history forward & back shortcut key ([#154](https://github.com/netease/tango/issues/154)) ([df3021f](https://github.com/netease/tango/commit/df3021f75e057e229756b429321c14d181223698))
|
||||
- add select parent node of selected node ([#158](https://github.com/netease/tango/issues/158)) ([fe31246](https://github.com/netease/tango/commit/fe3124648325e72abfc58da8b2f8ff83301d40b8))
|
||||
- event-setter & expression-setter use popover ([#159](https://github.com/netease/tango/issues/159)) ([9daf387](https://github.com/netease/tango/commit/9daf3872e743fcf706184877020bcbf1f75ffd25))
|
||||
|
||||
# [1.1.0](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.3...@music163/tango-designer@1.1.0) (2024-05-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-designer",
|
||||
"version": "1.1.0",
|
||||
"version": "1.3.3",
|
||||
"description": "lowcode designer",
|
||||
"keywords": [
|
||||
"react"
|
||||
@@ -33,12 +33,12 @@
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/request": "^0.2.0",
|
||||
"@music163/tango-context": "^1.1.0",
|
||||
"@music163/tango-core": "^1.1.0",
|
||||
"@music163/tango-helpers": "^1.1.0",
|
||||
"@music163/tango-sandbox": "^1.0.3",
|
||||
"@music163/tango-setting-form": "^1.2.0",
|
||||
"@music163/tango-ui": "^1.1.0",
|
||||
"@music163/tango-context": "^1.1.5",
|
||||
"@music163/tango-core": "^1.3.3",
|
||||
"@music163/tango-helpers": "^1.2.0",
|
||||
"@music163/tango-sandbox": "^1.0.8",
|
||||
"@music163/tango-setting-form": "^1.2.8",
|
||||
"@music163/tango-ui": "^1.3.3",
|
||||
"antd": "^4.24.2",
|
||||
"cash-dom": "^8.1.2",
|
||||
"classnames": "^2.5.1",
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
import { IconFont, DragPanel } from '@music163/tango-ui';
|
||||
import { ComponentsPanel, ComponentsPanelProps } from '../sidebar';
|
||||
import { IComponentPrototype } from '@music163/tango-helpers';
|
||||
|
||||
interface ComponentsPopoverProps {
|
||||
// 添加组件位置
|
||||
type?: 'inner' | 'before' | 'after';
|
||||
// 弹出方式 手动触发/DOM 触发
|
||||
isControlled?: boolean;
|
||||
title?: string;
|
||||
prototype?: IComponentPrototype;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ComponentsPopover = observer(
|
||||
({
|
||||
type = 'inner',
|
||||
title = '添加组件',
|
||||
isControlled = false,
|
||||
children,
|
||||
...popoverProps
|
||||
}: ComponentsPopoverProps) => {
|
||||
const [layout, setLayout] = useState<ComponentsPanelProps['layout']>('grid');
|
||||
const workspace = useWorkspace();
|
||||
const designer = useDesigner();
|
||||
|
||||
const { addComponentPopoverPosition, showAddComponentPopover } = designer;
|
||||
const selectedNode = workspace.selectSource.selected?.[0];
|
||||
const selectedNodeId = selectedNode?.codeId ?? '未选中';
|
||||
const prototype =
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
workspace.componentPrototypes.get(selectedNode?.name) ?? ({} as IComponentPrototype);
|
||||
|
||||
// 推荐使用的子组件
|
||||
const insertedList = useMemo(
|
||||
() =>
|
||||
Array.isArray(prototype?.childrenName)
|
||||
? prototype?.childrenName
|
||||
: [prototype?.childrenName].filter(Boolean),
|
||||
[prototype?.childrenName],
|
||||
);
|
||||
|
||||
// 推荐使用的代码片段
|
||||
const siblingList = useMemo(() => prototype?.siblingNames ?? [], [prototype.siblingNames]);
|
||||
|
||||
const tipsTextMap = useMemo(
|
||||
() => ({
|
||||
before: `点击,在 ${selectedNodeId} 的前方添加节点`,
|
||||
after: `点击,在 ${selectedNodeId} 的后方添加节点`,
|
||||
inner: `点击,在 ${selectedNodeId} 内部添加节点`,
|
||||
}),
|
||||
[selectedNodeId],
|
||||
);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(name: string) => {
|
||||
switch (type) {
|
||||
case 'before':
|
||||
workspace.insertBeforeSelectedNode(name);
|
||||
break;
|
||||
case 'after':
|
||||
workspace.insertAfterSelectedNode(name);
|
||||
break;
|
||||
case 'inner':
|
||||
workspace.insertToSelectedNode(name);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[type, workspace],
|
||||
);
|
||||
|
||||
const changeLayout = useCallback(() => {
|
||||
setLayout(layout === 'grid' ? 'line' : 'grid');
|
||||
}, [layout]);
|
||||
|
||||
const menuData = useMemo(() => {
|
||||
const menuList = JSON.parse(JSON.stringify(designer.menuData));
|
||||
|
||||
const commonList = menuList['common'] ?? [];
|
||||
if (commonList?.length && siblingList?.length) {
|
||||
commonList.unshift({
|
||||
title: '代码片段',
|
||||
items: siblingList,
|
||||
});
|
||||
}
|
||||
|
||||
if (commonList?.length && insertedList?.length) {
|
||||
commonList.unshift({
|
||||
title: '推荐使用',
|
||||
items: insertedList,
|
||||
});
|
||||
}
|
||||
|
||||
return menuList;
|
||||
}, [insertedList, siblingList, designer.menuData]);
|
||||
|
||||
const innerTypeProps =
|
||||
// 手动触发 适用于 点击添加组件
|
||||
type === 'inner' && isControlled
|
||||
? {
|
||||
open: showAddComponentPopover,
|
||||
onOpenChange: (open: boolean) => designer.toggleAddComponentPopover(open),
|
||||
left: addComponentPopoverPosition.clientX,
|
||||
top: addComponentPopoverPosition.clientY,
|
||||
}
|
||||
: {};
|
||||
|
||||
return (
|
||||
<DragPanel
|
||||
{...innerTypeProps}
|
||||
title={title}
|
||||
extra={
|
||||
<Box fontSize="12px">
|
||||
{layout === 'grid' ? (
|
||||
<IconFont type="icon-liebiaoitem" onClick={changeLayout} />
|
||||
) : (
|
||||
<IconFont type="icon-grid1" onClick={changeLayout} />
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
footer={tipsTextMap[type]}
|
||||
width="330px"
|
||||
height="450px"
|
||||
resizeable
|
||||
maskClosable
|
||||
body={
|
||||
<ComponentsPanel
|
||||
isScope
|
||||
showBizComps={false}
|
||||
menuData={menuData}
|
||||
layout={layout}
|
||||
onItemSelect={handleSelect}
|
||||
/>
|
||||
}
|
||||
{...popoverProps}
|
||||
>
|
||||
{children}
|
||||
</DragPanel>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,130 @@
|
||||
import React from 'react';
|
||||
import { getWidget } from '../widgets';
|
||||
import { Menu, MenuProps } from 'antd';
|
||||
import { observer, useWorkspace } from '@music163/tango-context';
|
||||
import { ISelectedItemData, isString } from '@music163/tango-helpers';
|
||||
import { Box, css } from 'coral-system';
|
||||
import { IconFont } from '@music163/tango-ui';
|
||||
|
||||
const contextMenuStyle = css`
|
||||
.ant-dropdown-menu {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-title-content {
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-item-icon {
|
||||
font-size: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-item-icon + .ant-dropdown-menu-title-content {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-submenu-popup {
|
||||
padding: 0;
|
||||
margin-top: -4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ParentNodesMenuItem = ({
|
||||
record,
|
||||
onClick,
|
||||
key,
|
||||
}: {
|
||||
record: ISelectedItemData;
|
||||
onClick: () => any;
|
||||
key?: string;
|
||||
}) => {
|
||||
const workspace = useWorkspace();
|
||||
const componentPrototype = workspace.componentPrototypes.get(record.name);
|
||||
const icon = componentPrototype?.icon || 'icon-placeholder';
|
||||
|
||||
const iconRender = icon.startsWith('icon-') ? (
|
||||
<IconFont className="material-icon" type={icon} />
|
||||
) : (
|
||||
<img className="material-icon" src={icon} alt={componentPrototype.name} />
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu.Item key={key || record.id} icon={iconRender} onClick={onClick}>
|
||||
{record.name}
|
||||
{!!record.codeId && ` (${record.codeId})`}
|
||||
</Menu.Item>
|
||||
);
|
||||
};
|
||||
|
||||
const ParentNodesMenu = observer(() => {
|
||||
const workspace = useWorkspace();
|
||||
const selectSource = workspace.selectSource;
|
||||
const parents = selectSource.first?.parents;
|
||||
|
||||
if (!parents?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu.SubMenu key="parentNodes" title="选取父节点">
|
||||
{parents.map((item, index) => (
|
||||
<ParentNodesMenuItem
|
||||
key={item.id}
|
||||
record={item}
|
||||
onClick={() => {
|
||||
selectSource.select({
|
||||
...item,
|
||||
parents: parents.slice(index + 1),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Menu.SubMenu>
|
||||
);
|
||||
});
|
||||
|
||||
export interface ContextMenuProps extends MenuProps {
|
||||
/**
|
||||
* 动作列表,默认列出全部
|
||||
*/
|
||||
actions?: Array<string | React.ReactElement>;
|
||||
/**
|
||||
* 是否显示父节点选项
|
||||
*/
|
||||
showParents?: boolean;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
menuStyle?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export const ContextMenu = observer(
|
||||
({
|
||||
showParents,
|
||||
actions: actionsProp,
|
||||
className,
|
||||
style,
|
||||
menuStyle,
|
||||
...rest
|
||||
}: ContextMenuProps) => {
|
||||
const actions = actionsProp || Object.keys(getWidget('contextMenu'));
|
||||
const menus = actions.map((item) => {
|
||||
if (isString(item)) {
|
||||
const widget = getWidget(['contextMenu', item].join('.'));
|
||||
return widget ? React.createElement(widget) : null;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
if (showParents) {
|
||||
menus.unshift(<ParentNodesMenu />);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box display="inline-block" css={contextMenuStyle} className={className} style={style}>
|
||||
<Menu activeKey={null} {...rest} style={menuStyle}>
|
||||
{menus}
|
||||
</Menu>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -1,4 +1,6 @@
|
||||
export * from './context-menu';
|
||||
export * from './drag-box';
|
||||
export * from './input-kv';
|
||||
export * from './variable-tree';
|
||||
export * from './variable-tree-modal';
|
||||
export * from './components-popover';
|
||||
|
||||
@@ -166,7 +166,7 @@ export function VariableTree(props: VariableTreeProps) {
|
||||
|
||||
return (
|
||||
<Box display="flex" columnGap="l" className="VariableTree" css={varTreeStyle} {...rest}>
|
||||
<Box className="VariableList" width="40%">
|
||||
<Box className="VariableList" width="40%" overflow="auto">
|
||||
{renderHeaderExtra?.(props, state)}
|
||||
<Box mb="m" position="sticky" top="0" bg="white" zIndex={2}>
|
||||
<Search placeholder="请输入变量名" onChange={(val) => setKeyword(val?.trim())} />
|
||||
@@ -203,7 +203,11 @@ export function VariableTree(props: VariableTreeProps) {
|
||||
<Box flex="0 0 72px" textAlign="right">
|
||||
{node.showRemoveButton && (
|
||||
<Popconfirm
|
||||
zIndex={99999}
|
||||
title={`确认删除吗 ${node.title}?该操作会导致引用此模型的代码报错,请谨慎操作!`}
|
||||
onCancel={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onConfirm={(e) => {
|
||||
e.stopPropagation();
|
||||
if (isStoreVariablePath(node.key)) {
|
||||
@@ -333,7 +337,14 @@ export function VariableTree(props: VariableTreeProps) {
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ValueDefine data={activeNode} onSave={onUpdateStoreVariable} />
|
||||
<ValueDefine
|
||||
data={activeNode}
|
||||
onSave={(variableKey, code) => {
|
||||
onUpdateStoreVariable(variableKey, code);
|
||||
// 更新当前节点 code
|
||||
setActiveNode((pre) => ({ ...pre, raw: code }));
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</ValueDetail>
|
||||
|
||||
@@ -84,6 +84,8 @@ export function ValueDefine({ data, onSave = noop }: ValueDefineProps) {
|
||||
onClick={() => {
|
||||
off();
|
||||
setError('');
|
||||
// 重置到原始值
|
||||
setValue(data.raw);
|
||||
}}
|
||||
>
|
||||
取消
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { useWorkspace, observer } from '@music163/tango-context';
|
||||
import { ContextAction } from '@music163/tango-ui';
|
||||
import { CopyOutlined } from '@ant-design/icons';
|
||||
|
||||
export const CopyNodeContextAction = observer(() => {
|
||||
const workspace = useWorkspace();
|
||||
return (
|
||||
<ContextAction
|
||||
icon={<CopyOutlined />}
|
||||
hotkey="Command+C"
|
||||
onClick={() => {
|
||||
workspace.copySelectedNode();
|
||||
}}
|
||||
>
|
||||
复制节点
|
||||
</ContextAction>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { useWorkspace, observer } from '@music163/tango-context';
|
||||
import { ContextAction } from '@music163/tango-ui';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
|
||||
export const DeleteNodeContextAction = observer(() => {
|
||||
const workspace = useWorkspace();
|
||||
return (
|
||||
<ContextAction
|
||||
icon={<DeleteOutlined />}
|
||||
hotkey="Backspace"
|
||||
onClick={() => {
|
||||
workspace.removeSelectedNode();
|
||||
}}
|
||||
>
|
||||
删除节点
|
||||
</ContextAction>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './copy-node';
|
||||
export * from './delete-node';
|
||||
export * from './paste-node';
|
||||
export * from './view-source';
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { useWorkspace, observer } from '@music163/tango-context';
|
||||
import { ContextAction } from '@music163/tango-ui';
|
||||
import { SnippetsOutlined } from '@ant-design/icons';
|
||||
|
||||
export const PasteNodeContextAction = observer(() => {
|
||||
const workspace = useWorkspace();
|
||||
return (
|
||||
<ContextAction
|
||||
icon={<SnippetsOutlined />}
|
||||
hotkey="Command+V"
|
||||
onClick={() => {
|
||||
workspace.pasteSelectedNode();
|
||||
}}
|
||||
>
|
||||
粘贴节点
|
||||
</ContextAction>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { useDesigner, observer } from '@music163/tango-context';
|
||||
import { ContextAction } from '@music163/tango-ui';
|
||||
import { CodeOutlined } from '@ant-design/icons';
|
||||
|
||||
export const ViewSourceContextAction = observer(() => {
|
||||
const designer = useDesigner();
|
||||
return (
|
||||
<ContextAction
|
||||
icon={<CodeOutlined />}
|
||||
onClick={() => {
|
||||
designer.setActiveView('code');
|
||||
}}
|
||||
>
|
||||
查看源码
|
||||
</ContextAction>
|
||||
);
|
||||
});
|
||||
@@ -6,10 +6,14 @@ export interface IDesignerContext<S = any> {
|
||||
* 沙箱查询实例
|
||||
*/
|
||||
sandboxQuery: DndQuery;
|
||||
/**
|
||||
* 预览沙箱查询实例
|
||||
*/
|
||||
previewSandboxQuery: DndQuery;
|
||||
/**
|
||||
* 远程服务
|
||||
*/
|
||||
remoteServices?: Record<string, S>;
|
||||
remoteServices: Record<string, S>;
|
||||
}
|
||||
|
||||
const [DesignerProvider, useDesigner] = createContext<IDesignerContext>({
|
||||
@@ -19,9 +23,13 @@ const [DesignerProvider, useDesigner] = createContext<IDesignerContext>({
|
||||
export { DesignerProvider };
|
||||
|
||||
export const useSandboxQuery = () => {
|
||||
return useDesigner()?.sandboxQuery;
|
||||
return useDesigner().sandboxQuery;
|
||||
};
|
||||
|
||||
export const usePreviewSandboxQuery = () => {
|
||||
return useDesigner().previewSandboxQuery;
|
||||
};
|
||||
|
||||
export const useRemoteServices = () => {
|
||||
return useDesigner()?.remoteServices;
|
||||
return useDesigner().remoteServices;
|
||||
};
|
||||
|
||||
@@ -5,8 +5,18 @@ import { TangoEngineProvider, ITangoEngineContext } from '@music163/tango-contex
|
||||
import zhCN from 'antd/lib/locale/zh_CN';
|
||||
import { DesignerProvider, IDesignerContext } from './context';
|
||||
import defaultTheme from './themes/default';
|
||||
import { DndQuery } from './dnd';
|
||||
import { DESIGN_SANDBOX_ID, PREVIEW_SANDBOX_ID } from './helpers';
|
||||
|
||||
export interface DesignerProps extends IDesignerContext, ITangoEngineContext {
|
||||
const builtinSandboxQuery = new DndQuery({
|
||||
context: `#${DESIGN_SANDBOX_ID}`,
|
||||
});
|
||||
|
||||
const builtinPreviewSandboxQuery = new DndQuery({
|
||||
context: `#${PREVIEW_SANDBOX_ID}`,
|
||||
});
|
||||
|
||||
export interface DesignerProps extends Partial<IDesignerContext>, ITangoEngineContext {
|
||||
/**
|
||||
* 主题包
|
||||
*/
|
||||
@@ -20,13 +30,23 @@ export interface DesignerProps extends IDesignerContext, ITangoEngineContext {
|
||||
* @returns
|
||||
*/
|
||||
export function Designer(props: DesignerProps) {
|
||||
const { engine, config, theme: themeProp, sandboxQuery, remoteServices = {}, children } = props;
|
||||
const {
|
||||
engine,
|
||||
config,
|
||||
theme: themeProp,
|
||||
sandboxQuery = builtinSandboxQuery,
|
||||
previewSandboxQuery = builtinPreviewSandboxQuery,
|
||||
remoteServices = {},
|
||||
children,
|
||||
} = props;
|
||||
const theme = useMemo(() => extendTheme(themeProp, defaultTheme), [themeProp]);
|
||||
return (
|
||||
<SystemProvider theme={theme} prefix="--tango">
|
||||
<ConfigProvider locale={zhCN}>
|
||||
<TangoEngineProvider value={{ engine, config }}>
|
||||
<DesignerProvider value={{ sandboxQuery, remoteServices }}>{children}</DesignerProvider>
|
||||
<DesignerProvider value={{ sandboxQuery, previewSandboxQuery, remoteServices }}>
|
||||
{children}
|
||||
</DesignerProvider>
|
||||
</TangoEngineProvider>
|
||||
</ConfigProvider>
|
||||
</SystemProvider>
|
||||
|
||||
@@ -12,7 +12,6 @@ export const DRAGGABLE_SELECTOR = `[${SLOT.dnd}]`;
|
||||
interface DndQueryOptions {
|
||||
/**
|
||||
* DOM 查询上下文选择器
|
||||
* TODO: 是不是可以合并成一个 API
|
||||
*/
|
||||
context?: string;
|
||||
/**
|
||||
@@ -63,6 +62,9 @@ export class DndQuery {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 沙箱内的 window 对象
|
||||
*/
|
||||
get window() {
|
||||
if (this.context && 'defaultView' in this.context) {
|
||||
return (this.context as unknown as Document).defaultView;
|
||||
@@ -71,6 +73,9 @@ export class DndQuery {
|
||||
return window;
|
||||
}
|
||||
|
||||
/**
|
||||
* 沙箱内的全局滚动偏移
|
||||
*/
|
||||
get scrollTop() {
|
||||
if (this.context && 'documentElement' in this.context) {
|
||||
return (this.context as unknown as Document).documentElement.scrollTop;
|
||||
|
||||
@@ -58,6 +58,15 @@ export function useDnd({
|
||||
'command+v,ctrl+v': () => {
|
||||
workspace.pasteSelectedNode();
|
||||
},
|
||||
'command+arrowup,ctrl+arrowup': () => {
|
||||
workspace.selectSource.selectParent();
|
||||
},
|
||||
'command+z,ctrl+z': () => {
|
||||
workspace.history.back();
|
||||
},
|
||||
'command+shift+z,ctrl+shift+z': () => {
|
||||
workspace.history.forward();
|
||||
},
|
||||
});
|
||||
}, [workspace]);
|
||||
|
||||
@@ -68,6 +77,9 @@ export function useDnd({
|
||||
if (designer.isPreview) {
|
||||
return;
|
||||
}
|
||||
if (designer.showContextMenu) {
|
||||
designer.toggleContextMenu(false);
|
||||
}
|
||||
|
||||
const point = sandboxQuery.getRelativePoint({ x: e.clientX, y: e.clientY });
|
||||
selectSource.setStart({
|
||||
@@ -84,10 +96,10 @@ export function useDnd({
|
||||
const onMouseMove = (e: React.MouseEvent) => {
|
||||
const point = sandboxQuery.getRelativePoint({ x: e.clientX, y: e.clientY });
|
||||
setElementStyle('.SelectionMask', {
|
||||
width: Math.abs(selectSource.start?.point.x - point.x) + 'px',
|
||||
height: Math.abs(selectSource.start?.point.y - point.y) + 'px',
|
||||
left: Math.min(selectSource.start?.point.x, point.x) + 'px',
|
||||
top: Math.min(selectSource.start?.point.y, point.y) + 'px',
|
||||
width: `${Math.abs(selectSource.start?.point.x - point.x)}px`,
|
||||
height: `${Math.abs(selectSource.start?.point.y - point.y)}px`,
|
||||
left: `${Math.min(selectSource.start?.point.x, point.x)}px`,
|
||||
top: `${Math.min(selectSource.start?.point.y, point.y)}px`,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -119,6 +131,9 @@ export function useDnd({
|
||||
};
|
||||
|
||||
const onClick = (e: React.MouseEvent) => {
|
||||
if (designer.showContextMenu) {
|
||||
designer.toggleContextMenu(false);
|
||||
}
|
||||
const data = sandboxQuery.getDraggableParentsData(e.target as HTMLElement, true);
|
||||
if (data && data.id) {
|
||||
selectSource.select(data);
|
||||
@@ -371,7 +386,8 @@ export function useDnd({
|
||||
};
|
||||
|
||||
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||
const isInputElement = isInputElements((e.target as HTMLElement).nodeName);
|
||||
const target = e.target as HTMLElement;
|
||||
const isInputElement = target.isContentEditable || isInputElements(target.nodeName);
|
||||
// 禁用输入事件的触发的快捷键逻辑
|
||||
if (!isInputElement) {
|
||||
const key = getHotkey(e);
|
||||
@@ -379,6 +395,56 @@ export function useDnd({
|
||||
}
|
||||
};
|
||||
|
||||
const onContextMenu = (event: React.MouseEvent) => {
|
||||
if (designer.showContextMenu) {
|
||||
designer.toggleContextMenu(false);
|
||||
}
|
||||
// 按下其他按键时,视为用户有特殊操作,此时不展示右键菜单
|
||||
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
const { clientX, clientY } = event;
|
||||
let target;
|
||||
if (workspace.selectSource.isSelected) {
|
||||
for (const item of workspace.selectSource.selected) {
|
||||
if (
|
||||
// 如果选中的节点是页面根节点(无 parents),则忽略
|
||||
item.parents?.length &&
|
||||
item.bounding &&
|
||||
clientX >= item.bounding.left &&
|
||||
clientX <= item.bounding.left + item.bounding.width &&
|
||||
clientY >= item.bounding.top &&
|
||||
clientY <= item.bounding.top + item.bounding.height
|
||||
) {
|
||||
// 右键坐标已经在当前选中组件的选区内,直接展示右键菜单
|
||||
target = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 否则,根据右键的元素选中最接近的组件
|
||||
if (!target) {
|
||||
target = sandboxQuery.getDraggableParentsData(event.target as HTMLElement, true);
|
||||
}
|
||||
if (target && target.id) {
|
||||
if (!target.parents?.length) {
|
||||
// 页面根节点不展示右键菜单操作
|
||||
return;
|
||||
}
|
||||
// 右键时高亮选中当前元素
|
||||
// 以防之前选区有多个元素,即便已经是选中的元素也再选中一遍
|
||||
event.preventDefault();
|
||||
selectSource.select(target);
|
||||
// 在下一周期再展示右键菜单,以让先前的菜单先被销毁
|
||||
requestAnimationFrame(() => {
|
||||
designer.toggleContextMenu(true, {
|
||||
clientX,
|
||||
clientY,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onTango = (e: CustomEvent) => {
|
||||
const detail = e.detail || {};
|
||||
|
||||
@@ -406,6 +472,13 @@ export function useDnd({
|
||||
// 打开智能向导弹窗
|
||||
designer.toggleSmartWizard(true);
|
||||
break;
|
||||
case 'addComponent':
|
||||
// 打开添加组件面板
|
||||
designer.toggleAddComponentPopover(true, {
|
||||
clientX: (e.detail.meta as any).clientX + 40,
|
||||
clientY: (e.detail.meta as any).clientY + 110,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -421,6 +494,7 @@ export function useDnd({
|
||||
onDragEnd,
|
||||
onScroll,
|
||||
onKeyDown,
|
||||
onContextMenu,
|
||||
onTango,
|
||||
...selectHandler,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useRef, useEffect, useCallback } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { MultiEditor, MultiEditorProps } from '@music163/tango-ui';
|
||||
import { observer, useWorkspace } from '@music163/tango-context';
|
||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
import { isValidCode } from '@music163/tango-core';
|
||||
import { Modal } from 'antd';
|
||||
|
||||
@@ -23,105 +23,111 @@ const ideConfig = {
|
||||
|
||||
export interface CodeEditorProps extends Partial<MultiEditorProps> {
|
||||
/**
|
||||
* 是否自动清楚未使用的导入
|
||||
* 是否自动清除未使用的导入
|
||||
*/
|
||||
autoRemoveUnusedImports?: boolean;
|
||||
}
|
||||
|
||||
export const CodeEditor = observer(
|
||||
({ autoRemoveUnusedImports = true, ...rest }: CodeEditorProps) => {
|
||||
const editorRef = useRef(null);
|
||||
const workspace = useWorkspace();
|
||||
const files = workspace.listFiles();
|
||||
const activeFile = workspace.activeFile;
|
||||
export const CodeEditor = observer(({ autoRemoveUnusedImports, ...rest }: CodeEditorProps) => {
|
||||
const editorRef = useRef(null);
|
||||
const workspace = useWorkspace();
|
||||
const designer = useDesigner();
|
||||
const files = workspace.listFiles();
|
||||
const activeFile = workspace.activeFile;
|
||||
|
||||
let loc: any; // 记录视图代码的选中位置
|
||||
const selectNode = workspace.selectSource.firstNode;
|
||||
if (selectNode && activeFile === workspace.activeViewFile) {
|
||||
loc = selectNode.loc;
|
||||
}
|
||||
let loc: any; // 记录视图代码的选中位置
|
||||
const selectNode = workspace.selectSource.firstNode;
|
||||
if (selectNode && activeFile === workspace.activeViewFile) {
|
||||
loc = selectNode.loc;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
editorRef.current?.refresh(files, activeFile, loc);
|
||||
}, [files, activeFile, loc]);
|
||||
useEffect(() => {
|
||||
editorRef.current?.refresh(files, activeFile, loc);
|
||||
}, [files, activeFile, loc]);
|
||||
|
||||
const fileSave = useCallback(
|
||||
(path: string, value: string) => {
|
||||
if (!isJsFile(path)) {
|
||||
// 非 js 文件直接保存
|
||||
workspace.updateFile(path, value, autoRemoveUnusedImports);
|
||||
return;
|
||||
const fileSave = useCallback(
|
||||
(path: string, value: string) => {
|
||||
if (!isJsFile(path)) {
|
||||
// 非 js 文件直接保存
|
||||
workspace.updateFile(path, value, autoRemoveUnusedImports);
|
||||
return;
|
||||
}
|
||||
|
||||
// js 文件需要先检查语法,只有语法正确才会保存
|
||||
if (isValidCode(value)) {
|
||||
workspace.updateFile(path, value, autoRemoveUnusedImports);
|
||||
} else {
|
||||
Modal.confirm({
|
||||
title: '检测到代码中存在语法错误,暂时无法将代码同步给设计器,是否回退到安全代码?',
|
||||
onOk: () => {
|
||||
editorRef.current?.refresh(files, activeFile);
|
||||
},
|
||||
onCancel: () => {},
|
||||
});
|
||||
}
|
||||
},
|
||||
[workspace, autoRemoveUnusedImports, activeFile, files],
|
||||
);
|
||||
|
||||
const handleFileChange = useCallback(
|
||||
(type: string, info: any) => {
|
||||
switch (type) {
|
||||
case 'addFile':
|
||||
workspace.addFile(info.path, info.value);
|
||||
break;
|
||||
case 'deleteFile':
|
||||
case 'deleteFolder':
|
||||
workspace.removeFile(info.path);
|
||||
break;
|
||||
case 'renameFile':
|
||||
workspace.renameFile(info.path, info.newpath);
|
||||
workspace.setActiveFile(info.newpath);
|
||||
break;
|
||||
case 'renameFolder':
|
||||
workspace.renameFolder(info.path, info.newpath);
|
||||
break;
|
||||
case 'addFolder':
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[workspace],
|
||||
);
|
||||
|
||||
const handlePathChange = useCallback(
|
||||
(path: string) => {
|
||||
workspace.setActiveFile(path);
|
||||
},
|
||||
[workspace],
|
||||
);
|
||||
|
||||
const borderStyle =
|
||||
designer.activeView === 'dual'
|
||||
? {
|
||||
borderLeft: 'solid 1px var(--tango-colors-line2)',
|
||||
}
|
||||
: {};
|
||||
|
||||
// js 文件需要先检查语法,只有语法正确才会保存
|
||||
if (isValidCode(value)) {
|
||||
workspace.updateFile(path, value, autoRemoveUnusedImports);
|
||||
} else {
|
||||
Modal.confirm({
|
||||
title: '检测到代码中存在语法错误,暂时无法将代码同步给设计器,是否回退到安全代码?',
|
||||
onOk: () => {
|
||||
editorRef.current?.refresh(files, activeFile);
|
||||
},
|
||||
onCancel: () => {},
|
||||
});
|
||||
}
|
||||
},
|
||||
[workspace, autoRemoveUnusedImports, activeFile, files],
|
||||
);
|
||||
|
||||
const handleFileChange = useCallback(
|
||||
(type: string, info: any) => {
|
||||
switch (type) {
|
||||
case 'addFile':
|
||||
workspace.addFile(info.path, info.value);
|
||||
break;
|
||||
case 'deleteFile':
|
||||
case 'deleteFolder':
|
||||
workspace.removeFile(info.path);
|
||||
break;
|
||||
case 'renameFile':
|
||||
workspace.renameFile(info.path, info.newpath);
|
||||
workspace.setActiveFile(info.newpath);
|
||||
break;
|
||||
case 'renameFolder':
|
||||
workspace.renameFolder(info.path, info.newpath);
|
||||
break;
|
||||
case 'addFolder':
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[workspace],
|
||||
);
|
||||
|
||||
const handlePathChange = useCallback(
|
||||
(path: string) => {
|
||||
workspace.setActiveFile(path);
|
||||
},
|
||||
[workspace],
|
||||
);
|
||||
|
||||
return (
|
||||
<Box display="flex" flexDirection="row" height="100%" bg="white">
|
||||
<MultiEditor
|
||||
ref={editorRef}
|
||||
options={{
|
||||
fontSize: 14,
|
||||
automaticLayout: true,
|
||||
}}
|
||||
ideConfig={ideConfig}
|
||||
onFileSave={fileSave}
|
||||
onPathChange={handlePathChange}
|
||||
onFileChange={handleFileChange}
|
||||
defaultPath={activeFile}
|
||||
defaultTheme="GithubLightDefault"
|
||||
defaultFiles={files}
|
||||
{...rest}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
);
|
||||
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);
|
||||
|
||||
@@ -7,6 +7,20 @@ import {
|
||||
parseDndId,
|
||||
} from '@music163/tango-helpers';
|
||||
|
||||
// -----------
|
||||
// CONSTANTS
|
||||
// -----------
|
||||
|
||||
export const DRAG_GHOST_ID = 'dragGhost';
|
||||
|
||||
export const DESIGN_SANDBOX_ID = 'sandbox-container';
|
||||
|
||||
export const PREVIEW_SANDBOX_ID = 'preview-sandbox-container';
|
||||
|
||||
// -----------
|
||||
// DOM Helpers
|
||||
// -----------
|
||||
|
||||
export function buildQueryBySlotId(id: string) {
|
||||
return `[${SLOT.dnd}="${id}"]`;
|
||||
}
|
||||
@@ -15,8 +29,6 @@ export function getElement(selector: Selector) {
|
||||
return $(selector).get(0);
|
||||
}
|
||||
|
||||
export const DRAG_GHOST_ID = 'dragGhost';
|
||||
|
||||
export const getDragGhostElement = () => getElement(`#${DRAG_GHOST_ID}`);
|
||||
|
||||
export function setElementStyle(selector: Selector, style: any) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useSandboxQuery } from '../context';
|
||||
import { DndQuery, useDnd } from '../dnd';
|
||||
import { Navigator } from './navigator';
|
||||
import { SelectionToolsProps } from '../simulator/selection';
|
||||
import { DESIGN_SANDBOX_ID, PREVIEW_SANDBOX_ID } from '../helpers';
|
||||
|
||||
interface ISandboxEventHandlerConfig {
|
||||
sandboxQuery?: DndQuery;
|
||||
@@ -48,6 +49,10 @@ export interface CombinedSandboxRef {
|
||||
|
||||
const LANDING_PAGE_PATH = '/__background_landing_page__';
|
||||
|
||||
function fixRouterMode(routerType: string): CodeSandboxProps['routerMode'] {
|
||||
return routerType === 'hash' ? 'hash' : undefined;
|
||||
}
|
||||
|
||||
function useSandbox({
|
||||
isPreview: isPreviewProp,
|
||||
configFormatter,
|
||||
@@ -62,6 +67,7 @@ function useSandbox({
|
||||
const workspace = useWorkspace();
|
||||
const designer = useDesigner();
|
||||
const sandboxQuery = useSandboxQuery();
|
||||
|
||||
const isPreview = isPreviewProp ?? designer.isPreview;
|
||||
|
||||
// 组件不一定会立即刷新,因此 isActive 需要实时获取
|
||||
@@ -103,23 +109,26 @@ function useSandbox({
|
||||
// 根据当前 workspace 状态与组件传入的状态是否一致,控制是否需要切换到空白路由
|
||||
const display = isActive ? 'block' : 'none';
|
||||
const routePath = isActive ? startRoute || workspace.activeRoute : LANDING_PAGE_PATH;
|
||||
const routerMode = fixRouterMode(workspace.appEntryModule?.routerType);
|
||||
|
||||
const sandboxProps = isPreview
|
||||
? {
|
||||
files,
|
||||
eventHandlers: pick(dndHandlers, ['onTango']),
|
||||
onMessage,
|
||||
onLoad,
|
||||
display,
|
||||
startRoute: routePath,
|
||||
onLoad,
|
||||
routerMode,
|
||||
}
|
||||
: {
|
||||
files,
|
||||
eventHandlers: dndHandlers as any,
|
||||
onMessage,
|
||||
onLoad,
|
||||
display,
|
||||
startRoute: routePath,
|
||||
onLoad,
|
||||
routerMode,
|
||||
};
|
||||
|
||||
return sandboxProps;
|
||||
@@ -140,7 +149,7 @@ const PreviewSandbox = observer(
|
||||
|
||||
return (
|
||||
<Box display={display} width="100%" height="100%">
|
||||
<CodeSandbox ref={ref} iframeId="preview-sandbox-container" {...sandboxProps} {...rest} />
|
||||
<CodeSandbox ref={ref} iframeId={PREVIEW_SANDBOX_ID} {...sandboxProps} {...rest} />
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
@@ -188,7 +197,7 @@ const DesignSandbox = observer(
|
||||
|
||||
return (
|
||||
<Box display={display} width="100%" height="100%">
|
||||
<CodeSandbox ref={ref} {...sandboxProps} {...rest} />
|
||||
<CodeSandbox ref={ref} iframeId={DESIGN_SANDBOX_ID} {...sandboxProps} {...rest} />
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './copy-node';
|
||||
export * from './delete-node';
|
||||
export * from './more-actions';
|
||||
export * from './select-parent-node';
|
||||
export * from './view-source';
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { observer } from '@music163/tango-context';
|
||||
import { SelectAction } from '@music163/tango-ui';
|
||||
import { MoreOutlined } from '@ant-design/icons';
|
||||
import { Dropdown } from 'antd';
|
||||
import { ContextMenu } from '../components';
|
||||
|
||||
export const MoreActionsAction = observer(() => {
|
||||
return (
|
||||
<Dropdown placement="bottomRight" overlay={<ContextMenu showParents />}>
|
||||
<SelectAction tooltip="更多">
|
||||
<MoreOutlined />
|
||||
</SelectAction>
|
||||
</Dropdown>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { useWorkspace, observer } from '@music163/tango-context';
|
||||
import { IconFont, SelectAction } from '@music163/tango-ui';
|
||||
|
||||
export const SelectParentNodeAction = observer(() => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
return (
|
||||
<SelectAction
|
||||
tooltip="选中父节点"
|
||||
onClick={() => {
|
||||
workspace.selectSource.selectParent();
|
||||
}}
|
||||
>
|
||||
<IconFont type="icon-huiche1" rotate={90} />
|
||||
</SelectAction>
|
||||
);
|
||||
});
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { css, Box } from 'coral-system';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { css, Box, Text } from 'coral-system';
|
||||
import { AutoComplete } from 'antd';
|
||||
import { ActionSelect } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||
import { wrapCode } from '@music163/tango-helpers';
|
||||
import { ExpressionModal } from './expression-setter';
|
||||
import { ExpressionPopover } from './expression-setter';
|
||||
import { value2code } from '@music163/tango-core';
|
||||
|
||||
enum EventAction {
|
||||
@@ -25,15 +25,6 @@ const wrapperStyle = css`
|
||||
}
|
||||
`;
|
||||
|
||||
const options = [
|
||||
{ label: '无动作', value: EventAction.NoAction },
|
||||
{ label: '打印事件', value: EventAction.ConsoleLog },
|
||||
{ label: '绑定 JS 表达式', value: EventAction.BindExpression },
|
||||
{ label: '打开页面', value: EventAction.NavigateTo },
|
||||
{ label: '打开弹窗', value: EventAction.OpenModal },
|
||||
{ label: '关闭弹窗', value: EventAction.CloseModal },
|
||||
];
|
||||
|
||||
export type EventSetterProps = FormItemComponentProps<string>;
|
||||
|
||||
/**
|
||||
@@ -43,7 +34,6 @@ export function EventSetter(props: EventSetterProps) {
|
||||
const { value, onChange, modalTitle } = props;
|
||||
const [type, setType] = useState<EventAction>(); // 事件类型
|
||||
const [temp, setTemp] = useState(''); // 二级暂存值
|
||||
const [expModalVisible, setExpModalVisible] = useState(false); // 弹窗是否显示
|
||||
const { actionVariables, routeOptions } = useWorkspaceData();
|
||||
const workspace = useWorkspace();
|
||||
const modalOptions = workspace.activeViewModule.listModals() || [];
|
||||
@@ -61,15 +51,37 @@ export function EventSetter(props: EventSetterProps) {
|
||||
},
|
||||
[onChange, code],
|
||||
);
|
||||
const options = useMemo(
|
||||
() => [
|
||||
{ label: '无动作', value: EventAction.NoAction },
|
||||
{ label: '打印事件', value: EventAction.ConsoleLog },
|
||||
{
|
||||
label: (
|
||||
<ExpressionPopover
|
||||
title={modalTitle}
|
||||
value={value}
|
||||
onOk={(nextValue) => {
|
||||
handleChange(nextValue);
|
||||
}}
|
||||
dataSource={actionVariables}
|
||||
>
|
||||
<Text>绑定 JS 表达式</Text>
|
||||
</ExpressionPopover>
|
||||
),
|
||||
value: EventAction.BindExpression,
|
||||
},
|
||||
{ label: '打开页面', value: EventAction.NavigateTo },
|
||||
{ label: '打开弹窗', value: EventAction.OpenModal },
|
||||
{ label: '关闭弹窗', value: EventAction.CloseModal },
|
||||
],
|
||||
[modalTitle, value, actionVariables, handleChange],
|
||||
);
|
||||
|
||||
const onAction = (key: string) => {
|
||||
setType(key as EventAction); // 记录事件类型
|
||||
setTemp(''); // 重置二级选项值
|
||||
|
||||
switch (key) {
|
||||
case EventAction.BindExpression:
|
||||
setExpModalVisible(true);
|
||||
break;
|
||||
case EventAction.ConsoleLog:
|
||||
handleChange('(...args) => console.log(...args)');
|
||||
break;
|
||||
@@ -86,17 +98,6 @@ export function EventSetter(props: EventSetterProps) {
|
||||
return (
|
||||
<Box css={wrapperStyle}>
|
||||
<ActionSelect options={options} onSelect={onAction} text={actionText} />
|
||||
<ExpressionModal
|
||||
title={modalTitle}
|
||||
value={code}
|
||||
visible={expModalVisible}
|
||||
onCancel={() => setExpModalVisible(false)}
|
||||
onOk={(nextValue) => {
|
||||
setExpModalVisible(false);
|
||||
handleChange(nextValue);
|
||||
}}
|
||||
dataSource={actionVariables}
|
||||
/>
|
||||
{type === EventAction.NavigateTo && (
|
||||
<AutoComplete
|
||||
placeholder="选择或输入页面路由"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Box, Text, css } from 'coral-system';
|
||||
import { Dropdown, Modal } from 'antd';
|
||||
import { Dropdown, Button } from 'antd';
|
||||
import { isValidExpressionCode } from '@music163/tango-core';
|
||||
import { noop, useBoolean, getValue, IVariableTreeNode } from '@music163/tango-helpers';
|
||||
import { CloseCircleFilled, ExpandAltOutlined, MenuOutlined } from '@ant-design/icons';
|
||||
import { Panel, InputCode, Action } from '@music163/tango-ui';
|
||||
import { getValue, IVariableTreeNode, noop } from '@music163/tango-helpers';
|
||||
import { CloseCircleFilled, MenuOutlined } from '@ant-design/icons';
|
||||
import { Panel, InputCode, Action, DragPanel, PopOutOutlined } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||
import { VariableTree } from '../components';
|
||||
@@ -60,7 +60,6 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
} = props;
|
||||
// const codeValue = getCodeOfWrappedCode(valueProp);
|
||||
const [inputValue, setInputValue] = useState(valueProp);
|
||||
const [visible, { on, off }] = useBoolean();
|
||||
|
||||
// when receive new value, sync state
|
||||
useEffect(() => {
|
||||
@@ -115,11 +114,16 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
<Action tooltip="使用预设代码片段" icon={<MenuOutlined />} size="small" />
|
||||
</Dropdown>
|
||||
)}
|
||||
<Action
|
||||
tooltip="打开表达式变量选择面板"
|
||||
icon={<ExpandAltOutlined />}
|
||||
onClick={on}
|
||||
size="small"
|
||||
<ExpressionPopover
|
||||
title={modalTitle}
|
||||
subTitle={modalTip}
|
||||
placeholder={placeholder}
|
||||
autoCompleteOptions={autoCompleteOptions}
|
||||
newStoreTemplate={newStoreTemplate}
|
||||
value={inputValue}
|
||||
onOk={(value) => {
|
||||
change(value);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
@@ -133,32 +137,16 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
status={status}
|
||||
maxHeight="200px"
|
||||
/>
|
||||
<ExpressionModal
|
||||
title={modalTitle}
|
||||
subTitle={modalTip}
|
||||
placeholder={placeholder}
|
||||
autoCompleteOptions={autoCompleteOptions}
|
||||
newStoreTemplate={newStoreTemplate}
|
||||
visible={visible}
|
||||
value={inputValue}
|
||||
onCancel={() => off()}
|
||||
onOk={(value) => {
|
||||
change(value);
|
||||
off();
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export interface ExpressionModalProps {
|
||||
export interface ExpressionPopoverProps {
|
||||
title?: string;
|
||||
subTitle?: string;
|
||||
placeholder?: string;
|
||||
visible?: boolean;
|
||||
defaultValue?: string;
|
||||
value?: string;
|
||||
onCancel?: () => void;
|
||||
onOk?: (value: string) => void;
|
||||
dataSource?: IVariableTreeNode[];
|
||||
autoCompleteOptions?: string[];
|
||||
@@ -166,25 +154,27 @@ export interface ExpressionModalProps {
|
||||
* 新建 store 的模板代码
|
||||
*/
|
||||
newStoreTemplate?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ExpressionModal({
|
||||
export function ExpressionPopover({
|
||||
title,
|
||||
subTitle,
|
||||
placeholder,
|
||||
visible,
|
||||
onCancel = noop,
|
||||
onOk = noop,
|
||||
defaultValue,
|
||||
value,
|
||||
dataSource,
|
||||
autoCompleteOptions,
|
||||
newStoreTemplate = CODE_TEMPLATES.newStoreTemplate,
|
||||
}: ExpressionModalProps) {
|
||||
children,
|
||||
}: ExpressionPopoverProps) {
|
||||
const [exp, setExp] = useState(value ?? defaultValue);
|
||||
const [error, setError] = useState('');
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const selectNodePath = workspace.selectSource.selected[0]?.codeId;
|
||||
|
||||
const { expressionVariables } = useWorkspaceData();
|
||||
const serviceModules = Object.keys(workspace.serviceModules).map((key) => ({
|
||||
label: key === 'index' ? '默认模块' : key,
|
||||
@@ -203,118 +193,159 @@ export function ExpressionModal({
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
closable={false}
|
||||
destroyOnClose
|
||||
width="60%"
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
onOk={() => {
|
||||
onOk(exp);
|
||||
}}
|
||||
bodyStyle={{
|
||||
padding: 0,
|
||||
<DragPanel
|
||||
width={700}
|
||||
height={615}
|
||||
resizeable
|
||||
title={`使用代码设置 ${selectNodePath} 组件的${title}`}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setExp(undefined);
|
||||
setError('');
|
||||
}
|
||||
}}
|
||||
body={
|
||||
<Box display="flex" flexDirection="column">
|
||||
<Box p="m">
|
||||
<InputCode
|
||||
shape="inset"
|
||||
minHeight="56px"
|
||||
maxHeight="160px"
|
||||
value={exp}
|
||||
placeholder={placeholder}
|
||||
onChange={handleExpInputChange}
|
||||
onBlur={() => {
|
||||
if (exp) {
|
||||
setError(expressionValueValidate(exp));
|
||||
}
|
||||
}}
|
||||
autoCompleteContext={evaluateContext}
|
||||
autoCompleteOptions={autoCompleteOptions}
|
||||
/>
|
||||
{error ? (
|
||||
<Text color="red" fontSize="12px" as="p">
|
||||
出错了!输入的表达式存在语法错误,请修改后再提交!
|
||||
</Text>
|
||||
) : null}
|
||||
{subTitle && (
|
||||
<Text fontSize="12px" color="text3" as="p">
|
||||
{subTitle}
|
||||
</Text>
|
||||
)}
|
||||
<Text fontSize="12px" color="text3" as="p">
|
||||
说明:你可以在上面的代码输入框里输入常规的 javascript 代码,还可以直接使用 jsx
|
||||
代码,但需要符合该属性的接受值定义。
|
||||
</Text>
|
||||
</Box>
|
||||
<Panel
|
||||
title="从变量列表中选中"
|
||||
shape="solid"
|
||||
borderLeft="0"
|
||||
borderRight="0"
|
||||
flex={1}
|
||||
bodyProps={{
|
||||
style: {
|
||||
overflow: 'hidden',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<VariableTree
|
||||
height="100%"
|
||||
showViewButton
|
||||
dataSource={dataSource || expressionVariables}
|
||||
appContext={sandbox?.window['tango']}
|
||||
getStoreNames={() => Object.keys(workspace.storeModules)}
|
||||
serviceModules={serviceModules}
|
||||
getServiceData={(serviceKey) => {
|
||||
const data = workspace.getServiceFunction(serviceKey);
|
||||
return {
|
||||
name: data.name,
|
||||
moduleName: data.moduleName,
|
||||
method: 'get',
|
||||
...data.config,
|
||||
};
|
||||
}}
|
||||
onSelect={(node) => {
|
||||
if (!node.key) {
|
||||
return;
|
||||
}
|
||||
if (node.key.split('.').length < 2) {
|
||||
return;
|
||||
}
|
||||
let str;
|
||||
if (/^(stores|services)\./.test(node.key)) {
|
||||
str = `tango.${node.key.replaceAll('.', '?.')}`;
|
||||
} else {
|
||||
str = `${node.key}`;
|
||||
}
|
||||
setExp(str);
|
||||
}}
|
||||
onAddStoreVariable={(storeName, data) => {
|
||||
workspace.addStoreState(storeName, data.name, data.initialValue);
|
||||
}}
|
||||
onUpdateStoreVariable={(variableKey, code) => {
|
||||
workspace.updateStoreVariable(variableKey, code);
|
||||
}}
|
||||
onAddStore={(storeName) => {
|
||||
workspace.addStoreFile(storeName, newStoreTemplate);
|
||||
}}
|
||||
onRemoveStoreVariable={(variableKey) => {
|
||||
workspace.removeStoreVariable(variableKey);
|
||||
}}
|
||||
onRemoveService={(serviceKey) => {
|
||||
workspace.removeServiceFunction(serviceKey);
|
||||
}}
|
||||
onAddService={(data) => {
|
||||
const { name, moduleName, ...payload } = shapeServiceValues(data);
|
||||
workspace.addServiceFunction(name, payload, moduleName);
|
||||
}}
|
||||
onUpdateService={(data) => {
|
||||
const { name, moduleName, ...payload } = shapeServiceValues(data);
|
||||
workspace.updateServiceFunction(name, payload, moduleName);
|
||||
}}
|
||||
getPreviewValue={(node) => {
|
||||
if (!node || !node.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
return getValue(evaluateContext['tango'], node.key);
|
||||
}}
|
||||
/>
|
||||
</Panel>
|
||||
</Box>
|
||||
}
|
||||
footer={(close) => (
|
||||
<Box display="flex" width="100%" justifyContent="flex-end" gap="5px">
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
if (!error) {
|
||||
onOk(exp);
|
||||
close();
|
||||
}
|
||||
}}
|
||||
>
|
||||
确认
|
||||
</Button>
|
||||
<Button size="small" onClick={close}>
|
||||
取消
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
>
|
||||
<Panel title={`将 ${title} 设置为引用变量或自定义表达式`} subTitle={subTitle} shape="solid">
|
||||
<InputCode
|
||||
shape="inset"
|
||||
minHeight="56px"
|
||||
maxHeight="200px"
|
||||
value={exp}
|
||||
placeholder={placeholder}
|
||||
onChange={handleExpInputChange}
|
||||
onBlur={() => {
|
||||
setError(expressionValueValidate(exp));
|
||||
}}
|
||||
autoCompleteContext={evaluateContext}
|
||||
autoCompleteOptions={autoCompleteOptions}
|
||||
{children || (
|
||||
<Action
|
||||
tooltip="弹出代码编辑和变量选择面板"
|
||||
tooltipProps={{ placement: 'topLeft' }}
|
||||
icon={<PopOutOutlined />}
|
||||
size="small"
|
||||
/>
|
||||
{error ? (
|
||||
<Text color="red" fontSize="12px">
|
||||
出错了!输入的表达式存在语法错误,请修改后再提交!
|
||||
</Text>
|
||||
) : (
|
||||
<Text fontSize="12px" color="text3">
|
||||
说明:你可以在上面的代码输入框里输入常规的 javascript 代码,还可以直接使用 jsx
|
||||
代码,但需要符合该属性的接受值定义。
|
||||
</Text>
|
||||
)}
|
||||
</Panel>
|
||||
<Panel
|
||||
title="从变量列表中选中"
|
||||
shape="solid"
|
||||
borderTop="0"
|
||||
overflow="hidden"
|
||||
bodyProps={{ overflow: 'hidden' }}
|
||||
>
|
||||
<VariableTree
|
||||
height={380}
|
||||
showViewButton
|
||||
dataSource={dataSource || expressionVariables}
|
||||
appContext={sandbox?.window['tango']}
|
||||
getStoreNames={() => Object.keys(workspace.storeModules)}
|
||||
serviceModules={serviceModules}
|
||||
getServiceData={(serviceKey) => {
|
||||
const data = workspace.getServiceFunction(serviceKey);
|
||||
return {
|
||||
name: data.name,
|
||||
moduleName: data.moduleName,
|
||||
method: 'get',
|
||||
...data.config,
|
||||
};
|
||||
}}
|
||||
onSelect={(node) => {
|
||||
if (!node.key) {
|
||||
return;
|
||||
}
|
||||
if (node.key.split('.').length < 2) {
|
||||
return;
|
||||
}
|
||||
let str;
|
||||
if (/^(stores|services)\./.test(node.key)) {
|
||||
str = `tango.${node.key.replaceAll('.', '?.')}`;
|
||||
} else {
|
||||
str = `${node.key}`;
|
||||
}
|
||||
setExp(str);
|
||||
}}
|
||||
onAddStoreVariable={(storeName, data) => {
|
||||
workspace.addStoreState(storeName, data.name, data.initialValue);
|
||||
}}
|
||||
onUpdateStoreVariable={(variableKey, code) => {
|
||||
workspace.updateStoreVariable(variableKey, code);
|
||||
}}
|
||||
onAddStore={(storeName) => {
|
||||
workspace.addStoreFile(storeName, newStoreTemplate);
|
||||
}}
|
||||
onRemoveStoreVariable={(variableKey) => {
|
||||
workspace.removeStoreVariable(variableKey);
|
||||
}}
|
||||
onRemoveService={(serviceKey) => {
|
||||
workspace.removeServiceFunction(serviceKey);
|
||||
}}
|
||||
onAddService={(data) => {
|
||||
const { name, moduleName, ...payload } = shapeServiceValues(data);
|
||||
workspace.addServiceFunction(name, payload, moduleName);
|
||||
}}
|
||||
onUpdateService={(data) => {
|
||||
const { name, moduleName, ...payload } = shapeServiceValues(data);
|
||||
workspace.updateServiceFunction(name, payload, moduleName);
|
||||
}}
|
||||
getPreviewValue={(node) => {
|
||||
if (!node || !node.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
return getValue(evaluateContext['tango'], node.key);
|
||||
}}
|
||||
/>
|
||||
</Panel>
|
||||
</Modal>
|
||||
)}
|
||||
</DragPanel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,24 @@ import {
|
||||
FlexDirectionSetter,
|
||||
} from './style-setter';
|
||||
import { ChoiceSetter } from './choice-setter';
|
||||
import { isValidExpressionCode } from '@music163/tango-core';
|
||||
|
||||
const codeValidate: IFormItemCreateOptions['validate'] = (value, field) => {
|
||||
if (!value) return;
|
||||
const rawCode = field.detail.rawCode;
|
||||
if (!rawCode) return;
|
||||
return isValidExpressionCode(rawCode) ? '' : '请输入合法的 Javascript 代码片段';
|
||||
};
|
||||
|
||||
const jsonValidate: IFormItemCreateOptions['validate'] = (value, field) => {
|
||||
if (!value) return;
|
||||
try {
|
||||
JSON.parse(field.detail.rawCode);
|
||||
return;
|
||||
} catch (e) {
|
||||
return '请输入合法的 JSON 字符串';
|
||||
}
|
||||
};
|
||||
|
||||
export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
|
||||
{
|
||||
@@ -33,6 +51,7 @@ export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
|
||||
alias: ['expSetter', 'expressionSetter'],
|
||||
component: ExpressionSetter,
|
||||
type: 'code',
|
||||
validate: codeValidate,
|
||||
},
|
||||
{
|
||||
name: 'radioGroupSetter',
|
||||
@@ -81,11 +100,13 @@ export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
|
||||
name: 'jsonSetter',
|
||||
component: JSONSetter,
|
||||
type: 'code',
|
||||
validate: jsonValidate,
|
||||
},
|
||||
{
|
||||
name: 'jsxSetter',
|
||||
component: JsxSetter,
|
||||
type: 'code',
|
||||
validate: codeValidate,
|
||||
},
|
||||
{
|
||||
name: 'listSetter',
|
||||
@@ -108,6 +129,7 @@ export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
|
||||
name: 'renderPropsSetter',
|
||||
component: RenderSetter,
|
||||
type: 'code',
|
||||
validate: codeValidate,
|
||||
},
|
||||
{
|
||||
name: 'tableCellSetter',
|
||||
@@ -118,6 +140,7 @@ export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
|
||||
name: 'tableExpandableSetter',
|
||||
component: TableExpandableSetter,
|
||||
type: 'code',
|
||||
validate: codeValidate,
|
||||
},
|
||||
{
|
||||
name: 'routerSetter',
|
||||
|
||||
@@ -6,7 +6,11 @@ import { clone, parseDndId } from '@music163/tango-helpers';
|
||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
import { registerBuiltinSetters } from './setters';
|
||||
|
||||
registerBuiltinSetters();
|
||||
let registered = false;
|
||||
if (!registered) {
|
||||
registerBuiltinSetters();
|
||||
registered = true;
|
||||
}
|
||||
|
||||
export interface SettingPanelProps extends SettingFormProps {
|
||||
title?: React.ReactNode;
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Box, Grid, Text } from 'coral-system';
|
||||
import styled from 'styled-components';
|
||||
import styled, { css } from 'styled-components';
|
||||
import {
|
||||
IComponentPrototype,
|
||||
MenuDataType,
|
||||
MenuValueType,
|
||||
createContext,
|
||||
logger,
|
||||
PartialRecord,
|
||||
upperCamelCase,
|
||||
} from '@music163/tango-helpers';
|
||||
import { CollapsePanel, IconFont, Search, Tabs } from '@music163/tango-ui';
|
||||
import { observer, useWorkspace } from '@music163/tango-context';
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { Button, Empty, Spin, Popover } from 'antd';
|
||||
import { Button, Empty, Spin, Popover, TabsProps } from 'antd';
|
||||
import { getDragGhostElement } from '../helpers';
|
||||
|
||||
type MenuKeyType = 'common' | 'atom' | 'snippet' | 'bizComp' | 'localComp';
|
||||
type MenuValueType = Array<{ title: string; items: string[] }>;
|
||||
export type MenuDataType = PartialRecord<MenuKeyType, MenuValueType>;
|
||||
type IComponentsPanelContext = {
|
||||
isScope: boolean;
|
||||
onItemSelect: (name: string) => void;
|
||||
layout: 'grid' | 'line';
|
||||
};
|
||||
|
||||
const [ComponentsPanelProvider, usePanelContext] = createContext<IComponentsPanelContext>({
|
||||
name: 'ComponentsPanelContext',
|
||||
});
|
||||
|
||||
export interface ComponentsPanelProps {
|
||||
/**
|
||||
@@ -36,6 +44,26 @@ export interface ComponentsPanelProps {
|
||||
* @returns
|
||||
*/
|
||||
getBizCompName?: (name: string) => string;
|
||||
/**
|
||||
* 是否局部模式 (快捷添加组件面板中使用)
|
||||
*/
|
||||
isScope?: boolean;
|
||||
/**
|
||||
* 组件选中回调
|
||||
*/
|
||||
onItemSelect?: (name: string) => void;
|
||||
/**
|
||||
* 自定义样式
|
||||
*/
|
||||
style?: React.CSSProperties;
|
||||
/**
|
||||
* tabProps
|
||||
*/
|
||||
tabProps?: TabsProps;
|
||||
/**
|
||||
* 布局模式,默认网格布局
|
||||
*/
|
||||
layout?: 'grid' | 'line';
|
||||
}
|
||||
|
||||
const localeMap = {
|
||||
@@ -72,10 +100,15 @@ export function useFlatMenuData<T>(menuData: T) {
|
||||
|
||||
export const ComponentsPanel = observer(
|
||||
({
|
||||
isScope = false,
|
||||
menuData = emptyMenuData,
|
||||
showBizComps = true,
|
||||
getBizCompName = upperCamelCase,
|
||||
loading = false,
|
||||
style,
|
||||
tabProps,
|
||||
onItemSelect,
|
||||
layout = 'grid',
|
||||
}: ComponentsPanelProps) => {
|
||||
const [keyword, setKeyword] = useState<string>('');
|
||||
const allList = useFlatMenuData<MenuDataType>(menuData);
|
||||
@@ -109,22 +142,44 @@ export const ComponentsPanel = observer(
|
||||
children: <MaterialList data={localCompData} />,
|
||||
});
|
||||
}
|
||||
|
||||
const contentNode =
|
||||
tabs.length === 1 ? (
|
||||
tabs[0].children
|
||||
) : (
|
||||
<Tabs centered isTabBarSticky tabBarStickyOffset={48} items={tabs} />
|
||||
<Tabs
|
||||
size={isScope ? 'small' : 'middle'}
|
||||
centered
|
||||
isTabBarSticky
|
||||
tabBarStickyOffset={48}
|
||||
items={tabs}
|
||||
{...tabProps}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box className="ComponentsView" overflowY="auto">
|
||||
<Box px="l" py="m" position="sticky" top="0" zIndex={1} bg="white">
|
||||
<Search placeholder="搜索物料" onChange={setKeyword} />
|
||||
<ComponentsPanelProvider
|
||||
value={{
|
||||
isScope,
|
||||
onItemSelect,
|
||||
layout,
|
||||
}}
|
||||
>
|
||||
<Box className="ComponentsView" overflowY="auto" style={style}>
|
||||
<Box px="l" py="m" position="sticky" top="0" zIndex={3} bg="white">
|
||||
<Search
|
||||
style={{
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
placeholder="搜索物料"
|
||||
onChange={setKeyword}
|
||||
/>
|
||||
</Box>
|
||||
<Spin spinning={loading} tip="正在加载物料列表...">
|
||||
{!keyword ? contentNode : <MaterialList data={allList} filterKeyword={keyword} />}
|
||||
</Spin>
|
||||
</Box>
|
||||
<Spin spinning={loading} tip="正在加载物料列表...">
|
||||
{!keyword ? contentNode : <MaterialList data={allList} filterKeyword={keyword} />}
|
||||
</Spin>
|
||||
</Box>
|
||||
</ComponentsPanelProvider>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -146,6 +201,9 @@ interface MaterialListProps {
|
||||
|
||||
function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProps) {
|
||||
const workspace = useWorkspace();
|
||||
const { layout } = usePanelContext();
|
||||
const isGrid = layout === 'grid';
|
||||
|
||||
return (
|
||||
<Box className="ComponentsViewList">
|
||||
{data.map((cate) => {
|
||||
@@ -166,15 +224,21 @@ function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProp
|
||||
title={cate.title}
|
||||
borderBottom="solid"
|
||||
borderColor="line.normal"
|
||||
showBottomBorder={false}
|
||||
bodyProps={{
|
||||
padding: isGrid ? '4px 12px 12px' : '0',
|
||||
}}
|
||||
>
|
||||
{!items.length && (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="没有匹配到任何组件" />
|
||||
)}
|
||||
<Grid
|
||||
columns={type === 'localComp' ? 1 : 2}
|
||||
templateColumns={isGrid ? 'repeat(auto-fit,minmax(55px,1fr))' : '1fr'}
|
||||
spacing="1px"
|
||||
bg="background.normal"
|
||||
padding="0"
|
||||
gap={isGrid ? '12px 8px' : '0'}
|
||||
backgroundColor="white"
|
||||
>
|
||||
{items.map((item) => {
|
||||
const prototype = workspace.componentPrototypes.get(item);
|
||||
@@ -202,9 +266,8 @@ const StyledCommonGridItem = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px;
|
||||
cursor: move;
|
||||
justify-content: start;
|
||||
cursor: ${(props) => (props.draggable ? 'grab' : 'pointer')};
|
||||
text-align: center;
|
||||
color: var(--tango-colors-text-body);
|
||||
background-color: #fff;
|
||||
@@ -213,7 +276,23 @@ const StyledCommonGridItem = styled.div`
|
||||
white-space: nowrap;
|
||||
|
||||
.material-icon {
|
||||
font-size: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 36px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ebebeb;
|
||||
width: 100%;
|
||||
height: 52px;
|
||||
position: relative;
|
||||
transition: 0.15s ease-in-out;
|
||||
transition-property: transform;
|
||||
will-change: transform;
|
||||
img {
|
||||
width: unset;
|
||||
height: 85%;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
@@ -225,19 +304,13 @@ const StyledCommonGridItem = styled.div`
|
||||
|
||||
.anticon-question-circle {
|
||||
display: none;
|
||||
font-size: 13px;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 10px rgb(0 0 0 / 10%);
|
||||
|
||||
> span {
|
||||
color: var(--tango-colors-brand);
|
||||
}
|
||||
@@ -245,11 +318,26 @@ const StyledCommonGridItem = styled.div`
|
||||
.anticon-question-circle {
|
||||
display: inline-block;
|
||||
}
|
||||
.material-icon {
|
||||
border-color: #c7c7c7;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const GridLineItemStyle = css`
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--tango-colors-fill2);
|
||||
}
|
||||
`;
|
||||
|
||||
function MaterialGrid({ data }: MaterialProps) {
|
||||
const workspace = useWorkspace();
|
||||
const { isScope, onItemSelect, layout } = usePanelContext();
|
||||
|
||||
const isLine = layout === 'line';
|
||||
|
||||
const handleDragStart = (e: React.DragEvent) => {
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
@@ -266,28 +354,92 @@ function MaterialGrid({ data }: MaterialProps) {
|
||||
workspace.dragSource.clear();
|
||||
};
|
||||
|
||||
const handleSelect = () => {
|
||||
onItemSelect?.(data.name);
|
||||
};
|
||||
|
||||
const icon = data.icon || 'icon-placeholder';
|
||||
|
||||
const iconNode = icon.startsWith('icon-') ? (
|
||||
<IconFont className="material-icon" type={data.icon || 'icon-placeholder'} />
|
||||
) : (
|
||||
<Box className="material-icon">
|
||||
<img src={icon} alt={data.name} />
|
||||
</Box>
|
||||
);
|
||||
|
||||
if (isLine) {
|
||||
return (
|
||||
<Box
|
||||
key={data.name}
|
||||
display="flex"
|
||||
columnGap="m"
|
||||
px="l"
|
||||
py="m"
|
||||
fontSize="12px"
|
||||
css={GridLineItemStyle}
|
||||
onClick={handleSelect}
|
||||
>
|
||||
<Box
|
||||
p="m"
|
||||
fontSize="32px"
|
||||
width="46px"
|
||||
height="46px"
|
||||
background="fill1"
|
||||
border="1px solid"
|
||||
borderRadius="4px"
|
||||
borderColor="line2"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
{iconNode}
|
||||
</Box>
|
||||
<Box flex={1}>
|
||||
<Box fontWeight="500" display="flex" justifyContent="space-between">
|
||||
<Text flex={1}>{data.title}</Text>
|
||||
{data.docs ? (
|
||||
<Popover
|
||||
zIndex={9999}
|
||||
placement="right"
|
||||
title={data.title}
|
||||
content={<CommonMaterialInfoBox docs={data.docs} />}
|
||||
>
|
||||
<QuestionCircleOutlined />
|
||||
</Popover>
|
||||
) : null}
|
||||
</Box>
|
||||
<Box color="text2" fontStyle="italic">
|
||||
{data.help ?? data.title}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledCommonGridItem
|
||||
draggable
|
||||
key={data.name}
|
||||
draggable={!isScope}
|
||||
data-name={data.name}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onClick={handleSelect}
|
||||
>
|
||||
{icon.startsWith('icon-') ? (
|
||||
<IconFont className="material-icon" type={data.icon || 'icon-placeholder'} />
|
||||
) : (
|
||||
<img src={icon} alt={data.name} />
|
||||
)}
|
||||
<Text fontSize="12px" lineHeight="1.5">
|
||||
{data.title}
|
||||
{iconNode}
|
||||
<Text fontSize="12px" marginTop="4px">
|
||||
{data.title ?? data.name}
|
||||
</Text>
|
||||
<Text fontSize="12px" color="gray.50">
|
||||
<Text fontSize="10px" color="gray.50">
|
||||
{data.name}
|
||||
</Text>
|
||||
{data.docs || data.help ? (
|
||||
<Popover placement="right" title={data.title} content={<CommonMaterialInfoBox {...data} />}>
|
||||
<Popover
|
||||
zIndex={9999}
|
||||
placement="right"
|
||||
title={data.title}
|
||||
content={<CommonMaterialInfoBox {...data} />}
|
||||
>
|
||||
<QuestionCircleOutlined />
|
||||
</Popover>
|
||||
) : null}
|
||||
@@ -295,7 +447,7 @@ function MaterialGrid({ data }: MaterialProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function CommonMaterialInfoBox({ help, docs }: IComponentPrototype) {
|
||||
function CommonMaterialInfoBox({ help, docs }: Pick<IComponentPrototype, 'help' | 'docs'>) {
|
||||
return (
|
||||
<Box maxWidth={300}>
|
||||
{!!help && <Box mb="m">{help}</Box>}
|
||||
|
||||
@@ -68,17 +68,6 @@ export const DependencyPanel = observer(
|
||||
[dependencies, baseDependencies, bizDependencies, packages, packageNames],
|
||||
);
|
||||
|
||||
// 业务组件依赖
|
||||
const bizDeps = useMemo(
|
||||
() =>
|
||||
bizDependencies?.map((name: string) => ({
|
||||
name,
|
||||
version: dependencies[name] ?? packages?.[name]?.version,
|
||||
package: packages?.[name],
|
||||
})) || [],
|
||||
[bizDependencies, dependencies, packages],
|
||||
);
|
||||
|
||||
// 基础包
|
||||
const baseDeps = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DropMethod, ITangoViewNodeData } from '@music163/tango-core';
|
||||
import { noop, parseDndId } from '@music163/tango-helpers';
|
||||
import { useSandboxQuery } from '../../context';
|
||||
import { buildQueryBySlotId } from '../../helpers';
|
||||
import { ContextMenu } from '../../components';
|
||||
|
||||
export interface ComponentsTreeProps {
|
||||
/**
|
||||
@@ -143,6 +144,18 @@ export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
|
||||
const file = workspace.activeViewModule;
|
||||
const nodesTree = (file?.nodesTree ?? []) as ITangoViewNodeData[];
|
||||
const [expandedKeys, setExpandedKeys] = useState(getNodeKeys(nodesTree));
|
||||
const [contextMenuOpen, setContextMenuOpen] = useState(false);
|
||||
|
||||
const handleSelect = (keys: React.Key[]) => {
|
||||
const slotKey = keys?.[0] as string;
|
||||
const data = sandboxQuery.getDraggableParentsData(buildQueryBySlotId(slotKey), true);
|
||||
if (data && data.id) {
|
||||
workspace.selectSource.select(data);
|
||||
}
|
||||
// export selected
|
||||
onSelect(slotKey);
|
||||
setSelectedKeys(keys);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedKeys(workspace.selectSource.selected.map((item) => item.id));
|
||||
@@ -164,64 +177,83 @@ export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
|
||||
|
||||
return (
|
||||
<Box css={outlineStyle}>
|
||||
<Tree
|
||||
selectedKeys={selectedKeys as string[]}
|
||||
fieldNames={filedNames}
|
||||
treeData={nodesTree as any[]}
|
||||
onSelect={(keys) => {
|
||||
const slotKey = keys?.[0] as string;
|
||||
const data = sandboxQuery.getDraggableParentsData(buildQueryBySlotId(slotKey), true);
|
||||
if (data && data.id) {
|
||||
workspace.selectSource.select(data);
|
||||
<Dropdown
|
||||
placement="bottomLeft"
|
||||
trigger={['contextMenu']}
|
||||
open={contextMenuOpen}
|
||||
onOpenChange={(val) => {
|
||||
if (!val) {
|
||||
setContextMenuOpen(val);
|
||||
}
|
||||
// export selected
|
||||
onSelect(slotKey);
|
||||
setSelectedKeys(keys);
|
||||
}}
|
||||
blockNode
|
||||
expandedKeys={expandedKeys}
|
||||
onExpand={(keys) => setExpandedKeys(keys as string[])}
|
||||
draggable
|
||||
onDragStart={(data) => {
|
||||
const prototype = workspace.componentPrototypes.get(data.node.component);
|
||||
if (!prototype) {
|
||||
return;
|
||||
}
|
||||
const { canDrag } = prototype.rules || {};
|
||||
if (canDrag && !canDrag()) {
|
||||
return;
|
||||
}
|
||||
workspace.dragSource.set({
|
||||
id: data.node.id,
|
||||
name: data.node.component,
|
||||
});
|
||||
}}
|
||||
onDrop={(data) => {
|
||||
const dropKey = data.node.key as string;
|
||||
let method;
|
||||
if (data.dropToGap) {
|
||||
// 插入节点的后面
|
||||
method = DropMethod.InsertAfter;
|
||||
} else {
|
||||
// 作为第一个子节点
|
||||
method = DropMethod.InsertFirstChild;
|
||||
}
|
||||
workspace.dragSource.dropTarget.set(
|
||||
{
|
||||
id: dropKey,
|
||||
},
|
||||
method,
|
||||
);
|
||||
workspace.dropNode();
|
||||
}}
|
||||
titleRender={(node) => (
|
||||
<OutlineTreeNode
|
||||
actionItems={actionItems}
|
||||
showToggleVisibleIcon={showToggleVisibleIcon}
|
||||
node={node}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
overlay={<ContextMenu onClick={() => setContextMenuOpen(false)} showParents={false} />}
|
||||
>
|
||||
<Tree
|
||||
selectedKeys={selectedKeys as string[]}
|
||||
fieldNames={filedNames}
|
||||
treeData={nodesTree as any[]}
|
||||
onSelect={handleSelect}
|
||||
blockNode
|
||||
expandedKeys={expandedKeys}
|
||||
onExpand={(keys) => setExpandedKeys(keys as string[])}
|
||||
draggable
|
||||
onDragStart={(data) => {
|
||||
const prototype = workspace.componentPrototypes.get(data.node.component);
|
||||
if (!prototype) {
|
||||
return;
|
||||
}
|
||||
const { canDrag } = prototype.rules || {};
|
||||
if (canDrag && !canDrag()) {
|
||||
return;
|
||||
}
|
||||
workspace.dragSource.set({
|
||||
id: data.node.id,
|
||||
name: data.node.component,
|
||||
});
|
||||
}}
|
||||
onDrop={(data) => {
|
||||
const dropKey = data.node.key as string;
|
||||
let method;
|
||||
if (data.dropToGap) {
|
||||
// 插入节点的后面
|
||||
method = DropMethod.InsertAfter;
|
||||
} else {
|
||||
// 作为第一个子节点
|
||||
method = DropMethod.InsertFirstChild;
|
||||
}
|
||||
workspace.dragSource.dropTarget.set(
|
||||
{
|
||||
id: dropKey,
|
||||
},
|
||||
method,
|
||||
);
|
||||
workspace.dropNode();
|
||||
}}
|
||||
onRightClick={({ event, node }) => {
|
||||
// 按下其他按键时,视为用户有特殊操作,此时不展示右键菜单
|
||||
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) {
|
||||
setContextMenuOpen(false);
|
||||
return;
|
||||
}
|
||||
// 顶层的 Page 组件不展示右键菜单
|
||||
if (node.component === 'Page') {
|
||||
setContextMenuOpen(false);
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
// 由于部分操作是基于 selectSource 实现的,因此右键时默认选中当前项
|
||||
handleSelect([node.key]);
|
||||
setContextMenuOpen(true);
|
||||
}}
|
||||
titleRender={(node) => (
|
||||
<OutlineTreeNode
|
||||
actionItems={actionItems}
|
||||
showToggleVisibleIcon={showToggleVisibleIcon}
|
||||
node={node}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Dropdown>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -30,6 +30,7 @@ export const StateTree = observer(() => {
|
||||
'pageStore',
|
||||
'services',
|
||||
'config',
|
||||
'routeData',
|
||||
]);
|
||||
const callback = () => {
|
||||
forceUpdate();
|
||||
|
||||
@@ -74,7 +74,7 @@ export interface SidebarPanelItemProps
|
||||
widgetProps?: object;
|
||||
}
|
||||
|
||||
function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 280, footer, children }: SidebarProps) {
|
||||
function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 266, footer, children }: SidebarProps) {
|
||||
const designer = useDesigner();
|
||||
|
||||
const items = useMemo(() => {
|
||||
|
||||
@@ -29,7 +29,7 @@ export const VariablePanel = observer(
|
||||
新建模型
|
||||
</Button>
|
||||
}
|
||||
bodyProps={{ p: 'm' }}
|
||||
bodyProps={{ p: 'm', height: '100%' }}
|
||||
>
|
||||
{isAdd ? (
|
||||
<AddStoreForm
|
||||
@@ -42,6 +42,7 @@ export const VariablePanel = observer(
|
||||
/>
|
||||
) : (
|
||||
<VariableTree
|
||||
height="100%"
|
||||
defaultValueDetailMode="define"
|
||||
dataSource={storeVariables}
|
||||
onAddStoreVariable={(storeName, data) => {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
import styled, { css, keyframes } from 'styled-components';
|
||||
import { Box, Button, Group, HTMLCoralProps } from 'coral-system';
|
||||
import { Dropdown, DropdownProps, Tooltip } from 'antd';
|
||||
import { Tooltip } from 'antd';
|
||||
import { HolderOutlined, InfoCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { ISelectedItemData, isString, noop } from '@music163/tango-helpers';
|
||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
import { IconFont } from '@music163/tango-ui';
|
||||
import { getDragGhostElement } from '../helpers';
|
||||
import { getWidget } from '../widgets';
|
||||
import { ComponentsPopover } from '../components';
|
||||
|
||||
/**
|
||||
* 选择辅助工具的对齐方式
|
||||
@@ -24,7 +24,15 @@ export interface SelectionToolsProps {
|
||||
}
|
||||
|
||||
export const SelectionTools = observer(
|
||||
({ actions: actionsProp = ['viewSource', 'copyNode', 'deleteNode'] }: SelectionToolsProps) => {
|
||||
({
|
||||
actions: actionsProp = [
|
||||
'selectParentNode',
|
||||
'viewSource',
|
||||
'copyNode',
|
||||
'deleteNode',
|
||||
'moreActions',
|
||||
],
|
||||
}: SelectionToolsProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const selectSource = workspace.selectSource;
|
||||
const actions = actionsProp.map((item) => {
|
||||
@@ -79,13 +87,6 @@ const bottomAddSiblingBtnStyle = css`
|
||||
pointer-events: auto;
|
||||
`;
|
||||
|
||||
interface IInsertedData {
|
||||
name: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface SelectionBoxProps {
|
||||
/**
|
||||
* 是否显示操作按钮
|
||||
@@ -117,36 +118,6 @@ function SelectionBox({ showActions, actions, data }: SelectionBoxProps) {
|
||||
const prototype = workspace.componentPrototypes.get(data.name);
|
||||
const isPage = prototype?.type === 'page';
|
||||
|
||||
// 如果声明了 childrenName,提供快捷子元素创建入口
|
||||
let insertedList: IInsertedData[] = [];
|
||||
if (prototype?.childrenName) {
|
||||
const names = Array.isArray(prototype?.childrenName)
|
||||
? prototype.childrenName
|
||||
: [prototype.childrenName];
|
||||
insertedList = names.map((child) => {
|
||||
const proto = workspace.componentPrototypes.get(child);
|
||||
return {
|
||||
name: child,
|
||||
label: proto?.title || child,
|
||||
icon: proto?.icon,
|
||||
description: proto?.help,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
let siblingList: IInsertedData[] = [];
|
||||
if (prototype?.siblingNames) {
|
||||
siblingList = prototype.siblingNames?.map((item) => {
|
||||
const proto = workspace.componentPrototypes.get(item);
|
||||
return {
|
||||
name: item,
|
||||
label: proto?.title || item,
|
||||
icon: proto?.icon,
|
||||
description: proto?.help,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
let selectionHelpersAlign: SelectionHelperAlignType = 'top-right';
|
||||
if (data.bounding) {
|
||||
if (data.bounding.left + data.bounding.width + boundingOffset < designer.viewport.width) {
|
||||
@@ -159,6 +130,7 @@ function SelectionBox({ showActions, actions, data }: SelectionBoxProps) {
|
||||
}
|
||||
|
||||
const isFromCurrentFile = data.filename === workspace.activeViewFile;
|
||||
const selectedNodeName = workspace.selectSource?.selected?.[0]?.codeId ?? '未选中';
|
||||
|
||||
let style: React.CSSProperties;
|
||||
if (data.bounding) {
|
||||
@@ -183,30 +155,18 @@ function SelectionBox({ showActions, actions, data }: SelectionBoxProps) {
|
||||
css={selectionBoxStyle}
|
||||
style={style}
|
||||
>
|
||||
{siblingList.length > 0 ? (
|
||||
{!isPage ? (
|
||||
<>
|
||||
<InsertedDropdown
|
||||
title="在当前节点的前方添加兄弟节点"
|
||||
options={siblingList}
|
||||
onSelect={(name) => {
|
||||
workspace.insertBeforeSelectedNode(name);
|
||||
}}
|
||||
>
|
||||
<Tooltip title="在当前节点的前方添加兄弟节点">
|
||||
<ComponentsPopover type="before">
|
||||
<Tooltip title={`在 ${selectedNodeName} 的前方添加兄弟节点`}>
|
||||
<SelectionHelper icon={<PlusOutlined />} css={topAddSiblingBtnStyle} />
|
||||
</Tooltip>
|
||||
</InsertedDropdown>
|
||||
<InsertedDropdown
|
||||
title="在当前节点的后方添加兄弟节点"
|
||||
options={siblingList}
|
||||
onSelect={(name) => {
|
||||
workspace.insertAfterSelectedNode(name);
|
||||
}}
|
||||
>
|
||||
<Tooltip title="在当前节点的后方添加兄弟节点">
|
||||
</ComponentsPopover>
|
||||
<ComponentsPopover type="after">
|
||||
<Tooltip title={`在 ${selectedNodeName} 的后方添加兄弟节点`}>
|
||||
<SelectionHelper icon={<PlusOutlined />} css={bottomAddSiblingBtnStyle} />
|
||||
</Tooltip>
|
||||
</InsertedDropdown>
|
||||
</ComponentsPopover>
|
||||
</>
|
||||
) : null}
|
||||
{showActions && (
|
||||
@@ -246,17 +206,12 @@ function SelectionBox({ showActions, actions, data }: SelectionBoxProps) {
|
||||
}
|
||||
/>
|
||||
<SelectionToolSet>{!isPage && actions}</SelectionToolSet>
|
||||
{insertedList.length > 0 && (
|
||||
<InsertedDropdown
|
||||
options={insertedList}
|
||||
onSelect={(name) => {
|
||||
workspace.insertToSelectedNode(name);
|
||||
}}
|
||||
>
|
||||
{isFromCurrentFile && prototype?.hasChildren && (
|
||||
<ComponentsPopover>
|
||||
<Tooltip title="快捷添加子元素">
|
||||
<SelectionHelper icon={<PlusOutlined />} />
|
||||
</Tooltip>
|
||||
</InsertedDropdown>
|
||||
</ComponentsPopover>
|
||||
)}
|
||||
</SelectionHelpers>
|
||||
)}
|
||||
@@ -429,105 +384,3 @@ const NameSelector = ({ label, parents = [], onSelect = noop }: NameSelectorProp
|
||||
</NameSelectorWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
interface InsertedDropdownProps extends DropdownProps {
|
||||
title?: string;
|
||||
options?: IInsertedData[];
|
||||
onSelect?: (name: string) => void;
|
||||
}
|
||||
|
||||
function InsertedDropdown({
|
||||
title = '为当前节点添加子元素',
|
||||
options = [],
|
||||
onSelect,
|
||||
...props
|
||||
}: InsertedDropdownProps) {
|
||||
return (
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
dropdownRender={() => {
|
||||
return (
|
||||
<Box
|
||||
bg="#FFF"
|
||||
borderRadius="m"
|
||||
boxShadow="lowDown"
|
||||
border="solid"
|
||||
borderColor="line2"
|
||||
overflow="hidden"
|
||||
width="320px"
|
||||
>
|
||||
<Box px="l" py="m" color="text2">
|
||||
{title}
|
||||
</Box>
|
||||
<Box maxHeight={360} overflowY="auto">
|
||||
{options.map((item) => (
|
||||
<InsertedItem
|
||||
key={item.name}
|
||||
label={item.label}
|
||||
icon={item.icon}
|
||||
description={item.description || '暂无组件描述'}
|
||||
onClick={() => onSelect?.(item.name)}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const insertedItemStyle = css`
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--tango-colors-fill2);
|
||||
}
|
||||
`;
|
||||
|
||||
function InsertedItem({
|
||||
label,
|
||||
icon,
|
||||
description,
|
||||
...rest
|
||||
}: HTMLCoralProps<'div'> & Omit<IInsertedData, 'name'>) {
|
||||
let iconNode;
|
||||
if (!icon) {
|
||||
iconNode = <IconFont className="material-icon" type="icon-placeholder" />;
|
||||
} else if (icon.startsWith('icon-')) {
|
||||
iconNode = <IconFont className="material-icon" type={icon} />;
|
||||
} else {
|
||||
iconNode = <img src={icon} alt={label} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
columnGap="m"
|
||||
px="l"
|
||||
py="m"
|
||||
fontSize="12px"
|
||||
css={insertedItemStyle}
|
||||
{...rest}
|
||||
>
|
||||
<Box
|
||||
size="35px"
|
||||
fontSize="32px"
|
||||
background="fill1"
|
||||
border="1px solid"
|
||||
borderColor="line2"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
{iconNode}
|
||||
</Box>
|
||||
<Box>
|
||||
<Box fontWeight="500">{label}</Box>
|
||||
<Box color="text2">{description}</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,61 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { Box, HTMLCoralProps } from 'coral-system';
|
||||
import { useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
import { Ghost } from './ghost';
|
||||
import { useSandboxQuery } from '../context';
|
||||
import { SelectionTools, SelectionToolsProps } from './selection';
|
||||
import { InsertionPrompt } from './insertion';
|
||||
import { DraggingMask } from './mask';
|
||||
import { Dropdown, DropdownProps } from 'antd';
|
||||
import { ContextMenu } from '../components';
|
||||
|
||||
export interface ViewportProps extends HTMLCoralProps<'div'> {
|
||||
selectionTools?: SelectionToolsProps['actions'];
|
||||
}
|
||||
|
||||
const ViewportContextMenu = observer((props: DropdownProps) => {
|
||||
const designer = useDesigner();
|
||||
|
||||
// FIXME: 考虑优化定位实现?
|
||||
// 由于 iframe 内的 contextmenu 事件无法被外层感知到,所以无法自动展示与定位
|
||||
// 而手动指定 align 会干扰 contextmenu 溢出时的位置处理,会定位到完全相反的位置
|
||||
// 因此,给 dropdown 单独提供一个定位容器,将其定位至 iframe 内触发点击的位置
|
||||
// 并在每次 contextmenu 隐藏时销毁 dropdown,让 dropdown 能重新获取新的位置
|
||||
return (
|
||||
designer.showContextMenu && (
|
||||
<Dropdown
|
||||
autoAdjustOverflow
|
||||
trigger={['contextMenu']}
|
||||
placement="bottomLeft"
|
||||
open
|
||||
onOpenChange={(val) => {
|
||||
if (!val) {
|
||||
designer.toggleContextMenu(val);
|
||||
}
|
||||
}}
|
||||
overlay={<ContextMenu onClick={() => designer.toggleContextMenu(false)} showParents />}
|
||||
{...props}
|
||||
>
|
||||
<Box
|
||||
width={0}
|
||||
height={0}
|
||||
position="absolute"
|
||||
style={{
|
||||
left: designer.contextMenuPosition.clientX,
|
||||
top: designer.contextMenuPosition.clientY,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
</Dropdown>
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
export function Viewport({ selectionTools, children, className, ...rest }: ViewportProps) {
|
||||
return (
|
||||
<Box position="relative" height="100%" className={cx('Viewport', className)} {...rest}>
|
||||
<ViewportContextMenu />
|
||||
<ViewportBody>{children}</ViewportBody>
|
||||
<div className="AuxTools">
|
||||
<SelectionTools actions={selectionTools} />
|
||||
|
||||
@@ -17,7 +17,7 @@ export const PreviewTool = observer(() => {
|
||||
designer.setActiveView('design');
|
||||
}
|
||||
}}
|
||||
tooltip="预览"
|
||||
tooltip={designer.isPreview ? '切换到设计模式' : '切换到预览模式'}
|
||||
>
|
||||
<EyeOutlined />
|
||||
</ToggleButton>
|
||||
|
||||
@@ -60,7 +60,7 @@ export function Toolbar({ children }: ToolbarProps) {
|
||||
<Group display="flex" alignItems="center" gap="m">
|
||||
{centerTools}
|
||||
</Group>
|
||||
<Group display="flex" alignItems="center" gap="m">
|
||||
<Group display="flex" alignItems="center" justifyContent="flex-end" gap="m">
|
||||
{rightTools}
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
@@ -13,14 +13,26 @@ import {
|
||||
OutlinePanel,
|
||||
VariablePanel,
|
||||
} from './sidebar';
|
||||
import { CopyNodeAction, DeleteNodeAction, ViewSourceAction } from './selection-menu';
|
||||
import {
|
||||
CopyNodeAction,
|
||||
DeleteNodeAction,
|
||||
ViewSourceAction,
|
||||
SelectParentNodeAction,
|
||||
MoreActionsAction,
|
||||
} from './selection-menu';
|
||||
import {
|
||||
CopyNodeContextAction,
|
||||
DeleteNodeContextAction,
|
||||
PasteNodeContextAction,
|
||||
ViewSourceContextAction,
|
||||
} from './context-menu';
|
||||
|
||||
const widgets = {};
|
||||
|
||||
export function registerWidget(key: string, component: React.ComponentType<any>) {
|
||||
if (!/^(toolbar|sidebar|selectionMenu)\.[a-zA-z]+$/.test(key)) {
|
||||
if (!/^(toolbar|sidebar|selectionMenu|contextMenu)\.[a-zA-z]+$/.test(key)) {
|
||||
throw new Error(
|
||||
`Invalid widget key(${key}), should start with toolbar, sidebar, or selection-menu`,
|
||||
`Invalid widget key(${key}), should start with toolbar, sidebar, contextMenu or selectionMenu`,
|
||||
);
|
||||
}
|
||||
setValue(widgets, key, component);
|
||||
@@ -48,4 +60,11 @@ registerWidget('sidebar.dataSource', DataSourcePanel);
|
||||
|
||||
registerWidget('selectionMenu.copyNode', CopyNodeAction);
|
||||
registerWidget('selectionMenu.deleteNode', DeleteNodeAction);
|
||||
registerWidget('selectionMenu.selectParentNode', SelectParentNodeAction);
|
||||
registerWidget('selectionMenu.viewSource', ViewSourceAction);
|
||||
registerWidget('selectionMenu.moreActions', MoreActionsAction);
|
||||
|
||||
registerWidget('contextMenu.viewSource', ViewSourceContextAction);
|
||||
registerWidget('contextMenu.copyNode', CopyNodeContextAction);
|
||||
registerWidget('contextMenu.pasteNode', PasteNodeContextAction);
|
||||
registerWidget('contextMenu.deleteNode', DeleteNodeContextAction);
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { observer, useDesigner } from '@music163/tango-context';
|
||||
import { DesignerViewType } from '@music163/tango-core';
|
||||
import { ComponentsPopover } from './components';
|
||||
|
||||
export interface WorkspaceViewProps {
|
||||
/**
|
||||
@@ -27,6 +28,8 @@ export const WorkspaceView = observer((props: WorkspaceViewProps) => {
|
||||
position="relative"
|
||||
>
|
||||
{children}
|
||||
{/* 添加组件弹层 */}
|
||||
{display === 'block' && <ComponentsPopover type="inner" isControlled />}
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [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
|
||||
|
||||
- add context menu ([#161](https://github.com/netease/tango/issues/161)) ([28040fc](https://github.com/netease/tango/commit/28040fc00604339a40ad3216b76baf7de93a13e0))
|
||||
|
||||
## [1.1.1](https://github.com/netease/tango/compare/@music163/tango-helpers@1.1.0...@music163/tango-helpers@1.1.1) (2024-05-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- support add components with popover ([#155](https://github.com/netease/tango/issues/155)) ([f17ccbb](https://github.com/netease/tango/commit/f17ccbb7f645f8047ecd96d9f3f2185048a3b726))
|
||||
|
||||
# [1.1.0](https://github.com/netease/tango/compare/@music163/tango-helpers@1.0.0...@music163/tango-helpers@1.1.0) (2024-05-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-helpers",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "Shared types, helpers, and hooks of tango-apps",
|
||||
"keywords": [
|
||||
"shared",
|
||||
|
||||
@@ -68,3 +68,10 @@ export function isServiceVariablePath(key: string) {
|
||||
export function isInTangoDesignMode() {
|
||||
return !!(window as any).__TANGO_DESIGNER__;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是 macOS 或 iOS like 设备
|
||||
*/
|
||||
export function isApplePlatform() {
|
||||
return /Mac|iPhone|iPad|iPod/i.test(navigator.platform);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import { OptionType } from './advanced';
|
||||
import { PartialRecord } from './base';
|
||||
|
||||
/**
|
||||
* @deprecated 请使用 IComponentProp 代替
|
||||
@@ -327,3 +328,19 @@ export interface ITangoConfigJson {
|
||||
autoGenerateComponentId: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 物料类型
|
||||
common: '基础组件',
|
||||
atom: '原子组件',
|
||||
snippet: '组合',
|
||||
bizComp: '业务组件',
|
||||
localComp: '本地组件',
|
||||
*/
|
||||
export type MenuKeyType = 'common' | 'atom' | 'snippet' | 'bizComp' | 'localComp';
|
||||
export type MenuValueType = Array<{ title: string; items: string[] }>;
|
||||
|
||||
/**
|
||||
* 菜单项类型
|
||||
*/
|
||||
export type MenuDataType = PartialRecord<MenuKeyType, MenuValueType>;
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.8](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.7...@music163/tango-sandbox@1.0.8) (2024-07-12)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
## [1.0.7](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.6...@music163/tango-sandbox@1.0.7) (2024-06-20)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
## [1.0.6](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.5...@music163/tango-sandbox@1.0.6) (2024-06-05)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
## [1.0.5](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.4...@music163/tango-sandbox@1.0.5) (2024-06-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- use addEventListener to bind iframe event handlers ([#169](https://github.com/netease/tango/issues/169)) ([c1306e2](https://github.com/netease/tango/commit/c1306e24e0c747d0f4bd845dc1714488ddd8cdac))
|
||||
|
||||
## [1.0.4](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.3...@music163/tango-sandbox@1.0.4) (2024-05-21)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
## [1.0.3](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.2...@music163/tango-sandbox@1.0.3) (2024-05-17)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-sandbox",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.8",
|
||||
"description": "sandbox of tango apps",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"homepage": "",
|
||||
@@ -29,8 +29,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/tango-core": "^1.1.0",
|
||||
"@music163/tango-helpers": "^1.1.0",
|
||||
"@music163/tango-core": "^1.3.3",
|
||||
"@music163/tango-helpers": "^1.2.0",
|
||||
"crypto-js": "^4.1.1",
|
||||
"lodash.isequal": "4.5.0",
|
||||
"react-frame-component": "^5.2.4"
|
||||
|
||||
@@ -3,7 +3,13 @@ import { TangoEventName } from '@music163/tango-helpers';
|
||||
import Loading from './loading';
|
||||
import Manager from './manager';
|
||||
import { createMissingPackageJSON, changeRoute } from './helper';
|
||||
import { IFiles, CodeSandboxState, CodeSandboxProps, UnsubscribeFunction } from '../types';
|
||||
import {
|
||||
IFiles,
|
||||
CodeSandboxState,
|
||||
CodeSandboxProps,
|
||||
UnsubscribeFunction,
|
||||
EventHandlers,
|
||||
} from '../types';
|
||||
|
||||
// CodeSandbox 使用示例
|
||||
// <CodeSandbox files={files} entry={entry} template={template} bundlerURL={`${window.location.protocol}//codesandbox.fn.netease.com/`} />
|
||||
@@ -32,6 +38,8 @@ export class CodeSandbox extends React.Component<CodeSandboxProps, CodeSandboxSt
|
||||
|
||||
iframe: HTMLIFrameElement;
|
||||
|
||||
private eventHandlers: EventHandlers;
|
||||
|
||||
constructor(props: CodeSandboxProps) {
|
||||
super(props);
|
||||
|
||||
@@ -75,11 +83,8 @@ export class CodeSandbox extends React.Component<CodeSandboxProps, CodeSandboxSt
|
||||
this.manager.cleanup();
|
||||
|
||||
// clear tango event
|
||||
if (this.iframe && this.props?.eventHandlers?.onTango) {
|
||||
this.iframe.contentDocument?.removeEventListener(
|
||||
TangoEventName.DesignerAction,
|
||||
this.props.eventHandlers.onTango,
|
||||
);
|
||||
if (this.iframe && this.eventHandlers) {
|
||||
this.unbindEvents(this.eventHandlers);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +121,29 @@ export class CodeSandbox extends React.Component<CodeSandboxProps, CodeSandboxSt
|
||||
externalResources: this.props.externalResources,
|
||||
});
|
||||
|
||||
normalizeEventType = (eventType: string) =>
|
||||
eventType === 'onTango'
|
||||
? TangoEventName.DesignerAction
|
||||
: eventType.replace(/^on/, '').toLowerCase();
|
||||
|
||||
bindEvents = (eventHandlers: EventHandlers) => {
|
||||
Object.keys(eventHandlers).forEach((eventType) => {
|
||||
this.iframe?.contentDocument?.addEventListener(
|
||||
this.normalizeEventType(eventType),
|
||||
eventHandlers[eventType],
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
unbindEvents = (eventHandlers: EventHandlers) => {
|
||||
Object.keys(eventHandlers).forEach((eventType) => {
|
||||
this.iframe?.contentDocument?.removeEventListener(
|
||||
this.normalizeEventType(eventType),
|
||||
eventHandlers[eventType],
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// Manager 实例化,即开始构建页面
|
||||
// 将 el 传给 Manager 作为实例化参数,当构建完成后,会将构建的页面 dom 设置给 el
|
||||
// 注:Manager是一个管理者的角色,从大局上把控整个转译和执行的流程
|
||||
@@ -155,17 +183,11 @@ export class CodeSandbox extends React.Component<CodeSandboxProps, CodeSandboxSt
|
||||
|
||||
// 注册监听函数
|
||||
const { eventHandlers } = this.props;
|
||||
Object.keys(eventHandlers).forEach((eventType) => {
|
||||
if (el.contentDocument) {
|
||||
el.contentDocument[eventType.toLowerCase()] = eventHandlers[eventType];
|
||||
}
|
||||
if (eventType === 'onTango') {
|
||||
el.contentDocument?.addEventListener(
|
||||
TangoEventName.DesignerAction,
|
||||
eventHandlers[eventType],
|
||||
);
|
||||
}
|
||||
});
|
||||
if (eventHandlers) {
|
||||
this.bindEvents(eventHandlers);
|
||||
// 保存一份当前的 eventHandlers,避免 props 变更导致无法取消现在已注册的事件绑定
|
||||
this.eventHandlers = eventHandlers;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,6 +3,43 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.2.8](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.7...@music163/tango-setting-form@1.2.8) (2024-07-12)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
## [1.2.7](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.6...@music163/tango-setting-form@1.2.7) (2024-06-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update designer style ([#176](https://github.com/netease/tango/issues/176)) ([0f3f0af](https://github.com/netease/tango/commit/0f3f0afdfa8aee2532a97c5c2e92ef4230397d86))
|
||||
|
||||
## [1.2.6](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.5...@music163/tango-setting-form@1.2.6) (2024-06-05)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
## [1.2.5](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.4...@music163/tango-setting-form@1.2.5) (2024-06-03)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
## [1.2.4](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.3...@music163/tango-setting-form@1.2.4) (2024-05-30)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
## [1.2.3](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.2...@music163/tango-setting-form@1.2.3) (2024-05-27)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
## [1.2.2](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.1...@music163/tango-setting-form@1.2.2) (2024-05-22)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
## [1.2.1](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.0...@music163/tango-setting-form@1.2.1) (2024-05-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- enhance code value validate in SettingForm ([#152](https://github.com/netease/tango/issues/152)) ([791fbb1](https://github.com/netease/tango/commit/791fbb162a7147243924e01f54e9c0b586f14438))
|
||||
- prototype2code & go back history error ([#156](https://github.com/netease/tango/issues/156)) ([8bf53a7](https://github.com/netease/tango/commit/8bf53a76f8a71eaf261ea68b9ee44e5bf19893aa))
|
||||
|
||||
# [1.2.0](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.1.0...@music163/tango-setting-form@1.2.0) (2024-05-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-setting-form",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.8",
|
||||
"description": "setting form of tango-apps",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"homepage": "",
|
||||
@@ -28,9 +28,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/tango-core": "^1.1.0",
|
||||
"@music163/tango-helpers": "^1.1.0",
|
||||
"@music163/tango-ui": "^1.1.0",
|
||||
"@music163/tango-core": "^1.3.3",
|
||||
"@music163/tango-helpers": "^1.2.0",
|
||||
"@music163/tango-ui": "^1.3.3",
|
||||
"antd": "^4.24.2",
|
||||
"coral-system": "^1.0.5",
|
||||
"mobx": "6.12.3",
|
||||
|
||||
@@ -88,7 +88,12 @@ function parseFieldValue(fieldValue: any) {
|
||||
const isCodeString = isString(fieldValue) && isWrappedCode(fieldValue);
|
||||
if (isCodeString) {
|
||||
code = getCodeOfWrappedCode(fieldValue);
|
||||
value = code2value(code);
|
||||
try {
|
||||
// 避免 code 报错的情况
|
||||
value = code2value(code);
|
||||
} catch (err) {
|
||||
// do nothing
|
||||
}
|
||||
} else {
|
||||
code = value2code(fieldValue);
|
||||
value = fieldValue;
|
||||
@@ -188,7 +193,7 @@ export function createFormItem(options: IFormItemCreateOptions) {
|
||||
extra,
|
||||
footer,
|
||||
noStyle,
|
||||
validate,
|
||||
validate = options.validate,
|
||||
}: FormItemProps) {
|
||||
const { disableSwitchExpressionSetter, showItemSubtitle } = useFormVariable();
|
||||
const model = useFormModel();
|
||||
@@ -202,14 +207,15 @@ export function createFormItem(options: IFormItemCreateOptions) {
|
||||
});
|
||||
|
||||
field.setConfig({
|
||||
validate: validate || options.validate,
|
||||
validate: setter === 'codeSetter' ? getSetter('codeSetter').config.validate : validate,
|
||||
});
|
||||
|
||||
let baseComponentProps: FormItemComponentProps = {
|
||||
value: setterValue,
|
||||
defaultValue,
|
||||
onChange(value, detail) {
|
||||
onChange(value, detail = {}) {
|
||||
if ((setterType === 'code' || isCodeSetter) && isString(value) && value) {
|
||||
detail.rawCode = value; // 在 detail 中记录原始的 code
|
||||
value = wrapCode(value);
|
||||
}
|
||||
field.setValue(value, detail);
|
||||
@@ -296,14 +302,24 @@ export function createFormItem(options: IFormItemCreateOptions) {
|
||||
// 已注册的 setter 查找表
|
||||
const REGISTERED_FORM_ITEM_MAP: Record<string, ReturnType<typeof createFormItem>> = {};
|
||||
|
||||
/**
|
||||
* 获取已注册的 setter
|
||||
* @param name
|
||||
* @returns
|
||||
*/
|
||||
export function getSetter(name: string) {
|
||||
return REGISTERED_FORM_ITEM_MAP[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter 注册
|
||||
* @param config 注册选项
|
||||
*/
|
||||
export function register(config: IFormItemCreateOptions) {
|
||||
// 允许直接覆盖同名 setter
|
||||
REGISTERED_FORM_ITEM_MAP[config.name] = createFormItem(config);
|
||||
(Array.isArray(config.alias) ? config.alias : []).forEach((alias) => {
|
||||
REGISTERED_FORM_ITEM_MAP[alias] = REGISTERED_FORM_ITEM_MAP[config.name];
|
||||
REGISTERED_FORM_ITEM_MAP[alias] = getSetter(config.name);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -249,14 +249,22 @@ interface FormHeaderProps {
|
||||
export function FormHeader({ title, extra, subTitle }: FormHeaderProps) {
|
||||
return (
|
||||
<Box className="FormHeader">
|
||||
<Box display="flex" alignItems="center">
|
||||
<Box flex="1" display="flex" alignItems="center">
|
||||
<Box fontSize="16px" fontWeight="500" mr="s">
|
||||
<Box display="flex" alignItems="center" className="FormHeaderMain">
|
||||
<Box flex="1" display="flex" alignItems="center" className="FormHeaderMainBody">
|
||||
<Box
|
||||
fontSize="16px"
|
||||
fontWeight="500"
|
||||
mr="s"
|
||||
whiteSpace="nowrap"
|
||||
className="FormHeaderTitle"
|
||||
textOverflow="ellipsis"
|
||||
overflow="hidden"
|
||||
>
|
||||
{title}
|
||||
</Box>
|
||||
{subTitle && <Box>{subTitle}</Box>}
|
||||
{subTitle && <Box className="FormHeaderSubTitle">{subTitle}</Box>}
|
||||
</Box>
|
||||
{extra && <Box>{extra}</Box>}
|
||||
{extra && <Box className="FormHeaderExtra">{extra}</Box>}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -235,6 +235,7 @@ export function SettingForm({
|
||||
icon={<QuestionCircleOutlined />}
|
||||
tooltip="查看组件文档"
|
||||
href={prototype.docs}
|
||||
size="small"
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
+16
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import { CodeSetter } from './code-setter';
|
||||
import { BoolSetter } from './bool-setter';
|
||||
import { IdSetter } from './id-setter';
|
||||
@@ -36,9 +37,24 @@ const BASIC_SETTERS: IFormItemCreateOptions[] = [
|
||||
name: 'idSetter',
|
||||
component: IdSetter,
|
||||
},
|
||||
{
|
||||
name: 'imageSetter',
|
||||
render: (props) => <TextSetter placeholder="请提供图片地址" {...props} />,
|
||||
},
|
||||
];
|
||||
|
||||
let registered = false;
|
||||
|
||||
/**
|
||||
* 注册内置的基础类型 Setter
|
||||
*/
|
||||
export function registerBuiltinSetters() {
|
||||
if (registered) {
|
||||
// 防止重复注册
|
||||
return;
|
||||
}
|
||||
|
||||
// 预注册基础 Setter
|
||||
BASIC_SETTERS.forEach(register);
|
||||
registered = true;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface ISetterOnChangeCallbackDetail {
|
||||
relatedImports?: string[];
|
||||
isRawCode?: boolean;
|
||||
rawCode?: string;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,57 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.3.3](https://github.com/netease/tango/compare/@music163/tango-ui@1.3.2...@music163/tango-ui@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))
|
||||
|
||||
## [1.3.2](https://github.com/netease/tango/compare/@music163/tango-ui@1.3.1...@music163/tango-ui@1.3.2) (2024-06-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- drag-panel add resizeable & height prop ([#175](https://github.com/netease/tango/issues/175)) ([b53a9f7](https://github.com/netease/tango/commit/b53a9f7eefc748ba8a7c94270c3a374b9d6ed211))
|
||||
- update designer style ([#176](https://github.com/netease/tango/issues/176)) ([0f3f0af](https://github.com/netease/tango/commit/0f3f0afdfa8aee2532a97c5c2e92ef4230397d86))
|
||||
|
||||
## [1.3.1](https://github.com/netease/tango/compare/@music163/tango-ui@1.3.0...@music163/tango-ui@1.3.1) (2024-06-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix context menu error on macOS ([#170](https://github.com/netease/tango/issues/170)) ([ca740c8](https://github.com/netease/tango/commit/ca740c83ae7f72a75c7db90d2e137a7e9a032326))
|
||||
|
||||
# [1.3.0](https://github.com/netease/tango/compare/@music163/tango-ui@1.2.3...@music163/tango-ui@1.3.0) (2024-06-03)
|
||||
|
||||
### Features
|
||||
|
||||
- add context menu ([#161](https://github.com/netease/tango/issues/161)) ([28040fc](https://github.com/netease/tango/commit/28040fc00604339a40ad3216b76baf7de93a13e0))
|
||||
|
||||
## [1.2.3](https://github.com/netease/tango/compare/@music163/tango-ui@1.2.2...@music163/tango-ui@1.2.3) (2024-05-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- popover re-click position bug & drag panel drag bug ([#165](https://github.com/netease/tango/issues/165)) ([f83c6bc](https://github.com/netease/tango/commit/f83c6bc0f69820582720512eefedc9ddf5db2975))
|
||||
|
||||
## [1.2.2](https://github.com/netease/tango/compare/@music163/tango-ui@1.2.1...@music163/tango-ui@1.2.2) (2024-05-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- export usePreviewSandboxQuery & add builtin sandboxQuery ([#163](https://github.com/netease/tango/issues/163)) ([163eced](https://github.com/netease/tango/commit/163ecedff3ed7baee59e200a8cc60dcc63a24e48))
|
||||
|
||||
## [1.2.1](https://github.com/netease/tango/compare/@music163/tango-ui@1.2.0...@music163/tango-ui@1.2.1) (2024-05-22)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-ui
|
||||
|
||||
# [1.2.0](https://github.com/netease/tango/compare/@music163/tango-ui@1.1.0...@music163/tango-ui@1.2.0) (2024-05-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- support add components with popover ([#155](https://github.com/netease/tango/issues/155)) ([f17ccbb](https://github.com/netease/tango/commit/f17ccbb7f645f8047ecd96d9f3f2185048a3b726))
|
||||
|
||||
### Features
|
||||
|
||||
- event-setter & expression-setter use popover ([#159](https://github.com/netease/tango/issues/159)) ([9daf387](https://github.com/netease/tango/commit/9daf3872e743fcf706184877020bcbf1f75ffd25))
|
||||
|
||||
# [1.1.0](https://github.com/netease/tango/compare/@music163/tango-ui@1.0.3...@music163/tango-ui@1.1.0) (2024-05-17)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-ui",
|
||||
"version": "1.1.0",
|
||||
"version": "1.3.3",
|
||||
"description": "ui widgets of tango",
|
||||
"keywords": [
|
||||
"react",
|
||||
@@ -38,14 +38,15 @@
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
"@codemirror/lint": "^6.7.1",
|
||||
"@codemirror/search": "^6.5.6",
|
||||
"@music163/tango-helpers": "^1.1.0",
|
||||
"@music163/tango-helpers": "^1.2.0",
|
||||
"@uiw/react-codemirror": "^4.22.0",
|
||||
"antd": "^4.24.2",
|
||||
"classnames": "^2.5.1",
|
||||
"coral-system": "^1.0.5",
|
||||
"eslint-linter-browserify": "^8.51.0",
|
||||
"react-draggable": "^4.4.5",
|
||||
"react-json-view": "^1.21.3",
|
||||
"react-monaco-editor-lite": "^1.3.9"
|
||||
"react-monaco-editor-lite": "^1.3.11"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Button, Dropdown, Input, Menu } from 'antd';
|
||||
import { DownOutlined, PlusSquareOutlined } from '@ant-design/icons';
|
||||
|
||||
type ActionOptionType = {
|
||||
label?: string;
|
||||
label?: string | React.ReactNode;
|
||||
value?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Tooltip } from 'antd';
|
||||
import { Tooltip, TooltipProps } from 'antd';
|
||||
import React from 'react';
|
||||
import { css, Button, Link, HTMLCoralProps } from 'coral-system';
|
||||
import cx from 'classnames';
|
||||
@@ -14,6 +14,7 @@ const actionStyle = css`
|
||||
border-radius: var(--tango-radii-s);
|
||||
color: var(--tango-colors-text2);
|
||||
background-color: transparent;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--tango-colors-fill2);
|
||||
@@ -47,6 +48,10 @@ export interface ActionProps extends HTMLCoralProps<any> {
|
||||
* 提示文本
|
||||
*/
|
||||
tooltip?: string;
|
||||
/**
|
||||
* Tooltip属性透传
|
||||
*/
|
||||
tooltipProps?: TooltipProps;
|
||||
/**
|
||||
* 超链接
|
||||
*/
|
||||
@@ -74,6 +79,7 @@ export interface ActionProps extends HTMLCoralProps<any> {
|
||||
export function Action({
|
||||
icon,
|
||||
tooltip,
|
||||
tooltipProps,
|
||||
href,
|
||||
size,
|
||||
shape,
|
||||
@@ -109,7 +115,11 @@ export function Action({
|
||||
}
|
||||
|
||||
if (tooltip) {
|
||||
return <Tooltip title={tooltip}>{ret}</Tooltip>;
|
||||
return (
|
||||
<Tooltip title={tooltip} {...tooltipProps}>
|
||||
{ret}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Box, HTMLCoralProps, css } from 'coral-system';
|
||||
import { UpOutlined } from '@ant-design/icons';
|
||||
import { CaretRightOutlined } from '@ant-design/icons';
|
||||
import { useControllableState } from '@music163/tango-helpers';
|
||||
|
||||
export interface CollapsePanelProps extends Omit<HTMLCoralProps<'div'>, 'title'> {
|
||||
@@ -39,8 +39,10 @@ export interface CollapsePanelProps extends Omit<HTMLCoralProps<'div'>, 'title'>
|
||||
const headerStyle = css`
|
||||
user-select: none;
|
||||
|
||||
.anticon-up {
|
||||
margin-right: 8px;
|
||||
.anticon-caret-right {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-right: 4px;
|
||||
color: var(--tango-colors-text2);
|
||||
transition: transform 0.2s ease 0s;
|
||||
}
|
||||
@@ -67,7 +69,7 @@ export function CollapsePanel(props: CollapsePanelProps) {
|
||||
});
|
||||
|
||||
const iconStyle = {
|
||||
transform: collapsed ? undefined : 'rotate(-180deg)',
|
||||
transform: collapsed ? undefined : 'rotate(90deg)',
|
||||
};
|
||||
|
||||
const stickHeaderProps: HTMLCoralProps<'div'> = stickyHeader
|
||||
@@ -84,7 +86,9 @@ export function CollapsePanel(props: CollapsePanelProps) {
|
||||
borderBottom: 'solid',
|
||||
borderBottomColor: 'line2',
|
||||
}
|
||||
: {};
|
||||
: {
|
||||
border: 'none!important',
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="CollapsePanel" {...CollapsePanelBorderStyle} {...rest}>
|
||||
@@ -94,13 +98,15 @@ export function CollapsePanel(props: CollapsePanelProps) {
|
||||
justifyContent="space-between"
|
||||
className="CollapsePanelHeader"
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
p="m"
|
||||
p="l"
|
||||
paddingTop={'m'}
|
||||
paddingBottom={'m'}
|
||||
{...stickHeaderProps}
|
||||
{...headerProps}
|
||||
css={headerStyle}
|
||||
>
|
||||
<Box display="flex" alignItems="center" fontSize="14px" fontWeight="500">
|
||||
<UpOutlined style={iconStyle} />
|
||||
<CaretRightOutlined style={iconStyle} />
|
||||
{title}
|
||||
</Box>
|
||||
<Box>{extra}</Box>
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Menu, Space } from 'antd';
|
||||
import { Box, css } from 'coral-system';
|
||||
import { isApplePlatform } from '@music163/tango-helpers';
|
||||
|
||||
const contextActionStyle = css`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.ContextActionContent {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ContextActionExtra {
|
||||
flex: none;
|
||||
color: var(--tango-colors-gray-40);
|
||||
}
|
||||
`;
|
||||
|
||||
interface ContextActionProps {
|
||||
icon?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
hotkey?: string;
|
||||
extra?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
key?: string;
|
||||
}
|
||||
|
||||
export function ContextAction({
|
||||
icon,
|
||||
children,
|
||||
hotkey,
|
||||
extra,
|
||||
disabled,
|
||||
onClick,
|
||||
className,
|
||||
key,
|
||||
}: ContextActionProps) {
|
||||
const normalizedHotKey = useMemo(() => {
|
||||
if (!hotkey) {
|
||||
return null;
|
||||
}
|
||||
const keyMap = isApplePlatform()
|
||||
? {
|
||||
command: '⌘',
|
||||
meta: '⌘',
|
||||
ctrl: '^',
|
||||
control: '^',
|
||||
alt: '⌥',
|
||||
option: '⌥',
|
||||
shift: '⇧',
|
||||
'+': '',
|
||||
}
|
||||
: {
|
||||
command: 'Ctrl',
|
||||
meta: 'Win',
|
||||
ctrl: 'Ctrl',
|
||||
control: 'Ctrl',
|
||||
alt: 'Alt',
|
||||
option: 'Alt',
|
||||
shift: 'Shift',
|
||||
};
|
||||
const regexp = new RegExp(Object.keys(keyMap).join('|').replace(/\+/, '\\+'), 'ig');
|
||||
return hotkey.replace(regexp, (match) => keyMap[match.toLowerCase()]);
|
||||
}, [hotkey]);
|
||||
|
||||
return (
|
||||
<Menu.Item key={key} onClick={onClick} icon={icon} className={className} disabled={disabled}>
|
||||
<Box className="ContextAction" css={contextActionStyle}>
|
||||
<Box className="ContextActionContent">{children}</Box>
|
||||
<Box className="ContextActionExtra">
|
||||
<Space>
|
||||
{normalizedHotKey}
|
||||
{extra}
|
||||
</Space>
|
||||
</Box>
|
||||
</Box>
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Text, styled } from 'coral-system';
|
||||
import Draggable from 'react-draggable';
|
||||
import { CloseOutlined, HolderOutlined } from '@ant-design/icons';
|
||||
import { noop } from '@music163/tango-helpers';
|
||||
import { Popover, PopoverProps } from './popover';
|
||||
|
||||
// Dragging over an iframe stops dragging when moving the mouse too fast #613
|
||||
// https://github.com/react-grid-layout/react-draggable/issues/613
|
||||
const injectStyleToBody = () => {
|
||||
const id = 'react-draggable-transparent-selection';
|
||||
if (document.getElementById(id)) {
|
||||
return;
|
||||
}
|
||||
const style = document.createElement('style');
|
||||
style.id = id;
|
||||
style.innerHTML = `
|
||||
/* Prevent iframes from stealing drag events */
|
||||
.react-draggable-transparent-selection iframe {
|
||||
pointer-events: none;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
};
|
||||
|
||||
const CloseIcon = styled(CloseOutlined)`
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
padding: 2px;
|
||||
font-size: 13px;
|
||||
&:hover {
|
||||
color: var(--tango-colors-text1);
|
||||
background-color: var(--tango-colors-line1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const DragPanelContainer = styled(Box)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid var(--tango-colors-line2);
|
||||
width: ${(props) => props.width}px;
|
||||
height: ${(props) => props.height}px;
|
||||
resize: ${(props) => (props.resizeable ? 'both' : 'none')};
|
||||
overflow: ${(props) => (props.resizeable ? 'auto' : 'hidden')};
|
||||
`;
|
||||
|
||||
const DragPanelHeader = styled(Box)`
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--tango-colors-line2);
|
||||
cursor: move;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
min-height: 36px;
|
||||
`;
|
||||
|
||||
const DragPanelBody = styled(Box)`
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
> * {
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const DragPanelFooter = styled(Box)`
|
||||
padding: 8px 12px;
|
||||
min-height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
background: var(--tango-colors-line1);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
border-top: 1px solid var(--tango-colors-line2);
|
||||
`;
|
||||
|
||||
interface DragPanelProps extends Omit<PopoverProps, 'overlay' | 'open'> {
|
||||
// 标题
|
||||
title?: React.ReactNode | string;
|
||||
// 内容
|
||||
body?: React.ReactNode | string;
|
||||
// 底部
|
||||
footer?: ((close: () => void) => React.ReactNode) | React.ReactNode | string;
|
||||
// 宽度
|
||||
width?: number | string;
|
||||
// 高度
|
||||
height?: number | string;
|
||||
// 右上角区域
|
||||
extra?: React.ReactNode | string;
|
||||
// 是否可以缩放
|
||||
resizeable?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function DragPanel({
|
||||
title,
|
||||
footer,
|
||||
body,
|
||||
children,
|
||||
width = 330,
|
||||
height = 330,
|
||||
extra,
|
||||
onOpenChange = noop,
|
||||
resizeable = false,
|
||||
...props
|
||||
}: DragPanelProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const footerNode =
|
||||
typeof footer === 'function'
|
||||
? footer(() => {
|
||||
setOpen(false);
|
||||
onOpenChange(false);
|
||||
})
|
||||
: footer;
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={(innerOpen) => {
|
||||
setOpen(innerOpen);
|
||||
onOpenChange(innerOpen);
|
||||
}}
|
||||
overlay={
|
||||
<Draggable
|
||||
handle=".drag-panel-header"
|
||||
onStart={() => {
|
||||
injectStyleToBody();
|
||||
}}
|
||||
>
|
||||
<DragPanelContainer
|
||||
className="drag-panel"
|
||||
width={width}
|
||||
height={height}
|
||||
resizeable={resizeable}
|
||||
>
|
||||
<DragPanelHeader className="drag-panel-header">
|
||||
<Box fontSize="12px" color="text2">
|
||||
<HolderOutlined />
|
||||
<Text marginLeft={'5px'}>{title}</Text>
|
||||
</Box>
|
||||
<Box color="text2" fontSize="12px" display="flex" alignItems="center">
|
||||
{extra}
|
||||
<CloseIcon
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
onOpenChange(false);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</DragPanelHeader>
|
||||
<DragPanelBody className="drag-panel-body">{body}</DragPanelBody>
|
||||
{footer && (
|
||||
<DragPanelFooter className="drag-panel-footer">{footerNode}</DragPanelFooter>
|
||||
)}
|
||||
</DragPanelContainer>
|
||||
</Draggable>
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -2,10 +2,12 @@ export * from './create-icon';
|
||||
export * from './code-outlined';
|
||||
export * from './line-dashed-outlined';
|
||||
export * from './line-solid-outlined';
|
||||
export * from './package-outlined';
|
||||
export * from './open-panel-filled-left-outlined';
|
||||
export * from './open-panel-filled-right-outlined';
|
||||
export * from './open-panel-left-outlined';
|
||||
export * from './open-panel-right-outlined';
|
||||
export * from './undo-outlined';
|
||||
export * from './pop-out-outlined';
|
||||
export * from './redo-outlined';
|
||||
export * from './dual-outlined';
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { createIcon } from './create-icon';
|
||||
|
||||
const PackageOutlinedSvg = () => (
|
||||
<svg
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
>
|
||||
<path d="M915.2 284.8c0-3.2 0-3.2-3.2-6.4 0-3.2-3.2-3.2-6.4-6.4-9.6-12.8-22.4-25.6-38.4-35.2l-297.6-169.6c-35.2-22.4-80-22.4-118.4 0L313.6 147.2c-6.4 3.2-12.8 6.4-16 12.8L153.6 240c-12.8 9.6-25.6 22.4-38.4 35.2 0 3.2-3.2 3.2-3.2 6.4s0 3.2-3.2 6.4c-6.4 16-12.8 35.2-12.8 54.4v342.4c0 41.6 22.4 80 57.6 102.4l297.6 169.6c12.8 6.4 28.8 12.8 41.6 16 3.2 3.2 9.6 3.2 16 3.2s9.6-3.2 16-3.2c16-3.2 28.8-6.4 41.6-16l297.6-169.6c35.2-22.4 57.6-60.8 57.6-102.4V342.4c6.4-19.2 0-38.4-6.4-57.6z m-428.8-160c9.6-3.2 16-6.4 25.6-6.4 9.6 0 19.2 3.2 25.6 6.4L832 291.2l-128 73.6-316.8-182.4 99.2-57.6z m25.6 352L192 291.2l128-73.6 316.8 182.4-124.8 76.8zM185.6 729.6c-16-9.6-25.6-28.8-25.6-48V345.6l320 185.6V896l-294.4-166.4z m652.8 0L544 896V531.2l320-185.6v336c0 19.2-9.6 38.4-25.6 48z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const PackageOutlined = createIcon(PackageOutlinedSvg, 'PackageOutlined');
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { createIcon } from './create-icon';
|
||||
|
||||
const Svg = () => (
|
||||
<svg
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M832 544a32 32 0 0 1 64 0V832a128 128 0 0 1-128 128H192a128 128 0 0 1-128-128V256a128 128 0 0 1 128-128h288a32 32 0 0 1 0 64H192a64 64 0 0 0-64 64v576a64 64 0 0 0 64 64h576a64 64 0 0 0 64-64V544z m-327.68 21.696a32 32 0 1 1-45.184-45.248L851.52 128H672a32 32 0 0 1 0-64H896a64 64 0 0 1 64 64v224a32 32 0 0 1-64 0V174.08L504.32 565.632z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const PopOutOutlined = createIcon(Svg, 'PopOutOutlined');
|
||||
@@ -22,3 +22,6 @@ export * from './tabs';
|
||||
export * from './select-action';
|
||||
export * from './copy-clipboard';
|
||||
export * from './tag-select';
|
||||
export * from './popover';
|
||||
export * from './drag-panel';
|
||||
export * from './context-action';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, HTMLCoralProps, css } from 'coral-system';
|
||||
import { Box, HTMLCoralProps } from 'coral-system';
|
||||
import CodeMirror, { ReactCodeMirrorProps } from '@uiw/react-codemirror';
|
||||
import { javascript, javascriptLanguage, esLint } from '@codemirror/lang-javascript';
|
||||
import { CompletionContext } from '@codemirror/autocomplete';
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import React, { useState, useRef, useEffect, useMemo, useCallback, useLayoutEffect } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { noop } from '@music163/tango-helpers';
|
||||
import { Box } from 'coral-system';
|
||||
|
||||
export interface PopoverProps {
|
||||
open?: boolean;
|
||||
/**
|
||||
* 浮层内容
|
||||
*/
|
||||
overlay: React.ReactNode;
|
||||
/**
|
||||
* 浮层打开或关闭时的回调
|
||||
*/
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
/**
|
||||
* 浮层被遮挡时自动调整位置
|
||||
*/
|
||||
autoAdjustOverflow?: boolean;
|
||||
/**
|
||||
* 点击蒙层是否允许关闭
|
||||
*/
|
||||
maskClosable?: boolean;
|
||||
/**
|
||||
* 手动唤起时的位置
|
||||
*/
|
||||
left?: number;
|
||||
/**
|
||||
* 手动唤起时的位置
|
||||
*/
|
||||
top?: number;
|
||||
/**
|
||||
* z-index
|
||||
*/
|
||||
zIndex?: number;
|
||||
/**
|
||||
* popoverStyle
|
||||
*/
|
||||
popoverStyle?: React.CSSProperties;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Popover: React.FC<PopoverProps> = ({
|
||||
open,
|
||||
overlay,
|
||||
maskClosable = false,
|
||||
autoAdjustOverflow = true,
|
||||
left: controlledLeft,
|
||||
top: controlledTop,
|
||||
children,
|
||||
popoverStyle,
|
||||
onOpenChange = noop,
|
||||
zIndex = 9999,
|
||||
}) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [left, setLeft] = useState(0);
|
||||
const [top, setTop] = useState(0);
|
||||
const popoverRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 唤起位置受控
|
||||
const isControlledPostion = useMemo(
|
||||
() => controlledLeft !== undefined || controlledTop !== undefined,
|
||||
[controlledLeft, controlledTop],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof controlledTop === 'number') {
|
||||
setTop(controlledTop);
|
||||
}
|
||||
if (typeof controlledLeft === 'number') {
|
||||
setLeft(controlledLeft);
|
||||
}
|
||||
}, [controlledTop, controlledLeft]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const handleDocumentClick = (e: MouseEvent) => {
|
||||
if (
|
||||
maskClosable &&
|
||||
visible &&
|
||||
popoverRef.current &&
|
||||
!popoverRef.current.contains(e.target as Node)
|
||||
) {
|
||||
setVisible(false);
|
||||
onOpenChange(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (maskClosable && visible) {
|
||||
document.addEventListener('click', handleDocumentClick, true);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleDocumentClick);
|
||||
};
|
||||
}, [maskClosable, onOpenChange, visible]);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
if (visible) {
|
||||
setVisible(false);
|
||||
onOpenChange(false);
|
||||
return;
|
||||
}
|
||||
const x = e.clientX;
|
||||
const y = e.clientY;
|
||||
setLeft(x);
|
||||
setTop(y + 10);
|
||||
setVisible(true);
|
||||
onOpenChange(true);
|
||||
},
|
||||
[visible, onOpenChange],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setVisible(open);
|
||||
}, [open]);
|
||||
|
||||
const getAdjustedPosition = () => {
|
||||
const popoverElement = popoverRef.current;
|
||||
if (popoverElement) {
|
||||
const popoverRect = popoverElement.getBoundingClientRect();
|
||||
if (popoverRect.right > window.innerWidth) {
|
||||
setLeft(window.innerWidth - popoverRect.width);
|
||||
}
|
||||
if (popoverRect.bottom > window.innerHeight) {
|
||||
setTop(window.innerHeight - popoverRect.height);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && autoAdjustOverflow) {
|
||||
getAdjustedPosition();
|
||||
}
|
||||
}, [visible, autoAdjustOverflow]);
|
||||
|
||||
const overlayStyle: React.CSSProperties = useMemo(
|
||||
() => ({
|
||||
display: visible ? 'block' : 'none',
|
||||
position: 'fixed',
|
||||
left,
|
||||
top,
|
||||
zIndex,
|
||||
...popoverStyle,
|
||||
}),
|
||||
[left, popoverStyle, top, visible, zIndex],
|
||||
);
|
||||
|
||||
const overlayDom = (
|
||||
<Box className="popover">
|
||||
<Box ref={popoverRef} className="overlay" style={overlayStyle}>
|
||||
{overlay}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isControlledPostion &&
|
||||
React.cloneElement(children as any, {
|
||||
onClick: (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
handleClick(e);
|
||||
(children as any).props?.onClick?.(e);
|
||||
},
|
||||
})}
|
||||
{visible ? ReactDOM.createPortal(overlayDom, document.body) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -14,8 +14,14 @@ const buttonStyle = css`
|
||||
padding: 0 8px;
|
||||
|
||||
.anticon-down {
|
||||
font-size: 12px !important;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-left: 4px;
|
||||
transition: transform 0.1s ease-in;
|
||||
}
|
||||
|
||||
&:hover .anticon-down {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
&.size-s {
|
||||
@@ -91,7 +97,7 @@ const buttonStyle = css`
|
||||
background-color: var(--tango-colors-custom-toolbarButtonBg, rgba(223, 223, 223, 0.08));
|
||||
color: var(--tango-colors-custom-toolbarButtonTextColor, #999);
|
||||
border: 0;
|
||||
border-radius: var(--tango-radii-m);
|
||||
border-radius: var(--tango-radii-s);
|
||||
|
||||
&:hover {
|
||||
color: var(--tango-colors-custom-toolbarButtonTextColorHover, #fff);
|
||||
@@ -154,6 +160,10 @@ export interface ToggleButtonProps extends Omit<HTMLCoralProps<'button'>, 'type'
|
||||
* 下拉菜单的属性
|
||||
*/
|
||||
dropdownProps?: DropDownProps;
|
||||
/**
|
||||
* 是否显示下拉图标
|
||||
*/
|
||||
showDropdownIcon?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,6 +186,7 @@ export function ToggleButton(props: ToggleButtonProps) {
|
||||
overlayStyle,
|
||||
onItemClick,
|
||||
dropdownProps = {},
|
||||
showDropdownIcon = true,
|
||||
...rest
|
||||
} = props;
|
||||
const clazz = cx({
|
||||
@@ -198,7 +209,7 @@ export function ToggleButton(props: ToggleButtonProps) {
|
||||
)}
|
||||
{children}
|
||||
</Box>
|
||||
{showDropdown ? <DownOutlined /> : null}
|
||||
{showDropdown && showDropdownIcon ? <DownOutlined /> : null}
|
||||
</Button>
|
||||
);
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 422 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 899 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
@@ -16101,7 +16101,7 @@ react-dom@^17.0.0:
|
||||
object-assign "^4.1.1"
|
||||
scheduler "^0.20.2"
|
||||
|
||||
react-draggable@^4.0.3:
|
||||
react-draggable@^4.0.3, react-draggable@^4.4.5:
|
||||
version "4.4.6"
|
||||
resolved "https://registry.npmmirror.com/react-draggable/-/react-draggable-4.4.6.tgz"
|
||||
integrity sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==
|
||||
@@ -16195,10 +16195,10 @@ react-merge-refs@^1.1.0:
|
||||
resolved "https://registry.npmmirror.com/react-merge-refs/-/react-merge-refs-1.1.0.tgz"
|
||||
integrity sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ==
|
||||
|
||||
react-monaco-editor-lite@^1.3.9:
|
||||
version "1.3.9"
|
||||
resolved "https://registry.npmmirror.com/react-monaco-editor-lite/-/react-monaco-editor-lite-1.3.9.tgz#377a2126f2a26de7e478323c70b13a50f0c5c760"
|
||||
integrity sha512-pE6CydX/kkisHArbV8GE+TvhukIiT9YYUYBq/cnDL7tfwhX/h1tKQKMJwItnkJvjozaB5uS+oK77GWV874figQ==
|
||||
react-monaco-editor-lite@^1.3.11:
|
||||
version "1.3.11"
|
||||
resolved "https://registry.npmmirror.com/react-monaco-editor-lite/-/react-monaco-editor-lite-1.3.11.tgz#b81b681d23f3ca46f54d4e01f13781bcff6a4a03"
|
||||
integrity sha512-+9xlIn98Yp2hD8OFjRNpQiBx+wLFzsvvT5EgNTxReFrDhFAPPjivdLUCiex1ktzpkGTdo3fxDFUokckvO7hVvQ==
|
||||
dependencies:
|
||||
monaco-editor "^0.38.0"
|
||||
monaco-editor-textmate "^4.0.0"
|
||||
@@ -17697,16 +17697,7 @@ string-length@^4.0.1:
|
||||
char-regex "^1.0.2"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
|
||||
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -17819,7 +17810,7 @@ string_decoder@~1.1.1:
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@@ -17833,13 +17824,6 @@ strip-ansi@^3.0.1:
|
||||
dependencies:
|
||||
ansi-regex "^2.0.0"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^7.0.1, strip-ansi@^7.1.0:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz"
|
||||
@@ -19376,7 +19360,7 @@ workerpool@^9.1.1:
|
||||
resolved "https://registry.npmmirror.com/workerpool/-/workerpool-9.1.1.tgz#9ba4d534a79a5517c1e1b9d1014151516829be8d"
|
||||
integrity sha512-EFoFTSEo9m4V4wNrwzVRjxnf/E/oBpOzcI/R5CIugJhl9RsCiq525rszo4AtqcjQQoqFdu2E3H82AnbtpaQHvg==
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
@@ -19394,15 +19378,6 @@ wrap-ansi@^6.0.1:
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz"
|
||||
|
||||
Reference in New Issue
Block a user