mirror of
https://github.com/NetEase/tango.git
synced 2026-09-01 15:03:03 +08:00
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 00bf52a411 | |||
| c1306e24e0 | |||
| 28040fc006 | |||
| 3f3981bb9d | |||
| 253ceca969 | |||
| a0ed57ff4f | |||
| f83c6bc0f6 | |||
| bb92210594 | |||
| 163ecedff3 | |||
| 22a2e9bdd1 | |||
| 696f82bd26 | |||
| a0484e8f64 | |||
| cf31ef19f4 | |||
| fe31246483 | |||
| 9daf3872e7 | |||
| f17ccbb7f6 | |||
| f854f75b8a | |||
| df3021f75e | |||
| 8bf53a76f8 | |||
| 791fbb162a | |||
| 704277b8f0 | |||
| 1fc22a4535 | |||
| ffaa276b5c | |||
| 56d0877507 | |||
| 897fe0a5a1 | |||
| b52bbad547 | |||
| f7c837a8a9 | |||
| f64d178ab8 | |||
| f2dcc942ed | |||
| ac48a8880e | |||
| 0f1711868d | |||
| 06cd3b3a18 | |||
| 2664613ab2 | |||
| e39a913a87 | |||
| 52864b5400 | |||
| 27166ec949 | |||
| 1a8d15c9da | |||
| 43b99de01c | |||
| 1c1ad9e5eb | |||
| 776837079a | |||
| 14375284e4 | |||
| cb6aabf6ce | |||
| 3f42516816 | |||
| 93608d1037 | |||
| 41a61cce4e | |||
| 47364e4219 | |||
| 9ed6a7d1a4 | |||
| 6f2c8f239a | |||
| 6215ed96f2 | |||
| 190aa1d8ee | |||
| bceb7a5c9c | |||
| d64042922b | |||
| 9ada3fecb3 | |||
| 07febb385f | |||
| ddbda0e6fb | |||
| 9f7441c134 |
@@ -7,6 +7,7 @@ const resolvePackageIndex = (relativeEntry: string) =>
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
routes: [
|
routes: [
|
||||||
{ path: '/', component: 'index' },
|
{ path: '/', component: 'index' },
|
||||||
|
{ path: '/mail', component: 'mail' },
|
||||||
{ path: '/docs', component: 'docs' },
|
{ path: '/docs', component: 'docs' },
|
||||||
],
|
],
|
||||||
npmClient: 'yarn',
|
npmClient: 'yarn',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"author": "Wells <ww.sun@outlook.com>",
|
"author": "Wells <ww.sun@outlook.com>",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "HOST=local.netease.com PORT=8001 umi dev",
|
"dev": "cross-env HOST=local.netease.com PORT=8001 umi dev",
|
||||||
"build": "umi build",
|
"build": "umi build",
|
||||||
"postinstall": "umi setup",
|
"postinstall": "umi setup",
|
||||||
"setup": "umi setup",
|
"setup": "umi setup",
|
||||||
@@ -12,13 +12,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^4.8.0",
|
"@ant-design/icons": "^4.8.0",
|
||||||
|
"@music163/antd": "^0.2.4",
|
||||||
"antd": "^4.24.2",
|
"antd": "^4.24.2",
|
||||||
"coral-system": "^1.0.5",
|
"coral-system": "^1.0.5",
|
||||||
"umi": "^4.0.89"
|
"umi": "^4.2.3"
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/react": "^18.0.33",
|
|
||||||
"@types/react-dom": "^18.0.11",
|
|
||||||
"typescript": "^5.0.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Box, Group } from 'coral-system';
|
|||||||
import { Avatar, Space, Switch } from 'antd';
|
import { Avatar, Space, Switch } from 'antd';
|
||||||
import { BranchesOutlined, MenuOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
import { BranchesOutlined, MenuOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
||||||
import { registerSetter } from '@music163/tango-designer';
|
import { registerSetter } from '@music163/tango-designer';
|
||||||
import type { ComponentPrototypeType, IVariableTreeNode } from '@music163/tango-helpers';
|
import type { IVariableTreeNode } from '@music163/tango-helpers';
|
||||||
import { FooSetter } from '../components';
|
import { FooSetter } from '../components';
|
||||||
|
|
||||||
export * from './mock-files';
|
export * from './mock-files';
|
||||||
@@ -37,113 +37,6 @@ export const bootHelperVariables: IVariableTreeNode[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// folder-name
|
|
||||||
// 物料列表定义
|
|
||||||
const bizToggleButtonPrototype: ComponentPrototypeType = {
|
|
||||||
name: 'CtPcToggleButton',
|
|
||||||
exportType: 'defaultExport',
|
|
||||||
title: '示例业务组件',
|
|
||||||
icon: 'icon-tupian',
|
|
||||||
type: 'element',
|
|
||||||
docs: 'https://redstone.fn.netease.com/mt/fe-comp/w8bq8px7n5/toggle-button',
|
|
||||||
hasChildren: false,
|
|
||||||
props: [
|
|
||||||
{
|
|
||||||
name: 'checked',
|
|
||||||
title: '是否选中',
|
|
||||||
setter: 'boolSetter',
|
|
||||||
defaultValue: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'children',
|
|
||||||
title: '文本',
|
|
||||||
setter: 'textSetter',
|
|
||||||
initValue: '按钮',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
package: '@music/ct-pc-toggle-button',
|
|
||||||
};
|
|
||||||
|
|
||||||
const sampleBlockCode = `
|
|
||||||
<Section>
|
|
||||||
<Result
|
|
||||||
status="success"
|
|
||||||
title="Successfully Purchased Cloud Server ECS!"
|
|
||||||
subTitle="Order number: 2017182818828182881 Cloud server configuration takes 1-5 minutes, please wait."
|
|
||||||
extra={[
|
|
||||||
<Button type="primary" key="console">
|
|
||||||
Go Console
|
|
||||||
</Button>,
|
|
||||||
<Button key="buy">Buy Again</Button>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Section>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SnippetSuccessResult: ComponentPrototypeType = {
|
|
||||||
name: 'SnippetSuccessResult',
|
|
||||||
title: '成功结果',
|
|
||||||
icon: 'icon-tupian',
|
|
||||||
type: 'snippet',
|
|
||||||
package: '@music163/antd',
|
|
||||||
initChildren: sampleBlockCode,
|
|
||||||
relatedImports: ['Section', 'Result', 'Button'],
|
|
||||||
};
|
|
||||||
|
|
||||||
const Snippet2ColumnLayout: ComponentPrototypeType = {
|
|
||||||
name: 'Snippet2ColumnLayout',
|
|
||||||
title: '两列布局',
|
|
||||||
icon: 'icon-columns',
|
|
||||||
type: 'snippet',
|
|
||||||
package: '@music163/antd',
|
|
||||||
initChildren: `
|
|
||||||
<Columns columns={12}>
|
|
||||||
<Column colSpan={6}></Column>
|
|
||||||
<Column colSpan={6}></Column>
|
|
||||||
</Columns>
|
|
||||||
`,
|
|
||||||
relatedImports: ['Columns', 'Column'],
|
|
||||||
};
|
|
||||||
|
|
||||||
const Snippet3ColumnLayout: ComponentPrototypeType = {
|
|
||||||
name: 'Snippet3ColumnLayout',
|
|
||||||
title: '三列布局',
|
|
||||||
icon: 'icon-column3',
|
|
||||||
type: 'snippet',
|
|
||||||
package: '@music163/antd',
|
|
||||||
initChildren: `
|
|
||||||
<Columns columns={12}>
|
|
||||||
<Column colSpan={4}></Column>
|
|
||||||
<Column colSpan={4}></Column>
|
|
||||||
<Column colSpan={4}></Column>
|
|
||||||
</Columns>
|
|
||||||
`,
|
|
||||||
relatedImports: ['Columns', 'Column'],
|
|
||||||
};
|
|
||||||
|
|
||||||
const SnippetButtonGroup: ComponentPrototypeType = {
|
|
||||||
name: 'SnippetButtonGroup',
|
|
||||||
title: '按钮组',
|
|
||||||
icon: 'icon-anniuzu',
|
|
||||||
type: 'snippet',
|
|
||||||
package: '@music163/antd',
|
|
||||||
initChildren: `
|
|
||||||
<Space>
|
|
||||||
<Button type="primary">按钮1</Button>
|
|
||||||
<Button>按钮2</Button>
|
|
||||||
</Space>
|
|
||||||
`,
|
|
||||||
relatedImports: ['Space', 'Button'],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const prototypes = {
|
|
||||||
CtPcToggleButton: bizToggleButtonPrototype,
|
|
||||||
SnippetSuccessResult,
|
|
||||||
Snippet2ColumnLayout,
|
|
||||||
Snippet3ColumnLayout,
|
|
||||||
SnippetButtonGroup,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 注册自定义 setter
|
// 注册自定义 setter
|
||||||
registerSetter({
|
registerSetter({
|
||||||
name: 'fooSetter',
|
name: 'fooSetter',
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
const packageJson = {
|
||||||
|
name: 'demo',
|
||||||
|
private: true,
|
||||||
|
dependencies: {
|
||||||
|
'@music163/tango-mail': '0.1.1',
|
||||||
|
'@music163/tango-boot': '0.2.5',
|
||||||
|
react: '17.0.2',
|
||||||
|
'react-dom': '17.0.2',
|
||||||
|
'prop-types': '15.7.2',
|
||||||
|
tslib: '2.5.0',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tangoConfigJson = {
|
||||||
|
designerConfig: {
|
||||||
|
autoGenerateComponentId: true,
|
||||||
|
},
|
||||||
|
packages: {
|
||||||
|
react: {
|
||||||
|
version: '17.0.2',
|
||||||
|
library: 'React',
|
||||||
|
type: 'dependency',
|
||||||
|
resources: ['https://unpkg.com/react@{{version}}/umd/react.development.js'],
|
||||||
|
},
|
||||||
|
'react-dom': {
|
||||||
|
version: '17.0.2',
|
||||||
|
library: 'ReactDOM',
|
||||||
|
type: 'dependency',
|
||||||
|
resources: ['https://unpkg.com/react-dom@{{version}}/umd/react-dom.development.js'],
|
||||||
|
},
|
||||||
|
'react-is': {
|
||||||
|
version: '16.13.1',
|
||||||
|
library: 'ReactIs',
|
||||||
|
type: 'dependency',
|
||||||
|
resources: ['https://unpkg.com/react-is@{{version}}/umd/react-is.production.min.js'],
|
||||||
|
},
|
||||||
|
'@music163/tango-boot': {
|
||||||
|
description: '云音乐低代码运行时框架',
|
||||||
|
version: '0.2.5',
|
||||||
|
library: 'TangoBoot',
|
||||||
|
type: 'baseDependency',
|
||||||
|
resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'],
|
||||||
|
// resources: ['http://localhost:9001/boot.js'],
|
||||||
|
},
|
||||||
|
'@music163/tango-mail': {
|
||||||
|
description: 'TangoMail 基础物料',
|
||||||
|
version: '0.1.1',
|
||||||
|
library: 'TangoMail',
|
||||||
|
type: 'baseDependency',
|
||||||
|
resources: ['https://unpkg.com/@music163/tango-mail@{{version}}/dist/index.js'],
|
||||||
|
designerResources: ['https://unpkg.com/@music163/tango-mail@{{version}}/dist/designer.js'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const routesCode = `
|
||||||
|
import Mail from "./pages/mail";
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
exact: true,
|
||||||
|
component: Mail,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const entryCode = `
|
||||||
|
import { runApp } from '@music163/tango-boot';
|
||||||
|
import routes from './routes';
|
||||||
|
|
||||||
|
runApp({
|
||||||
|
boot: {
|
||||||
|
mountElement: document.querySelector('#root'),
|
||||||
|
qiankun: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
router: {
|
||||||
|
type: 'browser',
|
||||||
|
config: routes,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
`;
|
||||||
|
|
||||||
|
const viewHomeCode = `
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
Head,
|
||||||
|
Hr,
|
||||||
|
Html,
|
||||||
|
Preview,
|
||||||
|
Section,
|
||||||
|
Text,
|
||||||
|
} from '@music163/tango-mail';
|
||||||
|
|
||||||
|
const WelcomeEmail = () => (
|
||||||
|
<Html>
|
||||||
|
<Head />
|
||||||
|
<Preview>The sales intelligence platform that helps you uncover qualified leads.</Preview>
|
||||||
|
<Body style={main}>
|
||||||
|
<Container style={container}>
|
||||||
|
<Text style={paragraph}>Hi wells,</Text>
|
||||||
|
<Text style={paragraph}>
|
||||||
|
Welcome to Koala, the sales intelligence platform that helps you uncover qualified leads
|
||||||
|
and close deals faster.
|
||||||
|
</Text>
|
||||||
|
<Section style={btnContainer}>
|
||||||
|
<Button style={button} href="https://getkoala.com">
|
||||||
|
Get started
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
<Text style={paragraph}>
|
||||||
|
Best,
|
||||||
|
<br />
|
||||||
|
The Koala team
|
||||||
|
</Text>
|
||||||
|
<Hr style={hr} />
|
||||||
|
<Text style={footer}>470 Noor Ave STE B #1148, South San Francisco, CA 94080</Text>
|
||||||
|
</Container>
|
||||||
|
</Body>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
|
||||||
|
const main = {
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
fontFamily:
|
||||||
|
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif',
|
||||||
|
};
|
||||||
|
|
||||||
|
const container = {
|
||||||
|
margin: '0 auto',
|
||||||
|
padding: '20px 0 48px',
|
||||||
|
};
|
||||||
|
|
||||||
|
const paragraph = {
|
||||||
|
fontSize: '16px',
|
||||||
|
lineHeight: '26px',
|
||||||
|
};
|
||||||
|
|
||||||
|
const btnContainer = {
|
||||||
|
textAlign: 'center',
|
||||||
|
};
|
||||||
|
|
||||||
|
const button = {
|
||||||
|
backgroundColor: '#5F51E8',
|
||||||
|
borderRadius: '3px',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: '16px',
|
||||||
|
textDecoration: 'none',
|
||||||
|
textAlign: 'center',
|
||||||
|
display: 'block',
|
||||||
|
padding: '12px',
|
||||||
|
};
|
||||||
|
|
||||||
|
const hr = {
|
||||||
|
borderColor: '#cccccc',
|
||||||
|
margin: '20px 0',
|
||||||
|
};
|
||||||
|
|
||||||
|
const footer = {
|
||||||
|
color: '#8898aa',
|
||||||
|
fontSize: '12px',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WelcomeEmail;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const mailFiles = [
|
||||||
|
{ filename: '/package.json', code: JSON.stringify(packageJson) },
|
||||||
|
{ filename: '/tango.config.json', code: JSON.stringify(tangoConfigJson) },
|
||||||
|
{ filename: '/README.md', code: '# readme' },
|
||||||
|
{ filename: '/src/index.js', code: entryCode },
|
||||||
|
{ filename: '/src/pages/mail.js', code: viewHomeCode },
|
||||||
|
{ filename: '/src/routes.js', code: routesCode },
|
||||||
|
];
|
||||||
@@ -2,7 +2,7 @@ const packageJson = {
|
|||||||
name: 'demo',
|
name: 'demo',
|
||||||
private: true,
|
private: true,
|
||||||
dependencies: {
|
dependencies: {
|
||||||
'@music163/antd': '0.2.1',
|
'@music163/antd': '0.2.5',
|
||||||
'@music163/tango-boot': '0.2.5',
|
'@music163/tango-boot': '0.2.5',
|
||||||
react: '17.0.2',
|
react: '17.0.2',
|
||||||
'react-dom': '17.0.2',
|
'react-dom': '17.0.2',
|
||||||
@@ -56,11 +56,12 @@ const tangoConfigJson = {
|
|||||||
},
|
},
|
||||||
'@music163/antd': {
|
'@music163/antd': {
|
||||||
description: '云音乐低代码中后台应用基础物料',
|
description: '云音乐低代码中后台应用基础物料',
|
||||||
version: '0.2.1',
|
version: '0.2.6',
|
||||||
library: 'TangoAntd',
|
library: 'TangoAntd',
|
||||||
type: 'baseDependency',
|
type: 'baseDependency',
|
||||||
resources: [
|
resources: [
|
||||||
'https://unpkg.com/@music163/antd@{{version}}/dist/index.js',
|
'https://unpkg.com/@music163/antd@{{version}}/dist/index.js',
|
||||||
|
// 'http://localhost:9002/designer.js',
|
||||||
'https://unpkg.com/antd@4.24.13/dist/antd.css',
|
'https://unpkg.com/antd@4.24.13/dist/antd.css',
|
||||||
],
|
],
|
||||||
designerResources: [
|
designerResources: [
|
||||||
@@ -85,6 +86,7 @@ export function registerComponentPrototype(proto) {
|
|||||||
|
|
||||||
const routesCode = `
|
const routesCode = `
|
||||||
import Index from "./pages/list";
|
import Index from "./pages/list";
|
||||||
|
import Detail from "./pages/detail";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@@ -92,6 +94,11 @@ const routes = [
|
|||||||
exact: true,
|
exact: true,
|
||||||
component: Index
|
component: Index
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/detail',
|
||||||
|
exact: true,
|
||||||
|
component: Detail
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default routes;
|
export default routes;
|
||||||
@@ -149,28 +156,120 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Input,
|
Input,
|
||||||
FormilyForm,
|
FormilyForm,
|
||||||
|
FormilyFormItem,
|
||||||
|
Table,
|
||||||
} from "@music163/antd";
|
} from "@music163/antd";
|
||||||
import { Space } from '@music163/antd';
|
import { Space } from "@music163/antd";
|
||||||
import { LocalButton } from '../components';
|
import { LocalButton } from "../components";
|
||||||
|
import { OutButton } from "../components";
|
||||||
|
|
||||||
class App extends React.Component {
|
class App extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Page title={tango.stores.app.title}>
|
<Page title={tango.stores.app.title} subTitle={111}>
|
||||||
<Section tid="section1" title="Section Title">
|
<Section tid="section0" />
|
||||||
your input: <Input tid="input1" defaultValue="hello" />
|
<Section tid="section1" title="Section Title">
|
||||||
copy input: <Input value={tango.page.input1?.value} />
|
your input: <Input tid="input1" defaultValue="hello" />
|
||||||
</Section>
|
copy input: <Input value={tango.page.input1?.value} />
|
||||||
<Section tid="section2">
|
<Table
|
||||||
<Space tid="space1">
|
columns={[
|
||||||
<LocalButton />
|
{ title: "姓名", dataIndex: "name", key: "name" },
|
||||||
<Button tid="button1">button</Button>
|
{ title: "年龄", dataIndex: "age", key: "age" },
|
||||||
|
{ title: "住址", dataIndex: "address", key: "address" },
|
||||||
|
]}
|
||||||
|
tid="table1"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
<Section tid="section2">
|
||||||
|
<Space tid="space1">
|
||||||
|
<LocalButton />
|
||||||
|
<OutButton />
|
||||||
|
<Button tid="button1">button</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Section>
|
</Section>
|
||||||
|
<Section title="区块标题" tid="section3">
|
||||||
|
<FormilyForm tid="formilyForm1">
|
||||||
|
<FormilyFormItem name="input1" component="Input" label="表单项" />
|
||||||
|
<FormilyFormItem name="select1" component="Select" label="表单项" />
|
||||||
|
</FormilyForm>
|
||||||
|
</Section>
|
||||||
|
<Section title="原生 DOM" tid="section4">
|
||||||
|
<h1 style={{ ...{ color: "red" }, fontSize: 64 }}>
|
||||||
|
hello world
|
||||||
|
</h1>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border: "1px solid #ccc",
|
||||||
|
borderRadius: "5px",
|
||||||
|
backgroundColor: "#f4f4f4",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<form onSubmit={tango.stores.app.submitForm} style={{
|
||||||
|
padding: "10px",
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<label>Username:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
style={{ margin: "5px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Password:</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
style={{ margin: "5px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Role:</label>
|
||||||
|
<select id="role" name="role" style={{ margin: "5px" }}>
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
<option value="user">User</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
style={{
|
||||||
|
marginTop: '5px',
|
||||||
|
padding: "3px 10px",
|
||||||
|
backgroundColor: "#007bff",
|
||||||
|
color: "#fff",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export default definePage(App);
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const emptyPageCode = `
|
||||||
|
import React from "react";
|
||||||
|
import { definePage } from "@music163/tango-boot";
|
||||||
|
import {
|
||||||
|
Page,
|
||||||
|
Section,
|
||||||
|
} from "@music163/antd";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (<Page title="Detail Page">
|
||||||
|
<Section></Section>
|
||||||
|
</Page>)
|
||||||
|
}
|
||||||
|
|
||||||
export default definePage(App);
|
export default definePage(App);
|
||||||
`;
|
`;
|
||||||
@@ -194,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 = `
|
const componentsInputCode = `
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { registerComponentPrototype } from '../utils';
|
import { registerComponentPrototype } from '../utils';
|
||||||
@@ -216,44 +323,28 @@ registerComponentPrototype({
|
|||||||
const componentsEntryCode = `
|
const componentsEntryCode = `
|
||||||
export { default as LocalButton } from './button';
|
export { default as LocalButton } from './button';
|
||||||
export { default as LocalInput } from './input';
|
export { default as LocalInput } from './input';
|
||||||
|
export { default as OutButton } from './out-button';
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const storeApp = `
|
const storeApp = `
|
||||||
import { defineStore } from '@music163/tango-boot';
|
import { defineStore } from '@music163/tango-boot';
|
||||||
|
|
||||||
export default defineStore({
|
export default defineStore({
|
||||||
|
|
||||||
title: 'Page Title',
|
title: 'Page Title',
|
||||||
|
|
||||||
array: [1, 2, 3],
|
array: [1, 2, 3],
|
||||||
|
|
||||||
test: function() {
|
test: function() {
|
||||||
console.log('test');
|
console.log('test');
|
||||||
console.log('test');
|
},
|
||||||
console.log('test');
|
|
||||||
console.log('test');
|
submitForm: function(event) {
|
||||||
console.log('test');
|
event.preventDefault();
|
||||||
console.log('test');
|
const formData = new FormData(event.target);
|
||||||
console.log('test');
|
const username = formData.get("username");
|
||||||
console.log('test');
|
const password = formData.get("password");
|
||||||
console.log('test');
|
const role = formData.get("role");
|
||||||
console.log('test');
|
console.log("Submitted Data:", { username, password, role });
|
||||||
console.log('test');
|
|
||||||
console.log('test');
|
|
||||||
console.log('test');
|
|
||||||
console.log('test');
|
|
||||||
console.log('test');
|
|
||||||
console.log('test');
|
|
||||||
console.log('test');
|
|
||||||
console.log('test');
|
|
||||||
console.log('test');
|
|
||||||
console.log('test');
|
|
||||||
console.log('test');
|
|
||||||
console.log('test');
|
|
||||||
console.log('test');
|
|
||||||
console.log('test');
|
|
||||||
console.log('test');
|
|
||||||
console.log('test');
|
|
||||||
}
|
}
|
||||||
}, 'app');
|
}, 'app');
|
||||||
`;
|
`;
|
||||||
@@ -331,7 +422,9 @@ export const sampleFiles = [
|
|||||||
{ filename: '/src/style.css', code: cssCode },
|
{ filename: '/src/style.css', code: cssCode },
|
||||||
{ filename: '/src/index.js', code: entryCode },
|
{ filename: '/src/index.js', code: entryCode },
|
||||||
{ filename: '/src/pages/list.js', code: viewHomeCode },
|
{ filename: '/src/pages/list.js', code: viewHomeCode },
|
||||||
|
{ filename: '/src/pages/detail.js', code: emptyPageCode },
|
||||||
{ filename: '/src/components/button.js', code: componentsButtonCode },
|
{ 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/input.js', code: componentsInputCode },
|
||||||
{ filename: '/src/components/index.js', code: componentsEntryCode },
|
{ filename: '/src/components/index.js', code: componentsEntryCode },
|
||||||
{ filename: '/src/routes.js', code: routesCode },
|
{ filename: '/src/routes.js', code: routesCode },
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
import * as basePrototypes from '@music163/antd/prototypes';
|
||||||
|
import type { IComponentPrototype, Dict } from '@music163/tango-helpers';
|
||||||
|
|
||||||
|
const sampleBlockCode = `
|
||||||
|
<Section>
|
||||||
|
<Result
|
||||||
|
status="success"
|
||||||
|
title="Successfully Purchased Cloud Server ECS!"
|
||||||
|
subTitle="Order number: 2017182818828182881 Cloud server configuration takes 1-5 minutes, please wait."
|
||||||
|
extra={[
|
||||||
|
<Button type="primary" key="console">
|
||||||
|
Go Console
|
||||||
|
</Button>,
|
||||||
|
<Button key="buy">Buy Again</Button>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SnippetSuccessResult: IComponentPrototype = {
|
||||||
|
name: 'SnippetSuccessResult',
|
||||||
|
title: '成功结果',
|
||||||
|
icon: 'icon-tupian',
|
||||||
|
type: 'snippet',
|
||||||
|
package: '@music163/antd',
|
||||||
|
initChildren: sampleBlockCode,
|
||||||
|
relatedImports: ['Section', 'Result', 'Button'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const Snippet2ColumnLayout: IComponentPrototype = {
|
||||||
|
name: 'Snippet2ColumnLayout',
|
||||||
|
title: '两列布局',
|
||||||
|
icon: 'icon-columns',
|
||||||
|
type: 'snippet',
|
||||||
|
package: '@music163/antd',
|
||||||
|
initChildren: `
|
||||||
|
<Columns columns={12}>
|
||||||
|
<Column colSpan={6}></Column>
|
||||||
|
<Column colSpan={6}></Column>
|
||||||
|
</Columns>
|
||||||
|
`,
|
||||||
|
relatedImports: ['Columns', 'Column'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const Snippet3ColumnLayout: IComponentPrototype = {
|
||||||
|
name: 'Snippet3ColumnLayout',
|
||||||
|
title: '三列布局',
|
||||||
|
icon: 'icon-column-3',
|
||||||
|
type: 'snippet',
|
||||||
|
package: '@music163/antd',
|
||||||
|
// FIXME: Column 组件 需要更新为 defineComponent
|
||||||
|
initChildren: `
|
||||||
|
<Columns columns={12}>
|
||||||
|
<Column colSpan={4}>
|
||||||
|
</Column>
|
||||||
|
<Column colSpan={4}>
|
||||||
|
</Column>
|
||||||
|
<Column colSpan={4}>
|
||||||
|
</Column>
|
||||||
|
</Columns>
|
||||||
|
`,
|
||||||
|
relatedImports: ['Columns', 'Column'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const SnippetButtonGroup: IComponentPrototype = {
|
||||||
|
name: 'SnippetButtonGroup',
|
||||||
|
title: '按钮组',
|
||||||
|
icon: 'icon-anniuzu',
|
||||||
|
type: 'snippet',
|
||||||
|
package: '@music163/antd',
|
||||||
|
initChildren: `
|
||||||
|
<Space>
|
||||||
|
<Button type="primary">按钮1</Button>
|
||||||
|
<Button>按钮2</Button>
|
||||||
|
</Space>
|
||||||
|
`,
|
||||||
|
relatedImports: ['Space', 'Button'],
|
||||||
|
};
|
||||||
|
|
||||||
|
// hack some prototypes
|
||||||
|
basePrototypes['Section'].siblingNames = [
|
||||||
|
'SnippetButtonGroup',
|
||||||
|
'Snippet2ColumnLayout',
|
||||||
|
'Snippet3ColumnLayout',
|
||||||
|
'SnippetSuccessResult',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const nativeDomPrototypes = () => {
|
||||||
|
const doms = [
|
||||||
|
'div',
|
||||||
|
'span',
|
||||||
|
'h1',
|
||||||
|
'h2',
|
||||||
|
'p',
|
||||||
|
'a',
|
||||||
|
'img',
|
||||||
|
'ul',
|
||||||
|
'ol',
|
||||||
|
'li',
|
||||||
|
'input',
|
||||||
|
'button',
|
||||||
|
'form',
|
||||||
|
'table',
|
||||||
|
'tr',
|
||||||
|
'td',
|
||||||
|
'header',
|
||||||
|
'footer',
|
||||||
|
'nav',
|
||||||
|
'section',
|
||||||
|
'article',
|
||||||
|
'aside',
|
||||||
|
'main',
|
||||||
|
'video',
|
||||||
|
'audio',
|
||||||
|
'label',
|
||||||
|
'select',
|
||||||
|
'option',
|
||||||
|
'textarea',
|
||||||
|
'iframe',
|
||||||
|
];
|
||||||
|
const componentPrototypes: Dict<IComponentPrototype> = doms.reduce(
|
||||||
|
(acc: Dict<IComponentPrototype>, tag) => {
|
||||||
|
acc[tag] = {
|
||||||
|
name: tag,
|
||||||
|
title: tag,
|
||||||
|
hasChildren: true,
|
||||||
|
package: '',
|
||||||
|
type: 'element',
|
||||||
|
props: [
|
||||||
|
{
|
||||||
|
name: 'style',
|
||||||
|
title: '样式',
|
||||||
|
group: 'style',
|
||||||
|
setter: 'expressionSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'className',
|
||||||
|
title: '类名',
|
||||||
|
setter: 'textSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
title: 'ID',
|
||||||
|
setter: 'textSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'onClick',
|
||||||
|
title: '点击事件',
|
||||||
|
setter: 'actionSetter',
|
||||||
|
group: 'event',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
return componentPrototypes;
|
||||||
|
};
|
||||||
|
|
||||||
|
// iconfont: https://www.iconfont.cn/manage/index?manage_type=myprojects&projectId=2891794
|
||||||
|
const prototypes: Dict<IComponentPrototype> = {
|
||||||
|
...nativeDomPrototypes(),
|
||||||
|
...(basePrototypes as any),
|
||||||
|
SnippetSuccessResult,
|
||||||
|
Snippet2ColumnLayout,
|
||||||
|
Snippet3ColumnLayout,
|
||||||
|
SnippetButtonGroup,
|
||||||
|
Box: {
|
||||||
|
name: 'Box',
|
||||||
|
title: '盒子',
|
||||||
|
icon: 'icon-mianban',
|
||||||
|
type: 'container',
|
||||||
|
package: '@music163/antd',
|
||||||
|
hasChildren: true,
|
||||||
|
siblingNames: ['Box'],
|
||||||
|
props: [
|
||||||
|
{
|
||||||
|
name: 'aaa',
|
||||||
|
title: 'aaa',
|
||||||
|
setter: 'textSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bbb',
|
||||||
|
title: 'bbb',
|
||||||
|
setter: 'textSetter',
|
||||||
|
deprecated: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ccc',
|
||||||
|
title: 'ccc',
|
||||||
|
setter: 'textSetter',
|
||||||
|
deprecated: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'd',
|
||||||
|
title: 'd',
|
||||||
|
setter: 'textSetter',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Columns: {
|
||||||
|
name: 'Columns',
|
||||||
|
type: 'container',
|
||||||
|
icon: 'icon-column-4',
|
||||||
|
package: '@music163/antd',
|
||||||
|
hasChildren: true,
|
||||||
|
childrenNames: ['Column'],
|
||||||
|
},
|
||||||
|
Column: {
|
||||||
|
name: 'Column',
|
||||||
|
type: 'container',
|
||||||
|
icon: 'icon-juxing',
|
||||||
|
package: '@music163/antd',
|
||||||
|
hasChildren: true,
|
||||||
|
siblingNames: ['Column'],
|
||||||
|
},
|
||||||
|
Text: {
|
||||||
|
name: 'Text',
|
||||||
|
type: 'element',
|
||||||
|
icon: 'icon-wenzi',
|
||||||
|
package: '@music163/antd',
|
||||||
|
initChildren: '文本内容',
|
||||||
|
},
|
||||||
|
Placeholder: {
|
||||||
|
name: 'Placeholder',
|
||||||
|
type: 'element',
|
||||||
|
package: '@music163/antd',
|
||||||
|
},
|
||||||
|
ButtonGroup: {
|
||||||
|
name: 'ButtonGroup',
|
||||||
|
type: 'element',
|
||||||
|
package: '@music163/antd',
|
||||||
|
hasChildren: true,
|
||||||
|
childrenNames: ['Button'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default prototypes;
|
||||||
@@ -3,9 +3,5 @@ import { Outlet } from 'umi';
|
|||||||
import './index.less';
|
import './index.less';
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
return (
|
return <Outlet />;
|
||||||
<div>
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { Box } from 'coral-system';
|
import { Box } from 'coral-system';
|
||||||
import { Button, Space } from 'antd';
|
import { Button, Form, Input, Modal, Space } from 'antd';
|
||||||
import {
|
import {
|
||||||
Designer,
|
Designer,
|
||||||
DesignerPanel,
|
DesignerPanel,
|
||||||
@@ -15,25 +14,69 @@ import {
|
|||||||
themeLight,
|
themeLight,
|
||||||
} from '@music163/tango-designer';
|
} from '@music163/tango-designer';
|
||||||
import { createEngine, Workspace } from '@music163/tango-core';
|
import { createEngine, Workspace } from '@music163/tango-core';
|
||||||
import { Logo, ProjectDetail, bootHelperVariables, sampleFiles } from '../helpers';
|
import prototypes from '../helpers/prototypes';
|
||||||
|
import { Logo, ProjectDetail, bootHelperVariables, emptyPageCode, sampleFiles } from '../helpers';
|
||||||
import {
|
import {
|
||||||
ApiOutlined,
|
ApiOutlined,
|
||||||
AppstoreAddOutlined,
|
AppstoreAddOutlined,
|
||||||
BuildOutlined,
|
BuildOutlined,
|
||||||
ClusterOutlined,
|
ClusterOutlined,
|
||||||
FunctionOutlined,
|
FunctionOutlined,
|
||||||
|
PlusOutlined,
|
||||||
createFromIconfontCN,
|
createFromIconfontCN,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
import { Action } from '@music163/tango-ui';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
const menuData = {
|
||||||
|
common: [
|
||||||
|
{
|
||||||
|
title: '常用',
|
||||||
|
items: [
|
||||||
|
'Button',
|
||||||
|
'Section',
|
||||||
|
'Columns',
|
||||||
|
'Column',
|
||||||
|
'Box',
|
||||||
|
'Text',
|
||||||
|
'Space',
|
||||||
|
'Typography',
|
||||||
|
'Title',
|
||||||
|
'Paragraph',
|
||||||
|
'Table',
|
||||||
|
'Each',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '输入',
|
||||||
|
items: ['Input', 'InputNumber', 'Select'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Formily表单',
|
||||||
|
items: ['FormilyForm', 'FormilyFormItem', 'FormilySubmit', 'FormilyReset'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '数据展示',
|
||||||
|
items: ['Comment'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
// 1. 实例化工作区
|
// 1. 实例化工作区
|
||||||
const workspace = new Workspace({
|
const workspace = new Workspace({
|
||||||
entry: '/src/index.js',
|
entry: '/src/index.js',
|
||||||
files: sampleFiles,
|
files: sampleFiles,
|
||||||
|
prototypes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// inject workspace to window for debug
|
||||||
|
(window as any).__workspace__ = workspace;
|
||||||
|
|
||||||
// 2. 引擎初始化
|
// 2. 引擎初始化
|
||||||
const engine = createEngine({
|
const engine = createEngine({
|
||||||
workspace,
|
workspace,
|
||||||
|
menuData,
|
||||||
|
defaultActiveView: 'design', // dual code design
|
||||||
});
|
});
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -46,15 +89,15 @@ const sandboxQuery = new DndQuery({
|
|||||||
|
|
||||||
// 4. 图标库初始化(物料面板和组件树使用了 iconfont 里的图标)
|
// 4. 图标库初始化(物料面板和组件树使用了 iconfont 里的图标)
|
||||||
createFromIconfontCN({
|
createFromIconfontCN({
|
||||||
scriptUrl: '//at.alicdn.com/t/c/font_2891794_lzc7rtwuzf.js',
|
scriptUrl: '//at.alicdn.com/t/c/font_2891794_151xsllxqd7.js',
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 5. 平台初始化,访问 https://local.netease.com:6006/
|
* 5. 平台初始化,访问 https://local.netease.com:6006/
|
||||||
*/
|
*/
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [menuLoading, setMenuLoading] = useState(true);
|
const [showNewPageModal, setShowNewPageModal] = useState(false);
|
||||||
const [menuData, setMenuData] = useState(false);
|
const [form] = Form.useForm();
|
||||||
return (
|
return (
|
||||||
<Designer
|
<Designer
|
||||||
theme={themeLight}
|
theme={themeLight}
|
||||||
@@ -72,6 +115,14 @@ export default function App() {
|
|||||||
<Box px="l">
|
<Box px="l">
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<Toolbar.Item key="routeSwitch" placement="left" />
|
<Toolbar.Item key="routeSwitch" placement="left" />
|
||||||
|
<Toolbar.Item key="addPage" placement="left">
|
||||||
|
<Action
|
||||||
|
tooltip="添加页面"
|
||||||
|
shape="outline"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => setShowNewPageModal(true)}
|
||||||
|
/>
|
||||||
|
</Toolbar.Item>
|
||||||
<Toolbar.Item key="history" placement="left" />
|
<Toolbar.Item key="history" placement="left" />
|
||||||
<Toolbar.Item key="preview" placement="left" />
|
<Toolbar.Item key="preview" placement="left" />
|
||||||
<Toolbar.Item key="modeSwitch" placement="right" />
|
<Toolbar.Item key="modeSwitch" placement="right" />
|
||||||
@@ -83,6 +134,30 @@ export default function App() {
|
|||||||
</Space>
|
</Space>
|
||||||
</Toolbar.Item>
|
</Toolbar.Item>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
<Modal
|
||||||
|
title="添加新页面"
|
||||||
|
open={showNewPageModal}
|
||||||
|
onCancel={() => setShowNewPageModal(false)}
|
||||||
|
footer={null}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
onFinish={(values) => {
|
||||||
|
workspace.addViewFile(values.name, emptyPageCode);
|
||||||
|
setShowNewPageModal(false);
|
||||||
|
}}
|
||||||
|
layout="vertical"
|
||||||
|
>
|
||||||
|
<Form.Item label="文件名" name="name" required rules={[{ required: true }]}>
|
||||||
|
<Input placeholder="请输入文件名" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
提交
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -92,8 +167,7 @@ export default function App() {
|
|||||||
label="组件"
|
label="组件"
|
||||||
icon={<AppstoreAddOutlined />}
|
icon={<AppstoreAddOutlined />}
|
||||||
widgetProps={{
|
widgetProps={{
|
||||||
menuData: menuData as any,
|
menuData,
|
||||||
loading: menuLoading,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Sidebar.Item key="outline" label="结构" icon={<BuildOutlined />} />
|
<Sidebar.Item key="outline" label="结构" icon={<BuildOutlined />} />
|
||||||
@@ -119,18 +193,17 @@ export default function App() {
|
|||||||
onMessage={(e) => {
|
onMessage={(e) => {
|
||||||
if (e.type === 'done') {
|
if (e.type === 'done') {
|
||||||
const sandboxWindow: any = sandboxQuery.window;
|
const sandboxWindow: any = sandboxQuery.window;
|
||||||
if (sandboxWindow.TangoAntd) {
|
// if (sandboxWindow.TangoAntd) {
|
||||||
if (sandboxWindow.TangoAntd.menuData) {
|
// if (sandboxWindow.TangoAntd.menuData) {
|
||||||
setMenuData(sandboxWindow.TangoAntd.menuData);
|
// setMenuData(sandboxWindow.TangoAntd.menuData);
|
||||||
}
|
// }
|
||||||
if (sandboxWindow.TangoAntd.prototypes) {
|
// if (sandboxWindow.TangoAntd.prototypes) {
|
||||||
workspace.setComponentPrototypes(sandboxWindow.TangoAntd.prototypes);
|
// workspace.setComponentPrototypes(sandboxWindow.TangoAntd.prototypes);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
if (sandboxWindow.localTangoComponentPrototypes) {
|
if (sandboxWindow.localTangoComponentPrototypes) {
|
||||||
workspace.setComponentPrototypes(sandboxWindow.localTangoComponentPrototypes);
|
workspace.setComponentPrototypes(sandboxWindow.localTangoComponentPrototypes);
|
||||||
}
|
}
|
||||||
setMenuLoading(false);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
navigatorExtra={<Button size="small">hello world</Button>}
|
navigatorExtra={<Button size="small">hello world</Button>}
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Box } from 'coral-system';
|
||||||
|
import { Button, Space } from 'antd';
|
||||||
|
import {
|
||||||
|
Designer,
|
||||||
|
DesignerPanel,
|
||||||
|
SettingPanel,
|
||||||
|
Sidebar,
|
||||||
|
Toolbar,
|
||||||
|
WorkspacePanel,
|
||||||
|
WorkspaceView,
|
||||||
|
CodeEditor,
|
||||||
|
Sandbox,
|
||||||
|
DndQuery,
|
||||||
|
themeLight,
|
||||||
|
} from '@music163/tango-designer';
|
||||||
|
import { createEngine, Workspace } from '@music163/tango-core';
|
||||||
|
import { Logo, ProjectDetail, bootHelperVariables } from '@/helpers';
|
||||||
|
import {
|
||||||
|
AppstoreAddOutlined,
|
||||||
|
BuildOutlined,
|
||||||
|
ClusterOutlined,
|
||||||
|
createFromIconfontCN,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { mailFiles } from '@/helpers/mail-files';
|
||||||
|
|
||||||
|
// 1. 实例化工作区
|
||||||
|
const workspace = new Workspace({
|
||||||
|
entry: '/src/index.js',
|
||||||
|
files: mailFiles,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 引擎初始化
|
||||||
|
const engine = createEngine({
|
||||||
|
workspace,
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
window.__mailWorkspace__ = workspace;
|
||||||
|
|
||||||
|
// 3. 沙箱初始化
|
||||||
|
const sandboxQuery = new DndQuery({
|
||||||
|
context: 'iframe',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. 图标库初始化(物料面板和组件树使用了 iconfont 里的图标)
|
||||||
|
createFromIconfontCN({
|
||||||
|
scriptUrl: '//at.alicdn.com/t/c/font_2891794_151xsllxqd7.js',
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 5. 平台初始化,访问 https://local.netease.com:6006/
|
||||||
|
*/
|
||||||
|
export default function App() {
|
||||||
|
const [menuLoading, setMenuLoading] = useState(true);
|
||||||
|
const [menuData, setMenuData] = useState(false);
|
||||||
|
return (
|
||||||
|
<Designer
|
||||||
|
theme={themeLight}
|
||||||
|
engine={engine}
|
||||||
|
config={{
|
||||||
|
customActionVariables: bootHelperVariables,
|
||||||
|
customExpressionVariables: bootHelperVariables,
|
||||||
|
}}
|
||||||
|
sandboxQuery={sandboxQuery}
|
||||||
|
>
|
||||||
|
<DesignerPanel
|
||||||
|
logo={<Logo />}
|
||||||
|
description={<ProjectDetail />}
|
||||||
|
actions={
|
||||||
|
<Box px="l">
|
||||||
|
<Toolbar>
|
||||||
|
<Toolbar.Item key="history" placement="left" />
|
||||||
|
<Toolbar.Item key="preview" placement="left" />
|
||||||
|
<Toolbar.Item key="modeSwitch" placement="right" />
|
||||||
|
<Toolbar.Item key="togglePanel" placement="right" />
|
||||||
|
<Toolbar.Separator />
|
||||||
|
<Toolbar.Item placement="right">
|
||||||
|
<Space>
|
||||||
|
<Button type="primary">发布</Button>
|
||||||
|
</Space>
|
||||||
|
</Toolbar.Item>
|
||||||
|
</Toolbar>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Sidebar>
|
||||||
|
<Sidebar.Item
|
||||||
|
key="components"
|
||||||
|
label="组件"
|
||||||
|
icon={<AppstoreAddOutlined />}
|
||||||
|
widgetProps={{
|
||||||
|
menuData: menuData as any,
|
||||||
|
loading: menuLoading,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Sidebar.Item key="outline" label="结构" icon={<BuildOutlined />} />
|
||||||
|
<Sidebar.Item
|
||||||
|
key="dependency"
|
||||||
|
label="依赖"
|
||||||
|
icon={<ClusterOutlined />}
|
||||||
|
isFloat
|
||||||
|
width={800}
|
||||||
|
/>
|
||||||
|
</Sidebar>
|
||||||
|
<WorkspacePanel>
|
||||||
|
<WorkspaceView mode="design">
|
||||||
|
<Sandbox
|
||||||
|
onMessage={(e) => {
|
||||||
|
if (e.type === 'done') {
|
||||||
|
const sandboxWindow: any = sandboxQuery.window;
|
||||||
|
if (sandboxWindow.TangoMail) {
|
||||||
|
if (sandboxWindow.TangoMail.menuData) {
|
||||||
|
setMenuData(sandboxWindow.TangoMail.menuData);
|
||||||
|
engine.designer.setMenuData(sandboxWindow.TangoMail.menuData);
|
||||||
|
}
|
||||||
|
if (sandboxWindow.TangoMail.prototypes) {
|
||||||
|
workspace.setComponentPrototypes(sandboxWindow.TangoMail.prototypes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setMenuLoading(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
navigatorExtra={<Button size="small">hello world</Button>}
|
||||||
|
/>
|
||||||
|
</WorkspaceView>
|
||||||
|
<WorkspaceView mode="code">
|
||||||
|
<CodeEditor />
|
||||||
|
</WorkspaceView>
|
||||||
|
</WorkspacePanel>
|
||||||
|
<SettingPanel />
|
||||||
|
</DesignerPanel>
|
||||||
|
</Designer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -16,8 +16,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@music163/tango-setting-form": "*",
|
"@music163/tango-setting-form": "*",
|
||||||
"@music163/tango-ui": "*",
|
"@music163/tango-ui": "*",
|
||||||
"mobx": "6.12.0",
|
"mobx": "6.12.3",
|
||||||
"mobx-react-lite": "4.0.5"
|
"mobx-react-lite": "4.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/icons": "^4.8.0",
|
"@ant-design/icons": "^4.8.0",
|
||||||
|
|||||||
@@ -1,158 +1,403 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormModel, SettingForm, register } from '@music163/tango-setting-form';
|
import { FormModel, SettingForm, register } from '@music163/tango-setting-form';
|
||||||
import { ComponentPrototypeType } from '@music163/tango-helpers';
|
import { IComponentPrototype } from '@music163/tango-helpers';
|
||||||
import { BorderSetter } from '@music163/tango-designer/src/setters/style-setter';
|
import { BUILT_IN_SETTERS } from '@music163/tango-designer/src/setters';
|
||||||
import { Box } from 'coral-system';
|
import { Box } from 'coral-system';
|
||||||
import { JsonView } from '@music163/tango-ui';
|
import { JsonView } from '@music163/tango-ui';
|
||||||
import { toJS } from 'mobx';
|
import { toJS } from 'mobx';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { Card } from 'antd';
|
import { Card } from 'antd';
|
||||||
|
import { createFromIconfontCN } from '@ant-design/icons';
|
||||||
|
|
||||||
register({
|
const BLACK_LIST = ['codeSetter', 'eventSetter', 'modelSetter', 'routerSetter'];
|
||||||
name: 'borderSetter',
|
|
||||||
component: BorderSetter,
|
BUILT_IN_SETTERS.filter((setter) => !BLACK_LIST.includes(setter.name)).forEach(register);
|
||||||
|
|
||||||
|
createFromIconfontCN({
|
||||||
|
scriptUrl: '//at.alicdn.com/t/c/font_2891794_151xsllxqd7.js',
|
||||||
});
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'SettingForm',
|
title: 'SettingForm',
|
||||||
};
|
};
|
||||||
|
|
||||||
const prototype: ComponentPrototypeType = {
|
/**
|
||||||
name: 'Test',
|
* 表单值预览
|
||||||
exportType: 'namedExport',
|
*/
|
||||||
title: '测试',
|
const FormValuePreview = observer(({ model }: { model: FormModel }) => {
|
||||||
icon: 'icon-test',
|
const data = toJS(model.values);
|
||||||
|
return <JsonView src={data} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
interface SettingFormDemoProps {
|
||||||
|
initValues?: object;
|
||||||
|
prototype?: IComponentPrototype;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SettingFormDemo({ initValues, prototype }: SettingFormDemoProps) {
|
||||||
|
const model = new FormModel(initValues, { onChange: console.log });
|
||||||
|
return (
|
||||||
|
<Box display="flex">
|
||||||
|
<Box flex="0 0 320px" overflow="hidden">
|
||||||
|
<SettingForm
|
||||||
|
model={model}
|
||||||
|
prototype={prototype}
|
||||||
|
showIdentifier={{
|
||||||
|
identifierKey: 'tid',
|
||||||
|
getIdentifier: () => `time${Date.now()}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box position="relative">
|
||||||
|
<Card title="表单状态预览" style={{ position: 'sticky', top: 0 }}>
|
||||||
|
<FormValuePreview model={model} />
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prototypeHasBasicProps: IComponentPrototype = {
|
||||||
|
name: 'Sample',
|
||||||
|
title: '演示组件',
|
||||||
|
package: 'sample-pkg',
|
||||||
type: 'element',
|
type: 'element',
|
||||||
category: 'basic',
|
docs: 'https://4x-ant-design.antgroup.com/components/slider-cn',
|
||||||
package: '@music163/antd',
|
|
||||||
hasChildren: false,
|
|
||||||
props: [
|
props: [
|
||||||
|
{
|
||||||
|
name: 'code',
|
||||||
|
title: 'codeSetter',
|
||||||
|
setter: 'codeSetter',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'text',
|
name: 'text',
|
||||||
title: 'textSetter',
|
title: 'textSetter',
|
||||||
setter: 'textSetter',
|
setter: 'textSetter',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'border',
|
name: 'text2',
|
||||||
title: 'borderSetter',
|
title: 'textAreaSetter',
|
||||||
setter: 'borderSetter',
|
setter: 'textAreaSetter',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'router',
|
name: 'number',
|
||||||
title: 'routerSetter',
|
title: 'numberSetter',
|
||||||
setter: 'routerSetter',
|
setter: 'numberSetter',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'object',
|
name: 'number2',
|
||||||
title: '对象属性',
|
title: 'sliderSetter',
|
||||||
tip: '一个嵌套的对象',
|
setter: 'sliderSetter',
|
||||||
props: [
|
},
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'bool',
|
||||||
title: 'Name',
|
title: 'boolSetter',
|
||||||
setter: 'textSetter',
|
setter: 'boolSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'enum',
|
||||||
|
title: 'enumSetter',
|
||||||
|
setter: 'enumSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'list',
|
||||||
|
title: 'listSetter',
|
||||||
|
setter: 'listSetter',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Basic() {
|
||||||
|
return (
|
||||||
|
<SettingFormDemo
|
||||||
|
initValues={{
|
||||||
|
bool: true,
|
||||||
|
enum: {
|
||||||
|
aaa: 'aaa',
|
||||||
|
bbb: 'bbb',
|
||||||
|
ccc: 'ccc',
|
||||||
},
|
},
|
||||||
{
|
list: [{ key: 1 }, { key: 2 }],
|
||||||
name: 'age',
|
}}
|
||||||
title: 'Age',
|
prototype={prototypeHasBasicProps}
|
||||||
setter: 'numberSetter',
|
/>
|
||||||
},
|
);
|
||||||
{
|
}
|
||||||
name: 'address',
|
|
||||||
title: 'Address',
|
export function DeprecatedProp() {
|
||||||
props: [
|
return (
|
||||||
{
|
<SettingFormDemo
|
||||||
name: 'city',
|
prototype={{
|
||||||
title: 'City',
|
name: 'Deprecated',
|
||||||
setter: 'textSetter',
|
package: 'sample-pkg',
|
||||||
|
type: 'element',
|
||||||
|
props: [
|
||||||
|
{
|
||||||
|
name: 'number',
|
||||||
|
title: 'numberSetter',
|
||||||
|
setter: 'numberSetter',
|
||||||
|
tip: '这是一个文本属性',
|
||||||
|
docs: 'https://4x-ant-design.antgroup.com/components/slider-cn',
|
||||||
|
deprecated: '使用 text2 替代',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'number1',
|
||||||
|
title: 'sliderSetter',
|
||||||
|
setter: 'sliderSetter',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Validate() {
|
||||||
|
return (
|
||||||
|
<SettingFormDemo
|
||||||
|
prototype={{
|
||||||
|
name: 'Validate',
|
||||||
|
package: 'sample-pkg',
|
||||||
|
type: 'element',
|
||||||
|
props: [
|
||||||
|
{
|
||||||
|
name: 'number',
|
||||||
|
title: 'numberSetter',
|
||||||
|
setter: 'numberSetter',
|
||||||
|
validate: (value) => {
|
||||||
|
if (!value && value !== 0) {
|
||||||
|
return '必填';
|
||||||
|
}
|
||||||
|
if (value < 0) {
|
||||||
|
return '必须大于 0';
|
||||||
|
}
|
||||||
|
if (value > 10) {
|
||||||
|
return '必须小于等于 10';
|
||||||
|
}
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ObjectSetter() {
|
||||||
|
return (
|
||||||
|
<SettingFormDemo
|
||||||
|
prototype={{
|
||||||
|
name: 'Object',
|
||||||
|
package: 'sample-pkg',
|
||||||
|
type: 'element',
|
||||||
|
props: [
|
||||||
|
{
|
||||||
|
name: 'text',
|
||||||
|
title: 'textSetter',
|
||||||
|
setter: 'textSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'object',
|
||||||
|
title: 'objectSetter',
|
||||||
|
setter: 'objectSetter',
|
||||||
|
props: [
|
||||||
|
{
|
||||||
|
name: 'text',
|
||||||
|
title: 'textSetter',
|
||||||
|
setter: 'textSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'number',
|
||||||
|
title: 'numberSetter',
|
||||||
|
setter: 'numberSetter',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InitValues() {
|
||||||
|
return (
|
||||||
|
<SettingFormDemo
|
||||||
|
initValues={{
|
||||||
|
bool: true,
|
||||||
|
bool1: '{{true}}',
|
||||||
|
style: {
|
||||||
|
background: 'red',
|
||||||
},
|
},
|
||||||
|
object: {
|
||||||
|
text: 'text',
|
||||||
|
number: 10,
|
||||||
|
},
|
||||||
|
object1: {
|
||||||
|
text: 'text',
|
||||||
|
number: '{{tango.stores.user?.age}}',
|
||||||
|
},
|
||||||
|
// 只会有一种情况传下来的是字符串,就是用户代码里存在 rest operator,这时候不需要额外处理,提示用户就使用代码模式
|
||||||
|
object2: '{{{ text: "text22", number: 22, ...{ extra: "some" } }}}',
|
||||||
|
list: [{ key: 'aaa' }, { key: 'bbb' }], // list object
|
||||||
|
list1: "{{[{ key: 'aaa' }, { key: 'bbb' }]}}", // raw code
|
||||||
|
}}
|
||||||
|
prototype={{
|
||||||
|
name: 'InitValues',
|
||||||
|
package: 'sample-pkg',
|
||||||
|
type: 'element',
|
||||||
|
props: [
|
||||||
|
{
|
||||||
|
name: 'bool',
|
||||||
|
title: 'value初始化',
|
||||||
|
setter: 'boolSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bool1',
|
||||||
|
title: 'value初始化',
|
||||||
|
setter: 'boolSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bool2',
|
||||||
|
title: '无初值',
|
||||||
|
setter: 'boolSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'style',
|
||||||
|
title: 'codeSetter',
|
||||||
|
setter: 'codeSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'object',
|
||||||
|
props: [
|
||||||
|
{
|
||||||
|
name: 'text',
|
||||||
|
title: 'text',
|
||||||
|
setter: 'textSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'number',
|
||||||
|
title: 'number',
|
||||||
|
setter: 'numberSetter',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'object1',
|
||||||
|
props: [
|
||||||
|
{
|
||||||
|
name: 'text',
|
||||||
|
title: 'text',
|
||||||
|
setter: 'textSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'number',
|
||||||
|
title: 'number',
|
||||||
|
setter: 'numberSetter',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'object2',
|
||||||
|
props: [
|
||||||
|
{
|
||||||
|
name: 'text',
|
||||||
|
title: 'text',
|
||||||
|
setter: 'textSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'number',
|
||||||
|
title: 'number',
|
||||||
|
setter: 'numberSetter',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'list',
|
||||||
|
title: 'listSetter',
|
||||||
|
setter: 'listSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'list1',
|
||||||
|
title: 'listSetter',
|
||||||
|
setter: 'listSetter',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Lite() {
|
||||||
|
return (
|
||||||
|
<Box width={320} border="solid" borderColor="line2">
|
||||||
|
<SettingForm
|
||||||
|
showSearch={false}
|
||||||
|
showGroups={false}
|
||||||
|
showItemSubtitle={false}
|
||||||
|
prototype={prototypeHasBasicProps}
|
||||||
|
disableSwitchExpressionSetter
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HideToggleCode() {
|
||||||
|
const model = new FormModel({});
|
||||||
|
return (
|
||||||
|
<Box width={320} border="solid" borderColor="line2">
|
||||||
|
<SettingForm model={model} prototype={prototypeHasBasicProps} disableSwitchExpressionSetter />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prototypeHasExtraProps: IComponentPrototype = {
|
||||||
|
name: 'ExtraProps',
|
||||||
|
type: 'element',
|
||||||
|
package: '@music163/antd',
|
||||||
|
props: [
|
||||||
|
{
|
||||||
|
name: 'choice',
|
||||||
|
title: 'choiceSetter',
|
||||||
|
setter: 'choiceSetter',
|
||||||
|
options: [
|
||||||
|
{ label: '选项1', value: '1' },
|
||||||
|
{ label: '选项2', value: '2' },
|
||||||
|
{ label: '选项3', value: '3' },
|
||||||
],
|
],
|
||||||
getVisible: (form) => {
|
|
||||||
return form.getValue('text') !== 'test';
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'expression',
|
name: 'picker',
|
||||||
title: 'expressionSetter',
|
title: 'pickerSetter',
|
||||||
setter: 'expressionSetter',
|
setter: 'pickerSetter',
|
||||||
|
options: [
|
||||||
|
{ label: '选项1', value: '1' },
|
||||||
|
{ label: '选项2', value: '2' },
|
||||||
|
{ label: '选项3', value: '3' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'model',
|
name: 'actionList',
|
||||||
title: 'modelSetter',
|
title: 'actionListSetter',
|
||||||
setter: 'modelSetter',
|
setter: 'actionListSetter',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'image',
|
name: 'list',
|
||||||
title: 'imageSetter',
|
title: 'listSetter',
|
||||||
setter: 'imageSetter',
|
setter: 'listSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'options',
|
||||||
|
title: 'optionSetter',
|
||||||
|
setter: 'optionSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'columns',
|
||||||
|
title: 'tableColumnsSetter',
|
||||||
|
setter: 'tableColumnsSetter',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'css',
|
name: 'css',
|
||||||
title: 'cssSetter',
|
title: 'cssSetter',
|
||||||
setter: 'cssSetter',
|
setter: 'cssSetter',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'extra',
|
|
||||||
title: 'jsxSetter',
|
|
||||||
setter: 'jsxSetter',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'icon',
|
|
||||||
title: 'iconSetter',
|
|
||||||
setter: 'iconSetter',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'iconType',
|
|
||||||
title: 'iconTypeSetter',
|
|
||||||
setter: 'iconTypeSetter',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'onClick',
|
|
||||||
title: 'eventSetter',
|
|
||||||
tip: '当点击按钮时',
|
|
||||||
setter: 'eventSetter',
|
|
||||||
group: 'event',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'onClick2',
|
|
||||||
title: 'actionSetter',
|
|
||||||
tip: '当点击按钮时',
|
|
||||||
setter: 'actionSetter',
|
|
||||||
group: 'event',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'disabled',
|
|
||||||
title: 'boolSetter',
|
|
||||||
tip: 'disabled 是否禁用',
|
|
||||||
defaultValue: false,
|
|
||||||
setter: 'boolSetter',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'size',
|
|
||||||
title: 'choiceSetter',
|
|
||||||
tip: 'size 按钮的尺寸',
|
|
||||||
defaultValue: 'medium',
|
|
||||||
setter: 'choiceSetter',
|
|
||||||
setterProps: {
|
|
||||||
options: [
|
|
||||||
{ label: '小', value: 'small' },
|
|
||||||
{ label: '中', value: 'medium' },
|
|
||||||
{ label: '大', value: 'large' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'color',
|
|
||||||
title: 'colorSetter',
|
|
||||||
setter: 'colorSetter',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'columns',
|
|
||||||
title: 'columnSetter',
|
|
||||||
setter: 'columnSetter',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'date',
|
name: 'date',
|
||||||
title: 'dateSetter',
|
title: 'dateSetter',
|
||||||
@@ -163,127 +408,137 @@ const prototype: ComponentPrototypeType = {
|
|||||||
title: 'dateRangeSetter',
|
title: 'dateRangeSetter',
|
||||||
setter: 'dateRangeSetter',
|
setter: 'dateRangeSetter',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'enum',
|
|
||||||
title: 'enumSetter',
|
|
||||||
setter: 'enumSetter',
|
|
||||||
setterProps: {},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'time',
|
name: 'time',
|
||||||
title: 'timeSetter',
|
title: 'timeSetter',
|
||||||
setter: 'timeSetter',
|
setter: 'timeSetter',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'timeRange',
|
name: 'time',
|
||||||
title: 'timeRangeSetter',
|
title: 'timeRangeSetter',
|
||||||
setter: 'timeRangeSetter',
|
setter: 'timeRangeSetter',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'dataSource',
|
name: 'enum',
|
||||||
title: 'jsonSetter 不推荐',
|
title: 'enumSetter',
|
||||||
|
setter: 'enumSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'event',
|
||||||
|
title: 'eventSetter',
|
||||||
|
setter: 'eventSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'json',
|
||||||
|
title: 'jsonSetter',
|
||||||
setter: 'jsonSetter',
|
setter: 'jsonSetter',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'listSetter',
|
name: 'jsx',
|
||||||
title: 'listSetter',
|
title: 'jsxSetter',
|
||||||
setter: 'listSetter',
|
setter: 'jsxSetter',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'count',
|
name: 'render',
|
||||||
title: 'numberSetter',
|
title: 'renderPropsSetter',
|
||||||
setter: 'numberSetter',
|
setter: 'renderPropsSetter',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'options',
|
name: 'cell',
|
||||||
title: 'optionSetter',
|
title: 'tableCellSetter',
|
||||||
setter: 'optionSetter',
|
setter: 'tableCellSetter',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'type',
|
name: 'expandable',
|
||||||
title: 'pickerSetter',
|
title: 'tableExpandableSetter',
|
||||||
defaultValue: 'solid',
|
setter: 'tableExpandableSetter',
|
||||||
setter: 'pickerSetter',
|
|
||||||
setterProps: {
|
|
||||||
options: [
|
|
||||||
{ label: '文本型', value: 'text' },
|
|
||||||
{ label: '实体型', value: 'solid' },
|
|
||||||
{ label: '幽灵', value: 'ghost' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'title',
|
name: 'router',
|
||||||
title: 'textSetter',
|
title: 'routerSetter',
|
||||||
setter: 'textSetter',
|
setter: 'routerSetter',
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'invalid',
|
|
||||||
title: 'invalidSetter',
|
|
||||||
setter: 'invalidSetter',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
export function ExtraSetters() {
|
||||||
* 表单值预览
|
|
||||||
*/
|
|
||||||
const FormValuePreview = observer(({ model }: { model: FormModel }) => {
|
|
||||||
const data = toJS(model.values);
|
|
||||||
return <JsonView src={data} />;
|
|
||||||
});
|
|
||||||
|
|
||||||
export function Basic() {
|
|
||||||
const model = new FormModel(
|
|
||||||
{
|
|
||||||
router: 'www.163.com',
|
|
||||||
expression: `{ foo: 'foo' }`,
|
|
||||||
object: {
|
|
||||||
name: 'Alice',
|
|
||||||
},
|
|
||||||
image:
|
|
||||||
'https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13270238619/2cc5/0782/1d6e/009b96bf90c557b9bbde09b1687a2c80.png',
|
|
||||||
},
|
|
||||||
{ onChange: console.log },
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box display="flex">
|
<SettingFormDemo
|
||||||
<SettingForm
|
initValues={{
|
||||||
model={model}
|
router: 'www.163.com',
|
||||||
prototype={prototype}
|
expression: `{ foo: 'foo' }`,
|
||||||
showIdentifier={{
|
object: {
|
||||||
identifierKey: 'tid',
|
name: 'Alice',
|
||||||
}}
|
},
|
||||||
/>
|
image:
|
||||||
<Box position="relative">
|
'https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13270238619/2cc5/0782/1d6e/009b96bf90c557b9bbde09b1687a2c80.png',
|
||||||
<Card title="表单状态预览" style={{ position: 'sticky', top: 0 }}>
|
}}
|
||||||
<FormValuePreview model={model} />
|
prototype={prototypeHasExtraProps}
|
||||||
</Card>
|
/>
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Lite() {
|
export function StyleProps() {
|
||||||
return (
|
return (
|
||||||
<Box width={320} border="solid">
|
<SettingFormDemo
|
||||||
<SettingForm
|
prototype={{
|
||||||
showSearch={false}
|
name: 'Box',
|
||||||
showGroups={false}
|
title: 'Box',
|
||||||
showItemSubtitle={false}
|
type: 'element',
|
||||||
prototype={prototype}
|
package: '@music163/antd',
|
||||||
disableSwitchExpressionSetter
|
props: [
|
||||||
/>
|
{
|
||||||
</Box>
|
name: 'style',
|
||||||
);
|
title: 'styleSetter',
|
||||||
}
|
setter: 'styleSetter',
|
||||||
|
},
|
||||||
export function NoExpressionSwitch() {
|
{
|
||||||
const model = new FormModel({});
|
name: 'display',
|
||||||
return (
|
title: 'displaySetter',
|
||||||
<Box>
|
setter: 'displaySetter',
|
||||||
<SettingForm model={model} prototype={prototype} disableSwitchExpressionSetter />
|
},
|
||||||
</Box>
|
{
|
||||||
|
name: 'flexDirection',
|
||||||
|
title: 'flexDirectionSetter',
|
||||||
|
setter: 'flexDirectionSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'flexGap',
|
||||||
|
title: 'flexGapSetter',
|
||||||
|
setter: 'flexGapSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'flexJustifyContent',
|
||||||
|
title: 'flexJustifyContentSetter',
|
||||||
|
setter: 'flexJustifyContentSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'flexAlignItems',
|
||||||
|
title: 'flexAlignItemsSetter',
|
||||||
|
setter: 'flexAlignItemsSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'spacing',
|
||||||
|
title: 'spacingSetter',
|
||||||
|
setter: 'spacingSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'color',
|
||||||
|
title: 'colorSetter',
|
||||||
|
setter: 'colorSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bg',
|
||||||
|
title: 'bgSetter',
|
||||||
|
setter: 'bgSetter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'border',
|
||||||
|
title: 'borderSetter',
|
||||||
|
setter: 'borderSetter',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+4
-3
@@ -40,6 +40,7 @@
|
|||||||
"@typescript-eslint/parser": "^6.13.2",
|
"@typescript-eslint/parser": "^6.13.2",
|
||||||
"conventional-changelog-cli": "^4.1.0",
|
"conventional-changelog-cli": "^4.1.0",
|
||||||
"copyfiles": "^2.4.1",
|
"copyfiles": "^2.4.1",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.55.0",
|
"eslint": "^8.55.0",
|
||||||
"eslint-config-ali": "^14.0.2",
|
"eslint-config-ali": "^14.0.2",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
@@ -51,9 +52,9 @@
|
|||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"lerna": "^8.0.0",
|
"lerna": "^8.1.2",
|
||||||
"lint-staged": "^15.2.0",
|
"lint-staged": "^15.2.2",
|
||||||
"prettier": "^3.1.0",
|
"prettier": "^3.2.5",
|
||||||
"react": "^17.0.0",
|
"react": "^17.0.0",
|
||||||
"react-dom": "^17.0.0",
|
"react-dom": "^17.0.0",
|
||||||
"styled-components": "^5.3.6",
|
"styled-components": "^5.3.6",
|
||||||
|
|||||||
@@ -3,6 +3,38 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.1.2](https://github.com/netease/tango/compare/@music163/tango-context@1.1.1...@music163/tango-context@1.1.2) (2024-06-03)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @music163/tango-context
|
||||||
|
|
||||||
|
## [1.1.1](https://github.com/netease/tango/compare/@music163/tango-context@1.1.0...@music163/tango-context@1.1.1) (2024-05-21)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @music163/tango-context
|
||||||
|
|
||||||
|
# [1.1.0](https://github.com/netease/tango/compare/@music163/tango-context@1.0.2...@music163/tango-context@1.1.0) (2024-05-17)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- refactor parse attribute value ([#149](https://github.com/netease/tango/issues/149)) ([ffaa276](https://github.com/netease/tango/commit/ffaa276b5c205ed962d37e2fdb358a703f8fad01))
|
||||||
|
|
||||||
|
## [1.0.1](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0...@music163/tango-context@1.0.1) (2024-04-22)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- allow reload state tree & display pageStore in variable tree ([#135](https://github.com/netease/tango/issues/135)) ([2664613](https://github.com/netease/tango/commit/2664613ab263aead2a5239fa012454fc7fd5ff99))
|
||||||
|
|
||||||
|
# [1.0.0-alpha.10](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.9...@music163/tango-context@1.0.0-alpha.10) (2024-04-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @music163/tango-context
|
||||||
|
|
||||||
|
# [1.0.0-alpha.9](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.8...@music163/tango-context@1.0.0-alpha.9) (2024-03-26)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @music163/tango-context
|
||||||
|
|
||||||
|
# [1.0.0-alpha.8](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.7...@music163/tango-context@1.0.0-alpha.8) (2024-03-18)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @music163/tango-context
|
||||||
|
|
||||||
# [1.0.0-alpha.7](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.6...@music163/tango-context@1.0.0-alpha.7) (2024-03-18)
|
# [1.0.0-alpha.7](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.6...@music163/tango-context@1.0.0-alpha.7) (2024-03-18)
|
||||||
|
|
||||||
**Note:** Version bump only for package @music163/tango-context
|
**Note:** Version bump only for package @music163/tango-context
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@music163/tango-context",
|
"name": "@music163/tango-context",
|
||||||
"version": "1.0.0-alpha.7",
|
"version": "1.1.2",
|
||||||
"description": "react context for tango-apps",
|
"description": "react context for tango-apps",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react",
|
"react",
|
||||||
@@ -31,9 +31,9 @@
|
|||||||
"react": ">= 16.8"
|
"react": ">= 16.8"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@music163/tango-core": "^1.0.0-alpha.7",
|
"@music163/tango-core": "^1.3.0",
|
||||||
"@music163/tango-helpers": "^1.0.0-alpha.3",
|
"@music163/tango-helpers": "^1.2.0",
|
||||||
"mobx-react-lite": "4.0.5"
|
"mobx-react-lite": "4.0.7"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
|
|||||||
@@ -35,8 +35,14 @@ export const useWorkspaceData = () => {
|
|||||||
const modelVariables: IVariableTreeNode[] = []; // 绑定变量列表
|
const modelVariables: IVariableTreeNode[] = []; // 绑定变量列表
|
||||||
const storeActionVariables: IVariableTreeNode[] = []; // 模型中的所有 actions
|
const storeActionVariables: IVariableTreeNode[] = []; // 模型中的所有 actions
|
||||||
const storeVariables: IVariableTreeNode[] = []; // 模型中的所有变量
|
const storeVariables: IVariableTreeNode[] = []; // 模型中的所有变量
|
||||||
|
const pageStoreVariables: IVariableTreeNode[] = []; // 页面中的组件实例变量
|
||||||
const serviceVariables: IVariableTreeNode[] = []; // 服务中的所有变量
|
const serviceVariables: IVariableTreeNode[] = []; // 服务中的所有变量
|
||||||
|
|
||||||
|
pageStoreVariables.push({
|
||||||
|
title: 'pageStore',
|
||||||
|
key: 'pageStore',
|
||||||
|
});
|
||||||
|
|
||||||
Object.values(workspace.storeModules).forEach((file) => {
|
Object.values(workspace.storeModules).forEach((file) => {
|
||||||
const prefix = `stores.${file.name}`;
|
const prefix = `stores.${file.name}`;
|
||||||
const states: IVariableTreeNode[] = file.states.map((item) => ({
|
const states: IVariableTreeNode[] = file.states.map((item) => ({
|
||||||
@@ -109,6 +115,7 @@ export const useWorkspaceData = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let expressionVariables: IVariableTreeNode[] = [
|
let expressionVariables: IVariableTreeNode[] = [
|
||||||
|
buildVariableOptions('当前页面组件实例', '$pageStore', pageStoreVariables),
|
||||||
buildVariableOptions('数据模型', '$stores', storeVariables),
|
buildVariableOptions('数据模型', '$stores', storeVariables),
|
||||||
buildVariableOptions('服务函数', '$services', serviceVariables),
|
buildVariableOptions('服务函数', '$services', serviceVariables),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -3,6 +3,59 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [1.3.0](https://github.com/netease/tango/compare/@music163/tango-core@1.2.0...@music163/tango-core@1.3.0) (2024-06-03)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- parse app entry file & pass correct routerType to sandbox ([#168](https://github.com/netease/tango/issues/168)) ([3f3981b](https://github.com/netease/tango/commit/3f3981bb9db874b738a67fd449c579c35c58d08b))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- add context menu ([#161](https://github.com/netease/tango/issues/161)) ([28040fc](https://github.com/netease/tango/commit/28040fc00604339a40ad3216b76baf7de93a13e0))
|
||||||
|
|
||||||
|
# [1.2.0](https://github.com/netease/tango/compare/@music163/tango-core@1.1.0...@music163/tango-core@1.2.0) (2024-05-21)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- enhance code value validate in SettingForm ([#152](https://github.com/netease/tango/issues/152)) ([791fbb1](https://github.com/netease/tango/commit/791fbb162a7147243924e01f54e9c0b586f14438))
|
||||||
|
- prototype2code & go back history error ([#156](https://github.com/netease/tango/issues/156)) ([8bf53a7](https://github.com/netease/tango/commit/8bf53a76f8a71eaf261ea68b9ee44e5bf19893aa))
|
||||||
|
- support add components with popover ([#155](https://github.com/netease/tango/issues/155)) ([f17ccbb](https://github.com/netease/tango/commit/f17ccbb7f645f8047ecd96d9f3f2185048a3b726))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- add select parent node of selected node ([#158](https://github.com/netease/tango/issues/158)) ([fe31246](https://github.com/netease/tango/commit/fe3124648325e72abfc58da8b2f8ff83301d40b8))
|
||||||
|
- supoort set default active view ([#157](https://github.com/netease/tango/issues/157)) ([f854f75](https://github.com/netease/tango/commit/f854f75b8a8c25384d290bd76d6b8bb6fb69f22d))
|
||||||
|
|
||||||
|
# [1.1.0](https://github.com/netease/tango/compare/@music163/tango-core@1.0.2...@music163/tango-core@1.1.0) (2024-05-17)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- parse module with module alias ([#147](https://github.com/netease/tango/issues/147)) ([56d0877](https://github.com/netease/tango/commit/56d0877507b0138877eac6f36db288147b43c7d9))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- refactor parse attribute value ([#149](https://github.com/netease/tango/issues/149)) ([ffaa276](https://github.com/netease/tango/commit/ffaa276b5c205ed962d37e2fdb358a703f8fad01))
|
||||||
|
|
||||||
|
## [1.0.1](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0...@music163/tango-core@1.0.1) (2024-04-22)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @music163/tango-core
|
||||||
|
|
||||||
|
# [1.0.0-alpha.10](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.9...@music163/tango-core@1.0.0-alpha.10) (2024-04-09)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- update setters and use tabOptions to filter props ([#129](https://github.com/netease/tango/issues/129)) ([93608d1](https://github.com/netease/tango/commit/93608d1037327afa4f755976b86427b6128ae3d0))
|
||||||
|
|
||||||
|
# [1.0.0-alpha.9](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.8...@music163/tango-core@1.0.0-alpha.9) (2024-03-26)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- quick add sibling nodes ([#127](https://github.com/netease/tango/issues/127)) ([9ed6a7d](https://github.com/netease/tango/commit/9ed6a7d1a4944d69d96e034f243b61531862e317))
|
||||||
|
|
||||||
|
# [1.0.0-alpha.8](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.7...@music163/tango-core@1.0.0-alpha.8) (2024-03-18)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @music163/tango-core
|
||||||
|
|
||||||
# [1.0.0-alpha.7](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.6...@music163/tango-core@1.0.0-alpha.7) (2024-03-18)
|
# [1.0.0-alpha.7](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.6...@music163/tango-core@1.0.0-alpha.7) (2024-03-18)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@music163/tango-core",
|
"name": "@music163/tango-core",
|
||||||
"version": "1.0.0-alpha.7",
|
"version": "1.3.0",
|
||||||
"description": "tango core",
|
"description": "tango core",
|
||||||
"author": "wwsun <ww.sun@outlook.com>",
|
"author": "wwsun <ww.sun@outlook.com>",
|
||||||
"homepage": "",
|
"homepage": "",
|
||||||
@@ -28,10 +28,10 @@
|
|||||||
"@babel/parser": "^7.23.5",
|
"@babel/parser": "^7.23.5",
|
||||||
"@babel/traverse": "^7.23.5",
|
"@babel/traverse": "^7.23.5",
|
||||||
"@babel/types": "^7.23.5",
|
"@babel/types": "^7.23.5",
|
||||||
"@music163/tango-helpers": "^1.0.0-alpha.3",
|
"@music163/tango-helpers": "^1.2.0",
|
||||||
"@types/babel__generator": "^7.6.7",
|
"@types/babel__generator": "^7.6.7",
|
||||||
"@types/babel__traverse": "^7.20.4",
|
"@types/babel__traverse": "^7.20.4",
|
||||||
"mobx": "6.12.0",
|
"mobx": "6.12.3",
|
||||||
"path-browserify": "^1.0.1"
|
"path-browserify": "^1.0.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
@@ -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';
|
import { IWorkspace } from './models/interfaces';
|
||||||
|
|
||||||
interface ICreateEngineOptions {
|
interface ICreateEngineOptions {
|
||||||
@@ -6,6 +7,10 @@ interface ICreateEngineOptions {
|
|||||||
* 自定义工作区
|
* 自定义工作区
|
||||||
*/
|
*/
|
||||||
workspace?: IWorkspace;
|
workspace?: IWorkspace;
|
||||||
|
/**
|
||||||
|
* 菜单信息
|
||||||
|
*/
|
||||||
|
menuData?: MenuDataType;
|
||||||
/**
|
/**
|
||||||
* 默认的模拟器模式
|
* 默认的模拟器模式
|
||||||
*/
|
*/
|
||||||
@@ -14,6 +19,10 @@ interface ICreateEngineOptions {
|
|||||||
* 默认激活的侧边栏
|
* 默认激活的侧边栏
|
||||||
*/
|
*/
|
||||||
defaultActiveSidebarPanel?: string;
|
defaultActiveSidebarPanel?: string;
|
||||||
|
/**
|
||||||
|
* 默认激活的视图
|
||||||
|
*/
|
||||||
|
defaultActiveView?: DesignerViewType;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,15 +32,19 @@ interface ICreateEngineOptions {
|
|||||||
*/
|
*/
|
||||||
export function createEngine({
|
export function createEngine({
|
||||||
workspace,
|
workspace,
|
||||||
|
defaultActiveView = 'design',
|
||||||
defaultSimulatorMode = 'desktop',
|
defaultSimulatorMode = 'desktop',
|
||||||
defaultActiveSidebarPanel = '',
|
defaultActiveSidebarPanel = '',
|
||||||
|
menuData,
|
||||||
}: ICreateEngineOptions) {
|
}: ICreateEngineOptions) {
|
||||||
const engine = new Engine({
|
const engine = new Engine({
|
||||||
workspace,
|
workspace,
|
||||||
designer: new Designer({
|
designer: new Designer({
|
||||||
workspace,
|
workspace,
|
||||||
simulator: defaultSimulatorMode,
|
simulator: defaultSimulatorMode,
|
||||||
|
activeView: defaultActiveView,
|
||||||
activeSidebarPanel: defaultActiveSidebarPanel,
|
activeSidebarPanel: defaultActiveSidebarPanel,
|
||||||
|
menuData,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const templatePattern = /^{(.+)}$/s;
|
|||||||
/**
|
/**
|
||||||
* 判断给定字符串是否被表达式容器`{expCode}`包裹
|
* 判断给定字符串是否被表达式容器`{expCode}`包裹
|
||||||
* @param code
|
* @param code
|
||||||
|
* @deprecated 新版改为 {{code}} 作为容器,使用 isWrappedCode 代替
|
||||||
*/
|
*/
|
||||||
export function isWrappedByExpressionContainer(code: string, isStrict = true) {
|
export function isWrappedByExpressionContainer(code: string, isStrict = true) {
|
||||||
if (isStrict && isValidExpressionCode(code)) {
|
if (isStrict && isValidExpressionCode(code)) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
import generator, { GeneratorOptions } from '@babel/generator';
|
import generator, { GeneratorOptions } from '@babel/generator';
|
||||||
import * as t from '@babel/types';
|
import * as t from '@babel/types';
|
||||||
import { logger } from '@music163/tango-helpers';
|
import { logger, wrapCode } from '@music163/tango-helpers';
|
||||||
import { formatCode } from '../string';
|
import { formatCode } from '../string';
|
||||||
|
|
||||||
const defaultGeneratorOptions: GeneratorOptions = {
|
const defaultGeneratorOptions: GeneratorOptions = {
|
||||||
@@ -156,10 +156,10 @@ export function node2code(node: t.Node) {
|
|||||||
/**
|
/**
|
||||||
* 将 t.Node 生成为 js 值
|
* 将 t.Node 生成为 js 值
|
||||||
* @param node ast node
|
* @param node ast node
|
||||||
* @param hasExpressionWrapper 是否包裹表达式
|
* @param isWrapCode 是否包裹代码,例如 code -> {{code}}
|
||||||
* @returns a plain javascript value
|
* @returns a plain javascript value
|
||||||
*/
|
*/
|
||||||
export function node2value(node: t.Node, hasExpressionWrapper = true): any {
|
export function node2value(node: t.Node, isWrapCode = true): any {
|
||||||
let ret;
|
let ret;
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
case 'StringLiteral':
|
case 'StringLiteral':
|
||||||
@@ -171,39 +171,50 @@ export function node2value(node: t.Node, hasExpressionWrapper = true): any {
|
|||||||
case 'NullLiteral':
|
case 'NullLiteral':
|
||||||
ret = null;
|
ret = null;
|
||||||
break;
|
break;
|
||||||
case 'Identifier': // {data}
|
case 'Identifier': // {{data}}
|
||||||
case 'MemberExpression': // {this.props.data}
|
case 'MemberExpression': // {{this.props.data}}
|
||||||
case 'OptionalMemberExpression': // {a?.b}
|
case 'OptionalMemberExpression': // {{a?.b}}
|
||||||
case 'UnaryExpression': // {!false}
|
case 'UnaryExpression': // {{!false}}
|
||||||
case 'ArrowFunctionExpression': // {() => {}}
|
case 'ArrowFunctionExpression': // {{() => {}}}
|
||||||
case 'TemplateLiteral': // {`hello ${text}`}
|
case 'TemplateLiteral': // {{`hello ${text}`}}
|
||||||
case 'ConditionalExpression': // {a ? 'foo' : 'bar'}
|
case 'ConditionalExpression': // {{a ? 'foo' : 'bar'}}
|
||||||
case 'LogicalExpression': // { a || b}
|
case 'LogicalExpression': // {{ a || b}}
|
||||||
case 'BinaryExpression': // { a + b}
|
case 'BinaryExpression': // {{ a + b}}
|
||||||
case 'TaggedTemplateExpression': // {css``}
|
case 'TaggedTemplateExpression': // {{css``}}
|
||||||
case 'CallExpression': // {[1,2,3].map(fn)}
|
case 'CallExpression': // {{[1,2,3].map(fn)}}
|
||||||
case 'JSXElement': // {<Box>hello</Box>}
|
case 'JSXElement': // {{<Box>hello</Box>}}
|
||||||
case 'JSXFragment': // <><Box /></>
|
case 'JSXFragment': // {{<><Box /></>}}
|
||||||
ret = expression2code(node);
|
ret = expression2code(node);
|
||||||
if (hasExpressionWrapper) {
|
if (isWrapCode) {
|
||||||
ret = `{${ret}}`;
|
ret = wrapCode(ret);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'ObjectExpression': {
|
case 'ObjectExpression': {
|
||||||
ret = node.properties.reduce((prev, propertyNode) => {
|
const isSimpleObject = node.properties.every(
|
||||||
if (propertyNode.type === 'ObjectProperty') {
|
(propertyNode) => propertyNode.type === 'ObjectProperty',
|
||||||
const key = keyNode2value(propertyNode.key);
|
);
|
||||||
const value = node2value(propertyNode.value, hasExpressionWrapper);
|
if (isSimpleObject) {
|
||||||
// key 可能是字符串,也可能是数字
|
// simple object: { key1, key2, key3 }
|
||||||
prev[key] = value;
|
ret = node.properties.reduce((prev, propertyNode) => {
|
||||||
|
if (propertyNode.type === 'ObjectProperty') {
|
||||||
|
const key = keyNode2value(propertyNode.key);
|
||||||
|
const value = node2value(propertyNode.value, isWrapCode);
|
||||||
|
prev[key] = value; // key 可能是字符串,也可能是数字
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
}, {});
|
||||||
|
} else {
|
||||||
|
// mixed object, object property maybe SpreadElement or ObjectMethod, e.g. { key1, fn() {}, ...obj1 }
|
||||||
|
ret = expression2code(node);
|
||||||
|
if (wrapCode) {
|
||||||
|
ret = wrapCode(ret);
|
||||||
}
|
}
|
||||||
// FIXME: property is a SpreadElement
|
}
|
||||||
return prev;
|
|
||||||
}, {});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'ArrayExpression': {
|
case 'ArrayExpression': {
|
||||||
ret = node.elements.map((elementNode) => node2value(elementNode, hasExpressionWrapper));
|
// FIXME: 有可能会解析失败
|
||||||
|
ret = node.elements.map((elementNode) => node2value(elementNode, isWrapCode));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -214,7 +225,7 @@ export function node2value(node: t.Node, hasExpressionWrapper = true): any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* jsx 属性值节点转为 js value
|
* jsx prop value 节点转为 js value
|
||||||
*/
|
*/
|
||||||
export function jsxAttributeValueNode2value(node: t.Node): any {
|
export function jsxAttributeValueNode2value(node: t.Node): any {
|
||||||
// e.g. <Checkbox checked /> 此时没有 value node
|
// e.g. <Checkbox checked /> 此时没有 value node
|
||||||
@@ -232,9 +243,16 @@ export function jsxAttributeValueNode2value(node: t.Node): any {
|
|||||||
// <Foo bar={[]}>
|
// <Foo bar={[]}>
|
||||||
ret = jsxAttributeValueNode2value(node.expression);
|
ret = jsxAttributeValueNode2value(node.expression);
|
||||||
break;
|
break;
|
||||||
default:
|
case 'ArrayExpression': {
|
||||||
|
// 数组统一处理为 code
|
||||||
|
ret = expression2code(node);
|
||||||
|
ret = wrapCode(ret);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
ret = node2value(node);
|
ret = node2value(node);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import { parse, parseExpression, ParserOptions } from '@babel/parser';
|
|||||||
import * as t from '@babel/types';
|
import * as t from '@babel/types';
|
||||||
import {
|
import {
|
||||||
logger,
|
logger,
|
||||||
isValidObjectString,
|
|
||||||
getVariableContent,
|
getVariableContent,
|
||||||
isPlainObject,
|
isPlainObject,
|
||||||
Dict,
|
Dict,
|
||||||
|
isWrappedCode,
|
||||||
|
getCodeOfWrappedCode,
|
||||||
} from '@music163/tango-helpers';
|
} from '@music163/tango-helpers';
|
||||||
import { isWrappedByExpressionContainer } from '../assert';
|
|
||||||
|
|
||||||
// @see https://babeljs.io/docs/en/babel-parser#pluginss
|
// @see https://babeljs.io/docs/en/babel-parser#pluginss
|
||||||
const babelParserConfig: ParserOptions = {
|
const babelParserConfig: ParserOptions = {
|
||||||
@@ -97,8 +97,7 @@ export function code2expression(code: string) {
|
|||||||
try {
|
try {
|
||||||
expNode = t.cloneNode(parseExpression(code, babelParserConfig), false, true);
|
expNode = t.cloneNode(parseExpression(code, babelParserConfig), false, true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('invalid code', err);
|
logger.error('code2expression failed, invalid code:', code);
|
||||||
// expNode = t.identifier('undefined');
|
|
||||||
}
|
}
|
||||||
return expNode;
|
return expNode;
|
||||||
}
|
}
|
||||||
@@ -109,9 +108,6 @@ export function code2expression(code: string) {
|
|||||||
* @returns File
|
* @returns File
|
||||||
*/
|
*/
|
||||||
export function expressionCode2ast(code: string) {
|
export function expressionCode2ast(code: string) {
|
||||||
if (isWrappedByExpressionContainer(code)) {
|
|
||||||
code = getVariableContent(code);
|
|
||||||
}
|
|
||||||
const node = code2expression(code);
|
const node = code2expression(code);
|
||||||
return t.file(t.program([t.blockStatement([t.expressionStatement(node)])]));
|
return t.file(t.program([t.blockStatement([t.expressionStatement(node)])]));
|
||||||
}
|
}
|
||||||
@@ -132,18 +128,19 @@ export function value2node(
|
|||||||
| t.Expression {
|
| t.Expression {
|
||||||
let ret;
|
let ret;
|
||||||
switch (typeof value) {
|
switch (typeof value) {
|
||||||
case 'number':
|
|
||||||
ret = t.numericLiteral(value);
|
|
||||||
break;
|
|
||||||
case 'string':
|
case 'string':
|
||||||
if (isWrappedByExpressionContainer(value)) {
|
if (isWrappedCode(value)) {
|
||||||
// 再检查是否是表达式容器,例如 {this.foo}, {1}
|
// 再检查是否是代码 {{code}},例如 {{this.foo}}, {{1}}
|
||||||
const innerString = getVariableContent(value);
|
const innerCode = getCodeOfWrappedCode(value);
|
||||||
ret = code2expression(innerString);
|
ret = code2expression(innerCode);
|
||||||
} else {
|
} else {
|
||||||
|
// 否则当成字符串处理
|
||||||
ret = t.stringLiteral(value);
|
ret = t.stringLiteral(value);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'number':
|
||||||
|
ret = t.numericLiteral(value);
|
||||||
|
break;
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
ret = t.booleanLiteral(value);
|
ret = t.booleanLiteral(value);
|
||||||
break;
|
break;
|
||||||
@@ -167,7 +164,7 @@ export function value2node(
|
|||||||
ret = t.identifier('undefined');
|
ret = t.identifier('undefined');
|
||||||
break;
|
break;
|
||||||
default: {
|
default: {
|
||||||
logger.error(`value2node: unsupport value <${value}>`);
|
logger.error(`value2node: value <${value}> transform failed!`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,21 +193,23 @@ export function code2jsxAttributeValueNode(code: string) {
|
|||||||
return t.jsxExpressionContainer(code2expression(code));
|
return t.jsxExpressionContainer(code2expression(code));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIXME: 统一处理为 code2jsxAttributeValueNode
|
||||||
|
* 将 js value 转为 JSXAttributeValueNode
|
||||||
|
* @param value js value, or wrapped code
|
||||||
|
* @returns 返回 JSXAttributeValueNode,转换失败返回Node为 {undefined}
|
||||||
|
*/
|
||||||
export function value2jsxAttributeValueNode(value: any) {
|
export function value2jsxAttributeValueNode(value: any) {
|
||||||
let ret;
|
let ret;
|
||||||
switch (typeof value) {
|
switch (typeof value) {
|
||||||
// FIXME: 重构这个逻辑,是不是统一当成 code 处理
|
|
||||||
case 'string': {
|
case 'string': {
|
||||||
if (value.length > 1) {
|
if (value.length > 1) {
|
||||||
value = value.trim();
|
value = value.trim();
|
||||||
}
|
}
|
||||||
if (isValidObjectString(value)) {
|
if (isWrappedCode(value)) {
|
||||||
// 先检查是否是对象字符串
|
const innerCode = getCodeOfWrappedCode(value);
|
||||||
ret = t.jsxExpressionContainer(code2expression(value));
|
const node = code2expression(innerCode);
|
||||||
} else if (isWrappedByExpressionContainer(value)) {
|
ret = t.jsxExpressionContainer(node || t.identifier('undefined'));
|
||||||
// 再检查是否是表达式容器,例如 {this.foo}, {1}
|
|
||||||
const innerString = getVariableContent(value);
|
|
||||||
ret = t.jsxExpressionContainer(code2expression(innerString));
|
|
||||||
} else {
|
} else {
|
||||||
ret = t.stringLiteral(value);
|
ret = t.stringLiteral(value);
|
||||||
}
|
}
|
||||||
@@ -223,11 +222,12 @@ export function value2jsxAttributeValueNode(value: any) {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: 待校验
|
||||||
export function value2jsxChildrenValueNode(value: any) {
|
export function value2jsxChildrenValueNode(value: any) {
|
||||||
let ret: t.JSXElement | t.JSXFragment | t.JSXExpressionContainer | t.JSXSpreadChild | t.JSXText;
|
let ret: t.JSXElement | t.JSXFragment | t.JSXExpressionContainer | t.JSXSpreadChild | t.JSXText;
|
||||||
switch (typeof value) {
|
switch (typeof value) {
|
||||||
case 'string':
|
case 'string':
|
||||||
if (isWrappedByExpressionContainer(value)) {
|
if (isValidExpressionCode(value)) {
|
||||||
const innerString = getVariableContent(value);
|
const innerString = getVariableContent(value);
|
||||||
ret = t.jsxExpressionContainer(code2expression(innerString));
|
ret = t.jsxExpressionContainer(code2expression(innerString));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1425,3 +1425,22 @@ export function traverseFile(ast: t.File) {
|
|||||||
imports,
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,27 +1,8 @@
|
|||||||
import { getVariableContent } from '@music163/tango-helpers';
|
import { getCodeOfWrappedCode, isWrappedCode } from '@music163/tango-helpers';
|
||||||
import { value2node, expression2code, isValidExpressionCode } from './ast';
|
import { value2node, expression2code, code2expression, node2value } from './ast';
|
||||||
import { isWrappedByExpressionContainer } from './assert';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将 js value 转换为代码字符串
|
* js value 转为代码字符串
|
||||||
*/
|
|
||||||
export function value2code(value: any) {
|
|
||||||
const node = value2node(value);
|
|
||||||
const code = expression2code(node);
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否是字符串代码
|
|
||||||
* @param code
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function isStringCode(code: string) {
|
|
||||||
return /^".*"$/.test(code?.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* js value 转为表达式代码
|
|
||||||
* @example 1 => 1
|
* @example 1 => 1
|
||||||
* @example hello => "hello"
|
* @example hello => "hello"
|
||||||
* @example { foo: bar } => {{ foo: bar }}
|
* @example { foo: bar } => {{ foo: bar }}
|
||||||
@@ -30,34 +11,56 @@ function isStringCode(code: string) {
|
|||||||
* @param val js value
|
* @param val js value
|
||||||
* @returns 表达式代码
|
* @returns 表达式代码
|
||||||
*/
|
*/
|
||||||
export function value2expressionCode(val: any) {
|
export function value2code(val: any) {
|
||||||
if (!val) return '';
|
if (val === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val === null) {
|
||||||
|
return 'null';
|
||||||
|
}
|
||||||
|
|
||||||
let ret;
|
let ret;
|
||||||
|
|
||||||
switch (typeof val) {
|
switch (typeof val) {
|
||||||
case 'string': {
|
case 'string': {
|
||||||
if (isValidExpressionCode(val)) {
|
if (isWrappedCode(val)) {
|
||||||
ret = val;
|
ret = getCodeOfWrappedCode(val);
|
||||||
} else if (isWrappedByExpressionContainer(val, false)) {
|
|
||||||
ret = getVariableContent(val);
|
|
||||||
} else if (isStringCode(val)) {
|
|
||||||
ret = val;
|
|
||||||
} else {
|
} else {
|
||||||
ret = `"${val}"`;
|
ret = `"${val}"`;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'boolean':
|
||||||
|
case 'function':
|
||||||
case 'number':
|
case 'number':
|
||||||
ret = String(val);
|
ret = String(val);
|
||||||
break;
|
break;
|
||||||
case 'object':
|
default: {
|
||||||
ret = value2code(val);
|
// other cases, including array, object, null, undefined
|
||||||
break;
|
const node = value2node(val);
|
||||||
default:
|
ret = expression2code(node);
|
||||||
ret = '';
|
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const value2expressionCode = value2code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代码字符串转为具体的 js value
|
||||||
|
* @example `() => {}` 返回 undefined
|
||||||
|
*
|
||||||
|
* @param rawCode 代码字符串
|
||||||
|
* @returns 返回解析后的 js value,包括:string, number, boolean, simpleObject, simpleArray
|
||||||
|
*/
|
||||||
|
export function code2value(rawCode: string) {
|
||||||
|
const node = code2expression(rawCode);
|
||||||
|
const value = node2value(node);
|
||||||
|
if (isWrappedCode(value)) {
|
||||||
|
// 能转的就转,转不能的就返回空
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
import * as t from '@babel/types';
|
import * as t from '@babel/types';
|
||||||
import {
|
import {
|
||||||
ComponentPropType,
|
IComponentProp,
|
||||||
ComponentPrototypeType,
|
IComponentPrototype,
|
||||||
Dict,
|
Dict,
|
||||||
isNil,
|
|
||||||
logger,
|
logger,
|
||||||
uuid,
|
uuid,
|
||||||
|
isWrappedCode,
|
||||||
|
getCodeOfWrappedCode,
|
||||||
|
wrapCodeWithJSXExpressionContainer,
|
||||||
} from '@music163/tango-helpers';
|
} from '@music163/tango-helpers';
|
||||||
import { getRelativePath, isFilepath } from './string';
|
import { getRelativePath, isFilepath } from './string';
|
||||||
import type { IImportDeclarationPayload, IImportSpecifierData } from '../types';
|
import type { IImportDeclarationPayload, IImportSpecifierData } from '../types';
|
||||||
import { code2expression } from './ast';
|
import { code2expression } from './ast';
|
||||||
import { isWrappedByExpressionContainer } from './assert';
|
import { isWrappedByExpressionContainer } from './assert';
|
||||||
|
import { value2code } from './code-helpers';
|
||||||
|
|
||||||
export function prototype2importDeclarationData(
|
export function prototype2importDeclarationData(
|
||||||
prototype: ComponentPrototypeType,
|
prototype: IComponentPrototype,
|
||||||
relativeFilepath?: string,
|
relativeFilepath?: string,
|
||||||
): { source: string; specifiers: IImportSpecifierData[] } {
|
): { source: string; specifiers: IImportSpecifierData[] } {
|
||||||
let source = prototype.package;
|
let source = prototype.package;
|
||||||
|
const isSnippet = prototype.type === 'snippet';
|
||||||
if (relativeFilepath && isFilepath(source)) {
|
if (relativeFilepath && isFilepath(source)) {
|
||||||
source = getRelativePath(relativeFilepath, source);
|
source = getRelativePath(relativeFilepath, source);
|
||||||
}
|
}
|
||||||
@@ -32,12 +36,15 @@ export function prototype2importDeclarationData(
|
|||||||
type: 'ImportDefaultSpecifier',
|
type: 'ImportDefaultSpecifier',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
[prototype.name, ...(prototype.relatedImports || [])].forEach((item) => {
|
// 忽略代码片段 name
|
||||||
specifiers.push({
|
[...(isSnippet ? [] : [prototype.name]), ...(prototype.relatedImports || [])].forEach(
|
||||||
localName: item,
|
(item) => {
|
||||||
type: 'ImportSpecifier',
|
specifiers.push({
|
||||||
});
|
localName: item,
|
||||||
});
|
type: 'ImportSpecifier',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -51,7 +58,7 @@ export function prototype2importDeclarationData(
|
|||||||
* @deprecated
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
export function getImportDeclarationPayloadByPrototype(
|
export function getImportDeclarationPayloadByPrototype(
|
||||||
prototype: ComponentPrototypeType,
|
prototype: IComponentPrototype,
|
||||||
relativeFilepath?: string,
|
relativeFilepath?: string,
|
||||||
): IImportDeclarationPayload {
|
): IImportDeclarationPayload {
|
||||||
let defaultSpecifier;
|
let defaultSpecifier;
|
||||||
@@ -84,48 +91,56 @@ export function getImportDeclarationPayloadByPrototype(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 基于 key-value 生成 prop={value} 字符串
|
* 基于 key-value 生成 prop={value} 字符串
|
||||||
* @param key
|
* @example { name: 'foo', initValue: false } >> name={false}
|
||||||
* @param value
|
* @example { name: 'foo', initValue: 1 } >> name={1}
|
||||||
|
* @example { name: 'foo', initValue: () => {} } >> name={()=>{}}
|
||||||
|
* @example { name: 'foo', initValue: { foo: 'bar' } } >> name={{ foo: 'bar' }}
|
||||||
|
* @example { name: 'foo', initValue: [{ foo: 'bar' }] } >> name={[{ foo: 'bar' }]}
|
||||||
|
* @example { name: 'foo', initValue: 'bar' } >> name="bar"
|
||||||
|
* @example { name: 'foo', initValue: '{() => {}}' } >> name={()=>{}}
|
||||||
|
* @example { name: 'foo', initValue: '{{() => {}}}' } >> name={() => {}}
|
||||||
|
* @example { name: 'foo', initValue: '{bar}' } >> name={bar}
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function getPropKeyValuePair(item: ComponentPropType, generateValue: (...args: any[]) => string) {
|
export function propDataToKeyValueString(
|
||||||
|
item: IComponentProp,
|
||||||
|
generateValue?: (...args: any[]) => string,
|
||||||
|
) {
|
||||||
const key = item.name;
|
const key = item.name;
|
||||||
|
|
||||||
let value = item.initValue;
|
let value = item.initValue;
|
||||||
|
|
||||||
if (!value && item.autoInitValue) {
|
if (!value && item.autoInitValue) {
|
||||||
value = generateValue(3);
|
value = generateValue?.(3) || uuid(key, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNil(value)) {
|
if (value === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (typeof value) {
|
switch (typeof value) {
|
||||||
case 'number':
|
case 'number':
|
||||||
case 'boolean': {
|
case 'boolean':
|
||||||
value = `{${value}}`;
|
case 'function': {
|
||||||
|
value = wrapCodeWithJSXExpressionContainer(String(value));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'object': {
|
case 'object': {
|
||||||
// TIP: bugfix 如果 object 里有 jsx 或者 function 会失败
|
|
||||||
try {
|
try {
|
||||||
value = `{${JSON.stringify(value)}}`;
|
value = wrapCodeWithJSXExpressionContainer(value2code(value));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'function': {
|
|
||||||
value = `{${(value as object).toString()}}`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'string': {
|
case 'string': {
|
||||||
if (!isWrappedByExpressionContainer(value)) {
|
if (isWrappedCode(value)) {
|
||||||
// 不是变量字符串
|
const innerCode = getCodeOfWrappedCode(value);
|
||||||
value = `"${value}"`;
|
value = wrapCodeWithJSXExpressionContainer(innerCode);
|
||||||
|
} else if (isWrappedByExpressionContainer(value)) {
|
||||||
|
// TIP: 兼容旧版逻辑,如果是变量字符串,无需处理
|
||||||
} else {
|
} else {
|
||||||
// 如果是变量字符串,无需处理
|
value = `"${value}"`;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -141,25 +156,25 @@ function getPropKeyValuePair(item: ComponentPropType, generateValue: (...args: a
|
|||||||
* @param prototype
|
* @param prototype
|
||||||
* @param extraProps
|
* @param extraProps
|
||||||
*/
|
*/
|
||||||
export function prototype2code(prototype: ComponentPrototypeType, extraProps?: Dict) {
|
export function prototype2code(prototype: IComponentPrototype, extraProps?: Dict) {
|
||||||
let code;
|
let code;
|
||||||
switch (prototype.type) {
|
switch (prototype.type) {
|
||||||
case 'snippet':
|
case 'snippet':
|
||||||
code = prototype.initChildren || prototype.defaultChildren;
|
code = prototype.initChildren || prototype.defaultChildren;
|
||||||
break;
|
break;
|
||||||
default: {
|
default: {
|
||||||
const propList: ComponentPropType[] = extraProps
|
const propList: IComponentProp[] = extraProps
|
||||||
? Object.keys(extraProps).map((key) => ({
|
? Object.keys(extraProps).map((key) => ({
|
||||||
name: key,
|
name: key,
|
||||||
initValue: extraProps[key],
|
initValue: extraProps[key],
|
||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
// merge extraProps to props
|
// merge extraProps to props
|
||||||
const props = [...prototype.props, ...propList];
|
const props = [...(prototype.props || []), ...propList];
|
||||||
|
|
||||||
const keys =
|
const keys =
|
||||||
props.reduce((acc, item) => {
|
props.reduce((acc, item) => {
|
||||||
const pair = getPropKeyValuePair(item, (fractionDigits: number) =>
|
const pair = propDataToKeyValueString(item, (fractionDigits: number) =>
|
||||||
uuid(prototype.name, fractionDigits),
|
uuid(prototype.name, fractionDigits),
|
||||||
);
|
);
|
||||||
return pair ? ` ${acc} ${pair}` : acc;
|
return pair ? ` ${acc} ${pair}` : acc;
|
||||||
@@ -186,7 +201,7 @@ export function prototype2code(prototype: ComponentPrototypeType, extraProps?: D
|
|||||||
* @param prototype 组件的配置信息
|
* @param prototype 组件的配置信息
|
||||||
* @param props 额外的属性集
|
* @param props 额外的属性集
|
||||||
*/
|
*/
|
||||||
export function prototype2jsxElement(prototype: ComponentPrototypeType, props?: Dict) {
|
export function prototype2jsxElement(prototype: IComponentPrototype, props?: Dict) {
|
||||||
const code = prototype2code(prototype, props);
|
const code = prototype2code(prototype, props);
|
||||||
return code2expression(code) as t.JSXElement;
|
return code2expression(code) as t.JSXElement;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,15 +75,20 @@ export function inferFileType(filename: string): FileType {
|
|||||||
/**
|
/**
|
||||||
* 判断组件名是否合法
|
* 判断组件名是否合法
|
||||||
* @example Button -> valid
|
* @example Button -> valid
|
||||||
* @example div -> invalid
|
* @example isStrict ? div -> invalid : div -> valid
|
||||||
* @param name
|
* @param name
|
||||||
|
* @param isStrict 是否严格模式(严格模式不匹配原生标签)
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function isValidComponentName(name: string) {
|
export function isValidComponentName(name: string, isStrict = false) {
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isStrict) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const firstChar = name.charAt(0);
|
const firstChar = name.charAt(0);
|
||||||
return firstChar === firstChar.toUpperCase();
|
return firstChar === firstChar.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||||
import { IWorkspace } from './interfaces';
|
import { IWorkspace } from './interfaces';
|
||||||
|
import { MenuDataType } from '@music163/tango-helpers';
|
||||||
|
|
||||||
export type SimulatorNameType = 'desktop' | 'phone';
|
export type SimulatorNameType = 'desktop' | 'phone';
|
||||||
|
|
||||||
export type DesignerViewType = 'design' | 'code';
|
export type DesignerViewType = 'design' | 'code' | 'dual';
|
||||||
|
|
||||||
interface ISimulatorType {
|
interface ISimulatorType {
|
||||||
name: SimulatorNameType;
|
name: SimulatorNameType;
|
||||||
@@ -19,7 +20,15 @@ interface IViewportBounding {
|
|||||||
interface IDesignerOptions {
|
interface IDesignerOptions {
|
||||||
workspace: IWorkspace;
|
workspace: IWorkspace;
|
||||||
simulator?: SimulatorNameType | ISimulatorType;
|
simulator?: SimulatorNameType | ISimulatorType;
|
||||||
|
/**
|
||||||
|
* 菜单配置
|
||||||
|
*/
|
||||||
|
menuData: MenuDataType;
|
||||||
activeSidebarPanel?: string;
|
activeSidebarPanel?: string;
|
||||||
|
/**
|
||||||
|
* 默认激活的视图模式
|
||||||
|
*/
|
||||||
|
activeView?: DesignerViewType;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ISimulatorTypes: Record<string, ISimulatorType> = {
|
const ISimulatorTypes: Record<string, ISimulatorType> = {
|
||||||
@@ -64,6 +73,26 @@ export class Designer {
|
|||||||
*/
|
*/
|
||||||
_showSmartWizard = false;
|
_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;
|
_isPreview = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 默认展开的侧边栏
|
* 菜单列表
|
||||||
*/
|
*/
|
||||||
defaultActiveSidebarPanel?: string;
|
_menuData?: MenuDataType = null;
|
||||||
|
|
||||||
private readonly workspace: IWorkspace;
|
private readonly workspace: IWorkspace;
|
||||||
|
|
||||||
@@ -109,10 +138,39 @@ export class Designer {
|
|||||||
return this._showRightPanel;
|
return this._showRightPanel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get showAddComponentPopover() {
|
||||||
|
return this._showAddComponentPopover;
|
||||||
|
}
|
||||||
|
|
||||||
|
get addComponentPopoverPosition() {
|
||||||
|
return this._addComponentPopoverPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
get showContextMenu() {
|
||||||
|
return this._showContextMenu;
|
||||||
|
}
|
||||||
|
|
||||||
|
get contextMenuPosition() {
|
||||||
|
return this._contextMenuPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
get menuData() {
|
||||||
|
return this._menuData ?? ([] as MenuDataType);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(options: IDesignerOptions) {
|
constructor(options: IDesignerOptions) {
|
||||||
this.workspace = options.workspace;
|
this.workspace = options.workspace;
|
||||||
|
|
||||||
const { simulator, activeSidebarPanel: defaultActiveSidebarPanel } = options;
|
const {
|
||||||
|
simulator,
|
||||||
|
menuData,
|
||||||
|
activeSidebarPanel: defaultActiveSidebarPanel,
|
||||||
|
activeView: defaultActiveView,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
if (menuData) {
|
||||||
|
this.setMenuData(menuData);
|
||||||
|
}
|
||||||
|
|
||||||
// 默认设计器模式
|
// 默认设计器模式
|
||||||
if (simulator) {
|
if (simulator) {
|
||||||
@@ -124,6 +182,11 @@ export class Designer {
|
|||||||
this.setActiveSidebarPanel(defaultActiveSidebarPanel);
|
this.setActiveSidebarPanel(defaultActiveSidebarPanel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 默认激活的视图
|
||||||
|
if (defaultActiveView) {
|
||||||
|
this.setActiveView(defaultActiveView);
|
||||||
|
}
|
||||||
|
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
_simulator: observable,
|
_simulator: observable,
|
||||||
_viewport: observable,
|
_viewport: observable,
|
||||||
@@ -131,6 +194,11 @@ export class Designer {
|
|||||||
_activeSidebarPanel: observable,
|
_activeSidebarPanel: observable,
|
||||||
_showSmartWizard: observable,
|
_showSmartWizard: observable,
|
||||||
_showRightPanel: observable,
|
_showRightPanel: observable,
|
||||||
|
_showAddComponentPopover: observable,
|
||||||
|
_addComponentPopoverPosition: observable,
|
||||||
|
_showContextMenu: observable,
|
||||||
|
_contextMenuPosition: observable,
|
||||||
|
_menuData: observable,
|
||||||
_isPreview: observable,
|
_isPreview: observable,
|
||||||
simulator: computed,
|
simulator: computed,
|
||||||
viewport: computed,
|
viewport: computed,
|
||||||
@@ -139,6 +207,11 @@ export class Designer {
|
|||||||
isPreview: computed,
|
isPreview: computed,
|
||||||
showRightPanel: computed,
|
showRightPanel: computed,
|
||||||
showSmartWizard: computed,
|
showSmartWizard: computed,
|
||||||
|
showAddComponentPopover: computed,
|
||||||
|
addComponentPopoverPosition: computed,
|
||||||
|
showContextMenu: computed,
|
||||||
|
contextMenuPosition: computed,
|
||||||
|
menuData: computed,
|
||||||
setSimulator: action,
|
setSimulator: action,
|
||||||
setViewport: action,
|
setViewport: action,
|
||||||
setActiveView: action,
|
setActiveView: action,
|
||||||
@@ -147,6 +220,8 @@ export class Designer {
|
|||||||
toggleRightPanel: action,
|
toggleRightPanel: action,
|
||||||
toggleSmartWizard: action,
|
toggleSmartWizard: action,
|
||||||
toggleIsPreview: action,
|
toggleIsPreview: action,
|
||||||
|
toggleAddComponentPopover: action,
|
||||||
|
toggleContextMenu: action,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,6 +248,11 @@ export class Designer {
|
|||||||
this._activeSidebarPanel = '';
|
this._activeSidebarPanel = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setMenuData(menuData: MenuDataType) {
|
||||||
|
this._menuData = menuData;
|
||||||
|
}
|
||||||
|
|
||||||
closeSidebarPanel() {
|
closeSidebarPanel() {
|
||||||
this._activeSidebarPanel = '';
|
this._activeSidebarPanel = '';
|
||||||
}
|
}
|
||||||
@@ -185,10 +265,37 @@ export class Designer {
|
|||||||
this._showRightPanel = value ?? !this._showRightPanel;
|
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) {
|
toggleIsPreview(value: boolean) {
|
||||||
this._isPreview = value ?? !this._isPreview;
|
this._isPreview = value ?? !this._isPreview;
|
||||||
if (value) {
|
if (value) {
|
||||||
this.workspace.selectSource.clear();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ export enum HistoryMessage {
|
|||||||
ReplaceNode = 'replaceNode',
|
ReplaceNode = 'replaceNode',
|
||||||
CloneNode = 'cloneNode',
|
CloneNode = 'cloneNode',
|
||||||
InsertNode = 'insertNode',
|
InsertNode = 'insertNode',
|
||||||
|
InsertBeforeNode = 'insertBeforeNode',
|
||||||
|
InsertAfterNode = 'insertAfterNode',
|
||||||
DropNode = 'dropNode',
|
DropNode = 'dropNode',
|
||||||
UpdateAttribute = 'updateAttribute',
|
UpdateAttribute = 'updateAttribute',
|
||||||
UpdateCode = 'updateCode',
|
UpdateCode = 'updateCode',
|
||||||
@@ -56,7 +58,7 @@ export class TangoHistory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get couldBack() {
|
get couldBack() {
|
||||||
return this._records.length > 0 && this._index > -1;
|
return this._records.length > 0 && this._index > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
get couldForward() {
|
get couldForward() {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export * from './drag-source';
|
|||||||
export * from './history';
|
export * from './history';
|
||||||
export * from './file';
|
export * from './file';
|
||||||
export * from './module';
|
export * from './module';
|
||||||
|
export * from './entry-module';
|
||||||
export * from './route-module';
|
export * from './route-module';
|
||||||
export * from './service-module';
|
export * from './service-module';
|
||||||
export * from './store-module';
|
export * from './store-module';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ComponentPrototypeType, Dict, ITangoConfigJson } from '@music163/tango-helpers';
|
import { IComponentPrototype, Dict, ITangoConfigJson } from '@music163/tango-helpers';
|
||||||
import { TangoHistory } from './history';
|
import { TangoHistory } from './history';
|
||||||
import { SelectSource } from './select-source';
|
import { SelectSource } from './select-source';
|
||||||
import { DragSource } from './drag-source';
|
import { DragSource } from './drag-source';
|
||||||
@@ -16,6 +16,7 @@ import { TangoRouteModule } from './route-module';
|
|||||||
import { TangoStoreModule } from './store-module';
|
import { TangoStoreModule } from './store-module';
|
||||||
import { TangoServiceModule } from './service-module';
|
import { TangoServiceModule } from './service-module';
|
||||||
import { IdGenerator } from '../helpers';
|
import { IdGenerator } from '../helpers';
|
||||||
|
import { AppEntryModule } from './entry-module';
|
||||||
|
|
||||||
export interface IViewFile {
|
export interface IViewFile {
|
||||||
readonly workspace: IWorkspace;
|
readonly workspace: IWorkspace;
|
||||||
@@ -141,7 +142,7 @@ export interface IWorkspace {
|
|||||||
dragSource: DragSource;
|
dragSource: DragSource;
|
||||||
|
|
||||||
files: Map<string, TangoFile>;
|
files: Map<string, TangoFile>;
|
||||||
componentPrototypes: Map<string, ComponentPrototypeType>;
|
componentPrototypes: Map<string, IComponentPrototype>;
|
||||||
|
|
||||||
entry: string;
|
entry: string;
|
||||||
activeFile: string;
|
activeFile: string;
|
||||||
@@ -152,6 +153,10 @@ export interface IWorkspace {
|
|||||||
* 解析后的 tango.config.json 文件,如果要获取项目配置,推荐使用 projectConfig 获取
|
* 解析后的 tango.config.json 文件,如果要获取项目配置,推荐使用 projectConfig 获取
|
||||||
*/
|
*/
|
||||||
tangoConfigJson: TangoJsonFile;
|
tangoConfigJson: TangoJsonFile;
|
||||||
|
/**
|
||||||
|
* app.js 入口文件解析后的模块
|
||||||
|
*/
|
||||||
|
appEntryModule: AppEntryModule;
|
||||||
/**
|
/**
|
||||||
* 解析后的路由模块
|
* 解析后的路由模块
|
||||||
*/
|
*/
|
||||||
@@ -171,8 +176,8 @@ export interface IWorkspace {
|
|||||||
setActiveRoute: (path: string) => void;
|
setActiveRoute: (path: string) => void;
|
||||||
setActiveFile: (filename: string) => void;
|
setActiveFile: (filename: string) => void;
|
||||||
|
|
||||||
setComponentPrototypes: (prototypes: Record<string, ComponentPrototypeType>) => void;
|
setComponentPrototypes: (prototypes: Record<string, IComponentPrototype>) => void;
|
||||||
getPrototype: (name: string | ComponentPrototypeType) => ComponentPrototypeType;
|
getPrototype: (name: string | IComponentPrototype) => IComponentPrototype;
|
||||||
|
|
||||||
// ----------------- 文件操作 -----------------
|
// ----------------- 文件操作 -----------------
|
||||||
addFiles: (files: IFileConfig[]) => void;
|
addFiles: (files: IFileConfig[]) => void;
|
||||||
@@ -204,16 +209,12 @@ export interface IWorkspace {
|
|||||||
cloneSelectedNode: () => void;
|
cloneSelectedNode: () => void;
|
||||||
copySelectedNode: () => void;
|
copySelectedNode: () => void;
|
||||||
pasteSelectedNode: () => void;
|
pasteSelectedNode: () => void;
|
||||||
insertToSelectedNode: (childNameOrPrototype: string | ComponentPrototypeType) => void;
|
insertToSelectedNode: (childNameOrPrototype: string | IComponentPrototype) => void;
|
||||||
|
insertBeforeSelectedNode: (sourceNameOrPrototype: string | IComponentPrototype) => void;
|
||||||
|
insertAfterSelectedNode: (sourceNameOrPrototype: string | IComponentPrototype) => void;
|
||||||
dropNode: () => void;
|
dropNode: () => void;
|
||||||
insertToNode: (
|
insertToNode: (targetNodeId: string, sourceNameOrPrototype: string | IComponentPrototype) => void;
|
||||||
targetNodeId: string,
|
replaceNode: (targetNodeId: string, sourceNameOrPrototype: string | IComponentPrototype) => void;
|
||||||
sourceNameOrPrototype: string | ComponentPrototypeType,
|
|
||||||
) => void;
|
|
||||||
replaceNode: (
|
|
||||||
targetNodeId: string,
|
|
||||||
sourceNameOrPrototype: string | ComponentPrototypeType,
|
|
||||||
) => void;
|
|
||||||
updateSelectedNodeAttributes: (
|
updateSelectedNodeAttributes: (
|
||||||
attributes: Record<string, any>,
|
attributes: Record<string, any>,
|
||||||
relatedImports?: string[],
|
relatedImports?: string[],
|
||||||
@@ -289,9 +290,16 @@ export interface IWorkspace {
|
|||||||
* 解析后的项目配置信息
|
* 解析后的项目配置信息
|
||||||
*/
|
*/
|
||||||
get projectConfig(): ITangoConfigJson;
|
get projectConfig(): ITangoConfigJson;
|
||||||
|
/**
|
||||||
|
* 当前活动的视图文件
|
||||||
|
*/
|
||||||
get activeViewModule(): IViewFile;
|
get activeViewModule(): IViewFile;
|
||||||
get pages(): any[];
|
get pages(): any[];
|
||||||
get bizComps(): string[];
|
get bizComps(): string[];
|
||||||
get baseComps(): string[];
|
get baseComps(): string[];
|
||||||
get localComps(): string[];
|
get localComps(): string[];
|
||||||
|
/**
|
||||||
|
* 是否是合法的项目
|
||||||
|
*/
|
||||||
|
get isValid(): boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,20 @@ export class SelectSource {
|
|||||||
this._start = null;
|
this._start = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选中当前选中节点的父节点
|
||||||
|
*/
|
||||||
|
selectParent() {
|
||||||
|
const parents = this.first?.parents || [];
|
||||||
|
if (parents.length) {
|
||||||
|
const [parent, ...rest] = parents;
|
||||||
|
this.select({
|
||||||
|
...parent,
|
||||||
|
parents: rest,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setStart(data: StartDataType) {
|
setStart(data: StartDataType) {
|
||||||
this._start = data;
|
this._start = data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { action, computed, makeObservable, observable } from 'mobx';
|
import { action, computed, makeObservable, observable } from 'mobx';
|
||||||
import { JSXElement } from '@babel/types';
|
import { JSXElement } from '@babel/types';
|
||||||
import {
|
import {
|
||||||
ComponentPrototypeType,
|
IComponentPrototype,
|
||||||
Dict,
|
Dict,
|
||||||
ITangoConfigJson,
|
ITangoConfigJson,
|
||||||
hasFileExtension,
|
hasFileExtension,
|
||||||
@@ -35,6 +35,7 @@ import { TangoStoreEntryModule, TangoStoreModule } from './store-module';
|
|||||||
import { TangoServiceModule } from './service-module';
|
import { TangoServiceModule } from './service-module';
|
||||||
import { TangoViewModule } from './view-module';
|
import { TangoViewModule } from './view-module';
|
||||||
import { TangoComponentsEntryModule } from './component-module';
|
import { TangoComponentsEntryModule } from './component-module';
|
||||||
|
import { AppEntryModule } from './entry-module';
|
||||||
|
|
||||||
export interface IWorkspaceOptions {
|
export interface IWorkspaceOptions {
|
||||||
/**
|
/**
|
||||||
@@ -52,7 +53,7 @@ export interface IWorkspaceOptions {
|
|||||||
/**
|
/**
|
||||||
* 组件描述列表
|
* 组件描述列表
|
||||||
*/
|
*/
|
||||||
prototypes?: Record<string, ComponentPrototypeType>;
|
prototypes?: Record<string, IComponentPrototype>;
|
||||||
/**
|
/**
|
||||||
* 工作区文件变更事件
|
* 工作区文件变更事件
|
||||||
*/
|
*/
|
||||||
@@ -75,7 +76,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
|||||||
/**
|
/**
|
||||||
* 组件配置
|
* 组件配置
|
||||||
*/
|
*/
|
||||||
componentPrototypes: Map<string, ComponentPrototypeType>;
|
componentPrototypes: Map<string, IComponentPrototype>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 入口文件
|
* 入口文件
|
||||||
@@ -97,6 +98,11 @@ export class Workspace extends EventTarget implements IWorkspace {
|
|||||||
*/
|
*/
|
||||||
activeViewFile: string;
|
activeViewFile: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用入口模块
|
||||||
|
*/
|
||||||
|
appEntryModule: AppEntryModule;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 路由配置模块
|
* 路由配置模块
|
||||||
*/
|
*/
|
||||||
@@ -144,6 +150,13 @@ export class Workspace extends EventTarget implements IWorkspace {
|
|||||||
*/
|
*/
|
||||||
private copyTempNodes: TangoNode[];
|
private copyTempNodes: TangoNode[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否是合法的项目
|
||||||
|
*/
|
||||||
|
get isValid() {
|
||||||
|
return !!this.tangoConfigJson && !!this.activeViewModule;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 项目配置,返回解析后的 tango.config.json 文件
|
* 项目配置,返回解析后的 tango.config.json 文件
|
||||||
*/
|
*/
|
||||||
@@ -249,11 +262,11 @@ export class Workspace extends EventTarget implements IWorkspace {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getPrototype(name: string | ComponentPrototypeType) {
|
getPrototype(name: string | IComponentPrototype) {
|
||||||
if (isString(name)) {
|
if (isString(name)) {
|
||||||
return this.componentPrototypes.get(name);
|
return this.componentPrototypes.get(name);
|
||||||
}
|
}
|
||||||
return name as ComponentPrototypeType;
|
return name as IComponentPrototype;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -299,7 +312,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setComponentPrototypes(prototypes: Record<string, ComponentPrototypeType>) {
|
setComponentPrototypes(prototypes: Record<string, IComponentPrototype>) {
|
||||||
Object.keys(prototypes).forEach((name) => {
|
Object.keys(prototypes).forEach((name) => {
|
||||||
this.componentPrototypes.set(name, prototypes[name]);
|
this.componentPrototypes.set(name, prototypes[name]);
|
||||||
});
|
});
|
||||||
@@ -322,6 +335,9 @@ export class Workspace extends EventTarget implements IWorkspace {
|
|||||||
* @param fileType 模块类型
|
* @param fileType 模块类型
|
||||||
*/
|
*/
|
||||||
addFile(filename: string, code: string, fileType?: FileType) {
|
addFile(filename: string, code: string, fileType?: FileType) {
|
||||||
|
if (!fileType && filename === this.entry) {
|
||||||
|
fileType = FileType.AppEntryModule;
|
||||||
|
}
|
||||||
const moduleType = fileType || inferFileType(filename);
|
const moduleType = fileType || inferFileType(filename);
|
||||||
const props = {
|
const props = {
|
||||||
filename,
|
filename,
|
||||||
@@ -331,6 +347,10 @@ export class Workspace extends EventTarget implements IWorkspace {
|
|||||||
|
|
||||||
let module;
|
let module;
|
||||||
switch (moduleType) {
|
switch (moduleType) {
|
||||||
|
case FileType.AppEntryModule:
|
||||||
|
module = new AppEntryModule(this, props);
|
||||||
|
this.appEntryModule = module;
|
||||||
|
break;
|
||||||
case FileType.StoreEntryModule:
|
case FileType.StoreEntryModule:
|
||||||
module = new TangoStoreEntryModule(this, props);
|
module = new TangoStoreEntryModule(this, props);
|
||||||
this.storeEntryModule = module;
|
this.storeEntryModule = module;
|
||||||
@@ -954,7 +974,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
|||||||
* @param sourceName 插入的组件名称
|
* @param sourceName 插入的组件名称
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
insertToNode(targetNodeId: string, sourceName: string | ComponentPrototypeType) {
|
insertToNode(targetNodeId: string, sourceName: string | IComponentPrototype) {
|
||||||
if (!targetNodeId || !sourceName) {
|
if (!targetNodeId || !sourceName) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -978,7 +998,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
|||||||
/**
|
/**
|
||||||
* 替换目标节点
|
* 替换目标节点
|
||||||
*/
|
*/
|
||||||
replaceNode(targetNodeId: string, sourceName: string | ComponentPrototypeType) {
|
replaceNode(targetNodeId: string, sourceName: string | IComponentPrototype) {
|
||||||
if (!targetNodeId || !sourceName) {
|
if (!targetNodeId || !sourceName) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1000,7 +1020,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
|||||||
* 在选中节点中插入子节点
|
* 在选中节点中插入子节点
|
||||||
* @param childName 节点名
|
* @param childName 节点名
|
||||||
*/
|
*/
|
||||||
insertToSelectedNode(childName: string | ComponentPrototypeType) {
|
insertToSelectedNode(childName: string | IComponentPrototype) {
|
||||||
const insertedPrototype = this.getPrototype(childName);
|
const insertedPrototype = this.getPrototype(childName);
|
||||||
if (insertedPrototype) {
|
if (insertedPrototype) {
|
||||||
const newNode = prototype2jsxElement(insertedPrototype);
|
const newNode = prototype2jsxElement(insertedPrototype);
|
||||||
@@ -1022,6 +1042,46 @@ export class Workspace extends EventTarget implements IWorkspace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
insertBeforeSelectedNode(sourceName: string | IComponentPrototype) {
|
||||||
|
if (!sourceName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const targetNodeId = this.selectSource.first.id;
|
||||||
|
const sourcePrototype = this.getPrototype(sourceName);
|
||||||
|
const newNode = prototype2jsxElement(sourcePrototype);
|
||||||
|
const file = this.getNode(targetNodeId).file;
|
||||||
|
const { source, specifiers } = prototype2importDeclarationData(sourcePrototype, file.filename);
|
||||||
|
|
||||||
|
file.insertBefore(targetNodeId, newNode).addImportSpecifiers(source, specifiers).update();
|
||||||
|
|
||||||
|
this.history.push({
|
||||||
|
message: HistoryMessage.InsertBeforeNode,
|
||||||
|
data: {
|
||||||
|
[file.filename]: file.code,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
insertAfterSelectedNode(sourceName: string | IComponentPrototype) {
|
||||||
|
if (!sourceName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const targetNodeId = this.selectSource.first.id;
|
||||||
|
const sourcePrototype = this.getPrototype(sourceName);
|
||||||
|
const newNode = prototype2jsxElement(sourcePrototype);
|
||||||
|
const file = this.getNode(targetNodeId).file;
|
||||||
|
const { source, specifiers } = prototype2importDeclarationData(sourcePrototype, file.filename);
|
||||||
|
|
||||||
|
file.insertAfter(targetNodeId, newNode).addImportSpecifiers(source, specifiers).update();
|
||||||
|
|
||||||
|
this.history.push({
|
||||||
|
message: HistoryMessage.InsertAfterNode,
|
||||||
|
data: {
|
||||||
|
[file.filename]: file.code,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
updateSelectedNodeAttributes(
|
updateSelectedNodeAttributes(
|
||||||
attributes: Record<string, any> = {},
|
attributes: Record<string, any> = {},
|
||||||
relatedImports: string[] = [],
|
relatedImports: string[] = [],
|
||||||
@@ -1186,8 +1246,17 @@ export class Workspace extends EventTarget implements IWorkspace {
|
|||||||
let filename: string;
|
let filename: string;
|
||||||
this.routeModule?.routes.forEach((route) => {
|
this.routeModule?.routes.forEach((route) => {
|
||||||
if (isPathnameMatchRoute(routePath, route.path) && route.importPath) {
|
if (isPathnameMatchRoute(routePath, route.path) && route.importPath) {
|
||||||
const absolutePath = route.importPath.replace('.', '/src');
|
if (route.importPath.startsWith('@/')) {
|
||||||
filename = this.getRealViewFilePath(absolutePath);
|
filename = route.importPath;
|
||||||
|
const alias = this.tangoConfigJson.getValue('sandbox.alias') || {};
|
||||||
|
if (alias['@']) {
|
||||||
|
filename = filename.replace('@', alias['@']);
|
||||||
|
}
|
||||||
|
filename = this.getRealViewFilePath(filename);
|
||||||
|
} else {
|
||||||
|
const absolutePath = route.importPath.replace('.', '/src');
|
||||||
|
filename = this.getRealViewFilePath(absolutePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return filename;
|
return filename;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export type SimulatorMode = 'desktop' | 'tablet' | 'phone';
|
|||||||
export enum FileType {
|
export enum FileType {
|
||||||
// js 文件
|
// js 文件
|
||||||
Module = 'module',
|
Module = 'module',
|
||||||
|
AppEntryModule = 'appEntryModule',
|
||||||
StoreEntryModule = 'storeEntryModule',
|
StoreEntryModule = 'storeEntryModule',
|
||||||
RouteModule = 'routeModule',
|
RouteModule = 'routeModule',
|
||||||
ServiceModule = 'serviceModule',
|
ServiceModule = 'serviceModule',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isTangoVariable, isWrappedByExpressionContainer } from '../src/helpers';
|
import { isTangoVariable } from '../src/helpers';
|
||||||
|
|
||||||
describe('assert', () => {
|
describe('assert', () => {
|
||||||
it('isTangoVariable', () => {
|
it('isTangoVariable', () => {
|
||||||
@@ -7,19 +7,4 @@ describe('assert', () => {
|
|||||||
expect(isTangoVariable('tango.stores.app?.name')).toBeTruthy();
|
expect(isTangoVariable('tango.stores.app?.name')).toBeTruthy();
|
||||||
// expect(isTangoVariable('tango.copyToClipboard')).toBeTruthy();
|
// expect(isTangoVariable('tango.copyToClipboard')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('isWrappedByExpressionContainer', () => {
|
|
||||||
expect(isWrappedByExpressionContainer('{this.foo}')).toBeTruthy();
|
|
||||||
expect(isWrappedByExpressionContainer('{!false}')).toBeTruthy();
|
|
||||||
expect(isWrappedByExpressionContainer('{[]}')).toBeTruthy();
|
|
||||||
expect(isWrappedByExpressionContainer('{{ foo: "bar" }}')).toBeTruthy();
|
|
||||||
expect(isWrappedByExpressionContainer('{[{ foo: "bar" }]}')).toBeTruthy();
|
|
||||||
expect(isWrappedByExpressionContainer('{123}')).toBeTruthy();
|
|
||||||
expect(isWrappedByExpressionContainer('{"hello"}')).toBeTruthy();
|
|
||||||
expect(isWrappedByExpressionContainer('{ foo: "bar" }')).toBeFalsy();
|
|
||||||
expect(isWrappedByExpressionContainer('{ type: tango.stores?.homePage?.tabKey }')).toBeFalsy();
|
|
||||||
expect(isWrappedByExpressionContainer('{ type: tango.stores.homePage.tabKey }')).toBeFalsy();
|
|
||||||
expect(isWrappedByExpressionContainer('{ foo: "bar" }')).toBeFalsy();
|
|
||||||
expect(isWrappedByExpressionContainer('{ color: tango.stores.app.color }')).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,19 +1,28 @@
|
|||||||
|
import { Identifier } from '@babel/types';
|
||||||
import {
|
import {
|
||||||
object2node,
|
object2node,
|
||||||
serviceConfig2Node,
|
serviceConfig2Node,
|
||||||
isValidCode,
|
isValidCode,
|
||||||
isValidExpressionCode,
|
isValidExpressionCode,
|
||||||
code2expression,
|
code2expression,
|
||||||
|
value2jsxAttributeValueNode,
|
||||||
} from '../src/helpers';
|
} from '../src/helpers';
|
||||||
|
|
||||||
describe('ast helpers', () => {
|
describe('ast helpers', () => {
|
||||||
it('isValidCode', () => {
|
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();
|
expect(isValidCode('() => { hello world }')).toBeFalsy();
|
||||||
|
// invalid function
|
||||||
expect(isValidCode('function() {}')).toBeFalsy();
|
expect(isValidCode('function() {}')).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('isValidExpression', () => {
|
it('isValidExpressionCode', () => {
|
||||||
expect(isValidExpressionCode('() => { }')).toBeTruthy();
|
|
||||||
expect(isValidExpressionCode('1')).toBeTruthy();
|
expect(isValidExpressionCode('1')).toBeTruthy();
|
||||||
expect(isValidExpressionCode('a = 1')).toBeTruthy();
|
expect(isValidExpressionCode('a = 1')).toBeTruthy();
|
||||||
expect(isValidExpressionCode('1 + 1')).toBeTruthy();
|
expect(isValidExpressionCode('1 + 1')).toBeTruthy();
|
||||||
@@ -22,8 +31,11 @@ describe('ast helpers', () => {
|
|||||||
expect(isValidExpressionCode('{ bizId: "vip", type: "category" }')).toBeTruthy();
|
expect(isValidExpressionCode('{ bizId: "vip", type: "category" }')).toBeTruthy();
|
||||||
expect(isValidExpressionCode('[1,2,3]')).toBeTruthy();
|
expect(isValidExpressionCode('[1,2,3]')).toBeTruthy();
|
||||||
expect(isValidExpressionCode('<div>hello</div>')).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"}')).toBeFalsy();
|
expect(isValidExpressionCode('{"1"}')).toBeFalsy();
|
||||||
expect(isValidExpressionCode('{ 1+1 }')).toBeFalsy();
|
expect(isValidExpressionCode('{ 1+1 }')).toBeFalsy();
|
||||||
@@ -54,8 +66,7 @@ describe('ast helpers', () => {
|
|||||||
|
|
||||||
it('code2expression', () => {
|
it('code2expression', () => {
|
||||||
expect(code2expression('')).toBeUndefined();
|
expect(code2expression('')).toBeUndefined();
|
||||||
expect(code2expression('{tango.stores.app}')).toBeUndefined();
|
expect(code2expression('tango.stores.app').type).toBe('MemberExpression');
|
||||||
|
|
||||||
expect(code2expression('{ type: window.bar }').type).toEqual('ObjectExpression');
|
expect(code2expression('{ type: window.bar }').type).toEqual('ObjectExpression');
|
||||||
expect(code2expression('() => {};').type).toEqual('ArrowFunctionExpression');
|
expect(code2expression('() => {};').type).toEqual('ArrowFunctionExpression');
|
||||||
expect(code2expression('<Button>hello</Button>').type).toBe('JSXElement');
|
expect(code2expression('<Button>hello</Button>').type).toBe('JSXElement');
|
||||||
@@ -69,4 +80,16 @@ describe('ast helpers', () => {
|
|||||||
`;
|
`;
|
||||||
expect(code2expression(arrayCode).type).toEqual('ArrayExpression');
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
getJSXElementAttributes,
|
getJSXElementAttributes,
|
||||||
inferFileType,
|
inferFileType,
|
||||||
deepCloneNode,
|
deepCloneNode,
|
||||||
|
code2value,
|
||||||
} from '../src/helpers';
|
} from '../src/helpers';
|
||||||
import { FileType } from '../src/types';
|
import { FileType } from '../src/types';
|
||||||
|
|
||||||
@@ -27,15 +28,18 @@ describe('helpers', () => {
|
|||||||
|
|
||||||
it('parse jsxElement attributes', () => {
|
it('parse jsxElement attributes', () => {
|
||||||
const node = code2expression(
|
const node = code2expression(
|
||||||
"<XColumn dataIndex='col' enumMap={{ 1: '已解决', 2: '未解决' }} />",
|
"<Foo id={tango.user.id} num={1} str='col' enumMap={{ 1: '已解决', 2: '未解决' }} list={[{ key: 1 }, { key: 2 }]} />",
|
||||||
);
|
);
|
||||||
const attributes = getJSXElementAttributes(node as JSXElement);
|
const attributes = getJSXElementAttributes(node as JSXElement);
|
||||||
expect(attributes).toEqual({
|
expect(attributes).toEqual({
|
||||||
dataIndex: 'col',
|
id: '{{tango.user.id}}',
|
||||||
|
num: 1,
|
||||||
|
str: 'col',
|
||||||
enumMap: {
|
enumMap: {
|
||||||
1: '已解决',
|
1: '已解决',
|
||||||
2: '未解决',
|
2: '未解决',
|
||||||
},
|
},
|
||||||
|
list: '{{[{ key: 1 }, { key: 2 }]}}',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -80,15 +84,22 @@ describe('helpers', () => {
|
|||||||
|
|
||||||
it('expressionCode2ast', () => {
|
it('expressionCode2ast', () => {
|
||||||
expect(expressionCode2ast('<Button>hello</Button>').type).toEqual('File');
|
expect(expressionCode2ast('<Button>hello</Button>').type).toEqual('File');
|
||||||
expect(expressionCode2ast('{<Button>hello</Button>}').type).toEqual('File');
|
|
||||||
expect(expressionCode2ast('() => <Button>hello</Button>').type).toEqual('File');
|
expect(expressionCode2ast('() => <Button>hello</Button>').type).toEqual('File');
|
||||||
expect(expressionCode2ast('{() => <Button>hello</Button>}').type).toEqual('File');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('string helpers', () => {
|
describe('string helpers', () => {
|
||||||
it('value2code: empty array', () => {
|
it('value2code: empty array', () => {
|
||||||
expect(value2code([])).toEqual('[]');
|
expect(value2code([])).toEqual('[]');
|
||||||
|
expect(value2code({})).toEqual('{}');
|
||||||
|
expect(value2code(() => {})).toEqual('() => {}');
|
||||||
|
expect(value2code(true)).toEqual('true');
|
||||||
|
expect(value2code(false)).toEqual('false');
|
||||||
|
expect(value2code(1)).toEqual('1');
|
||||||
|
expect(value2code('hello')).toEqual('"hello"');
|
||||||
|
expect(value2code('{{window.tango}}')).toEqual('window.tango');
|
||||||
|
expect(value2code('{{() => {}}}')).toEqual('() => {}');
|
||||||
|
expect(value2code('{{1111}}')).toEqual('1111');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('value2code: array', () => {
|
it('value2code: array', () => {
|
||||||
@@ -161,6 +172,10 @@ describe('string helpers', () => {
|
|||||||
it('isValidComponentName', () => {
|
it('isValidComponentName', () => {
|
||||||
expect(isValidComponentName('Button')).toBeTruthy();
|
expect(isValidComponentName('Button')).toBeTruthy();
|
||||||
expect(isValidComponentName('Button.Group')).toBeTruthy();
|
expect(isValidComponentName('Button.Group')).toBeTruthy();
|
||||||
|
expect(isValidComponentName('div')).toBeTruthy();
|
||||||
|
expect(isValidComponentName('')).toBeFalsy();
|
||||||
|
expect(isValidComponentName(null)).toBeFalsy();
|
||||||
|
expect(isValidComponentName(undefined)).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('inferFileType', () => {
|
it('inferFileType', () => {
|
||||||
@@ -197,10 +212,26 @@ describe('schema helpers', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const cloned = deepCloneNode(schema);
|
it('deepCloneNode', () => {
|
||||||
expect(cloned.props.id).toBe(schema.props.id);
|
const cloned = deepCloneNode(schema);
|
||||||
expect(cloned.children[0].props.id).toBe(schema.children[0].props.id);
|
expect(cloned.props.id).toBe(schema.props.id);
|
||||||
|
expect(cloned.children[0].props.id).toBe(schema.children[0].props.id);
|
||||||
expect(cloned.id).not.toBe(schema.id);
|
expect(cloned.id).not.toBe(schema.id);
|
||||||
expect(cloned.children[0].id).not.toBe(schema.children[0].id);
|
expect(cloned.children[0].id).not.toBe(schema.children[0].id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('code helper', () => {
|
||||||
|
it('code2value', () => {
|
||||||
|
expect(code2value(`1`)).toEqual(1);
|
||||||
|
expect(code2value(`false`)).toEqual(false);
|
||||||
|
expect(code2value(`"foo"`)).toEqual('foo');
|
||||||
|
expect(code2value(`{ foo: "foo" }`)).toEqual({ foo: 'foo' });
|
||||||
|
expect(code2value(`[{ foo: "foo" }]`)).toEqual([{ foo: 'foo' }]);
|
||||||
|
expect(code2value(`{ foo: "foo", ...{ bar: "bar"} }`)).toBe(undefined);
|
||||||
|
expect(code2value(`() => {}`)).toBe(undefined);
|
||||||
|
expect(code2value(`tango.stores.app.name`)).toBe(undefined);
|
||||||
|
expect(code2value(`window`)).toBe(undefined);
|
||||||
|
expect(code2value(`<div>hello</div>`)).toBe(undefined);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { propDataToKeyValueString } from '../src/helpers';
|
||||||
|
|
||||||
|
describe('prototype helpers', () => {
|
||||||
|
it('propDataToKeyValueString', () => {
|
||||||
|
// basic
|
||||||
|
expect(propDataToKeyValueString({ name: 'foo', initValue: 'bar' })).toEqual('foo="bar"');
|
||||||
|
expect(propDataToKeyValueString({ name: 'foo', initValue: 1 })).toEqual('foo={1}');
|
||||||
|
expect(propDataToKeyValueString({ name: 'foo', initValue: false })).toEqual('foo={false}');
|
||||||
|
expect(propDataToKeyValueString({ name: 'foo', initValue: null })).toEqual('foo={null}');
|
||||||
|
expect(propDataToKeyValueString({ name: 'foo', initValue: [] })).toEqual('foo={[]}');
|
||||||
|
expect(propDataToKeyValueString({ name: 'foo', initValue: {} })).toEqual('foo={{}}');
|
||||||
|
expect(propDataToKeyValueString({ name: 'foo', initValue: () => {} })).toEqual(
|
||||||
|
'foo={() => {}}',
|
||||||
|
);
|
||||||
|
expect(propDataToKeyValueString({ name: 'foo', initValue: { foo: 'bar' } })).toEqual(
|
||||||
|
'foo={{ foo: "bar" }}',
|
||||||
|
);
|
||||||
|
expect(propDataToKeyValueString({ name: 'foo', initValue: [{ foo: 'bar' }] })).toEqual(
|
||||||
|
'foo={[{ foo: "bar" }]}',
|
||||||
|
);
|
||||||
|
|
||||||
|
// wrapped code
|
||||||
|
expect(
|
||||||
|
propDataToKeyValueString({ name: 'foo', initValue: '{{<Placeholder text="放置替换" />}}' }),
|
||||||
|
).toEqual('foo={<Placeholder text="放置替换" />}');
|
||||||
|
expect(propDataToKeyValueString({ name: 'foo', initValue: '{{tango}}' })).toEqual(
|
||||||
|
'foo={tango}',
|
||||||
|
);
|
||||||
|
expect(propDataToKeyValueString({ name: 'foo', initValue: '{{"bar"}}' })).toEqual(
|
||||||
|
'foo={"bar"}',
|
||||||
|
);
|
||||||
|
expect(propDataToKeyValueString({ name: 'foo', initValue: '{{() => {}}}' })).toEqual(
|
||||||
|
'foo={() => {}}',
|
||||||
|
);
|
||||||
|
|
||||||
|
// compatible with old version
|
||||||
|
expect(propDataToKeyValueString({ name: 'foo', initValue: '{() => {}}' })).toEqual(
|
||||||
|
'foo={() => {}}',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
propDataToKeyValueString({ name: 'foo', initValue: '{<Placeholder text="放置替换" />}' }),
|
||||||
|
).toEqual('foo={<Placeholder text="放置替换" />}');
|
||||||
|
// expect(propDataToKeyValueString({ name: 'foo', initValue: '{tango}' })).toEqual('foo={tango}');
|
||||||
|
});
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,110 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [1.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
|
||||||
|
|
||||||
|
- support quick set tid in SettingForm ([#151](https://github.com/netease/tango/issues/151)) ([1fc22a4](https://github.com/netease/tango/commit/1fc22a4535a8bdc12a018a41ebd5ab908fc46817))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- refactor parse attribute value ([#149](https://github.com/netease/tango/issues/149)) ([ffaa276](https://github.com/netease/tango/commit/ffaa276b5c205ed962d37e2fdb358a703f8fad01))
|
||||||
|
|
||||||
|
## [1.0.2](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.1...@music163/tango-designer@1.0.2) (2024-04-22)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- allow reload state tree & display pageStore in variable tree ([#135](https://github.com/netease/tango/issues/135)) ([2664613](https://github.com/netease/tango/commit/2664613ab263aead2a5239fa012454fc7fd5ff99))
|
||||||
|
|
||||||
|
## [1.0.1](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0...@music163/tango-designer@1.0.1) (2024-04-16)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- render-setter expression value bug ([#133](https://github.com/netease/tango/issues/133)) ([27166ec](https://github.com/netease/tango/commit/27166ec94976979cb59130788399d90977cf0aec))
|
||||||
|
- variable tree remove button event handling ([#134](https://github.com/netease/tango/issues/134)) ([52864b5](https://github.com/netease/tango/commit/52864b540095025232ae237134d6be5701d20549))
|
||||||
|
|
||||||
|
# [1.0.0-alpha.18](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.17...@music163/tango-designer@1.0.0-alpha.18) (2024-04-09)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- render invalid icon in inserted dropdown ([#128](https://github.com/netease/tango/issues/128)) ([41a61cc](https://github.com/netease/tango/commit/41a61cce4e6a1f9d42c7a3f4e357c5acdcd6db5f))
|
||||||
|
- update setters and use tabOptions to filter props ([#129](https://github.com/netease/tango/issues/129)) ([93608d1](https://github.com/netease/tango/commit/93608d1037327afa4f755976b86427b6128ae3d0))
|
||||||
|
- update SettingForm style ([#125](https://github.com/netease/tango/issues/125)) ([3f42516](https://github.com/netease/tango/commit/3f42516816968888554f719332d3cef7d8576aa8))
|
||||||
|
|
||||||
|
# [1.0.0-alpha.17](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.16...@music163/tango-designer@1.0.0-alpha.17) (2024-03-26)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- quick add sibling nodes ([#127](https://github.com/netease/tango/issues/127)) ([9ed6a7d](https://github.com/netease/tango/commit/9ed6a7d1a4944d69d96e034f243b61531862e317))
|
||||||
|
|
||||||
|
# [1.0.0-alpha.16](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.15...@music163/tango-designer@1.0.0-alpha.16) (2024-03-21)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- copy node path in variable tree ([6215ed9](https://github.com/netease/tango/commit/6215ed96f241adf6b67aa3019745bf7d40751787))
|
||||||
|
|
||||||
|
# [1.0.0-alpha.15](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.14...@music163/tango-designer@1.0.0-alpha.15) (2024-03-20)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- quick insert child ([d640429](https://github.com/netease/tango/commit/d64042922bc21723025b1a59586ef23f37f17ffc))
|
||||||
|
- update selection helper style ([bceb7a5](https://github.com/netease/tango/commit/bceb7a5c9cea9dc2acb091bf7967e5d3b7ba5210))
|
||||||
|
|
||||||
|
# [1.0.0-alpha.14](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.13...@music163/tango-designer@1.0.0-alpha.14) (2024-03-19)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- select outline node & render dependency list ([#118](https://github.com/netease/tango/issues/118)) ([07febb3](https://github.com/netease/tango/commit/07febb385f710f9a0bc43639ce22787bb05e97ed))
|
||||||
|
|
||||||
|
# [1.0.0-alpha.13](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.12...@music163/tango-designer@1.0.0-alpha.13) (2024-03-18)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- add deprecated and data group to ComponentPropType ([#116](https://github.com/netease/tango/issues/116)) ([9f7441c](https://github.com/netease/tango/commit/9f7441c13400dba55c58b7dd6ce9429013edbc45))
|
||||||
|
|
||||||
# [1.0.0-alpha.12](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.11...@music163/tango-designer@1.0.0-alpha.12) (2024-03-18)
|
# [1.0.0-alpha.12](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.11...@music163/tango-designer@1.0.0-alpha.12) (2024-03-18)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@music163/tango-designer",
|
"name": "@music163/tango-designer",
|
||||||
"version": "1.0.0-alpha.12",
|
"version": "1.3.0",
|
||||||
"description": "lowcode designer",
|
"description": "lowcode designer",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react"
|
"react"
|
||||||
@@ -32,25 +32,25 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^4.8.0",
|
"@ant-design/icons": "^4.8.0",
|
||||||
"@music163/request": "^0.1.2",
|
"@music163/request": "^0.2.0",
|
||||||
"@music163/tango-context": "^1.0.0-alpha.7",
|
"@music163/tango-context": "^1.1.2",
|
||||||
"@music163/tango-core": "^1.0.0-alpha.7",
|
"@music163/tango-core": "^1.3.0",
|
||||||
"@music163/tango-helpers": "^1.0.0-alpha.3",
|
"@music163/tango-helpers": "^1.2.0",
|
||||||
"@music163/tango-sandbox": "^1.0.0-alpha.7",
|
"@music163/tango-sandbox": "^1.0.5",
|
||||||
"@music163/tango-setting-form": "^1.0.0-alpha.10",
|
"@music163/tango-setting-form": "^1.2.5",
|
||||||
"@music163/tango-ui": "^1.0.0-alpha.8",
|
"@music163/tango-ui": "^1.3.0",
|
||||||
"antd": "^4.24.2",
|
"antd": "^4.24.2",
|
||||||
"cash-dom": "^8.1.2",
|
"cash-dom": "^8.1.2",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.5.1",
|
||||||
"color": "^4.2.3",
|
"color": "^4.2.3",
|
||||||
"coral-system": "^1.0.5",
|
"coral-system": "^1.0.5",
|
||||||
"cssjson": "^2.1.3",
|
"cssjson": "^2.1.3",
|
||||||
"date-fns": "^2.29.2",
|
"date-fns": "^2.29.2",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.30.1",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-resizable": "^3.0.5",
|
"react-resizable": "^3.0.5",
|
||||||
"semver": "^7.3.8"
|
"semver": "^7.6.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/color": "^3.0.5",
|
"@types/color": "^3.0.5",
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
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"
|
||||||
|
maskClosable
|
||||||
|
body={
|
||||||
|
<ComponentsPanel
|
||||||
|
isScope
|
||||||
|
showBizComps={false}
|
||||||
|
menuData={menuData}
|
||||||
|
layout={layout}
|
||||||
|
onItemSelect={handleSelect}
|
||||||
|
style={{
|
||||||
|
maxHeight: '400px',
|
||||||
|
overflow: 'auto',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{...popoverProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</DragPanel>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
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: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 './drag-box';
|
||||||
export * from './input-kv';
|
export * from './input-kv';
|
||||||
export * from './variable-tree';
|
export * from './variable-tree';
|
||||||
export * from './variable-tree-modal';
|
export * from './variable-tree-modal';
|
||||||
|
export * from './components-popover';
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ export function VariableTree(props: VariableTreeProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box display="flex" columnGap="l" className="VariableTree" css={varTreeStyle} {...rest}>
|
<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)}
|
{renderHeaderExtra?.(props, state)}
|
||||||
<Box mb="m" position="sticky" top="0" bg="white" zIndex={2}>
|
<Box mb="m" position="sticky" top="0" bg="white" zIndex={2}>
|
||||||
<Search placeholder="请输入变量名" onChange={(val) => setKeyword(val?.trim())} />
|
<Search placeholder="请输入变量名" onChange={(val) => setKeyword(val?.trim())} />
|
||||||
@@ -184,6 +184,12 @@ export function VariableTree(props: VariableTreeProps) {
|
|||||||
|
|
||||||
if (isLeaf) {
|
if (isLeaf) {
|
||||||
const showView = node.showViewButton ?? showViewButton;
|
const showView = node.showViewButton ?? showViewButton;
|
||||||
|
|
||||||
|
let nodePath = node.key;
|
||||||
|
if (/^(stores|services)\./.test(nodePath)) {
|
||||||
|
nodePath = `tango.${node.key.replaceAll('.', '?.').replace('?.', '.')}`;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||||
<Text flex="1" truncated>
|
<Text flex="1" truncated>
|
||||||
@@ -197,8 +203,13 @@ export function VariableTree(props: VariableTreeProps) {
|
|||||||
<Box flex="0 0 72px" textAlign="right">
|
<Box flex="0 0 72px" textAlign="right">
|
||||||
{node.showRemoveButton && (
|
{node.showRemoveButton && (
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
|
zIndex={99999}
|
||||||
title={`确认删除吗 ${node.title}?该操作会导致引用此模型的代码报错,请谨慎操作!`}
|
title={`确认删除吗 ${node.title}?该操作会导致引用此模型的代码报错,请谨慎操作!`}
|
||||||
onConfirm={() => {
|
onCancel={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onConfirm={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
if (isStoreVariablePath(node.key)) {
|
if (isStoreVariablePath(node.key)) {
|
||||||
onRemoveStoreVariable(node.key);
|
onRemoveStoreVariable(node.key);
|
||||||
} else {
|
} else {
|
||||||
@@ -216,7 +227,7 @@ export function VariableTree(props: VariableTreeProps) {
|
|||||||
/>
|
/>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
)}
|
)}
|
||||||
<CopyClipboard text={`tango.${node.key.replaceAll('.', '?.')}`}>
|
<CopyClipboard text={nodePath}>
|
||||||
{({ copied, onClick }) => {
|
{({ copied, onClick }) => {
|
||||||
const label = copied ? '已复制' : '复制变量路径';
|
const label = copied ? '已复制' : '复制变量路径';
|
||||||
return (
|
return (
|
||||||
@@ -326,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>
|
</ValueDetail>
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { Box } from 'coral-system';
|
|||||||
import { Button, Empty } from 'antd';
|
import { Button, Empty } from 'antd';
|
||||||
import { PlayCircleOutlined } from '@ant-design/icons';
|
import { PlayCircleOutlined } from '@ant-design/icons';
|
||||||
import { InputCode, Panel, JsonView } from '@music163/tango-ui';
|
import { InputCode, Panel, JsonView } from '@music163/tango-ui';
|
||||||
import { isNil, logger, code2object, getValue } from '@music163/tango-helpers';
|
import { isNil, logger, getValue } from '@music163/tango-helpers';
|
||||||
|
import { code2value } from '@music163/tango-core';
|
||||||
|
|
||||||
export interface ServicePreviewProps {
|
export interface ServicePreviewProps {
|
||||||
appContext?: any;
|
appContext?: any;
|
||||||
@@ -22,7 +23,7 @@ export function ServicePreview({ appContext, functionKey }: ServicePreviewProps)
|
|||||||
editable
|
editable
|
||||||
showLineNumbers
|
showLineNumbers
|
||||||
onChange={(value: string) => {
|
onChange={(value: string) => {
|
||||||
const obj = code2object(value);
|
const obj = code2value(value); // 转为 object 对象
|
||||||
setPayload(obj);
|
setPayload(obj);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ export function ValueDefine({ data, onSave = noop }: ValueDefineProps) {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
off();
|
off();
|
||||||
setError('');
|
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;
|
sandboxQuery: DndQuery;
|
||||||
|
/**
|
||||||
|
* 预览沙箱查询实例
|
||||||
|
*/
|
||||||
|
previewSandboxQuery: DndQuery;
|
||||||
/**
|
/**
|
||||||
* 远程服务
|
* 远程服务
|
||||||
*/
|
*/
|
||||||
remoteServices?: Record<string, S>;
|
remoteServices: Record<string, S>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [DesignerProvider, useDesigner] = createContext<IDesignerContext>({
|
const [DesignerProvider, useDesigner] = createContext<IDesignerContext>({
|
||||||
@@ -19,9 +23,13 @@ const [DesignerProvider, useDesigner] = createContext<IDesignerContext>({
|
|||||||
export { DesignerProvider };
|
export { DesignerProvider };
|
||||||
|
|
||||||
export const useSandboxQuery = () => {
|
export const useSandboxQuery = () => {
|
||||||
return useDesigner()?.sandboxQuery;
|
return useDesigner().sandboxQuery;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePreviewSandboxQuery = () => {
|
||||||
|
return useDesigner().previewSandboxQuery;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useRemoteServices = () => {
|
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 zhCN from 'antd/lib/locale/zh_CN';
|
||||||
import { DesignerProvider, IDesignerContext } from './context';
|
import { DesignerProvider, IDesignerContext } from './context';
|
||||||
import defaultTheme from './themes/default';
|
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
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function Designer(props: DesignerProps) {
|
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]);
|
const theme = useMemo(() => extendTheme(themeProp, defaultTheme), [themeProp]);
|
||||||
return (
|
return (
|
||||||
<SystemProvider theme={theme} prefix="--tango">
|
<SystemProvider theme={theme} prefix="--tango">
|
||||||
<ConfigProvider locale={zhCN}>
|
<ConfigProvider locale={zhCN}>
|
||||||
<TangoEngineProvider value={{ engine, config }}>
|
<TangoEngineProvider value={{ engine, config }}>
|
||||||
<DesignerProvider value={{ sandboxQuery, remoteServices }}>{children}</DesignerProvider>
|
<DesignerProvider value={{ sandboxQuery, previewSandboxQuery, remoteServices }}>
|
||||||
|
{children}
|
||||||
|
</DesignerProvider>
|
||||||
</TangoEngineProvider>
|
</TangoEngineProvider>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</SystemProvider>
|
</SystemProvider>
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ export const DRAGGABLE_SELECTOR = `[${SLOT.dnd}]`;
|
|||||||
interface DndQueryOptions {
|
interface DndQueryOptions {
|
||||||
/**
|
/**
|
||||||
* DOM 查询上下文选择器
|
* DOM 查询上下文选择器
|
||||||
* TODO: 是不是可以合并成一个 API
|
|
||||||
*/
|
*/
|
||||||
context?: string;
|
context?: string;
|
||||||
/**
|
/**
|
||||||
@@ -63,6 +62,9 @@ export class DndQuery {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 沙箱内的 window 对象
|
||||||
|
*/
|
||||||
get window() {
|
get window() {
|
||||||
if (this.context && 'defaultView' in this.context) {
|
if (this.context && 'defaultView' in this.context) {
|
||||||
return (this.context as unknown as Document).defaultView;
|
return (this.context as unknown as Document).defaultView;
|
||||||
@@ -71,6 +73,9 @@ export class DndQuery {
|
|||||||
return window;
|
return window;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 沙箱内的全局滚动偏移
|
||||||
|
*/
|
||||||
get scrollTop() {
|
get scrollTop() {
|
||||||
if (this.context && 'documentElement' in this.context) {
|
if (this.context && 'documentElement' in this.context) {
|
||||||
return (this.context as unknown as Document).documentElement.scrollTop;
|
return (this.context as unknown as Document).documentElement.scrollTop;
|
||||||
|
|||||||
@@ -58,6 +58,15 @@ export function useDnd({
|
|||||||
'command+v,ctrl+v': () => {
|
'command+v,ctrl+v': () => {
|
||||||
workspace.pasteSelectedNode();
|
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]);
|
}, [workspace]);
|
||||||
|
|
||||||
@@ -68,6 +77,9 @@ export function useDnd({
|
|||||||
if (designer.isPreview) {
|
if (designer.isPreview) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (designer.showContextMenu) {
|
||||||
|
designer.toggleContextMenu(false);
|
||||||
|
}
|
||||||
|
|
||||||
const point = sandboxQuery.getRelativePoint({ x: e.clientX, y: e.clientY });
|
const point = sandboxQuery.getRelativePoint({ x: e.clientX, y: e.clientY });
|
||||||
selectSource.setStart({
|
selectSource.setStart({
|
||||||
@@ -84,10 +96,10 @@ export function useDnd({
|
|||||||
const onMouseMove = (e: React.MouseEvent) => {
|
const onMouseMove = (e: React.MouseEvent) => {
|
||||||
const point = sandboxQuery.getRelativePoint({ x: e.clientX, y: e.clientY });
|
const point = sandboxQuery.getRelativePoint({ x: e.clientX, y: e.clientY });
|
||||||
setElementStyle('.SelectionMask', {
|
setElementStyle('.SelectionMask', {
|
||||||
width: Math.abs(selectSource.start?.point.x - point.x) + 'px',
|
width: `${Math.abs(selectSource.start?.point.x - point.x)}px`,
|
||||||
height: Math.abs(selectSource.start?.point.y - point.y) + 'px',
|
height: `${Math.abs(selectSource.start?.point.y - point.y)}px`,
|
||||||
left: Math.min(selectSource.start?.point.x, point.x) + 'px',
|
left: `${Math.min(selectSource.start?.point.x, point.x)}px`,
|
||||||
top: Math.min(selectSource.start?.point.y, point.y) + 'px',
|
top: `${Math.min(selectSource.start?.point.y, point.y)}px`,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -119,6 +131,9 @@ export function useDnd({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onClick = (e: React.MouseEvent) => {
|
const onClick = (e: React.MouseEvent) => {
|
||||||
|
if (designer.showContextMenu) {
|
||||||
|
designer.toggleContextMenu(false);
|
||||||
|
}
|
||||||
const data = sandboxQuery.getDraggableParentsData(e.target as HTMLElement, true);
|
const data = sandboxQuery.getDraggableParentsData(e.target as HTMLElement, true);
|
||||||
if (data && data.id) {
|
if (data && data.id) {
|
||||||
selectSource.select(data);
|
selectSource.select(data);
|
||||||
@@ -379,6 +394,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 onTango = (e: CustomEvent) => {
|
||||||
const detail = e.detail || {};
|
const detail = e.detail || {};
|
||||||
|
|
||||||
@@ -406,6 +471,13 @@ export function useDnd({
|
|||||||
// 打开智能向导弹窗
|
// 打开智能向导弹窗
|
||||||
designer.toggleSmartWizard(true);
|
designer.toggleSmartWizard(true);
|
||||||
break;
|
break;
|
||||||
|
case 'addComponent':
|
||||||
|
// 打开添加组件面板
|
||||||
|
designer.toggleAddComponentPopover(true, {
|
||||||
|
clientX: (e.detail.meta as any).clientX + 40,
|
||||||
|
clientY: (e.detail.meta as any).clientY + 110,
|
||||||
|
});
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -421,6 +493,7 @@ export function useDnd({
|
|||||||
onDragEnd,
|
onDragEnd,
|
||||||
onScroll,
|
onScroll,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
|
onContextMenu,
|
||||||
onTango,
|
onTango,
|
||||||
...selectHandler,
|
...selectHandler,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useRef, useEffect, useCallback } from 'react';
|
import React, { useRef, useEffect, useCallback } from 'react';
|
||||||
import { Box } from 'coral-system';
|
import { Box } from 'coral-system';
|
||||||
import { MultiEditor, MultiEditorProps } from '@music163/tango-ui';
|
import { MultiEditor, MultiEditorProps } from '@music163/tango-ui';
|
||||||
import { observer, useWorkspace } from '@music163/tango-context';
|
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||||
import { isValidCode } from '@music163/tango-core';
|
import { isValidCode } from '@music163/tango-core';
|
||||||
import { Modal } from 'antd';
|
import { Modal } from 'antd';
|
||||||
|
|
||||||
@@ -32,6 +32,7 @@ export const CodeEditor = observer(
|
|||||||
({ autoRemoveUnusedImports = true, ...rest }: CodeEditorProps) => {
|
({ autoRemoveUnusedImports = true, ...rest }: CodeEditorProps) => {
|
||||||
const editorRef = useRef(null);
|
const editorRef = useRef(null);
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
|
const designer = useDesigner();
|
||||||
const files = workspace.listFiles();
|
const files = workspace.listFiles();
|
||||||
const activeFile = workspace.activeFile;
|
const activeFile = workspace.activeFile;
|
||||||
|
|
||||||
@@ -101,8 +102,15 @@ export const CodeEditor = observer(
|
|||||||
[workspace],
|
[workspace],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const borderStyle =
|
||||||
|
designer.activeView === 'dual'
|
||||||
|
? {
|
||||||
|
borderLeft: 'solid 1px var(--tango-colors-line2)',
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box display="flex" flexDirection="row" height="100%" bg="white">
|
<Box display="flex" flexDirection="row" height="100%" bg="white" {...borderStyle}>
|
||||||
<MultiEditor
|
<MultiEditor
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
options={{
|
options={{
|
||||||
|
|||||||
@@ -7,6 +7,20 @@ import {
|
|||||||
parseDndId,
|
parseDndId,
|
||||||
} from '@music163/tango-helpers';
|
} 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) {
|
export function buildQueryBySlotId(id: string) {
|
||||||
return `[${SLOT.dnd}="${id}"]`;
|
return `[${SLOT.dnd}="${id}"]`;
|
||||||
}
|
}
|
||||||
@@ -15,8 +29,6 @@ export function getElement(selector: Selector) {
|
|||||||
return $(selector).get(0);
|
return $(selector).get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DRAG_GHOST_ID = 'dragGhost';
|
|
||||||
|
|
||||||
export const getDragGhostElement = () => getElement(`#${DRAG_GHOST_ID}`);
|
export const getDragGhostElement = () => getElement(`#${DRAG_GHOST_ID}`);
|
||||||
|
|
||||||
export function setElementStyle(selector: Selector, style: any) {
|
export function setElementStyle(selector: Selector, style: any) {
|
||||||
@@ -75,7 +87,7 @@ export function getElementData(
|
|||||||
const display = getElementCSSDisplay(element);
|
const display = getElementCSSDisplay(element);
|
||||||
return {
|
return {
|
||||||
id: dnd.id,
|
id: dnd.id,
|
||||||
codeId: dnd.index,
|
codeId: dnd.codeId,
|
||||||
name: dnd.component || element.tagName.toLowerCase(),
|
name: dnd.component || element.tagName.toLowerCase(),
|
||||||
filename: dnd.filename,
|
filename: dnd.filename,
|
||||||
bounding,
|
bounding,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useSandboxQuery } from '../context';
|
|||||||
import { DndQuery, useDnd } from '../dnd';
|
import { DndQuery, useDnd } from '../dnd';
|
||||||
import { Navigator } from './navigator';
|
import { Navigator } from './navigator';
|
||||||
import { SelectionToolsProps } from '../simulator/selection';
|
import { SelectionToolsProps } from '../simulator/selection';
|
||||||
|
import { DESIGN_SANDBOX_ID, PREVIEW_SANDBOX_ID } from '../helpers';
|
||||||
|
|
||||||
interface ISandboxEventHandlerConfig {
|
interface ISandboxEventHandlerConfig {
|
||||||
sandboxQuery?: DndQuery;
|
sandboxQuery?: DndQuery;
|
||||||
@@ -48,6 +49,10 @@ export interface CombinedSandboxRef {
|
|||||||
|
|
||||||
const LANDING_PAGE_PATH = '/__background_landing_page__';
|
const LANDING_PAGE_PATH = '/__background_landing_page__';
|
||||||
|
|
||||||
|
function fixRouterMode(routerType: string): CodeSandboxProps['routerMode'] {
|
||||||
|
return routerType === 'hash' ? 'hash' : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function useSandbox({
|
function useSandbox({
|
||||||
isPreview: isPreviewProp,
|
isPreview: isPreviewProp,
|
||||||
configFormatter,
|
configFormatter,
|
||||||
@@ -62,6 +67,7 @@ function useSandbox({
|
|||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
const designer = useDesigner();
|
const designer = useDesigner();
|
||||||
const sandboxQuery = useSandboxQuery();
|
const sandboxQuery = useSandboxQuery();
|
||||||
|
|
||||||
const isPreview = isPreviewProp ?? designer.isPreview;
|
const isPreview = isPreviewProp ?? designer.isPreview;
|
||||||
|
|
||||||
// 组件不一定会立即刷新,因此 isActive 需要实时获取
|
// 组件不一定会立即刷新,因此 isActive 需要实时获取
|
||||||
@@ -103,23 +109,26 @@ function useSandbox({
|
|||||||
// 根据当前 workspace 状态与组件传入的状态是否一致,控制是否需要切换到空白路由
|
// 根据当前 workspace 状态与组件传入的状态是否一致,控制是否需要切换到空白路由
|
||||||
const display = isActive ? 'block' : 'none';
|
const display = isActive ? 'block' : 'none';
|
||||||
const routePath = isActive ? startRoute || workspace.activeRoute : LANDING_PAGE_PATH;
|
const routePath = isActive ? startRoute || workspace.activeRoute : LANDING_PAGE_PATH;
|
||||||
|
const routerMode = fixRouterMode(workspace.appEntryModule?.routerType);
|
||||||
|
|
||||||
const sandboxProps = isPreview
|
const sandboxProps = isPreview
|
||||||
? {
|
? {
|
||||||
files,
|
files,
|
||||||
eventHandlers: pick(dndHandlers, ['onTango']),
|
eventHandlers: pick(dndHandlers, ['onTango']),
|
||||||
onMessage,
|
onMessage,
|
||||||
|
onLoad,
|
||||||
display,
|
display,
|
||||||
startRoute: routePath,
|
startRoute: routePath,
|
||||||
onLoad,
|
routerMode,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
files,
|
files,
|
||||||
eventHandlers: dndHandlers as any,
|
eventHandlers: dndHandlers as any,
|
||||||
onMessage,
|
onMessage,
|
||||||
|
onLoad,
|
||||||
display,
|
display,
|
||||||
startRoute: routePath,
|
startRoute: routePath,
|
||||||
onLoad,
|
routerMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
return sandboxProps;
|
return sandboxProps;
|
||||||
@@ -140,7 +149,7 @@ const PreviewSandbox = observer(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box display={display} width="100%" height="100%">
|
<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>
|
</Box>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -188,7 +197,7 @@ const DesignSandbox = observer(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box display={display} width="100%" height="100%">
|
<Box display={display} width="100%" height="100%">
|
||||||
<CodeSandbox ref={ref} {...sandboxProps} {...rest} />
|
<CodeSandbox ref={ref} iframeId={DESIGN_SANDBOX_ID} {...sandboxProps} {...rest} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
export * from './copy-node';
|
export * from './copy-node';
|
||||||
export * from './delete-node';
|
export * from './delete-node';
|
||||||
|
export * from './more-actions';
|
||||||
|
export * from './select-parent-node';
|
||||||
export * from './view-source';
|
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,7 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { observer, useDesigner } from '@music163/tango-context';
|
import { observer, useDesigner } from '@music163/tango-context';
|
||||||
import { SelectAction } from '@music163/tango-ui';
|
import { SelectAction, CodeOutlined } from '@music163/tango-ui';
|
||||||
import { AimOutlined } from '@ant-design/icons';
|
|
||||||
|
|
||||||
export const ViewSourceAction = observer(() => {
|
export const ViewSourceAction = observer(() => {
|
||||||
const designer = useDesigner();
|
const designer = useDesigner();
|
||||||
@@ -12,7 +11,7 @@ export const ViewSourceAction = observer(() => {
|
|||||||
designer.setActiveView('code');
|
designer.setActiveView('code');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AimOutlined />
|
<CodeOutlined />
|
||||||
</SelectAction>
|
</SelectAction>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Radio, RadioProps, Tooltip } from 'antd';
|
import { Radio, RadioProps, Tooltip } from 'antd';
|
||||||
import type { OptionType } from '@music163/tango-helpers';
|
import type { OptionType } from '@music163/tango-helpers';
|
||||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||||
|
import { IconFont } from '@music163/tango-ui';
|
||||||
|
|
||||||
interface ChoiceSetterProps {
|
interface ChoiceSetterProps {
|
||||||
mode?: 'text' | 'icon';
|
|
||||||
options?: OptionType[];
|
options?: OptionType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,6 +13,18 @@ export function ChoiceSetter({
|
|||||||
onChange,
|
onChange,
|
||||||
...props
|
...props
|
||||||
}: FormItemComponentProps<string> & ChoiceSetterProps) {
|
}: FormItemComponentProps<string> & ChoiceSetterProps) {
|
||||||
|
const renderIcon = useCallback((icon: string) => {
|
||||||
|
if (!icon) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (icon.startsWith('icon-')) {
|
||||||
|
return <IconFont type={icon} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Radio.Group
|
<Radio.Group
|
||||||
optionType="button"
|
optionType="button"
|
||||||
@@ -24,6 +36,7 @@ export function ChoiceSetter({
|
|||||||
>
|
>
|
||||||
{options.map((item) => (
|
{options.map((item) => (
|
||||||
<RadioButton tip={item.tip} key={item.value} value={item.value}>
|
<RadioButton tip={item.tip} key={item.value} value={item.value}>
|
||||||
|
{renderIcon(item.icon)}
|
||||||
{item.label}
|
{item.label}
|
||||||
</RadioButton>
|
</RadioButton>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import React, { useCallback, useEffect, useState } from 'react';
|
|||||||
import { Box } from 'coral-system';
|
import { Box } from 'coral-system';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { toJSON } from 'cssjson';
|
import { toJSON } from 'cssjson';
|
||||||
import { InputNumber, Space } from 'antd';
|
import { InputNumber, Slider, Space } from 'antd';
|
||||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||||
import { SliderSetter } from './number-setter';
|
|
||||||
import {
|
import {
|
||||||
BgSetter,
|
BgSetter,
|
||||||
BorderSetter,
|
BorderSetter,
|
||||||
@@ -16,10 +15,11 @@ import {
|
|||||||
FlexJustifyContentSetter,
|
FlexJustifyContentSetter,
|
||||||
SpacingSetter,
|
SpacingSetter,
|
||||||
} from './style-setter';
|
} from './style-setter';
|
||||||
|
import { wrapCode } from '@music163/tango-helpers';
|
||||||
|
|
||||||
const getRawCssValue = (value: string) => {
|
const getRawCssValue = (value: string) => {
|
||||||
if (value && value.startsWith('{css`')) {
|
if (value && value.startsWith('{{css`')) {
|
||||||
return value.split('').slice(5, -2).join('').trim();
|
return value.split('').slice(6, -3).join('').trim();
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
@@ -51,7 +51,8 @@ const cssPattern = (string: string, name: string): any => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 废弃
|
* coral-system css prop
|
||||||
|
* @example 提供 css-in-js 代码支持,例如 css`background: red;`
|
||||||
* @deprecated 使用嵌套属性代替
|
* @deprecated 使用嵌套属性代替
|
||||||
*/
|
*/
|
||||||
export function CssSetter(props: FormItemComponentProps<string>) {
|
export function CssSetter(props: FormItemComponentProps<string>) {
|
||||||
@@ -82,16 +83,14 @@ export function CssSetter(props: FormItemComponentProps<string>) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (contentValue || contentValue === '') {
|
if (contentValue || contentValue === '') {
|
||||||
onChange(`{css\`${contentValue}\`}`, {
|
onChange(wrapCode(`css\`${contentValue}\``));
|
||||||
// relatedImports: ['']
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [contentValue]);
|
}, [contentValue]);
|
||||||
|
|
||||||
const isFlex = ['flex', 'inline-flex'].includes(display);
|
const isFlex = ['flex', 'inline-flex'].includes(display);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box bg="fill1">
|
||||||
<ItemGroup title="布局方式">
|
<ItemGroup title="布局方式">
|
||||||
<DisplaySetter
|
<DisplaySetter
|
||||||
value={toJSON(contentValue).attributes?.display}
|
value={toJSON(contentValue).attributes?.display}
|
||||||
@@ -204,12 +203,12 @@ export function CssSetter(props: FormItemComponentProps<string>) {
|
|||||||
/>
|
/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup title="透明度">
|
<ItemGroup title="透明度">
|
||||||
<SliderSetter
|
<Slider
|
||||||
max={1}
|
max={1}
|
||||||
min={0}
|
min={0}
|
||||||
step={0.1}
|
step={0.1}
|
||||||
value={toJSON(contentValue).attributes?.opacity || 1}
|
value={toJSON(contentValue).attributes?.opacity || 1}
|
||||||
onChange={(v) => {
|
onChange={(v: number) => {
|
||||||
changeStyle(v, 'opacity');
|
changeStyle(v, 'opacity');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -25,13 +25,18 @@ const style = {
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function DateSetter({ value, onChange, format = 'YYYY-MM-DD', ...rest }: FormItemComponentProps<string>) {
|
export function DateSetter({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
format = 'YYYY-MM-DD',
|
||||||
|
...rest
|
||||||
|
}: FormItemComponentProps<string>) {
|
||||||
return (
|
return (
|
||||||
<DatePicker
|
<DatePicker
|
||||||
{...rest}
|
{...rest}
|
||||||
format={format}
|
format={format}
|
||||||
style={style}
|
style={style}
|
||||||
value={toMoment(value, format)}
|
value={value ? toMoment(value, format) : undefined}
|
||||||
onChange={(val, str) => {
|
onChange={(val, str) => {
|
||||||
onChange && onChange(str);
|
onChange && onChange(str);
|
||||||
}}
|
}}
|
||||||
@@ -46,7 +51,12 @@ function toMoments(value: string[], format: string): moment.Moment[] {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DateRangeSetter({ value, onChange, format = 'YYYY-MM-DD', ...rest }: FormItemComponentProps<string[]>) {
|
export function DateRangeSetter({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
format = 'YYYY-MM-DD',
|
||||||
|
...rest
|
||||||
|
}: FormItemComponentProps<string[]>) {
|
||||||
return (
|
return (
|
||||||
<DatePicker.RangePicker
|
<DatePicker.RangePicker
|
||||||
{...rest}
|
{...rest}
|
||||||
@@ -60,7 +70,12 @@ export function DateRangeSetter({ value, onChange, format = 'YYYY-MM-DD', ...res
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TimeSetter({ value, onChange, format = 'HH:mm:ss', ...rest }: FormItemComponentProps<string>) {
|
export function TimeSetter({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
format = 'HH:mm:ss',
|
||||||
|
...rest
|
||||||
|
}: FormItemComponentProps<string>) {
|
||||||
return (
|
return (
|
||||||
<TimePicker
|
<TimePicker
|
||||||
{...rest}
|
{...rest}
|
||||||
@@ -74,7 +89,12 @@ export function TimeSetter({ value, onChange, format = 'HH:mm:ss', ...rest }: Fo
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TimeRangeSetter({ value, onChange, format = 'HH:mm:ss', ...rest }: FormItemComponentProps<string[]>) {
|
export function TimeRangeSetter({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
format = 'HH:mm:ss',
|
||||||
|
...rest
|
||||||
|
}: FormItemComponentProps<string[]>) {
|
||||||
return (
|
return (
|
||||||
<TimePicker.RangePicker
|
<TimePicker.RangePicker
|
||||||
{...rest}
|
{...rest}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { css, Box } from 'coral-system';
|
import { css, Box, Text } from 'coral-system';
|
||||||
import { AutoComplete } from 'antd';
|
import { AutoComplete } from 'antd';
|
||||||
import { ActionSelect } from '@music163/tango-ui';
|
import { ActionSelect } from '@music163/tango-ui';
|
||||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||||
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||||
import { ExpressionModal, getWrappedExpressionCode } from './expression-setter';
|
import { wrapCode } from '@music163/tango-helpers';
|
||||||
|
import { ExpressionPopover } from './expression-setter';
|
||||||
|
import { value2code } from '@music163/tango-core';
|
||||||
|
|
||||||
enum EventAction {
|
enum EventAction {
|
||||||
NoAction = 'noAction',
|
NoAction = 'noAction',
|
||||||
|
ConsoleLog = 'consoleLog',
|
||||||
BindExpression = 'bindExpression',
|
BindExpression = 'bindExpression',
|
||||||
OpenModal = 'openModal',
|
OpenModal = 'openModal',
|
||||||
CloseModal = 'closeModal',
|
CloseModal = 'closeModal',
|
||||||
@@ -22,14 +25,6 @@ const wrapperStyle = css`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const options = [
|
|
||||||
{ label: '无动作', value: EventAction.NoAction },
|
|
||||||
{ label: '绑定 JS 表达式', value: EventAction.BindExpression },
|
|
||||||
{ label: '打开页面', value: EventAction.NavigateTo },
|
|
||||||
{ label: '打开弹窗', value: EventAction.OpenModal },
|
|
||||||
{ label: '关闭弹窗', value: EventAction.CloseModal },
|
|
||||||
];
|
|
||||||
|
|
||||||
export type EventSetterProps = FormItemComponentProps<string>;
|
export type EventSetterProps = FormItemComponentProps<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,20 +34,47 @@ export function EventSetter(props: EventSetterProps) {
|
|||||||
const { value, onChange, modalTitle } = props;
|
const { value, onChange, modalTitle } = props;
|
||||||
const [type, setType] = useState<EventAction>(); // 事件类型
|
const [type, setType] = useState<EventAction>(); // 事件类型
|
||||||
const [temp, setTemp] = useState(''); // 二级暂存值
|
const [temp, setTemp] = useState(''); // 二级暂存值
|
||||||
const [expModalVisible, setExpModalVisible] = useState(false); // 弹窗是否显示
|
|
||||||
|
|
||||||
const { actionVariables, routeOptions } = useWorkspaceData();
|
const { actionVariables, routeOptions } = useWorkspaceData();
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
const modalOptions = workspace.activeViewModule.listModals() || [];
|
const modalOptions = workspace.activeViewModule.listModals() || [];
|
||||||
|
|
||||||
|
const code = value2code(value);
|
||||||
|
|
||||||
const handleChange = useCallback<FormItemComponentProps['onChange']>(
|
const handleChange = useCallback<FormItemComponentProps['onChange']>(
|
||||||
(nextValue: any, ...args) => {
|
(nextValue: any, ...args) => {
|
||||||
const ret = getWrappedExpressionCode(nextValue);
|
if (!nextValue) {
|
||||||
if (ret !== value) {
|
onChange(undefined);
|
||||||
onChange(ret, ...args);
|
}
|
||||||
|
if (nextValue !== code) {
|
||||||
|
onChange(wrapCode(nextValue), ...args);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onChange, value],
|
[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) => {
|
const onAction = (key: string) => {
|
||||||
@@ -60,8 +82,8 @@ export function EventSetter(props: EventSetterProps) {
|
|||||||
setTemp(''); // 重置二级选项值
|
setTemp(''); // 重置二级选项值
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case EventAction.BindExpression:
|
case EventAction.ConsoleLog:
|
||||||
setExpModalVisible(true);
|
handleChange('(...args) => console.log(...args)');
|
||||||
break;
|
break;
|
||||||
case EventAction.NoAction:
|
case EventAction.NoAction:
|
||||||
handleChange(undefined);
|
handleChange(undefined);
|
||||||
@@ -71,23 +93,11 @@ export function EventSetter(props: EventSetterProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionText = getActionText(type, temp, value);
|
const actionText = getActionText(type, temp, code);
|
||||||
const inputValue = value;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box css={wrapperStyle}>
|
<Box css={wrapperStyle}>
|
||||||
<ActionSelect options={options} onSelect={onAction} text={actionText} />
|
<ActionSelect options={options} onSelect={onAction} text={actionText} />
|
||||||
<ExpressionModal
|
|
||||||
title={modalTitle}
|
|
||||||
value={inputValue}
|
|
||||||
visible={expModalVisible}
|
|
||||||
onCancel={() => setExpModalVisible(false)}
|
|
||||||
onOk={(nextValue) => {
|
|
||||||
setExpModalVisible(false);
|
|
||||||
handleChange(nextValue);
|
|
||||||
}}
|
|
||||||
dataSource={actionVariables}
|
|
||||||
/>
|
|
||||||
{type === EventAction.NavigateTo && (
|
{type === EventAction.NavigateTo && (
|
||||||
<AutoComplete
|
<AutoComplete
|
||||||
placeholder="选择或输入页面路由"
|
placeholder="选择或输入页面路由"
|
||||||
@@ -137,12 +147,12 @@ const handlerMap = {
|
|||||||
[EventAction.NavigateTo]: 'navigateTo',
|
[EventAction.NavigateTo]: 'navigateTo',
|
||||||
};
|
};
|
||||||
|
|
||||||
function getActionText(type: EventAction, temp: string, value: any) {
|
function getActionText(type: EventAction, temp: string, fallbackCode: string) {
|
||||||
let text;
|
let text;
|
||||||
if (handlerMap[type]) {
|
if (handlerMap[type]) {
|
||||||
text = getExpressionValue(type, temp);
|
text = getExpressionValue(type, temp);
|
||||||
} else if (value) {
|
} else if (fallbackCode) {
|
||||||
text = value;
|
text = fallbackCode;
|
||||||
}
|
}
|
||||||
text = text || '请选择';
|
text = text || '请选择';
|
||||||
return text;
|
return text;
|
||||||
@@ -151,6 +161,6 @@ function getActionText(type: EventAction, temp: string, value: any) {
|
|||||||
function getExpressionValue(type: EventAction, value = '') {
|
function getExpressionValue(type: EventAction, value = '') {
|
||||||
const handler = handlerMap[type];
|
const handler = handlerMap[type];
|
||||||
if (handler) {
|
if (handler) {
|
||||||
return `{() => tango.${handler}("${value}")}`;
|
return `() => tango.${handler}("${value}")`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { SingleMonacoEditor } from '@music163/tango-ui';
|
|
||||||
import { Box } from 'coral-system';
|
|
||||||
import { FormItemComponentProps } from '@music163/tango-setting-form/src/form-item';
|
|
||||||
|
|
||||||
export function ExpressionSetter({ value, onChange }: FormItemComponentProps) {
|
|
||||||
return (
|
|
||||||
<Box height="120px" border="solid" borderColor="line.normal" borderRadius="s">
|
|
||||||
<SingleMonacoEditor
|
|
||||||
defaultValue={value}
|
|
||||||
onBlur={(newValue) => {
|
|
||||||
if (newValue !== value) {
|
|
||||||
onChange(`{${newValue}}`);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
language="javascript"
|
|
||||||
options={{
|
|
||||||
lineNumbers: 'off',
|
|
||||||
minimap: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,10 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { Box, Text, css } from 'coral-system';
|
import { Box, Text, css } from 'coral-system';
|
||||||
import { Modal } from 'antd';
|
import { Dropdown, Button } from 'antd';
|
||||||
import {
|
import { isValidExpressionCode } from '@music163/tango-core';
|
||||||
isValidExpressionCode,
|
import { getValue, IVariableTreeNode, noop } from '@music163/tango-helpers';
|
||||||
isWrappedByExpressionContainer,
|
import { CloseCircleFilled, ExpandAltOutlined, MenuOutlined } from '@ant-design/icons';
|
||||||
value2expressionCode,
|
import { Panel, InputCode, Action, DragPanel } from '@music163/tango-ui';
|
||||||
} from '@music163/tango-core';
|
|
||||||
import {
|
|
||||||
getVariableContent,
|
|
||||||
noop,
|
|
||||||
useBoolean,
|
|
||||||
getValue,
|
|
||||||
IVariableTreeNode,
|
|
||||||
} from '@music163/tango-helpers';
|
|
||||||
import { CloseCircleFilled, ExpandAltOutlined } from '@ant-design/icons';
|
|
||||||
import { Panel, InputCode, Action } from '@music163/tango-ui';
|
|
||||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||||
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||||
import { VariableTree } from '../components';
|
import { VariableTree } from '../components';
|
||||||
@@ -23,43 +13,22 @@ import { CODE_TEMPLATES } from '../helpers';
|
|||||||
import { shapeServiceValues } from '../sidebar/datasource-panel/interface-config';
|
import { shapeServiceValues } from '../sidebar/datasource-panel/interface-config';
|
||||||
|
|
||||||
export const expressionValueValidate = (value: string) => {
|
export const expressionValueValidate = (value: string) => {
|
||||||
if (isWrappedByExpressionContainer(value)) {
|
if (!isValidExpressionCode(value)) {
|
||||||
const exp = getVariableContent(value);
|
return '表达式存在语法错误!';
|
||||||
if (!isValidExpressionCode(exp)) {
|
|
||||||
return '表达式存在语法错误!';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const jsonValueValidate = (value: string) => {
|
export const jsonValueValidate = (value: string) => {
|
||||||
if (isWrappedByExpressionContainer(value)) {
|
try {
|
||||||
const jsonStr = getVariableContent(value);
|
JSON.parse(value);
|
||||||
try {
|
} catch (e) {
|
||||||
JSON.parse(jsonStr);
|
return '不是合法的 JSON 语法!';
|
||||||
} catch (e) {
|
|
||||||
return '不是合法的 JSON 语法!';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 返回 `{}` 包裹后的表达式代码
|
|
||||||
* @param code 原始代码
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function getWrappedExpressionCode(code: string) {
|
|
||||||
let ret;
|
|
||||||
if (!code) {
|
|
||||||
// do nothing
|
|
||||||
} else if (isWrappedByExpressionContainer(code)) {
|
|
||||||
ret = code;
|
|
||||||
} else {
|
|
||||||
ret = `{${code}}`;
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
const suffixStyle = css`
|
const suffixStyle = css`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
.anticon-close-circle {
|
.anticon-close-circle {
|
||||||
color: rgba(0, 0, 0, 0.25);
|
color: rgba(0, 0, 0, 0.25);
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -73,6 +42,7 @@ export interface ExpressionSetterProps extends FormItemComponentProps<string> {
|
|||||||
modalTip?: string;
|
modalTip?: string;
|
||||||
autoCompleteOptions?: string[];
|
autoCompleteOptions?: string[];
|
||||||
allowClear?: boolean;
|
allowClear?: boolean;
|
||||||
|
showOptionsDropDown?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ExpressionSetter(props: ExpressionSetterProps) {
|
export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||||
@@ -81,22 +51,19 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
|||||||
modalTitle,
|
modalTitle,
|
||||||
modalTip,
|
modalTip,
|
||||||
autoCompleteOptions,
|
autoCompleteOptions,
|
||||||
placeholder = '输入JS表达式代码',
|
placeholder = '在这里输入JS代码',
|
||||||
value: valueProp,
|
value: valueProp,
|
||||||
status,
|
status,
|
||||||
allowClear = true,
|
allowClear = true,
|
||||||
newStoreTemplate,
|
newStoreTemplate,
|
||||||
|
showOptionsDropDown = true,
|
||||||
} = props;
|
} = props;
|
||||||
const [visible, { on, off }] = useBoolean();
|
// const codeValue = getCodeOfWrappedCode(valueProp);
|
||||||
const [inputValue, setInputValue] = useState(() => {
|
const [inputValue, setInputValue] = useState(valueProp);
|
||||||
return value2expressionCode(valueProp);
|
|
||||||
});
|
|
||||||
const sandbox = useSandboxQuery();
|
|
||||||
const evaluateContext = sandbox.window;
|
|
||||||
|
|
||||||
// when receive new value, sync state
|
// when receive new value, sync state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInputValue(value2expressionCode(valueProp));
|
setInputValue(valueProp);
|
||||||
}, [valueProp]);
|
}, [valueProp]);
|
||||||
|
|
||||||
const change = useCallback(
|
const change = useCallback(
|
||||||
@@ -104,20 +71,17 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
|||||||
if (code === valueProp) {
|
if (code === valueProp) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
onChange(code);
|
||||||
const ret = getWrappedExpressionCode(code);
|
|
||||||
|
|
||||||
if (ret === valueProp) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(ret);
|
|
||||||
},
|
},
|
||||||
[valueProp, onChange],
|
[valueProp, onChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sandbox = useSandboxQuery();
|
||||||
|
const evaluateContext = sandbox.window;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="ExpressionSetter">
|
<Box className="ExpressionSetter">
|
||||||
|
{/* 同时支持下拉框展示 */}
|
||||||
<InputCode
|
<InputCode
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
suffix={
|
suffix={
|
||||||
@@ -130,11 +94,36 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Action
|
{showOptionsDropDown && autoCompleteOptions?.length && (
|
||||||
tooltip="打开表达式变量选择面板"
|
<Dropdown
|
||||||
icon={<ExpandAltOutlined />}
|
menu={{
|
||||||
onClick={on}
|
items: autoCompleteOptions.map((option) => ({
|
||||||
size="small"
|
key: option,
|
||||||
|
label: (
|
||||||
|
<Text
|
||||||
|
onClick={() => {
|
||||||
|
change(option);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{option}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Action tooltip="使用预设代码片段" icon={<MenuOutlined />} size="small" />
|
||||||
|
</Dropdown>
|
||||||
|
)}
|
||||||
|
<ExpressionPopover
|
||||||
|
title={modalTitle}
|
||||||
|
subTitle={modalTip}
|
||||||
|
placeholder={placeholder}
|
||||||
|
autoCompleteOptions={autoCompleteOptions}
|
||||||
|
newStoreTemplate={newStoreTemplate}
|
||||||
|
value={inputValue}
|
||||||
|
onOk={(value) => {
|
||||||
|
change(value);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
@@ -148,32 +137,16 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
|||||||
status={status}
|
status={status}
|
||||||
maxHeight="200px"
|
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>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExpressionModalProps {
|
export interface ExpressionPopoverProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
subTitle?: string;
|
subTitle?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
visible?: boolean;
|
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
onCancel?: () => void;
|
|
||||||
onOk?: (value: string) => void;
|
onOk?: (value: string) => void;
|
||||||
dataSource?: IVariableTreeNode[];
|
dataSource?: IVariableTreeNode[];
|
||||||
autoCompleteOptions?: string[];
|
autoCompleteOptions?: string[];
|
||||||
@@ -181,25 +154,27 @@ export interface ExpressionModalProps {
|
|||||||
* 新建 store 的模板代码
|
* 新建 store 的模板代码
|
||||||
*/
|
*/
|
||||||
newStoreTemplate?: string;
|
newStoreTemplate?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ExpressionModal({
|
export function ExpressionPopover({
|
||||||
title,
|
title,
|
||||||
subTitle,
|
subTitle,
|
||||||
placeholder,
|
placeholder,
|
||||||
visible,
|
|
||||||
onCancel = noop,
|
|
||||||
onOk = noop,
|
onOk = noop,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
value,
|
value,
|
||||||
dataSource,
|
dataSource,
|
||||||
autoCompleteOptions,
|
autoCompleteOptions,
|
||||||
newStoreTemplate = CODE_TEMPLATES.newStoreTemplate,
|
newStoreTemplate = CODE_TEMPLATES.newStoreTemplate,
|
||||||
}: ExpressionModalProps) {
|
children,
|
||||||
|
}: ExpressionPopoverProps) {
|
||||||
const [exp, setExp] = useState(value ?? defaultValue);
|
const [exp, setExp] = useState(value ?? defaultValue);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
|
|
||||||
|
const selectNodePath = workspace.selectSource.selected[0]?.codeId;
|
||||||
|
|
||||||
const { expressionVariables } = useWorkspaceData();
|
const { expressionVariables } = useWorkspaceData();
|
||||||
const serviceModules = Object.keys(workspace.serviceModules).map((key) => ({
|
const serviceModules = Object.keys(workspace.serviceModules).map((key) => ({
|
||||||
label: key === 'index' ? '默认模块' : key,
|
label: key === 'index' ? '默认模块' : key,
|
||||||
@@ -218,109 +193,145 @@ export function ExpressionModal({
|
|||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<DragPanel
|
||||||
closable={false}
|
width={700}
|
||||||
destroyOnClose
|
title={`将 ${selectNodePath}/${title} 设置为引用变量或自定义表达式`}
|
||||||
width="60%"
|
extra={subTitle}
|
||||||
open={visible}
|
onOpenChange={(open) => {
|
||||||
onCancel={onCancel}
|
if (!open) {
|
||||||
onOk={() => {
|
setExp(undefined);
|
||||||
onOk(exp);
|
setError('');
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
bodyStyle={{
|
popoverStyle={{
|
||||||
padding: 0,
|
height: '615px',
|
||||||
}}
|
}}
|
||||||
|
body={
|
||||||
|
<>
|
||||||
|
<Panel shape="solid">
|
||||||
|
<InputCode
|
||||||
|
shape="inset"
|
||||||
|
minHeight="56px"
|
||||||
|
maxHeight="200px"
|
||||||
|
value={exp}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={handleExpInputChange}
|
||||||
|
onBlur={() => {
|
||||||
|
setError(expressionValueValidate(exp));
|
||||||
|
}}
|
||||||
|
autoCompleteContext={evaluateContext}
|
||||||
|
autoCompleteOptions={autoCompleteOptions}
|
||||||
|
/>
|
||||||
|
{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>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
footer={(close) => (
|
||||||
|
<Box display="flex" 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">
|
{children || (
|
||||||
<InputCode
|
<Action tooltip="打开表达式变量选择面板" icon={<ExpandAltOutlined />} size="small" />
|
||||||
shape="inset"
|
)}
|
||||||
minHeight="56px"
|
</DragPanel>
|
||||||
maxHeight="200px"
|
|
||||||
value={exp}
|
|
||||||
placeholder={placeholder}
|
|
||||||
onChange={handleExpInputChange}
|
|
||||||
onBlur={() => {
|
|
||||||
setError(expressionValueValidate(exp));
|
|
||||||
}}
|
|
||||||
autoCompleteContext={evaluateContext}
|
|
||||||
autoCompleteOptions={autoCompleteOptions}
|
|
||||||
/>
|
|
||||||
{error ? <Text color="red">输入的表达式存在语法错误,请修改后再提交!</Text> : null}
|
|
||||||
</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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { ListSetter } from './list-setter';
|
|||||||
import { ModelSetter } from './model-setter';
|
import { ModelSetter } from './model-setter';
|
||||||
import { OptionSetter } from './option-setter';
|
import { OptionSetter } from './option-setter';
|
||||||
import { PickerSetter } from './picker-setter';
|
import { PickerSetter } from './picker-setter';
|
||||||
import { RenderSetter, TableCellSetter, TableExpandableSetter } from './render-props-setter';
|
import { RenderSetter, TableCellSetter, TableExpandableSetter } from './render-setter';
|
||||||
import { RouterSetter } from './router-setter';
|
import { RouterSetter } from './router-setter';
|
||||||
import {
|
import {
|
||||||
SpacingSetter,
|
SpacingSetter,
|
||||||
@@ -25,44 +25,46 @@ import {
|
|||||||
FlexAlignItemsSetter,
|
FlexAlignItemsSetter,
|
||||||
FlexDirectionSetter,
|
FlexDirectionSetter,
|
||||||
} from './style-setter';
|
} from './style-setter';
|
||||||
import { BoolSetter } from './bool-setter';
|
|
||||||
import { ChoiceSetter } from './choice-setter';
|
import { ChoiceSetter } from './choice-setter';
|
||||||
import { NumberSetter, SliderSetter } from './number-setter';
|
import { isValidExpressionCode } from '@music163/tango-core';
|
||||||
import { TextAreaSetter, TextSetter } from './text-setter';
|
|
||||||
|
|
||||||
const setters: IFormItemCreateOptions[] = [
|
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[] = [
|
||||||
{
|
{
|
||||||
name: 'boolSetter',
|
name: 'codeSetter',
|
||||||
component: BoolSetter,
|
alias: ['expSetter', 'expressionSetter'],
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'choiceSetter',
|
|
||||||
component: ChoiceSetter,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'expSetter',
|
|
||||||
alias: ['expressionSetter'],
|
|
||||||
component: ExpressionSetter,
|
component: ExpressionSetter,
|
||||||
|
type: 'code',
|
||||||
|
validate: codeValidate,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'numberSetter',
|
name: 'radioGroupSetter',
|
||||||
component: NumberSetter,
|
alias: ['choiceSetter'],
|
||||||
},
|
component: ChoiceSetter,
|
||||||
{
|
|
||||||
name: 'textSetter',
|
|
||||||
component: TextSetter,
|
|
||||||
},
|
|
||||||
{ name: 'textAreaSetter', component: TextAreaSetter },
|
|
||||||
{
|
|
||||||
name: 'sliderSetter',
|
|
||||||
component: SliderSetter,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'actionListSetter',
|
name: 'actionListSetter',
|
||||||
component: ActionListSetter,
|
component: ActionListSetter,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'columnSetter',
|
name: 'tableColumnsSetter',
|
||||||
|
alias: ['columnSetter'], // 兼容
|
||||||
component: ColumnSetter,
|
component: ColumnSetter,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -94,18 +96,17 @@ const setters: IFormItemCreateOptions[] = [
|
|||||||
alias: ['actionSetter', 'functionSetter', 'callbackSetter'],
|
alias: ['actionSetter', 'functionSetter', 'callbackSetter'],
|
||||||
component: EventSetter,
|
component: EventSetter,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'expressionSetter',
|
|
||||||
alias: ['expSetter'],
|
|
||||||
component: ExpressionSetter,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'jsonSetter',
|
name: 'jsonSetter',
|
||||||
component: JSONSetter,
|
component: JSONSetter,
|
||||||
|
type: 'code',
|
||||||
|
validate: jsonValidate,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'jsxSetter',
|
name: 'jsxSetter',
|
||||||
component: JsxSetter,
|
component: JsxSetter,
|
||||||
|
type: 'code',
|
||||||
|
validate: codeValidate,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'listSetter',
|
name: 'listSetter',
|
||||||
@@ -120,20 +121,26 @@ const setters: IFormItemCreateOptions[] = [
|
|||||||
component: OptionSetter,
|
component: OptionSetter,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'pickerSetter',
|
name: 'selectSetter',
|
||||||
|
alias: ['pickerSetter'],
|
||||||
component: PickerSetter,
|
component: PickerSetter,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'renderPropsSetter',
|
name: 'renderPropsSetter',
|
||||||
component: RenderSetter,
|
component: RenderSetter,
|
||||||
|
type: 'code',
|
||||||
|
validate: codeValidate,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tableCellSetter',
|
name: 'tableCellSetter',
|
||||||
component: TableCellSetter,
|
component: TableCellSetter,
|
||||||
|
type: 'code',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tableExpandableSetter',
|
name: 'tableExpandableSetter',
|
||||||
component: TableExpandableSetter,
|
component: TableExpandableSetter,
|
||||||
|
type: 'code',
|
||||||
|
validate: codeValidate,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'routerSetter',
|
name: 'routerSetter',
|
||||||
@@ -169,5 +176,5 @@ const setters: IFormItemCreateOptions[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export function registerBuiltinSetters() {
|
export function registerBuiltinSetters() {
|
||||||
setters.forEach(register);
|
BUILT_IN_SETTERS.forEach(register);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Box } from 'coral-system';
|
import { Box } from 'coral-system';
|
||||||
import { SingleMonacoEditor } from '@music163/tango-ui';
|
import { SingleMonacoEditor } from '@music163/tango-ui';
|
||||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||||
@@ -6,14 +6,18 @@ import { FormItemComponentProps } from '@music163/tango-setting-form';
|
|||||||
/**
|
/**
|
||||||
* JSON Setter
|
* JSON Setter
|
||||||
*/
|
*/
|
||||||
export function JSONSetter({ value, onChange }: FormItemComponentProps) {
|
export function JSONSetter({ value: valueProp, onChange }: FormItemComponentProps) {
|
||||||
|
const [value, setValue] = useState(valueProp || '');
|
||||||
return (
|
return (
|
||||||
<Box height="120px" border="solid" borderColor="line.normal" borderRadius="s">
|
<Box height="120px" border="solid" borderColor="line.normal" borderRadius="s">
|
||||||
<SingleMonacoEditor
|
<SingleMonacoEditor
|
||||||
defaultValue={value}
|
defaultValue={value}
|
||||||
|
onChange={(newValue) => {
|
||||||
|
setValue(newValue);
|
||||||
|
}}
|
||||||
onBlur={(newValue) => {
|
onBlur={(newValue) => {
|
||||||
if (newValue !== value) {
|
if (newValue !== value) {
|
||||||
onChange(`{${newValue}}`);
|
onChange(newValue);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
language="json"
|
language="json"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { isPlainString } from '@music163/tango-helpers';
|
import { ActionSelect, InputCode } from '@music163/tango-ui';
|
||||||
import { ActionSelect } from '@music163/tango-ui';
|
|
||||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||||
|
import { Box } from 'coral-system';
|
||||||
|
|
||||||
const options = [
|
const options = [
|
||||||
{ label: '取消自定义', value: '' },
|
{ label: '取消自定义', value: '' },
|
||||||
@@ -25,26 +25,27 @@ interface JsxSetterProps extends FormItemComponentProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultGetTemplate = (key: string) => {
|
const defaultGetTemplate = (key: string) => {
|
||||||
let ret;
|
let ret: [string, string[]];
|
||||||
const deps = [];
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'Box':
|
case 'Box':
|
||||||
ret = '<Box></Box>';
|
ret = ['<Box></Box>', ['Box']];
|
||||||
break;
|
break;
|
||||||
case 'Text':
|
case 'Text':
|
||||||
ret = '<Text>示例文本</Text>';
|
ret = ['<Text>示例文本</Text>', ['Text']];
|
||||||
break;
|
break;
|
||||||
case 'Placeholder':
|
case 'Placeholder':
|
||||||
ret = '<Placeholder text="拖拽到此进行替换" />';
|
ret = ['<Placeholder text="拖拽到此进行替换" />', ['Placeholder']];
|
||||||
break;
|
break;
|
||||||
case 'ButtonGroup':
|
case 'ButtonGroup':
|
||||||
ret = '<ButtonGroup><Button>按钮1</Button><Button>按钮2</Button></ButtonGroup>';
|
ret = [
|
||||||
deps.push('Button');
|
'<ButtonGroup><Button>按钮1</Button><Button>按钮2</Button></ButtonGroup>',
|
||||||
|
['ButtonGroup', 'Button'],
|
||||||
|
];
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return [ret, deps];
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,21 +53,36 @@ const defaultGetTemplate = (key: string) => {
|
|||||||
*/
|
*/
|
||||||
export function JsxSetter(props: JsxSetterProps) {
|
export function JsxSetter(props: JsxSetterProps) {
|
||||||
const { showInput, getTemplate = defaultGetTemplate, value, onChange } = props;
|
const { showInput, getTemplate = defaultGetTemplate, value, onChange } = props;
|
||||||
|
const [inputValue, setInputValue] = useState(value);
|
||||||
|
useEffect(() => {
|
||||||
|
setInputValue(value);
|
||||||
|
}, [value]);
|
||||||
return (
|
return (
|
||||||
<ActionSelect
|
<Box>
|
||||||
showInput={showInput}
|
<ActionSelect
|
||||||
defaultInputValue={isPlainString(value) ? value : undefined}
|
showInput={showInput}
|
||||||
options={options}
|
defaultInputValue={value}
|
||||||
text="将此区域初始化为"
|
options={options}
|
||||||
onInputChange={onChange}
|
text="设置此区域为"
|
||||||
onSelect={(key) => {
|
onInputChange={onChange}
|
||||||
const [tpl, deps] = getTemplate(key);
|
onSelect={(key) => {
|
||||||
if (tpl) {
|
const [tpl, deps] = getTemplate(key);
|
||||||
onChange(`{${tpl}}`, { relatedImports: [...deps, key] });
|
if (tpl) {
|
||||||
} else {
|
onChange(tpl, { relatedImports: deps });
|
||||||
onChange(undefined);
|
} else {
|
||||||
}
|
onChange(undefined);
|
||||||
}}
|
}
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
{value && (
|
||||||
|
<InputCode
|
||||||
|
value={value}
|
||||||
|
onChange={(val) => setInputValue(val)}
|
||||||
|
onBlur={() => {
|
||||||
|
onChange(inputValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -234,6 +234,7 @@ function NewOptionForm({ fields = [], initialValues = {}, onSubmit }: NewOptionF
|
|||||||
label={item.label}
|
label={item.label}
|
||||||
name={item.name}
|
name={item.name}
|
||||||
required={item.required}
|
required={item.required}
|
||||||
|
rules={[{ required: item.required }]}
|
||||||
valuePropName={item.valuePropName}
|
valuePropName={item.valuePropName}
|
||||||
extra={item.extra}
|
extra={item.extra}
|
||||||
style={{
|
style={{
|
||||||
@@ -260,7 +261,7 @@ interface ListSetterProps extends FormItemComponentProps<any[]> {
|
|||||||
addBtnText?: string;
|
addBtnText?: string;
|
||||||
getListItemKey?: (item: any) => React.Key;
|
getListItemKey?: (item: any) => React.Key;
|
||||||
renderItem?: (item: any) => React.ReactNode;
|
renderItem?: (item: any) => React.ReactNode;
|
||||||
};
|
}
|
||||||
|
|
||||||
const defaultListItemFormFields: ListSetterItemProps['formFields'] = [
|
const defaultListItemFormFields: ListSetterItemProps['formFields'] = [
|
||||||
{ label: 'key', name: 'key', required: true },
|
{ label: 'key', name: 'key', required: true },
|
||||||
|
|||||||
+39
-10
@@ -1,6 +1,7 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { ActionSelect } from '@music163/tango-ui';
|
import { ActionSelect, InputCode } from '@music163/tango-ui';
|
||||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||||
|
import { Box } from 'coral-system';
|
||||||
|
|
||||||
interface IRenderOption {
|
interface IRenderOption {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -15,15 +16,26 @@ export interface RenderSetterProps {
|
|||||||
fallbackOption?: IRenderOption;
|
fallbackOption?: IRenderOption;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultOptions: IRenderOption[] = [
|
||||||
|
{ label: '取消自定义', value: '' },
|
||||||
|
{ label: '自定义渲染', value: 'Box', render: '() => <Box></Box>' },
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render Props Setters
|
* Render Props Setters
|
||||||
*/
|
*/
|
||||||
export function RenderSetter({
|
export function RenderSetter({
|
||||||
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
text = '自定义渲染为',
|
text = '自定义渲染为',
|
||||||
options = [],
|
options = defaultOptions,
|
||||||
fallbackOption,
|
fallbackOption,
|
||||||
}: FormItemComponentProps & RenderSetterProps) {
|
}: FormItemComponentProps & RenderSetterProps) {
|
||||||
|
const [inputValue, setInputValue] = useState(value || '');
|
||||||
|
useEffect(() => {
|
||||||
|
setInputValue(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
const optionsMap = useMemo(() => {
|
const optionsMap = useMemo(() => {
|
||||||
return options.reduce((prev, cur) => {
|
return options.reduce((prev, cur) => {
|
||||||
prev[cur.value] = cur;
|
prev[cur.value] = cur;
|
||||||
@@ -42,21 +54,37 @@ export function RenderSetter({
|
|||||||
},
|
},
|
||||||
[optionsMap, fallbackOption, onChange],
|
[optionsMap, fallbackOption, onChange],
|
||||||
);
|
);
|
||||||
return <ActionSelect text={text} options={options} onSelect={onSelect} />;
|
return (
|
||||||
|
<Box>
|
||||||
|
<ActionSelect text={text} options={options} onSelect={onSelect} />
|
||||||
|
{inputValue && (
|
||||||
|
<InputCode
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(val) => setInputValue(val)}
|
||||||
|
onBlur={() => onChange(inputValue)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRender = (content: string, type?: 'tableCell' | 'tableExpandable') => {
|
const getRender = (content: string, type?: 'tableCell' | 'tableExpandable') => {
|
||||||
|
let code;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'tableCell':
|
case 'tableCell':
|
||||||
return `{(value, record, index) => ${content}}`;
|
code = `(value, record, index) => ${content}`;
|
||||||
|
break;
|
||||||
case 'tableExpandable':
|
case 'tableExpandable':
|
||||||
return `{{
|
code = `{
|
||||||
expandedRowRender: (record) => ${content},
|
expandedRowRender: (record) => ${content},
|
||||||
rowExpandable: (record) => true
|
rowExpandable: (record) => true
|
||||||
}}`;
|
}`;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return `{() => ${content}}`;
|
code = `() => ${content}`;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
return code;
|
||||||
};
|
};
|
||||||
|
|
||||||
const tableCellOptions: RenderSetterProps['options'] = [
|
const tableCellOptions: RenderSetterProps['options'] = [
|
||||||
@@ -101,6 +129,7 @@ export function TableCellSetter(props: FormItemComponentProps) {
|
|||||||
return <RenderSetter options={tableCellOptions} {...props} />;
|
return <RenderSetter options={tableCellOptions} {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: 应该直接用 props 嵌套的模式
|
||||||
export function TableExpandableSetter(props: FormItemComponentProps) {
|
export function TableExpandableSetter(props: FormItemComponentProps) {
|
||||||
return <RenderSetter options={tableExpandableOptions} text="配置表格可展开行" {...props} />;
|
return <RenderSetter options={tableExpandableOptions} text="配置表格可展开行" {...props} />;
|
||||||
}
|
}
|
||||||
@@ -7,8 +7,6 @@ import { BgColorsOutlined, EyeInvisibleOutlined, FileImageOutlined } from '@ant-
|
|||||||
import { SingleMonacoEditor, LineSolidOutlined, LineDashedOutlined } from '@music163/tango-ui';
|
import { SingleMonacoEditor, LineSolidOutlined, LineDashedOutlined } from '@music163/tango-ui';
|
||||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||||
import { ChoiceSetter } from './choice-setter';
|
import { ChoiceSetter } from './choice-setter';
|
||||||
import { TextSetter } from './text-setter';
|
|
||||||
// import { ImageSetter } from './image-setter';
|
|
||||||
|
|
||||||
function getRawCssValue(value: string) {
|
function getRawCssValue(value: string) {
|
||||||
if (value && value.startsWith('{css`')) {
|
if (value && value.startsWith('{css`')) {
|
||||||
@@ -17,12 +15,13 @@ function getRawCssValue(value: string) {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: style object setter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 不稳定,暂不推荐使用
|
* 不稳定,暂不推荐使用
|
||||||
*/
|
*/
|
||||||
export function CssCodeSetter({ value, onChange }: FormItemComponentProps<string>) {
|
export function CssCodeSetter({ value, onChange }: FormItemComponentProps<string>) {
|
||||||
const contentValue = getRawCssValue(value);
|
const contentValue = getRawCssValue(value);
|
||||||
// TODO: relatedImports
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
placement="leftBottom"
|
placement="leftBottom"
|
||||||
@@ -34,9 +33,7 @@ export function CssCodeSetter({ value, onChange }: FormItemComponentProps<string
|
|||||||
value={contentValue}
|
value={contentValue}
|
||||||
onBlur={(newCode) => {
|
onBlur={(newCode) => {
|
||||||
if (newCode != contentValue) {
|
if (newCode != contentValue) {
|
||||||
onChange(`{css\`${newCode}\`}`, {
|
onChange(`{css\`${newCode}\`}`, {});
|
||||||
// relatedImports: ['']
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -239,9 +236,10 @@ export function BgSetter({ value, onChange }: FormItemComponentProps<string>) {
|
|||||||
<Box mt="m">
|
<Box mt="m">
|
||||||
{mode === 'color' && <ColorSetter value={value} onChange={onChange} />}
|
{mode === 'color' && <ColorSetter value={value} onChange={onChange} />}
|
||||||
{mode === 'image' && (
|
{mode === 'image' && (
|
||||||
<TextSetter
|
<Input
|
||||||
value={getImageUrl(value)}
|
value={getImageUrl(value)}
|
||||||
onChange={(imgUrl) => {
|
onChange={(e) => {
|
||||||
|
const imgUrl = e.target.value;
|
||||||
onChange(imgUrl ? `url(${imgUrl})` : undefined);
|
onChange(imgUrl ? `url(${imgUrl})` : undefined);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,11 +2,15 @@ import React from 'react';
|
|||||||
import { Box } from 'coral-system';
|
import { Box } from 'coral-system';
|
||||||
import { SettingForm, FormModel, SettingFormProps } from '@music163/tango-setting-form';
|
import { SettingForm, FormModel, SettingFormProps } from '@music163/tango-setting-form';
|
||||||
import { Panel } from '@music163/tango-ui';
|
import { Panel } from '@music163/tango-ui';
|
||||||
import { clone } from '@music163/tango-helpers';
|
import { clone, parseDndId } from '@music163/tango-helpers';
|
||||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||||
import { registerBuiltinSetters } from './setters';
|
import { registerBuiltinSetters } from './setters';
|
||||||
|
|
||||||
registerBuiltinSetters();
|
let registered = false;
|
||||||
|
if (!registered) {
|
||||||
|
registerBuiltinSetters();
|
||||||
|
registered = true;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SettingPanelProps extends SettingFormProps {
|
export interface SettingPanelProps extends SettingFormProps {
|
||||||
title?: React.ReactNode;
|
title?: React.ReactNode;
|
||||||
@@ -91,6 +95,9 @@ export const SettingPanel = observer((props: SettingPanelProps) => {
|
|||||||
borderLeftColor="line2"
|
borderLeftColor="line2"
|
||||||
bg="white"
|
bg="white"
|
||||||
headerProps={headerProps}
|
headerProps={headerProps}
|
||||||
|
bodyProps={{
|
||||||
|
overflowY: 'hidden',
|
||||||
|
}}
|
||||||
className="SettingPanel"
|
className="SettingPanel"
|
||||||
>
|
>
|
||||||
{prototype ? (
|
{prototype ? (
|
||||||
@@ -100,6 +107,11 @@ export const SettingPanel = observer((props: SettingPanelProps) => {
|
|||||||
prototype={prototype}
|
prototype={prototype}
|
||||||
showIdentifier={{
|
showIdentifier={{
|
||||||
identifierKey: 'tid',
|
identifierKey: 'tid',
|
||||||
|
getIdentifier: () => {
|
||||||
|
// 直接拿当前的虚拟 id 作为 tid
|
||||||
|
const { codeId } = parseDndId(workspace.selectSource.first.id);
|
||||||
|
return codeId;
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,21 +1,29 @@
|
|||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { Box, Grid, Text } from 'coral-system';
|
import { Box, Grid, Text } from 'coral-system';
|
||||||
import styled from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
import {
|
import {
|
||||||
ComponentPrototypeType,
|
IComponentPrototype,
|
||||||
|
MenuDataType,
|
||||||
|
MenuValueType,
|
||||||
|
createContext,
|
||||||
logger,
|
logger,
|
||||||
PartialRecord,
|
|
||||||
upperCamelCase,
|
upperCamelCase,
|
||||||
} from '@music163/tango-helpers';
|
} from '@music163/tango-helpers';
|
||||||
import { CollapsePanel, IconFont, Search, Tabs } from '@music163/tango-ui';
|
import { CollapsePanel, IconFont, Search, Tabs } from '@music163/tango-ui';
|
||||||
import { observer, useWorkspace } from '@music163/tango-context';
|
import { observer, useWorkspace } from '@music163/tango-context';
|
||||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
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';
|
import { getDragGhostElement } from '../helpers';
|
||||||
|
|
||||||
type MenuKeyType = 'common' | 'atom' | 'snippet' | 'bizComp' | 'localComp';
|
type IComponentsPanelContext = {
|
||||||
type MenuValueType = Array<{ title: string; items: string[] }>;
|
isScope: boolean;
|
||||||
export type MenuDataType = PartialRecord<MenuKeyType, MenuValueType>;
|
onItemSelect: (name: string) => void;
|
||||||
|
layout: 'grid' | 'line';
|
||||||
|
};
|
||||||
|
|
||||||
|
const [ComponentsPanelProvider, usePanelContext] = createContext<IComponentsPanelContext>({
|
||||||
|
name: 'ComponentsPanelContext',
|
||||||
|
});
|
||||||
|
|
||||||
export interface ComponentsPanelProps {
|
export interface ComponentsPanelProps {
|
||||||
/**
|
/**
|
||||||
@@ -36,6 +44,26 @@ export interface ComponentsPanelProps {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
getBizCompName?: (name: string) => string;
|
getBizCompName?: (name: string) => string;
|
||||||
|
/**
|
||||||
|
* 是否局部模式 (快捷添加组件面板中使用)
|
||||||
|
*/
|
||||||
|
isScope?: boolean;
|
||||||
|
/**
|
||||||
|
* 组件选中回调
|
||||||
|
*/
|
||||||
|
onItemSelect?: (name: string) => void;
|
||||||
|
/**
|
||||||
|
* 自定义样式
|
||||||
|
*/
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
/**
|
||||||
|
* tabProps
|
||||||
|
*/
|
||||||
|
tabProps?: TabsProps;
|
||||||
|
/**
|
||||||
|
* 布局模式,默认网格布局
|
||||||
|
*/
|
||||||
|
layout?: 'grid' | 'line';
|
||||||
}
|
}
|
||||||
|
|
||||||
const localeMap = {
|
const localeMap = {
|
||||||
@@ -72,10 +100,15 @@ export function useFlatMenuData<T>(menuData: T) {
|
|||||||
|
|
||||||
export const ComponentsPanel = observer(
|
export const ComponentsPanel = observer(
|
||||||
({
|
({
|
||||||
|
isScope = false,
|
||||||
menuData = emptyMenuData,
|
menuData = emptyMenuData,
|
||||||
showBizComps = true,
|
showBizComps = true,
|
||||||
getBizCompName = upperCamelCase,
|
getBizCompName = upperCamelCase,
|
||||||
loading = false,
|
loading = false,
|
||||||
|
style,
|
||||||
|
tabProps,
|
||||||
|
onItemSelect,
|
||||||
|
layout = 'grid',
|
||||||
}: ComponentsPanelProps) => {
|
}: ComponentsPanelProps) => {
|
||||||
const [keyword, setKeyword] = useState<string>('');
|
const [keyword, setKeyword] = useState<string>('');
|
||||||
const allList = useFlatMenuData<MenuDataType>(menuData);
|
const allList = useFlatMenuData<MenuDataType>(menuData);
|
||||||
@@ -109,22 +142,44 @@ export const ComponentsPanel = observer(
|
|||||||
children: <MaterialList data={localCompData} />,
|
children: <MaterialList data={localCompData} />,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentNode =
|
const contentNode =
|
||||||
tabs.length === 1 ? (
|
tabs.length === 1 ? (
|
||||||
tabs[0].children
|
tabs[0].children
|
||||||
) : (
|
) : (
|
||||||
<Tabs centered isTabBarSticky tabBarStickyOffset={48} items={tabs} />
|
<Tabs
|
||||||
|
size={isScope ? 'small' : 'middle'}
|
||||||
|
centered
|
||||||
|
isTabBarSticky
|
||||||
|
tabBarStickyOffset={48}
|
||||||
|
items={tabs}
|
||||||
|
{...tabProps}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="ComponentsView" overflowY="auto">
|
<ComponentsPanelProvider
|
||||||
<Box px="l" py="m" position="sticky" top="0" zIndex={1} bg="white">
|
value={{
|
||||||
<Search placeholder="搜索物料" onChange={setKeyword} />
|
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>
|
</Box>
|
||||||
<Spin spinning={loading} tip="正在加载物料列表...">
|
</ComponentsPanelProvider>
|
||||||
{!keyword ? contentNode : <MaterialList data={allList} filterKeyword={keyword} />}
|
|
||||||
</Spin>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -146,6 +201,9 @@ interface MaterialListProps {
|
|||||||
|
|
||||||
function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProps) {
|
function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProps) {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
|
const { layout } = usePanelContext();
|
||||||
|
const isGrid = layout === 'grid';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="ComponentsViewList">
|
<Box className="ComponentsViewList">
|
||||||
{data.map((cate) => {
|
{data.map((cate) => {
|
||||||
@@ -166,15 +224,21 @@ function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProp
|
|||||||
title={cate.title}
|
title={cate.title}
|
||||||
borderBottom="solid"
|
borderBottom="solid"
|
||||||
borderColor="line.normal"
|
borderColor="line.normal"
|
||||||
|
showBottomBorder={false}
|
||||||
|
bodyProps={{
|
||||||
|
padding: isGrid ? '4px 12px 12px' : '0',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{!items.length && (
|
{!items.length && (
|
||||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="没有匹配到任何组件" />
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="没有匹配到任何组件" />
|
||||||
)}
|
)}
|
||||||
<Grid
|
<Grid
|
||||||
columns={type === 'localComp' ? 1 : 2}
|
templateColumns={isGrid ? 'repeat(auto-fit,minmax(55px,1fr))' : '1fr'}
|
||||||
spacing="1px"
|
spacing="1px"
|
||||||
bg="background.normal"
|
bg="background.normal"
|
||||||
padding="0"
|
padding="0"
|
||||||
|
gap={isGrid ? '12px 8px' : '0'}
|
||||||
|
backgroundColor="white"
|
||||||
>
|
>
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
const prototype = workspace.componentPrototypes.get(item);
|
const prototype = workspace.componentPrototypes.get(item);
|
||||||
@@ -194,7 +258,7 @@ function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProp
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface MaterialProps {
|
interface MaterialProps {
|
||||||
data: ComponentPrototypeType;
|
data: IComponentPrototype;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledCommonGridItem = styled.div`
|
const StyledCommonGridItem = styled.div`
|
||||||
@@ -202,9 +266,8 @@ const StyledCommonGridItem = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: start;
|
||||||
padding: 8px;
|
cursor: ${(props) => (props.draggable ? 'grab' : 'pointer')};
|
||||||
cursor: move;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--tango-colors-text-body);
|
color: var(--tango-colors-text-body);
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
@@ -213,7 +276,23 @@ const StyledCommonGridItem = styled.div`
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
.material-icon {
|
.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 {
|
.info {
|
||||||
@@ -225,19 +304,13 @@ const StyledCommonGridItem = styled.div`
|
|||||||
|
|
||||||
.anticon-question-circle {
|
.anticon-question-circle {
|
||||||
display: none;
|
display: none;
|
||||||
|
font-size: 13px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: 4px;
|
||||||
right: 8px;
|
right: 4px;
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 40px;
|
|
||||||
width: 40px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: 0 0 10px rgb(0 0 0 / 10%);
|
|
||||||
|
|
||||||
> span {
|
> span {
|
||||||
color: var(--tango-colors-brand);
|
color: var(--tango-colors-brand);
|
||||||
}
|
}
|
||||||
@@ -245,11 +318,26 @@ const StyledCommonGridItem = styled.div`
|
|||||||
.anticon-question-circle {
|
.anticon-question-circle {
|
||||||
display: inline-block;
|
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) {
|
function MaterialGrid({ data }: MaterialProps) {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
|
const { isScope, onItemSelect, layout } = usePanelContext();
|
||||||
|
|
||||||
|
const isLine = layout === 'line';
|
||||||
|
|
||||||
const handleDragStart = (e: React.DragEvent) => {
|
const handleDragStart = (e: React.DragEvent) => {
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
@@ -266,28 +354,92 @@ function MaterialGrid({ data }: MaterialProps) {
|
|||||||
workspace.dragSource.clear();
|
workspace.dragSource.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSelect = () => {
|
||||||
|
onItemSelect?.(data.name);
|
||||||
|
};
|
||||||
|
|
||||||
const icon = data.icon || 'icon-placeholder';
|
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 (
|
return (
|
||||||
<StyledCommonGridItem
|
<StyledCommonGridItem
|
||||||
draggable
|
key={data.name}
|
||||||
|
draggable={!isScope}
|
||||||
data-name={data.name}
|
data-name={data.name}
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
|
onClick={handleSelect}
|
||||||
>
|
>
|
||||||
{icon.startsWith('icon-') ? (
|
{iconNode}
|
||||||
<IconFont className="material-icon" type={data.icon || 'icon-placeholder'} />
|
<Text fontSize="12px" marginTop="4px">
|
||||||
) : (
|
{data.title ?? data.name}
|
||||||
<img src={icon} alt={data.name} />
|
|
||||||
)}
|
|
||||||
<Text fontSize="12px" lineHeight="1.5">
|
|
||||||
{data.title}
|
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="12px" color="gray.50">
|
<Text fontSize="10px" color="gray.50">
|
||||||
{data.name}
|
{data.name}
|
||||||
</Text>
|
</Text>
|
||||||
{data.docs || data.help ? (
|
{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 />
|
<QuestionCircleOutlined />
|
||||||
</Popover>
|
</Popover>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -295,7 +447,7 @@ function MaterialGrid({ data }: MaterialProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommonMaterialInfoBox({ help, docs }: ComponentPrototypeType) {
|
function CommonMaterialInfoBox({ help, docs }: Pick<IComponentPrototype, 'help' | 'docs'>) {
|
||||||
return (
|
return (
|
||||||
<Box maxWidth={300}>
|
<Box maxWidth={300}>
|
||||||
{!!help && <Box mb="m">{help}</Box>}
|
{!!help && <Box mb="m">{help}</Box>}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { observer, useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
import { observer, useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||||
import { Box } from 'coral-system';
|
import { Box } from 'coral-system';
|
||||||
import { getVariableContent } from '@music163/tango-helpers';
|
|
||||||
import { isWrappedByExpressionContainer } from '@music163/tango-core';
|
|
||||||
import { VariableTree } from '../../components';
|
import { VariableTree } from '../../components';
|
||||||
import { useSandboxQuery } from '../../context';
|
import { useSandboxQuery } from '../../context';
|
||||||
|
|
||||||
@@ -10,10 +8,6 @@ import { useSandboxQuery } from '../../context';
|
|||||||
export function shapeServiceValues(val: any) {
|
export function shapeServiceValues(val: any) {
|
||||||
const shapeValues = { ...val };
|
const shapeValues = { ...val };
|
||||||
delete shapeValues.type;
|
delete shapeValues.type;
|
||||||
// 兼容旧版,如果 formatter 包裹了 {} 则删掉首尾
|
|
||||||
if (shapeValues.formatter && isWrappedByExpressionContainer(shapeValues.formatter)) {
|
|
||||||
shapeValues.formatter = getVariableContent(shapeValues.formatter);
|
|
||||||
}
|
|
||||||
return shapeValues;
|
return shapeValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
Popconfirm,
|
Popconfirm,
|
||||||
Radio,
|
Radio,
|
||||||
message,
|
message,
|
||||||
Tag,
|
|
||||||
Checkbox,
|
Checkbox,
|
||||||
ModalProps,
|
ModalProps,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
@@ -18,7 +17,7 @@ import { FormListProps } from 'antd/lib/form';
|
|||||||
import { Box, Text } from 'coral-system';
|
import { Box, Text } from 'coral-system';
|
||||||
import semverLt from 'semver/functions/lt';
|
import semverLt from 'semver/functions/lt';
|
||||||
import semverValid from 'semver/functions/valid';
|
import semverValid from 'semver/functions/valid';
|
||||||
import { ConfigGroup, ConfigItem } from '@music163/tango-ui';
|
import { ColorTag, ConfigGroup, ConfigItem } from '@music163/tango-ui';
|
||||||
import { MinusCircleOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
import { MinusCircleOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
||||||
import { useBoolean } from '@music163/tango-helpers';
|
import { useBoolean } from '@music163/tango-helpers';
|
||||||
import { isUndefined } from 'lodash-es';
|
import { isUndefined } from 'lodash-es';
|
||||||
@@ -101,8 +100,8 @@ export const DependencyPanel = observer(
|
|||||||
/>
|
/>
|
||||||
<ConfigGroup
|
<ConfigGroup
|
||||||
title={
|
title={
|
||||||
<>
|
<div>
|
||||||
基础包
|
基础包
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
<>
|
<>
|
||||||
@@ -115,7 +114,7 @@ export const DependencyPanel = observer(
|
|||||||
>
|
>
|
||||||
<QuestionCircleOutlined />
|
<QuestionCircleOutlined />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>
|
</div>
|
||||||
}
|
}
|
||||||
key={DependencyItemType.基础包}
|
key={DependencyItemType.基础包}
|
||||||
>
|
>
|
||||||
@@ -332,25 +331,19 @@ function RenderItem({
|
|||||||
const packageJsonIncluded = !!dependencies?.[name];
|
const packageJsonIncluded = !!dependencies?.[name];
|
||||||
const tangoConfigJsonIncluded = !!packageInfo;
|
const tangoConfigJsonIncluded = !!packageInfo;
|
||||||
|
|
||||||
return [
|
return (
|
||||||
hasUmd && (
|
<Box display="inline-block">
|
||||||
<Tooltip title="该依赖拥有 UMD 资源,可优化加载速度">
|
{hasUmd && (
|
||||||
<Tag key="hasUmd" color="green">
|
<ColorTag key="hasUmd" color="green" tooltip="该依赖拥有 UMD 资源,可优化加载速度">
|
||||||
UMD
|
UMD
|
||||||
</Tag>
|
</ColorTag>
|
||||||
</Tooltip>
|
)}
|
||||||
),
|
{packageJsonIncluded && <ColorTag tooltip="此依赖在 package.json 中存在">package</ColorTag>}
|
||||||
packageJsonIncluded && (
|
{tangoConfigJsonIncluded && (
|
||||||
<Tooltip title="此依赖在 package.json 中存在">
|
<ColorTag tooltip="此依赖在 tango.config.json 中存在">tango.config</ColorTag>
|
||||||
<Tag key="packageJson">package</Tag>
|
)}
|
||||||
</Tooltip>
|
</Box>
|
||||||
),
|
);
|
||||||
tangoConfigJsonIncluded && (
|
|
||||||
<Tooltip title="此依赖在 tango.config.json 中存在">
|
|
||||||
<Tag key="tangoConfigJson">tango.config</Tag>
|
|
||||||
</Tooltip>
|
|
||||||
),
|
|
||||||
].filter((e) => e);
|
|
||||||
}, [showTags, workspace, record]);
|
}, [showTags, workspace, record]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { DropMethod, ITangoViewNodeData } from '@music163/tango-core';
|
|||||||
import { noop, parseDndId } from '@music163/tango-helpers';
|
import { noop, parseDndId } from '@music163/tango-helpers';
|
||||||
import { useSandboxQuery } from '../../context';
|
import { useSandboxQuery } from '../../context';
|
||||||
import { buildQueryBySlotId } from '../../helpers';
|
import { buildQueryBySlotId } from '../../helpers';
|
||||||
|
import { ContextMenu } from '../../components';
|
||||||
|
|
||||||
export interface ComponentsTreeProps {
|
export interface ComponentsTreeProps {
|
||||||
/**
|
/**
|
||||||
@@ -81,8 +82,8 @@ const OutlineTreeNode: React.FC<{ node: ITangoViewNodeData } & ComponentsTreePro
|
|||||||
const sandboxQuery = useSandboxQuery();
|
const sandboxQuery = useSandboxQuery();
|
||||||
const [visible, setVisible] = useState(true);
|
const [visible, setVisible] = useState(true);
|
||||||
const nodeLabel = (() => {
|
const nodeLabel = (() => {
|
||||||
const { index } = parseDndId(node.id);
|
const { codeId } = parseDndId(node.id);
|
||||||
return index;
|
return codeId;
|
||||||
})();
|
})();
|
||||||
const componentPrototype = workspace.componentPrototypes.get(node.component);
|
const componentPrototype = workspace.componentPrototypes.get(node.component);
|
||||||
const icon = componentPrototype?.icon || 'icon-placeholder';
|
const icon = componentPrototype?.icon || 'icon-placeholder';
|
||||||
@@ -143,6 +144,18 @@ export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
|
|||||||
const file = workspace.activeViewModule;
|
const file = workspace.activeViewModule;
|
||||||
const nodesTree = (file?.nodesTree ?? []) as ITangoViewNodeData[];
|
const nodesTree = (file?.nodesTree ?? []) as ITangoViewNodeData[];
|
||||||
const [expandedKeys, setExpandedKeys] = useState(getNodeKeys(nodesTree));
|
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(() => {
|
useEffect(() => {
|
||||||
setSelectedKeys(workspace.selectSource.selected.map((item) => item.id));
|
setSelectedKeys(workspace.selectSource.selected.map((item) => item.id));
|
||||||
@@ -164,69 +177,83 @@ export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box css={outlineStyle}>
|
<Box css={outlineStyle}>
|
||||||
<Tree
|
<Dropdown
|
||||||
selectedKeys={selectedKeys as string[]}
|
placement="bottomLeft"
|
||||||
fieldNames={filedNames}
|
trigger={['contextMenu']}
|
||||||
treeData={nodesTree as any[]}
|
open={contextMenuOpen}
|
||||||
onSelect={(keys) => {
|
onOpenChange={(val) => {
|
||||||
const slotKey = keys?.[0] as string;
|
if (!val) {
|
||||||
const data = sandboxQuery.getDraggableParentsData(buildQueryBySlotId(slotKey), true);
|
setContextMenuOpen(val);
|
||||||
if (data && data.id) {
|
}
|
||||||
workspace.selectSource.select({
|
}}
|
||||||
id: data.id,
|
overlay={<ContextMenu onClick={() => setContextMenuOpen(false)} showParents={false} />}
|
||||||
name: data.name,
|
>
|
||||||
bounding: data.bounding,
|
<Tree
|
||||||
parents: data.parents,
|
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,
|
||||||
});
|
});
|
||||||
}
|
}}
|
||||||
// export selected
|
onDrop={(data) => {
|
||||||
onSelect(slotKey);
|
const dropKey = data.node.key as string;
|
||||||
setSelectedKeys(keys);
|
let method;
|
||||||
}}
|
if (data.dropToGap) {
|
||||||
blockNode
|
// 插入节点的后面
|
||||||
expandedKeys={expandedKeys}
|
method = DropMethod.InsertAfter;
|
||||||
onExpand={(keys) => setExpandedKeys(keys as string[])}
|
} else {
|
||||||
draggable
|
// 作为第一个子节点
|
||||||
onDragStart={(data) => {
|
method = DropMethod.InsertFirstChild;
|
||||||
const prototype = workspace.componentPrototypes.get(data.node.component);
|
}
|
||||||
if (!prototype) {
|
workspace.dragSource.dropTarget.set(
|
||||||
return;
|
{
|
||||||
}
|
id: dropKey,
|
||||||
const { canDrag } = prototype.rules || {};
|
},
|
||||||
if (canDrag && !canDrag()) {
|
method,
|
||||||
return;
|
);
|
||||||
}
|
workspace.dropNode();
|
||||||
workspace.dragSource.set({
|
}}
|
||||||
id: data.node.id,
|
onRightClick={({ event, node }) => {
|
||||||
name: data.node.component,
|
// 按下其他按键时,视为用户有特殊操作,此时不展示右键菜单
|
||||||
});
|
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) {
|
||||||
}}
|
setContextMenuOpen(false);
|
||||||
onDrop={(data) => {
|
return;
|
||||||
const dropKey = data.node.key as string;
|
}
|
||||||
let method;
|
// 顶层的 Page 组件不展示右键菜单
|
||||||
if (data.dropToGap) {
|
if (node.component === 'Page') {
|
||||||
// 插入节点的后面
|
setContextMenuOpen(false);
|
||||||
method = DropMethod.InsertAfter;
|
return;
|
||||||
} else {
|
}
|
||||||
// 作为第一个子节点
|
event.preventDefault();
|
||||||
method = DropMethod.InsertFirstChild;
|
// 由于部分操作是基于 selectSource 实现的,因此右键时默认选中当前项
|
||||||
}
|
handleSelect([node.key]);
|
||||||
workspace.dragSource.dropTarget.set(
|
setContextMenuOpen(true);
|
||||||
{
|
}}
|
||||||
id: dropKey,
|
titleRender={(node) => (
|
||||||
},
|
<OutlineTreeNode
|
||||||
method,
|
actionItems={actionItems}
|
||||||
);
|
showToggleVisibleIcon={showToggleVisibleIcon}
|
||||||
workspace.dropNode();
|
node={node}
|
||||||
}}
|
/>
|
||||||
titleRender={(node) => (
|
)}
|
||||||
<OutlineTreeNode
|
/>
|
||||||
actionItems={actionItems}
|
</Dropdown>
|
||||||
showToggleVisibleIcon={showToggleVisibleIcon}
|
|
||||||
node={node}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -23,11 +23,7 @@ export function OutlinePanel({ showStateView = true, ...treeProps }: OutlineView
|
|||||||
>
|
>
|
||||||
<ComponentsTree {...treeProps} />
|
<ComponentsTree {...treeProps} />
|
||||||
</CollapsePanel>
|
</CollapsePanel>
|
||||||
{showStateView && (
|
{showStateView && <StateTree />}
|
||||||
<CollapsePanel title="页面状态" stickyHeader maxHeight="90%" overflowY="auto">
|
|
||||||
<StateTree />
|
|
||||||
</CollapsePanel>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useEffect, useReducer } from 'react';
|
import React, { useEffect, useReducer } from 'react';
|
||||||
import { observer } from '@music163/tango-context';
|
import { observer } from '@music163/tango-context';
|
||||||
import { isFunction, isObject, pick } from '@music163/tango-helpers';
|
import { isFunction, isObject, pick } from '@music163/tango-helpers';
|
||||||
import { JsonView } from '@music163/tango-ui';
|
import { CollapsePanel, IconButton, JsonView } from '@music163/tango-ui';
|
||||||
|
import { ReloadOutlined } from '@ant-design/icons';
|
||||||
import { useSandboxQuery } from '../../context';
|
import { useSandboxQuery } from '../../context';
|
||||||
|
|
||||||
function processTango(obj: any = {}, index = 0) {
|
function processTango(obj: any = {}, index = 0) {
|
||||||
@@ -26,7 +27,7 @@ export const StateTree = observer(() => {
|
|||||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||||
const tangoContext = pick(sandboxQuery.window['tango'] || {}, [
|
const tangoContext = pick(sandboxQuery.window['tango'] || {}, [
|
||||||
'stores',
|
'stores',
|
||||||
'page',
|
'pageStore',
|
||||||
'services',
|
'services',
|
||||||
'config',
|
'config',
|
||||||
]);
|
]);
|
||||||
@@ -34,6 +35,7 @@ export const StateTree = observer(() => {
|
|||||||
forceUpdate();
|
forceUpdate();
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// FIXME: 优化这段代码,应该监听 tangoContext 的变化,来刷新
|
||||||
const settingForm = document.querySelector('.SettingPanel');
|
const settingForm = document.querySelector('.SettingPanel');
|
||||||
const ob = new MutationObserver(callback);
|
const ob = new MutationObserver(callback);
|
||||||
if (settingForm) {
|
if (settingForm) {
|
||||||
@@ -48,13 +50,31 @@ export const StateTree = observer(() => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<JsonView
|
<CollapsePanel
|
||||||
src={processTango(tangoContext)}
|
title="页面状态"
|
||||||
collapsed={1}
|
stickyHeader
|
||||||
name="tango"
|
maxHeight="90%"
|
||||||
displayObjectSize={false}
|
overflowY="auto"
|
||||||
indentWidth={2}
|
extra={
|
||||||
enableCopy
|
<IconButton
|
||||||
/>
|
tooltip="同步状态"
|
||||||
|
size="small"
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
forceUpdate();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<JsonView
|
||||||
|
src={processTango(tangoContext)}
|
||||||
|
collapsed={1}
|
||||||
|
name="tango"
|
||||||
|
displayObjectSize={false}
|
||||||
|
indentWidth={2}
|
||||||
|
enableCopy
|
||||||
|
/>
|
||||||
|
</CollapsePanel>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export interface SidebarPanelItemProps
|
|||||||
widgetProps?: object;
|
widgetProps?: object;
|
||||||
}
|
}
|
||||||
|
|
||||||
function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 280, footer, children }: SidebarProps) {
|
function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 266, footer, children }: SidebarProps) {
|
||||||
const designer = useDesigner();
|
const designer = useDesigner();
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const VariablePanel = observer(
|
|||||||
新建模型
|
新建模型
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
bodyProps={{ p: 'm' }}
|
bodyProps={{ p: 'm', height: '100%' }}
|
||||||
>
|
>
|
||||||
{isAdd ? (
|
{isAdd ? (
|
||||||
<AddStoreForm
|
<AddStoreForm
|
||||||
@@ -42,6 +42,7 @@ export const VariablePanel = observer(
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<VariableTree
|
<VariableTree
|
||||||
|
height="100%"
|
||||||
defaultValueDetailMode="define"
|
defaultValueDetailMode="define"
|
||||||
dataSource={storeVariables}
|
dataSource={storeVariables}
|
||||||
onAddStoreVariable={(storeName, data) => {
|
onAddStoreVariable={(storeName, data) => {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled, { css, keyframes } from 'styled-components';
|
import styled, { css, keyframes } from 'styled-components';
|
||||||
import { Box, Button, Group, HTMLCoralProps } from 'coral-system';
|
import { Box, Button, Group, HTMLCoralProps } from 'coral-system';
|
||||||
import { Dropdown, DropdownProps } from 'antd';
|
import { Tooltip } from 'antd';
|
||||||
import { PlusSquareOutlined, HolderOutlined, InfoCircleOutlined } from '@ant-design/icons';
|
import { HolderOutlined, InfoCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||||
import { ISelectedItemData, isString, noop } from '@music163/tango-helpers';
|
import { ISelectedItemData, isString, noop } from '@music163/tango-helpers';
|
||||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||||
import { Action, IconFont } from '@music163/tango-ui';
|
|
||||||
import { getDragGhostElement } from '../helpers';
|
import { getDragGhostElement } from '../helpers';
|
||||||
import { getWidget } from '../widgets';
|
import { getWidget } from '../widgets';
|
||||||
|
import { ComponentsPopover } from '../components';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 选择辅助工具的对齐方式
|
* 选择辅助工具的对齐方式
|
||||||
@@ -24,7 +24,15 @@ export interface SelectionToolsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SelectionTools = observer(
|
export const SelectionTools = observer(
|
||||||
({ actions: actionsProp = ['viewSource', 'copyNode', 'deleteNode'] }: SelectionToolsProps) => {
|
({
|
||||||
|
actions: actionsProp = [
|
||||||
|
'selectParentNode',
|
||||||
|
'viewSource',
|
||||||
|
'copyNode',
|
||||||
|
'deleteNode',
|
||||||
|
'moreActions',
|
||||||
|
],
|
||||||
|
}: SelectionToolsProps) => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
const selectSource = workspace.selectSource;
|
const selectSource = workspace.selectSource;
|
||||||
const actions = actionsProp.map((item) => {
|
const actions = actionsProp.map((item) => {
|
||||||
@@ -59,12 +67,25 @@ const selectionBoxStyle = css`
|
|||||||
top: 0;
|
top: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface IInsertedData {
|
const topAddSiblingBtnStyle = css`
|
||||||
name: string;
|
position: absolute;
|
||||||
label: string;
|
left: 50%;
|
||||||
icon: string;
|
transform: translate(-50%, -50%);
|
||||||
description: string;
|
top: 0;
|
||||||
}
|
border-radius: 32px;
|
||||||
|
z-index: 999;
|
||||||
|
pointer-events: auto;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const bottomAddSiblingBtnStyle = css`
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 50%);
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 32px;
|
||||||
|
z-index: 999;
|
||||||
|
pointer-events: auto;
|
||||||
|
`;
|
||||||
|
|
||||||
export interface SelectionBoxProps {
|
export interface SelectionBoxProps {
|
||||||
/**
|
/**
|
||||||
@@ -97,23 +118,6 @@ function SelectionBox({ showActions, actions, data }: SelectionBoxProps) {
|
|||||||
const prototype = workspace.componentPrototypes.get(data.name);
|
const prototype = workspace.componentPrototypes.get(data.name);
|
||||||
const isPage = prototype?.type === 'page';
|
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 selectionHelpersAlign: SelectionHelperAlignType = 'top-right';
|
let selectionHelpersAlign: SelectionHelperAlignType = 'top-right';
|
||||||
if (data.bounding) {
|
if (data.bounding) {
|
||||||
if (data.bounding.left + data.bounding.width + boundingOffset < designer.viewport.width) {
|
if (data.bounding.left + data.bounding.width + boundingOffset < designer.viewport.width) {
|
||||||
@@ -126,6 +130,7 @@ function SelectionBox({ showActions, actions, data }: SelectionBoxProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isFromCurrentFile = data.filename === workspace.activeViewFile;
|
const isFromCurrentFile = data.filename === workspace.activeViewFile;
|
||||||
|
const selectedNodeName = workspace.selectSource?.selected?.[0]?.codeId ?? '未选中';
|
||||||
|
|
||||||
let style: React.CSSProperties;
|
let style: React.CSSProperties;
|
||||||
if (data.bounding) {
|
if (data.bounding) {
|
||||||
@@ -150,6 +155,18 @@ function SelectionBox({ showActions, actions, data }: SelectionBoxProps) {
|
|||||||
css={selectionBoxStyle}
|
css={selectionBoxStyle}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
|
<>
|
||||||
|
<ComponentsPopover type="before">
|
||||||
|
<Tooltip title={`在 ${selectedNodeName} 的前方添加兄弟节点`}>
|
||||||
|
<SelectionHelper icon={<PlusOutlined />} css={topAddSiblingBtnStyle} />
|
||||||
|
</Tooltip>
|
||||||
|
</ComponentsPopover>
|
||||||
|
<ComponentsPopover type="after">
|
||||||
|
<Tooltip title={`在 ${selectedNodeName} 的后方添加兄弟节点`}>
|
||||||
|
<SelectionHelper icon={<PlusOutlined />} css={bottomAddSiblingBtnStyle} />
|
||||||
|
</Tooltip>
|
||||||
|
</ComponentsPopover>
|
||||||
|
</>
|
||||||
{showActions && (
|
{showActions && (
|
||||||
<SelectionHelpers align={selectionHelpersAlign}>
|
<SelectionHelpers align={selectionHelpersAlign}>
|
||||||
<SelectionHelper
|
<SelectionHelper
|
||||||
@@ -179,27 +196,20 @@ function SelectionBox({ showActions, actions, data }: SelectionBoxProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{!isFromCurrentFile && (
|
{!isFromCurrentFile && (
|
||||||
<Action
|
<Tooltip title={`该节点来自其他文件(${data.filename})`}>
|
||||||
as="span"
|
<InfoCircleOutlined />
|
||||||
color="white"
|
</Tooltip>
|
||||||
tooltip={`该节点来自其他文件(${data.filename})`}
|
|
||||||
icon={<InfoCircleOutlined />}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<SelectionToolSet>{!isPage && actions}</SelectionToolSet>
|
<SelectionToolSet>{!isPage && actions}</SelectionToolSet>
|
||||||
{insertedList.length > 0 && (
|
{isFromCurrentFile && prototype?.hasChildren !== false && (
|
||||||
<InsertedDropdown
|
<ComponentsPopover>
|
||||||
options={insertedList}
|
<Tooltip title="快捷添加子元素">
|
||||||
onSelect={(name) => {
|
<SelectionHelper icon={<PlusOutlined />} />
|
||||||
workspace.insertToSelectedNode(name);
|
</Tooltip>
|
||||||
}}
|
</ComponentsPopover>
|
||||||
>
|
|
||||||
<SelectionHelper icon={<PlusSquareOutlined />} />
|
|
||||||
</InsertedDropdown>
|
|
||||||
)}
|
)}
|
||||||
</SelectionHelpers>
|
</SelectionHelpers>
|
||||||
)}
|
)}
|
||||||
@@ -248,10 +258,6 @@ const selectionHelperStyle = css`
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: var(--color-active, var(--tango-colors-brand));
|
background: var(--color-active, var(--tango-colors-brand));
|
||||||
|
|
||||||
svg {
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--color-active-hover, var(--tango-colors-primary-50));
|
background: var(--color-active-hover, var(--tango-colors-primary-50));
|
||||||
}
|
}
|
||||||
@@ -262,7 +268,13 @@ interface SelectionHelperProps extends HTMLCoralProps<'button'> {
|
|||||||
label?: React.ReactNode;
|
label?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SelectionHelper = ({ icon, label, children, ...rest }: SelectionHelperProps) => {
|
const SelectionHelper = ({
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
css: customCss,
|
||||||
|
...rest
|
||||||
|
}: SelectionHelperProps) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
position="relative"
|
position="relative"
|
||||||
@@ -272,7 +284,7 @@ const SelectionHelper = ({ icon, label, children, ...rest }: SelectionHelperProp
|
|||||||
border="0"
|
border="0"
|
||||||
borderRadius="s"
|
borderRadius="s"
|
||||||
fontSize="12px"
|
fontSize="12px"
|
||||||
css={selectionHelperStyle}
|
css={[selectionHelperStyle, customCss]}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
@@ -370,83 +382,3 @@ const NameSelector = ({ label, parents = [], onSelect = noop }: NameSelectorProp
|
|||||||
</NameSelectorWrapper>
|
</NameSelectorWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface InsertedDropdownProps extends DropdownProps {
|
|
||||||
options?: IInsertedData[];
|
|
||||||
onSelect?: (name: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function InsertedDropdown({ options = [], onSelect, ...props }: InsertedDropdownProps) {
|
|
||||||
return (
|
|
||||||
<Dropdown
|
|
||||||
trigger={['click']}
|
|
||||||
dropdownRender={() => {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
bg="#FFF"
|
|
||||||
borderRadius="m"
|
|
||||||
boxShadow="lowDown"
|
|
||||||
border="solid"
|
|
||||||
borderColor="line2"
|
|
||||||
overflow="hidden"
|
|
||||||
>
|
|
||||||
<Box px="l" py="m" color="text2">
|
|
||||||
为当前节点添加子元素
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
{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;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--tango-colors-fill2);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
function InsertedItem({
|
|
||||||
label,
|
|
||||||
icon = 'icon-placeholder',
|
|
||||||
description,
|
|
||||||
}: HTMLCoralProps<'div'> & Omit<IInsertedData, 'name'>) {
|
|
||||||
return (
|
|
||||||
<Box display="flex" columnGap="m" px="l" py="m" fontSize="12px" css={insertedItemStyle}>
|
|
||||||
<Box
|
|
||||||
size="35px"
|
|
||||||
fontSize="32px"
|
|
||||||
background="fill1"
|
|
||||||
border="1px solid"
|
|
||||||
borderColor="line2"
|
|
||||||
display="flex"
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
>
|
|
||||||
{icon.startsWith('icon-') ? (
|
|
||||||
<IconFont className="material-icon" type={icon} />
|
|
||||||
) : (
|
|
||||||
<img src={icon} alt={label} />
|
|
||||||
)}
|
|
||||||
</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 React, { useEffect, useRef } from 'react';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { Box, HTMLCoralProps } from 'coral-system';
|
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 { Ghost } from './ghost';
|
||||||
import { useSandboxQuery } from '../context';
|
import { useSandboxQuery } from '../context';
|
||||||
import { SelectionTools, SelectionToolsProps } from './selection';
|
import { SelectionTools, SelectionToolsProps } from './selection';
|
||||||
import { InsertionPrompt } from './insertion';
|
import { InsertionPrompt } from './insertion';
|
||||||
import { DraggingMask } from './mask';
|
import { DraggingMask } from './mask';
|
||||||
|
import { Dropdown, DropdownProps } from 'antd';
|
||||||
|
import { ContextMenu } from '../components';
|
||||||
|
|
||||||
export interface ViewportProps extends HTMLCoralProps<'div'> {
|
export interface ViewportProps extends HTMLCoralProps<'div'> {
|
||||||
selectionTools?: SelectionToolsProps['actions'];
|
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) {
|
export function Viewport({ selectionTools, children, className, ...rest }: ViewportProps) {
|
||||||
return (
|
return (
|
||||||
<Box position="relative" height="100%" className={cx('Viewport', className)} {...rest}>
|
<Box position="relative" height="100%" className={cx('Viewport', className)} {...rest}>
|
||||||
|
<ViewportContextMenu />
|
||||||
<ViewportBody>{children}</ViewportBody>
|
<ViewportBody>{children}</ViewportBody>
|
||||||
<div className="AuxTools">
|
<div className="AuxTools">
|
||||||
<SelectionTools actions={selectionTools} />
|
<SelectionTools actions={selectionTools} />
|
||||||
|
|||||||
@@ -1,13 +1,25 @@
|
|||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Group } from 'coral-system';
|
import { Group } from 'coral-system';
|
||||||
import { Modal } from 'antd';
|
import { Modal } from 'antd';
|
||||||
import { ToggleButton, CodeOutlined } from '@music163/tango-ui';
|
import { ToggleButton, CodeOutlined, DualOutlined } from '@music163/tango-ui';
|
||||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||||
import { BorderOutlined } from '@ant-design/icons';
|
import { BorderOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
export const ModeSwitchTool = observer(() => {
|
export const ModeSwitchTool = observer(() => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
const designer = useDesigner();
|
const designer = useDesigner();
|
||||||
|
|
||||||
|
const activeFileCheck = useCallback(() => {
|
||||||
|
if (workspace.activeFile !== workspace.activeViewFile) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '当前打开的文件与视图不匹配,是否切换到当前视图对应的文件?',
|
||||||
|
onOk: () => {
|
||||||
|
workspace.setActiveFile(workspace.activeViewFile);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [workspace]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group attached>
|
<Group attached>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
@@ -26,19 +38,24 @@ export const ModeSwitchTool = observer(() => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
designer.setActiveView('code'); // 切换到源码视图
|
designer.setActiveView('code'); // 切换到源码视图
|
||||||
designer.setActiveSidebarPanel(''); // 关闭左侧面板
|
designer.setActiveSidebarPanel(''); // 关闭左侧面板
|
||||||
if (workspace.activeFile !== workspace.activeViewFile) {
|
activeFileCheck();
|
||||||
Modal.confirm({
|
|
||||||
title: '当前打开的文件与视图不匹配,是否切换到当前视图对应的文件?',
|
|
||||||
onOk: () => {
|
|
||||||
workspace.setActiveFile(workspace.activeViewFile);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
tooltip="源码视图"
|
tooltip="源码视图"
|
||||||
>
|
>
|
||||||
<CodeOutlined />
|
<CodeOutlined />
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
|
<ToggleButton
|
||||||
|
shape="ghost"
|
||||||
|
selected={designer.activeView === 'dual'}
|
||||||
|
onClick={() => {
|
||||||
|
designer.setActiveView('dual'); // 切换到双屏视图
|
||||||
|
designer.setActiveSidebarPanel(''); // 关闭左侧面板
|
||||||
|
activeFileCheck();
|
||||||
|
}}
|
||||||
|
tooltip="双屏视图"
|
||||||
|
>
|
||||||
|
<DualOutlined />
|
||||||
|
</ToggleButton>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ export const PreviewTool = observer(() => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
const nextIsPreview = !designer.isPreview;
|
const nextIsPreview = !designer.isPreview;
|
||||||
designer.toggleIsPreview(nextIsPreview);
|
designer.toggleIsPreview(nextIsPreview);
|
||||||
if (nextIsPreview) {
|
if (nextIsPreview && designer.activeView === 'code') {
|
||||||
designer.setActiveView('design');
|
designer.setActiveView('design');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
tooltip="预览"
|
tooltip={designer.isPreview ? '切换到设计模式' : '切换到预览模式'}
|
||||||
>
|
>
|
||||||
<EyeOutlined />
|
<EyeOutlined />
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export function Toolbar({ children }: ToolbarProps) {
|
|||||||
<Group display="flex" alignItems="center" gap="m">
|
<Group display="flex" alignItems="center" gap="m">
|
||||||
{centerTools}
|
{centerTools}
|
||||||
</Group>
|
</Group>
|
||||||
<Group display="flex" alignItems="center" gap="m">
|
<Group display="flex" alignItems="center" justifyContent="flex-end" gap="m">
|
||||||
{rightTools}
|
{rightTools}
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -13,14 +13,26 @@ import {
|
|||||||
OutlinePanel,
|
OutlinePanel,
|
||||||
VariablePanel,
|
VariablePanel,
|
||||||
} from './sidebar';
|
} 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 = {};
|
const widgets = {};
|
||||||
|
|
||||||
export function registerWidget(key: string, component: React.ComponentType<any>) {
|
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(
|
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);
|
setValue(widgets, key, component);
|
||||||
@@ -48,4 +60,11 @@ registerWidget('sidebar.dataSource', DataSourcePanel);
|
|||||||
|
|
||||||
registerWidget('selectionMenu.copyNode', CopyNodeAction);
|
registerWidget('selectionMenu.copyNode', CopyNodeAction);
|
||||||
registerWidget('selectionMenu.deleteNode', DeleteNodeAction);
|
registerWidget('selectionMenu.deleteNode', DeleteNodeAction);
|
||||||
|
registerWidget('selectionMenu.selectParentNode', SelectParentNodeAction);
|
||||||
registerWidget('selectionMenu.viewSource', ViewSourceAction);
|
registerWidget('selectionMenu.viewSource', ViewSourceAction);
|
||||||
|
registerWidget('selectionMenu.moreActions', MoreActionsAction);
|
||||||
|
|
||||||
|
registerWidget('contextMenu.viewSource', ViewSourceContextAction);
|
||||||
|
registerWidget('contextMenu.copyNode', CopyNodeContextAction);
|
||||||
|
registerWidget('contextMenu.pasteNode', PasteNodeContextAction);
|
||||||
|
registerWidget('contextMenu.deleteNode', DeleteNodeContextAction);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export function WorkspacePanel({ children }: WorkspacePanelProps) {
|
|||||||
<Box
|
<Box
|
||||||
flex="1"
|
flex="1"
|
||||||
display="flex"
|
display="flex"
|
||||||
flexDirection="column"
|
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
bg="colors.custom.viewportBg"
|
bg="colors.custom.viewportBg"
|
||||||
position="relative"
|
position="relative"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { Box } from 'coral-system';
|
import { Box } from 'coral-system';
|
||||||
import { observer, useDesigner } from '@music163/tango-context';
|
import { observer, useDesigner } from '@music163/tango-context';
|
||||||
import { DesignerViewType } from '@music163/tango-core';
|
import { DesignerViewType } from '@music163/tango-core';
|
||||||
|
import { ComponentsPopover } from './components';
|
||||||
|
|
||||||
export interface WorkspaceViewProps {
|
export interface WorkspaceViewProps {
|
||||||
/**
|
/**
|
||||||
@@ -21,12 +22,14 @@ export const WorkspaceView = observer((props: WorkspaceViewProps) => {
|
|||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
className={`ViewPanel ${mode}`}
|
className={`ViewPanel ${mode}`}
|
||||||
display={display}
|
display={designer.activeView === 'dual' ? 'block' : display}
|
||||||
flex="1"
|
flex="1"
|
||||||
overflow={overflow}
|
overflow={overflow}
|
||||||
position="relative"
|
position="relative"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
{/* 添加组件弹层 */}
|
||||||
|
{display === 'block' && <ComponentsPopover type="inner" isControlled />}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,54 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [1.2.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
|
||||||
|
|
||||||
|
- parse module with module alias ([#147](https://github.com/netease/tango/issues/147)) ([56d0877](https://github.com/netease/tango/commit/56d0877507b0138877eac6f36db288147b43c7d9))
|
||||||
|
- support quick set tid in SettingForm ([#151](https://github.com/netease/tango/issues/151)) ([1fc22a4](https://github.com/netease/tango/commit/1fc22a4535a8bdc12a018a41ebd5ab908fc46817))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- refactor parse attribute value ([#149](https://github.com/netease/tango/issues/149)) ([ffaa276](https://github.com/netease/tango/commit/ffaa276b5c205ed962d37e2fdb358a703f8fad01))
|
||||||
|
|
||||||
|
# [1.0.0-alpha.7](https://github.com/netease/tango/compare/@music163/tango-helpers@1.0.0-alpha.6...@music163/tango-helpers@1.0.0-alpha.7) (2024-04-22)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- allow reload state tree & display pageStore in variable tree ([#135](https://github.com/netease/tango/issues/135)) ([2664613](https://github.com/netease/tango/commit/2664613ab263aead2a5239fa012454fc7fd5ff99))
|
||||||
|
|
||||||
|
# [1.0.0-alpha.6](https://github.com/netease/tango/compare/@music163/tango-helpers@1.0.0-alpha.5...@music163/tango-helpers@1.0.0-alpha.6) (2024-04-09)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- check is in design mode ([cb6aabf](https://github.com/netease/tango/commit/cb6aabf6ce45a33ba9055c45734f7db0fba540ec))
|
||||||
|
- update setters and use tabOptions to filter props ([#129](https://github.com/netease/tango/issues/129)) ([93608d1](https://github.com/netease/tango/commit/93608d1037327afa4f755976b86427b6128ae3d0))
|
||||||
|
|
||||||
|
# [1.0.0-alpha.5](https://github.com/netease/tango/compare/@music163/tango-helpers@1.0.0-alpha.4...@music163/tango-helpers@1.0.0-alpha.5) (2024-03-26)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- quick add sibling nodes ([#127](https://github.com/netease/tango/issues/127)) ([9ed6a7d](https://github.com/netease/tango/commit/9ed6a7d1a4944d69d96e034f243b61531862e317))
|
||||||
|
|
||||||
|
# [1.0.0-alpha.4](https://github.com/netease/tango/compare/@music163/tango-helpers@1.0.0-alpha.3...@music163/tango-helpers@1.0.0-alpha.4) (2024-03-18)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- add deprecated and data group to ComponentPropType ([#116](https://github.com/netease/tango/issues/116)) ([9f7441c](https://github.com/netease/tango/commit/9f7441c13400dba55c58b7dd6ce9429013edbc45))
|
||||||
|
|
||||||
# [1.0.0-alpha.3](https://github.com/netease/tango/compare/@music163/tango-helpers@1.0.0-alpha.2...@music163/tango-helpers@1.0.0-alpha.3) (2024-03-18)
|
# [1.0.0-alpha.3](https://github.com/netease/tango/compare/@music163/tango-helpers@1.0.0-alpha.2...@music163/tango-helpers@1.0.0-alpha.3) (2024-03-18)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@music163/tango-helpers",
|
"name": "@music163/tango-helpers",
|
||||||
"version": "1.0.0-alpha.3",
|
"version": "1.2.0",
|
||||||
"description": "Shared types, helpers, and hooks of tango-apps",
|
"description": "Shared types, helpers, and hooks of tango-apps",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"shared",
|
"shared",
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export function isNil(val: any) {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function isStoreVariablePath(key: string) {
|
export function isStoreVariablePath(key: string) {
|
||||||
return /^stores\.[a-zA-Z0-9]+\.\w+$/.test(key);
|
return key === 'pageStore' || /^stores\.[a-zA-Z0-9]+\.\w+$/.test(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,5 +66,12 @@ export function isServiceVariablePath(key: string) {
|
|||||||
* 是否在 Tango 设计模式
|
* 是否在 Tango 设计模式
|
||||||
*/
|
*/
|
||||||
export function isInTangoDesignMode() {
|
export function isInTangoDesignMode() {
|
||||||
return '__TANGO_DESIGNER__' in window;
|
return !!(window as any).__TANGO_DESIGNER__;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否是 macOS 或 iOS like 设备
|
||||||
|
*/
|
||||||
|
export function isApplePlatform() {
|
||||||
|
return /Mac|iPhone|iPad|iPod/i.test(navigator.platform);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import { isString } from './assert';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 给定字符串是否是合法的 JSON 字符串
|
||||||
|
* @param str
|
||||||
|
*/
|
||||||
|
export function isJSONString(str: string) {
|
||||||
|
if (!str) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let json;
|
||||||
|
try {
|
||||||
|
json = JSON.parse(str);
|
||||||
|
return typeof json === 'object';
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 给定代码是否是有效的函数代码
|
||||||
|
* @param str
|
||||||
|
*/
|
||||||
|
export function isValidFunctionCode(str: string) {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-eval
|
||||||
|
const ret = eval(`typeof (${str})`);
|
||||||
|
return ret === 'function';
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const templatePattern = /^{{(.+)}}$/s;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断给定代码是否被双花括号包裹
|
||||||
|
* @example {{[]}}
|
||||||
|
* @example {{{}}}
|
||||||
|
* @example {{this.foo}}
|
||||||
|
* @example {{123}}
|
||||||
|
* @param str
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function isWrappedCode(str: string) {
|
||||||
|
// 排除简单对象后,再用正则匹配
|
||||||
|
return templatePattern.test(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isVariableString = isWrappedCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从包裹的代码中获取代码内容
|
||||||
|
* @param str
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function getCodeOfWrappedCode(str: string) {
|
||||||
|
const match = templatePattern.exec(str);
|
||||||
|
if (match && match.length) {
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getVariableContent = getCodeOfWrappedCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 给输入代码加上双花括号 code -> {{code}}
|
||||||
|
* @example foo -> {{foo}}
|
||||||
|
* @example "hello" => {{"hello"}}
|
||||||
|
* @example () => {} => {{() => {}}}
|
||||||
|
*
|
||||||
|
* @param code 输入代码
|
||||||
|
* @returns 加上花括号后的代码
|
||||||
|
*/
|
||||||
|
export function wrapCode(code: string) {
|
||||||
|
if (isWrappedCode(code)) {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
return `{{${code}}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 JSX 表达式容器包裹代码
|
||||||
|
* @example foo -> {foo}
|
||||||
|
* @example "hello" => {"hello"}
|
||||||
|
* @param code
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function wrapCodeWithJSXExpressionContainer(code: string) {
|
||||||
|
return `{${code}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否为简单字符串,非变量字符串
|
||||||
|
* @param str
|
||||||
|
*/
|
||||||
|
export function isPlainString(str: string) {
|
||||||
|
const isWrapped = isString(str) && isWrappedCode(str);
|
||||||
|
return isString && !isWrapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeBlockPattern = /```(\w*)([\s\S]*?)```/g;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 markdown 中解析出代码片段,仅返回第一个匹配的代码片段
|
||||||
|
* @param markdown
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function getCodeBlockFormMarkdown(markdown: string) {
|
||||||
|
const match = codeBlockPattern.exec(markdown.trim());
|
||||||
|
if (match && match.length) {
|
||||||
|
return match[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function url2serviceName(url: string) {
|
||||||
|
if (url.startsWith('http')) {
|
||||||
|
// 去除域名前缀
|
||||||
|
url = url
|
||||||
|
.replace(/https?:\/\//, '')
|
||||||
|
.split('/')
|
||||||
|
.slice(1)
|
||||||
|
.join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
url
|
||||||
|
// 去除 api + 模块名前缀
|
||||||
|
// - 云音乐 api 规范为 /api/模块名/
|
||||||
|
// - 后端公技基本使用 /模块名/api/
|
||||||
|
// - 中台类服务似乎常用 /api/middle/模块名/
|
||||||
|
// 目前的实现是去除了模块名,只干掉 /api/middle/ 和 /api/backend/ 这种常用前缀
|
||||||
|
.replace(/^\/[^/]+?\/api\/|^\/api\/middle\/|^\/api\/backend\/|^\/api\//, '')
|
||||||
|
// 去除路由参数
|
||||||
|
.replace(/\/\{.*?\}/, '')
|
||||||
|
// 忽略下划线与减号,将后面的字符转成大驼峰
|
||||||
|
.replace(/[-/_]+\w/g, (str) => str.replace(/[-/_]+/, '').toUpperCase())
|
||||||
|
// 首字母转小写
|
||||||
|
.replace(/^./, (str) => str.toLowerCase())
|
||||||
|
// 方法名以数字开头,添加 api 前缀
|
||||||
|
.replace(/^\d/, (str) => `api${str}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析状态变量的 path
|
||||||
|
* @example stores.foo.bar => { storeName: 'foo', variableName: 'bar' }
|
||||||
|
* @example stores.user.count => { storeName: 'user', variableName: 'count' }
|
||||||
|
*
|
||||||
|
* @param variablePath
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function parseStoreVariablePath(variablePath: string) {
|
||||||
|
const [, storeName, variableName] = variablePath.split('.');
|
||||||
|
return {
|
||||||
|
storeName,
|
||||||
|
variableName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析服务变量的 path
|
||||||
|
* @param variablePath
|
||||||
|
* @returns
|
||||||
|
*
|
||||||
|
* @example services.list => { moduleName: 'index', name: 'list' }
|
||||||
|
* @example services.sub.list => { moduleName: 'sub', name: 'list' }
|
||||||
|
* @example foo => undefined
|
||||||
|
*/
|
||||||
|
export function parseServiceVariablePath(variablePath: string) {
|
||||||
|
const parts = variablePath.split('.');
|
||||||
|
if (parts[0] !== 'services') {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
let moduleName = 'index';
|
||||||
|
let name = '';
|
||||||
|
switch (parts.length) {
|
||||||
|
case 2: {
|
||||||
|
name = parts[1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
moduleName = parts[1];
|
||||||
|
name = parts[2];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
moduleName,
|
||||||
|
name,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -7,5 +7,6 @@ export * from './events';
|
|||||||
export * from './function';
|
export * from './function';
|
||||||
export * from './logger';
|
export * from './logger';
|
||||||
export * from './string';
|
export * from './string';
|
||||||
export * from './react-helper';
|
|
||||||
export * from './object';
|
export * from './object';
|
||||||
|
export * from './react-helper';
|
||||||
|
export * from './code-helper';
|
||||||
|
|||||||
@@ -75,9 +75,13 @@ export function parseDndTrackId(str: string) {
|
|||||||
|
|
||||||
interface DndIdParsedType {
|
interface DndIdParsedType {
|
||||||
/**
|
/**
|
||||||
* 完整的 ID
|
* full id
|
||||||
*/
|
*/
|
||||||
id?: string;
|
id?: string;
|
||||||
|
/**
|
||||||
|
* 组件代码中的 id,一般对应组件的 tid 属性
|
||||||
|
*/
|
||||||
|
codeId?: string;
|
||||||
/**
|
/**
|
||||||
* 组件名
|
* 组件名
|
||||||
*/
|
*/
|
||||||
@@ -86,19 +90,11 @@ interface DndIdParsedType {
|
|||||||
* 文件名
|
* 文件名
|
||||||
*/
|
*/
|
||||||
filename?: string;
|
filename?: string;
|
||||||
/**
|
|
||||||
* @deprecated 使用 filename 代替
|
|
||||||
*/
|
|
||||||
module?: string;
|
|
||||||
/**
|
|
||||||
* 序号
|
|
||||||
*/
|
|
||||||
index?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析 dnd id
|
* 解析 dnd id
|
||||||
* @example Button:123 -> { component: "Button", id: "Button:123" }
|
* @example Button:button123 -> { component: "Button", id: "Button:123" }
|
||||||
* @example LocalBlock:Button:123 -> { filename: LocalBlock, component: "Button", id: "Button:123" }
|
* @example LocalBlock:Button:123 -> { filename: LocalBlock, component: "Button", id: "Button:123" }
|
||||||
* @param str
|
* @param str
|
||||||
*/
|
*/
|
||||||
@@ -109,277 +105,23 @@ export function parseDndId(str: string): DndIdParsedType {
|
|||||||
|
|
||||||
const parts = str.split(':');
|
const parts = str.split(':');
|
||||||
if (parts.length === 2) {
|
if (parts.length === 2) {
|
||||||
|
// e.g. Button:button123
|
||||||
return {
|
return {
|
||||||
component: parts[0],
|
|
||||||
index: parts[1],
|
|
||||||
id: str,
|
id: str,
|
||||||
|
component: parts[0],
|
||||||
|
codeId: parts[1],
|
||||||
};
|
};
|
||||||
} else if (parts.length >= 3) {
|
} else if (parts.length >= 3) {
|
||||||
|
// e.g. LocalBlock:Button:button123
|
||||||
const filename = decodeURIComponent(parts[0]);
|
const filename = decodeURIComponent(parts[0]);
|
||||||
return {
|
return {
|
||||||
module: filename,
|
id: str,
|
||||||
filename,
|
filename,
|
||||||
component: parts[1],
|
component: parts[1],
|
||||||
index: parts[2],
|
codeId: parts[2],
|
||||||
id: str,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: str,
|
id: str,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 给定字符串是否是合法的 JSON 字符串
|
|
||||||
* @param str
|
|
||||||
*/
|
|
||||||
export function isJSONString(str: string) {
|
|
||||||
if (!str) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let json;
|
|
||||||
try {
|
|
||||||
json = JSON.parse(str);
|
|
||||||
return typeof json === 'object';
|
|
||||||
} catch (err) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 给定代码是否是有效的函数代码
|
|
||||||
* @param str
|
|
||||||
*/
|
|
||||||
export function isValidFunctionCode(str: string) {
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line no-eval
|
|
||||||
const ret = eval(`typeof (${str})`);
|
|
||||||
return ret === 'function';
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否是有效的对象字符串
|
|
||||||
*
|
|
||||||
* @example { foo: 'foo' }
|
|
||||||
* @example [{ foo: 'foo' }]
|
|
||||||
* TODO: 考虑箭头函数的情况 () => {}
|
|
||||||
*
|
|
||||||
* @param str
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function isValidObjectString(str: string) {
|
|
||||||
const obj = code2object(str);
|
|
||||||
if (obj && typeof obj === 'object') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const templatePattern = /^{(.+)}$/s;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断给定字符串是否为变量字符串
|
|
||||||
* @deprecated 使用 isWrappedByExpressionContainer 代替
|
|
||||||
*
|
|
||||||
* @example {[]}
|
|
||||||
* @example {{}}
|
|
||||||
* @example {this.foo}
|
|
||||||
* @example {123}
|
|
||||||
* @param str
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function isVariableString(str: string) {
|
|
||||||
// 先检查是否是简单的对象
|
|
||||||
// FIXME: 这里有问题,如果代码中有引用,会被误判
|
|
||||||
if (code2object(str)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 排除简单对象后,再用正则匹配
|
|
||||||
return templatePattern.test(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 给输入代码加上花括号
|
|
||||||
* @example foo -> {foo}
|
|
||||||
* @example "hello" => {"hello"}
|
|
||||||
* @example () => {} => {() => {}}
|
|
||||||
*
|
|
||||||
* @deprecated 有问题,不要使用
|
|
||||||
*
|
|
||||||
* @param code 输入代码
|
|
||||||
* @returns 加上花括号后的代码
|
|
||||||
*/
|
|
||||||
export function wrapCode(code: string) {
|
|
||||||
if (isVariableString(code)) {
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
return `{${code}}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否为简单字符串,非变量字符串
|
|
||||||
* @param str
|
|
||||||
*/
|
|
||||||
export function isPlainString(str: string) {
|
|
||||||
const isString = typeof str === 'string';
|
|
||||||
const isVarString = isString && isVariableString(str);
|
|
||||||
return isString && !isVarString;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析并获取变量字符串的内容
|
|
||||||
* @param str
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function getVariableContent(str: string) {
|
|
||||||
const match = templatePattern.exec(str);
|
|
||||||
if (match && match.length) {
|
|
||||||
return match[1];
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提供给代码执行环境的全局变量
|
|
||||||
const patchCode = `
|
|
||||||
var tango = {
|
|
||||||
stores: {},
|
|
||||||
services: {},
|
|
||||||
config: {},
|
|
||||||
refs: {},
|
|
||||||
};
|
|
||||||
`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将代码放到函数体中进行执行
|
|
||||||
* @param code
|
|
||||||
* @returns 函数执行的结果
|
|
||||||
*/
|
|
||||||
export function runCode(code: string) {
|
|
||||||
let ret;
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line no-new-func
|
|
||||||
ret = new Function(`${patchCode}\n return ${code}`)();
|
|
||||||
} catch (err) {
|
|
||||||
// ignore error
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-useless-escape
|
|
||||||
const objectWrapperPattern = /^[{\[].*[}\]]$/s;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将代码片段转成 js 对象
|
|
||||||
* @param code 代码文本
|
|
||||||
* @param isStrict 是否为严格模式(是否废弃)
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function code2object(code: string, isStrict = true) {
|
|
||||||
// 非严格模式直接执行
|
|
||||||
// 严格模式下需检测 code 是一个对象
|
|
||||||
if (!isStrict || (isStrict && objectWrapperPattern.test(code))) {
|
|
||||||
const ret = runCode(code);
|
|
||||||
return typeof ret === 'object' ? ret : undefined;
|
|
||||||
}
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
const codeBlockPattern = /```(\w*)([\s\S]*?)```/g;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从 markdown 中解析出代码片段,仅返回第一个匹配的代码片段
|
|
||||||
* @param markdown
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function getCodeBlockFormMarkdown(markdown: string) {
|
|
||||||
const match = codeBlockPattern.exec(markdown.trim());
|
|
||||||
if (match && match.length) {
|
|
||||||
return match[2];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function url2serviceName(url: string) {
|
|
||||||
if (url.startsWith('http')) {
|
|
||||||
// 去除域名前缀
|
|
||||||
url = url
|
|
||||||
.replace(/https?:\/\//, '')
|
|
||||||
.split('/')
|
|
||||||
.slice(1)
|
|
||||||
.join('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
url
|
|
||||||
// 去除 api + 模块名前缀
|
|
||||||
// - 云音乐 api 规范为 /api/模块名/
|
|
||||||
// - 后端公技基本使用 /模块名/api/
|
|
||||||
// - 中台类服务似乎常用 /api/middle/模块名/
|
|
||||||
// 目前的实现是去除了模块名,只干掉 /api/middle/ 和 /api/backend/ 这种常用前缀
|
|
||||||
.replace(/^\/[^/]+?\/api\/|^\/api\/middle\/|^\/api\/backend\/|^\/api\//, '')
|
|
||||||
// 去除路由参数
|
|
||||||
.replace(/\/\{.*?\}/, '')
|
|
||||||
// 忽略下划线与减号,将后面的字符转成大驼峰
|
|
||||||
.replace(/[-/_]+\w/g, (str) => str.replace(/[-/_]+/, '').toUpperCase())
|
|
||||||
// 首字母转小写
|
|
||||||
.replace(/^./, (str) => str.toLowerCase())
|
|
||||||
// 方法名以数字开头,添加 api 前缀
|
|
||||||
.replace(/^\d/, (str) => `api${str}`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析状态变量的 path
|
|
||||||
* @example stores.foo.bar => { storeName: 'foo', variableName: 'bar' }
|
|
||||||
* @example stores.user.count => { storeName: 'user', variableName: 'count' }
|
|
||||||
*
|
|
||||||
* @param variablePath
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function parseStoreVariablePath(variablePath: string) {
|
|
||||||
const [, storeName, variableName] = variablePath.split('.');
|
|
||||||
return {
|
|
||||||
storeName,
|
|
||||||
variableName,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析服务变量的 path
|
|
||||||
* @param variablePath
|
|
||||||
* @returns
|
|
||||||
*
|
|
||||||
* @example services.list => { moduleName: 'index', name: 'list' }
|
|
||||||
* @example services.sub.list => { moduleName: 'sub', name: 'list' }
|
|
||||||
* @example foo => undefined
|
|
||||||
*/
|
|
||||||
export function parseServiceVariablePath(variablePath: string) {
|
|
||||||
const parts = variablePath.split('.');
|
|
||||||
if (parts[0] !== 'services') {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
let moduleName = 'index';
|
|
||||||
let name = '';
|
|
||||||
switch (parts.length) {
|
|
||||||
case 2: {
|
|
||||||
name = parts[1];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 3: {
|
|
||||||
moduleName = parts[1];
|
|
||||||
name = parts[2];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
moduleName,
|
|
||||||
name,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -132,8 +132,3 @@ export interface ISelectedItemData {
|
|||||||
element?: HTMLElement;
|
element?: HTMLElement;
|
||||||
parents?: ISelectedItemData[];
|
parents?: ISelectedItemData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SetterOnChangeDetailType = {
|
|
||||||
relatedImports?: string[];
|
|
||||||
isRawCode?: boolean;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -3,11 +3,36 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { OptionType } from './advanced';
|
import { OptionType } from './advanced';
|
||||||
|
import { PartialRecord } from './base';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated 请使用 IComponentProp 代替
|
||||||
|
*/
|
||||||
|
export type ComponentPropType<T = any> = IComponentProp<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
export type ComponentDndRulesType = IComponentDndRules;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
export type ComponentPrototypeType = IComponentPrototype;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件校验规则
|
||||||
|
*/
|
||||||
|
export type ComponentPropValidate = (
|
||||||
|
value: any,
|
||||||
|
field: any,
|
||||||
|
trigger: string,
|
||||||
|
) => string | Promise<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组件属性类型
|
* 组件属性类型
|
||||||
*/
|
*/
|
||||||
export type ComponentPropType<T = any> = {
|
export interface IComponentProp<T = any> {
|
||||||
/**
|
/**
|
||||||
* 属性名
|
* 属性名
|
||||||
*/
|
*/
|
||||||
@@ -23,11 +48,12 @@ export type ComponentPropType<T = any> = {
|
|||||||
/**
|
/**
|
||||||
* 属性的分组
|
* 属性的分组
|
||||||
* - basic 常用属性
|
* - basic 常用属性
|
||||||
* - event 事件属性
|
|
||||||
* - style 样式属性
|
* - style 样式属性
|
||||||
* - advanced 高级属性
|
* - advanced 高级属性
|
||||||
|
* - event 事件属性
|
||||||
|
* - data 数据属性
|
||||||
*/
|
*/
|
||||||
group?: 'basic' | 'event' | 'style' | 'advanced';
|
group?: 'basic' | 'event' | 'style' | 'data' | 'advanced';
|
||||||
/**
|
/**
|
||||||
* 帮助文档链接地址
|
* 帮助文档链接地址
|
||||||
* @example https://foo.bar/help
|
* @example https://foo.bar/help
|
||||||
@@ -49,7 +75,7 @@ export type ComponentPropType<T = any> = {
|
|||||||
/**
|
/**
|
||||||
* 如果是对象属性,这里声明子属性列表
|
* 如果是对象属性,这里声明子属性列表
|
||||||
*/
|
*/
|
||||||
props?: ComponentPropType[];
|
props?: IComponentProp[];
|
||||||
/**
|
/**
|
||||||
* 输入提示
|
* 输入提示
|
||||||
*/
|
*/
|
||||||
@@ -78,7 +104,7 @@ export type ComponentPropType<T = any> = {
|
|||||||
/**
|
/**
|
||||||
* 动态设置属性,覆盖已有的 prop 对象
|
* 动态设置属性,覆盖已有的 prop 对象
|
||||||
*/
|
*/
|
||||||
getProp?: (form: any) => Partial<Omit<ComponentPropType<T>, 'getProp'>>;
|
getProp?: (form: any) => Partial<Omit<IComponentProp<T>, 'getProp'>>;
|
||||||
/**
|
/**
|
||||||
* 配置项的可选值,setterProps.options 的简写
|
* 配置项的可选值,setterProps.options 的简写
|
||||||
*/
|
*/
|
||||||
@@ -91,12 +117,20 @@ export type ComponentPropType<T = any> = {
|
|||||||
* 动态设置表单项是否展示
|
* 动态设置表单项是否展示
|
||||||
*/
|
*/
|
||||||
getVisible?: (form: any) => boolean;
|
getVisible?: (form: any) => boolean;
|
||||||
};
|
/**
|
||||||
|
* 表单值校验逻辑
|
||||||
|
*/
|
||||||
|
validate?: ComponentPropValidate;
|
||||||
|
/**
|
||||||
|
* 标记属性是否已废弃
|
||||||
|
*/
|
||||||
|
deprecated?: boolean | string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组件拖拽规则类型
|
* 组件拖拽规则类型
|
||||||
*/
|
*/
|
||||||
export type ComponentDndRulesType = {
|
export interface IComponentDndRules {
|
||||||
/**
|
/**
|
||||||
* 当前节点是否可以被拖拽
|
* 当前节点是否可以被拖拽
|
||||||
*/
|
*/
|
||||||
@@ -117,12 +151,12 @@ export type ComponentDndRulesType = {
|
|||||||
* 子节点的容器选择器,用于快速定位子节点容器,适合组件存在多个可搭建区域时使用
|
* 子节点的容器选择器,用于快速定位子节点容器,适合组件存在多个可搭建区域时使用
|
||||||
*/
|
*/
|
||||||
childrenContainerSelector?: string;
|
childrenContainerSelector?: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组件原型类型
|
* 组件原型类型
|
||||||
*/
|
*/
|
||||||
export type ComponentPrototypeType = {
|
export interface IComponentPrototype {
|
||||||
/**
|
/**
|
||||||
* 组件名
|
* 组件名
|
||||||
* @example Button
|
* @example Button
|
||||||
@@ -228,20 +262,24 @@ export type ComponentPrototypeType = {
|
|||||||
* 直接子节点的类型
|
* 直接子节点的类型
|
||||||
*/
|
*/
|
||||||
childrenName?: string | string[];
|
childrenName?: string | string[];
|
||||||
|
/**
|
||||||
|
* 兄弟节点的类型
|
||||||
|
*/
|
||||||
|
siblingNames?: string[];
|
||||||
/**
|
/**
|
||||||
* 组件的可配置属性集
|
* 组件的可配置属性集
|
||||||
*/
|
*/
|
||||||
props?: ComponentPropType[];
|
props?: IComponentProp[];
|
||||||
/**
|
/**
|
||||||
* 组件的拖拽规则
|
* 组件的拖拽规则
|
||||||
*/
|
*/
|
||||||
rules?: ComponentDndRulesType;
|
rules?: IComponentDndRules;
|
||||||
/**
|
/**
|
||||||
* 用法
|
* 用法
|
||||||
* @example import { Button } from 'antd';
|
* @example import { Button } from 'antd';
|
||||||
*/
|
*/
|
||||||
usage?: string;
|
usage?: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TangoConfigJson 类型配置
|
* TangoConfigJson 类型配置
|
||||||
@@ -263,6 +301,19 @@ export interface ITangoConfigJson {
|
|||||||
* 外部资源,例如 js, css 文件等
|
* 外部资源,例如 js, css 文件等
|
||||||
*/
|
*/
|
||||||
externalResources?: Record<string, any>;
|
externalResources?: Record<string, any>;
|
||||||
|
/**
|
||||||
|
* 沙箱配置
|
||||||
|
*/
|
||||||
|
sandbox?: {
|
||||||
|
/**
|
||||||
|
* 在沙箱中执行的脚本
|
||||||
|
*/
|
||||||
|
evaluateJavaScript?: string;
|
||||||
|
/**
|
||||||
|
* 模块的别名
|
||||||
|
*/
|
||||||
|
alias?: Record<string, string>;
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* 设计器配置
|
* 设计器配置
|
||||||
*/
|
*/
|
||||||
@@ -277,3 +328,19 @@ export interface ITangoConfigJson {
|
|||||||
autoGenerateComponentId: boolean;
|
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>;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user