mirror of
https://github.com/NetEase/tango.git
synced 2026-08-31 09:30:28 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c48a49961a | |||
| eae45003c6 | |||
| 1802fc0eb8 | |||
| 8a3ab3c093 | |||
| 55602fc961 | |||
| a3c0e463ca | |||
| 79f9a8c179 | |||
| ea96872261 | |||
| a86e0e049c | |||
| c4e1ed67f5 |
@@ -153,6 +153,7 @@ import { definePage } from "@music163/tango-boot";
|
||||
import {
|
||||
Page,
|
||||
Section,
|
||||
Box,
|
||||
Button,
|
||||
Input,
|
||||
FormilyForm,
|
||||
@@ -167,7 +168,9 @@ class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Page title={tango.stores.app.title} subTitle={<><Button>hello</Button></>}>
|
||||
<Section tid="section0" />
|
||||
<Section tid="section0">
|
||||
<Box></Box>
|
||||
</Section>
|
||||
<Section tid="section1" title="Section Title">
|
||||
your input: <Input tid="input1" defaultValue="hello" />
|
||||
copy input: <Input value={tango.page.input1?.value} />
|
||||
@@ -374,13 +377,16 @@ export default defineServices({
|
||||
add: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
|
||||
method: 'post',
|
||||
description: '新增用户'
|
||||
},
|
||||
update: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
|
||||
method: 'post',
|
||||
description: '更新用户'
|
||||
},
|
||||
delete: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users?id=1',
|
||||
description: '删除用户'
|
||||
},
|
||||
});
|
||||
`;
|
||||
|
||||
@@ -48,7 +48,6 @@ const Snippet3ColumnLayout: IComponentPrototype = {
|
||||
icon: 'icon-column-3',
|
||||
type: 'snippet',
|
||||
package: '@music163/antd',
|
||||
// FIXME: Column 组件 需要更新为 defineComponent
|
||||
initChildren: `
|
||||
<Columns columns={12}>
|
||||
<Column colSpan={4}>
|
||||
@@ -139,7 +138,7 @@ export const nativeDomPrototypes = () => {
|
||||
{
|
||||
name: 'className',
|
||||
title: '类名',
|
||||
setter: 'textSetter',
|
||||
setter: 'classNameSetter',
|
||||
},
|
||||
{
|
||||
name: 'id',
|
||||
@@ -169,6 +168,38 @@ const prototypes: Dict<IComponentPrototype> = {
|
||||
Snippet2ColumnLayout,
|
||||
Snippet3ColumnLayout,
|
||||
SnippetButtonGroup,
|
||||
Section: {
|
||||
...basePrototypes['Section'],
|
||||
props: [
|
||||
// ...(basePrototypes['Section'].props as any),
|
||||
{
|
||||
name: 'title',
|
||||
title: '标题',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'extra',
|
||||
title: '额外内容',
|
||||
setter: 'renderSetter',
|
||||
options: [
|
||||
{
|
||||
label: '按钮组',
|
||||
value: 'ButtonGroup',
|
||||
renderBody:
|
||||
'<ButtonGroup><Button>button1</Button><Button>button2</Button></ButtonGroup>',
|
||||
relatedImports: ['ButtonGroup', 'Button'],
|
||||
},
|
||||
],
|
||||
template: '(v) => {\n return {{content}};\n}',
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
title: '类名',
|
||||
setter: 'classNameSetter',
|
||||
group: 'style',
|
||||
},
|
||||
],
|
||||
},
|
||||
Box: {
|
||||
name: 'Box',
|
||||
title: '盒子',
|
||||
@@ -200,6 +231,38 @@ const prototypes: Dict<IComponentPrototype> = {
|
||||
title: 'd',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'onClick',
|
||||
title: '点击事件',
|
||||
setter: 'eventSetter',
|
||||
template: '(e) => {\n {{content}}\n}',
|
||||
tip: '回调参数说明:e 为事件对象',
|
||||
},
|
||||
{
|
||||
name: 'renderFoo',
|
||||
title: 'foo自定义渲染',
|
||||
setter: 'renderSetter',
|
||||
options: [
|
||||
{
|
||||
label: '占位空间',
|
||||
value: 'Box',
|
||||
render: '() => <Box>test</Box>',
|
||||
relatedImports: ['Box'],
|
||||
},
|
||||
{
|
||||
label: '占位文本',
|
||||
value: 'Text',
|
||||
render: '() => <Text>text</Text>',
|
||||
relatedImports: ['Text'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
title: '类名',
|
||||
setter: 'classNameSetter',
|
||||
group: 'style',
|
||||
},
|
||||
],
|
||||
},
|
||||
Columns: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SystemProvider } from 'coral-system';
|
||||
import 'antd/dist/antd.css';
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
actions: { argTypesRegex: '^on.*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"dependencies": {
|
||||
"@music163/tango-setting-form": "*",
|
||||
"@music163/tango-ui": "*",
|
||||
"mobx": "6.12.3",
|
||||
"mobx": "6.13.2",
|
||||
"mobx-react-lite": "4.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { ActionSelect } from '@music163/tango-ui';
|
||||
|
||||
export default {
|
||||
title: 'UI/ActionSelect',
|
||||
component: ActionSelect,
|
||||
};
|
||||
|
||||
const options = [
|
||||
{ label: 'action1', value: 'action1' },
|
||||
{ label: 'action2', value: 'action2' },
|
||||
{ label: 'action3', value: 'action3' },
|
||||
];
|
||||
|
||||
export const Basic = {
|
||||
args: {
|
||||
defaultText: '选择动作',
|
||||
options,
|
||||
onSelect: console.log,
|
||||
},
|
||||
};
|
||||
|
||||
export const showInput = {
|
||||
args: {
|
||||
text: '选择动作',
|
||||
options,
|
||||
showInput: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ClassNameInput } from '@music163/tango-ui';
|
||||
|
||||
export default {
|
||||
title: 'UI/ClassNameInput',
|
||||
};
|
||||
|
||||
export function Basic() {
|
||||
return <ClassNameInput defaultValue="hello world" onChange={console.log} />;
|
||||
}
|
||||
|
||||
export function Controlled() {
|
||||
const [value, setValue] = useState('');
|
||||
return <ClassNameInput value={value} onChange={setValue} />;
|
||||
}
|
||||
+4
-4
@@ -23,9 +23,9 @@
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.23.6",
|
||||
"@babel/preset-react": "^7.23.3",
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@babel/preset-react": "^7.24.7",
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"@commitlint/cli": "^18.4.3",
|
||||
"@commitlint/config-conventional": "^18.4.3",
|
||||
"@types/jest": "^29.5.7",
|
||||
@@ -58,7 +58,7 @@
|
||||
"react": "^17.0.0",
|
||||
"react-dom": "^17.0.0",
|
||||
"styled-components": "^5.3.6",
|
||||
"typedoc": "^0.25.4",
|
||||
"typedoc": "^0.26.7",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.1.10](https://github.com/netease/tango/compare/@music163/tango-context@1.1.9...@music163/tango-context@1.1.10) (2024-09-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- service support set description field ([#206](https://github.com/netease/tango/issues/206)) ([79f9a8c](https://github.com/netease/tango/commit/79f9a8c1791424f35f194ab2a9aa7c263ef39f40))
|
||||
|
||||
## [1.1.9](https://github.com/netease/tango/compare/@music163/tango-context@1.1.8...@music163/tango-context@1.1.9) (2024-09-02)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.8](https://github.com/netease/tango/compare/@music163/tango-context@1.1.7...@music163/tango-context@1.1.8) (2024-08-06)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-context",
|
||||
"version": "1.1.8",
|
||||
"version": "1.1.10",
|
||||
"description": "react context for tango-apps",
|
||||
"keywords": [
|
||||
"react",
|
||||
@@ -31,8 +31,8 @@
|
||||
"react": ">= 16.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@music163/tango-core": "^1.4.2",
|
||||
"@music163/tango-helpers": "^1.2.2",
|
||||
"@music163/tango-core": "^1.4.4",
|
||||
"@music163/tango-helpers": "^1.2.4",
|
||||
"mobx-react-lite": "4.0.7"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Engine } from '@music163/tango-core';
|
||||
import type { AbstractCodeWorkspace, Engine } from '@music163/tango-core';
|
||||
import { IVariableTreeNode, createContext } from '@music163/tango-helpers';
|
||||
|
||||
export interface ITangoEngineContext {
|
||||
@@ -21,8 +21,12 @@ const [TangoEngineProvider, useTangoEngine] = createContext<ITangoEngineContext>
|
||||
|
||||
export { TangoEngineProvider };
|
||||
|
||||
/**
|
||||
* 获取 CodeWorkspace 实例
|
||||
* @returns
|
||||
*/
|
||||
export const useWorkspace = () => {
|
||||
return useTangoEngine()?.engine.workspace;
|
||||
return useTangoEngine()?.engine.workspace as AbstractCodeWorkspace;
|
||||
};
|
||||
|
||||
export const useDesigner = () => {
|
||||
@@ -90,12 +94,15 @@ export const useWorkspaceData = () => {
|
||||
key: prefix,
|
||||
selectable: false,
|
||||
showAddButton: true,
|
||||
children: Object.keys(file.serviceFunctions || {}).map((key) => ({
|
||||
title: key,
|
||||
key: [prefix, key].join('.'),
|
||||
type: 'function',
|
||||
showRemoveButton: true,
|
||||
})),
|
||||
children: Object.entries(file.serviceFunctions || {}).map(([key, value]) => {
|
||||
const title = value.description ? `${key}(${value.description})` : key;
|
||||
return {
|
||||
title,
|
||||
key: [prefix, key].join('.'),
|
||||
type: 'function',
|
||||
showRemoveButton: true,
|
||||
};
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.4.4](https://github.com/netease/tango/compare/@music163/tango-core@1.4.3...@music163/tango-core@1.4.4) (2024-09-09)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-core
|
||||
|
||||
## [1.4.3](https://github.com/netease/tango/compare/@music163/tango-core@1.4.2...@music163/tango-core@1.4.3) (2024-09-02)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-core
|
||||
|
||||
## [1.4.2](https://github.com/netease/tango/compare/@music163/tango-core@1.4.1...@music163/tango-core@1.4.2) (2024-08-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-core",
|
||||
"version": "1.4.2",
|
||||
"version": "1.4.4",
|
||||
"description": "tango core",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"homepage": "",
|
||||
@@ -24,14 +24,14 @@
|
||||
"prepublishOnly": "yarn build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/generator": "^7.23.5",
|
||||
"@babel/parser": "^7.23.5",
|
||||
"@babel/traverse": "^7.23.5",
|
||||
"@babel/types": "^7.23.5",
|
||||
"@music163/tango-helpers": "^1.2.2",
|
||||
"@babel/generator": "^7.25.6",
|
||||
"@babel/parser": "^7.25.6",
|
||||
"@babel/traverse": "^7.25.6",
|
||||
"@babel/types": "^7.25.6",
|
||||
"@music163/tango-helpers": "^1.2.4",
|
||||
"@types/babel__generator": "^7.6.7",
|
||||
"@types/babel__traverse": "^7.20.4",
|
||||
"mobx": "6.12.3",
|
||||
"@types/babel__traverse": "^7.20.6",
|
||||
"mobx": "6.13.2",
|
||||
"path-browserify": "^1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { MenuDataType } from '@music163/tango-helpers';
|
||||
import { Designer, DesignerViewType, Engine, SimulatorNameType } from './models';
|
||||
import { IWorkspace } from './models/interfaces';
|
||||
import { AbstractWorkspace } from './models/abstract-workspace';
|
||||
|
||||
interface ICreateEngineOptions {
|
||||
/**
|
||||
* 自定义工作区
|
||||
*/
|
||||
workspace?: IWorkspace;
|
||||
workspace?: AbstractWorkspace;
|
||||
/**
|
||||
* 菜单信息
|
||||
*/
|
||||
|
||||
@@ -28,7 +28,7 @@ import { isDefineService, isDefineStore, isTangoVariable } from '../assert';
|
||||
import type {
|
||||
IRouteData,
|
||||
IStorePropertyData,
|
||||
ITangoViewNodeData,
|
||||
IViewNodeData,
|
||||
IImportDeclarationPayload,
|
||||
InsertChildPositionType,
|
||||
IImportSpecifierData,
|
||||
@@ -1234,7 +1234,7 @@ export function cloneJSXElement(node: t.JSXElement, overrideProps?: Dict) {
|
||||
export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
|
||||
const imports: Record<string, IImportSpecifierData[]> = {};
|
||||
const importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]> = {};
|
||||
const nodes: Array<ITangoViewNodeData<t.JSXElement>> = [];
|
||||
const nodes: Array<IViewNodeData<t.JSXElement>> = [];
|
||||
const cloneAst = t.cloneNode(ast, true, true);
|
||||
const cleanAst = clearTrackingData(cloneAst);
|
||||
const variables: string[] = []; // 使用的 tango 变量
|
||||
|
||||
@@ -8,65 +8,57 @@ import { FileType } from './../types';
|
||||
export function inferFileType(filename: string): FileType {
|
||||
// 增加 tangoConfigJson Module
|
||||
if (/\/tango\.config\.json$/.test(filename)) {
|
||||
return FileType.TangoConfigJson;
|
||||
return FileType.TangoConfigJsonFile;
|
||||
}
|
||||
|
||||
if (/\/appJson\.json$/.test(filename)) {
|
||||
return FileType.AppJson;
|
||||
return FileType.AppJsonFile;
|
||||
}
|
||||
|
||||
if (/\/package\.json$/.test(filename)) {
|
||||
return FileType.PackageJson;
|
||||
return FileType.PackageJsonFile;
|
||||
}
|
||||
|
||||
if (/\/routes\.js$/.test(filename)) {
|
||||
return FileType.RouteModule;
|
||||
return FileType.JsRouteConfigFile;
|
||||
}
|
||||
|
||||
// 所有 pages 下的 js 文件均认为是有效的 viewModule
|
||||
if (/\/pages\/.+\.jsx?$/.test(filename)) {
|
||||
return FileType.JsxViewModule;
|
||||
return FileType.JsViewFile;
|
||||
}
|
||||
|
||||
// 所有 pages 下的 js 文件均认为是有效的 viewModule
|
||||
if (/\/pages\/.+\.schema\.json?$/.test(filename)) {
|
||||
return FileType.JsonViewModule;
|
||||
return FileType.JsonViewFile;
|
||||
}
|
||||
|
||||
if (/\/(blocks|components)\/index\.js/.test(filename)) {
|
||||
return FileType.ComponentsEntryModule;
|
||||
return FileType.JsLocalComponentsEntryFile;
|
||||
}
|
||||
|
||||
if (/\/services\/.+\.js$/.test(filename)) {
|
||||
return FileType.ServiceModule;
|
||||
return FileType.JsServiceFile;
|
||||
}
|
||||
|
||||
if (/service\.js$/.test(filename)) {
|
||||
return FileType.ServiceModule;
|
||||
return FileType.JsServiceFile;
|
||||
}
|
||||
|
||||
if (/\/stores\/index\.js$/.test(filename)) {
|
||||
return FileType.StoreEntryModule;
|
||||
return FileType.JsStoreEntryFile;
|
||||
}
|
||||
|
||||
if (/\/stores\/.+\.js$/.test(filename)) {
|
||||
return FileType.StoreModule;
|
||||
return FileType.JsStoreFile;
|
||||
}
|
||||
|
||||
if (/\.jsx?$/.test(filename)) {
|
||||
return FileType.Module;
|
||||
return FileType.JsFile;
|
||||
}
|
||||
|
||||
if (/\.json$/.test(filename)) {
|
||||
return FileType.Json;
|
||||
}
|
||||
|
||||
if (/\.less$/.test(filename)) {
|
||||
return FileType.Less;
|
||||
}
|
||||
|
||||
if (/\.scss$/.test(filename)) {
|
||||
return FileType.Scss;
|
||||
return FileType.JsonFile;
|
||||
}
|
||||
|
||||
return FileType.File;
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
import {
|
||||
Dict,
|
||||
isStoreVariablePath,
|
||||
parseServiceVariablePath,
|
||||
parseStoreVariablePath,
|
||||
} from '@music163/tango-helpers';
|
||||
import { inferFileType, getFilepath } from '../helpers';
|
||||
import { TangoFile } from './file';
|
||||
import { FileType } from '../types';
|
||||
import { JsRouteConfigFile } from './js-route-config-file';
|
||||
import { JsStoreEntryFile } from './js-store-entry-file';
|
||||
import { JsServiceFile } from './js-service-file';
|
||||
import { JsViewFile } from './js-view-file';
|
||||
import { JsLocalComponentsEntryFile } from './js-local-components-entry-file';
|
||||
import { JsAppEntryFile } from './js-app-entry-file';
|
||||
import { JsFile } from './js-file';
|
||||
import { AbstractWorkspace, IWorkspaceInitConfig } from './abstract-workspace';
|
||||
import { JsonFile } from './json-file';
|
||||
import { JsStoreFile } from './js-store-file';
|
||||
|
||||
/**
|
||||
* CodeWorkspace 抽象基类
|
||||
*/
|
||||
export abstract class AbstractCodeWorkspace extends AbstractWorkspace {
|
||||
/**
|
||||
* 模型入口配置模块
|
||||
*/
|
||||
storeEntryModule: JsStoreEntryFile;
|
||||
|
||||
/**
|
||||
* 状态管理模块
|
||||
*/
|
||||
storeModules: Record<string, JsStoreFile>;
|
||||
|
||||
/**
|
||||
* 数据服务模块
|
||||
*/
|
||||
serviceModules: Record<string, JsServiceFile>;
|
||||
|
||||
constructor(options: IWorkspaceInitConfig) {
|
||||
super(options);
|
||||
this.storeModules = {};
|
||||
this.serviceModules = {};
|
||||
if (options?.files) {
|
||||
this.addFiles(options.files);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加文件到工作区
|
||||
* @param filename 文件名
|
||||
* @param code 代码片段
|
||||
* @param fileType 模块类型
|
||||
*/
|
||||
addFile(filename: string, code: string, fileType?: FileType) {
|
||||
if (!fileType && filename === this.entry) {
|
||||
fileType = FileType.JsAppEntryFile;
|
||||
}
|
||||
const moduleType = fileType || inferFileType(filename);
|
||||
const props = {
|
||||
filename,
|
||||
code,
|
||||
type: moduleType,
|
||||
};
|
||||
|
||||
let module;
|
||||
switch (moduleType) {
|
||||
case FileType.JsAppEntryFile:
|
||||
module = new JsAppEntryFile(this, props);
|
||||
this.jsAppEntryFile = module;
|
||||
break;
|
||||
case FileType.JsStoreEntryFile:
|
||||
module = new JsStoreEntryFile(this, props);
|
||||
this.storeEntryModule = module;
|
||||
break;
|
||||
case FileType.JsLocalComponentsEntryFile:
|
||||
module = new JsLocalComponentsEntryFile(this, props);
|
||||
this.componentsEntryModule = module;
|
||||
break;
|
||||
case FileType.JsRouteConfigFile: {
|
||||
module = new JsRouteConfigFile(this, props);
|
||||
this.routeModule = module;
|
||||
// check if activeRoute exists
|
||||
const route = module.routes.find((item) => item.path === this.activeRoute);
|
||||
if (!route) {
|
||||
this.setActiveRoute(module.routes[0]?.path);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FileType.JsViewFile:
|
||||
module = new JsViewFile(this, props);
|
||||
break;
|
||||
case FileType.JsServiceFile:
|
||||
module = new JsServiceFile(this, props);
|
||||
this.serviceModules[module.name] = module;
|
||||
break;
|
||||
case FileType.JsStoreFile:
|
||||
module = new JsStoreFile(this, props);
|
||||
this.storeModules[module.name] = module;
|
||||
break;
|
||||
case FileType.JsFile:
|
||||
module = new JsFile(this, props);
|
||||
break;
|
||||
case FileType.PackageJsonFile:
|
||||
module = new JsonFile(this, props);
|
||||
this.packageJson = module;
|
||||
break;
|
||||
case FileType.TangoConfigJsonFile:
|
||||
module = new JsonFile(this, props);
|
||||
this.tangoConfigJson = module;
|
||||
break;
|
||||
case FileType.JsonFile:
|
||||
module = new JsonFile(this, props);
|
||||
break;
|
||||
default:
|
||||
module = new TangoFile(this, props);
|
||||
}
|
||||
|
||||
this.files.set(filename, module);
|
||||
}
|
||||
|
||||
addServiceFile(serviceName: string, code: string) {
|
||||
const filename = `/src/services/${serviceName}.js`;
|
||||
this.addFile(filename, code, FileType.JsServiceFile);
|
||||
const indexServiceModule = this.serviceModules.index;
|
||||
indexServiceModule?.addImportDeclaration(`./${serviceName}`, []).update();
|
||||
}
|
||||
|
||||
addStoreFile(storeName: string, code: string) {
|
||||
const filename = `/src/stores/${storeName}.js`;
|
||||
this.addFile(filename, code);
|
||||
if (!this.storeEntryModule) {
|
||||
this.addFile('/src/stores/index.js', '');
|
||||
}
|
||||
this.storeEntryModule.addStore(storeName).update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加新的模型文件
|
||||
* @deprecated 使用 addStoreFile 代替
|
||||
*/
|
||||
addStoreModule(name: string, code: string) {
|
||||
this.addStoreFile(name, code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除模型文件
|
||||
* @param name
|
||||
*/
|
||||
removeStoreModule(name: string) {
|
||||
const filename = getFilepath(name, '/src/stores', '.js');
|
||||
this.storeEntryModule.removeStore(name).update();
|
||||
this.removeFile(filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加模型属性
|
||||
* @param storeName
|
||||
* @param stateName
|
||||
* @param initValue
|
||||
*/
|
||||
addStoreState(storeName: string, stateName: string, initValue: string) {
|
||||
this.storeModules[storeName]?.addState(stateName, initValue).update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除模型属性
|
||||
* @param storeName
|
||||
* @param stateName
|
||||
*/
|
||||
removeStoreState(storeName: string, stateName: string) {
|
||||
this.storeModules[storeName]?.removeState(stateName).update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据变量路径删除状态变量
|
||||
* @param variablePath
|
||||
*/
|
||||
removeStoreVariable(variablePath: string) {
|
||||
const { storeName, variableName } = parseStoreVariablePath(variablePath);
|
||||
this.removeStoreState(storeName, variableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据变量路径更新状态变量的值
|
||||
* @param variablePath 变量路径
|
||||
* @param code 变量代码
|
||||
*/
|
||||
updateStoreVariable(variablePath: string, code: string) {
|
||||
if (isStoreVariablePath(variablePath)) {
|
||||
const { storeName, variableName } = parseStoreVariablePath(variablePath);
|
||||
this.storeModules[storeName]?.updateState(variableName, code).update();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务函数的详情
|
||||
* TODO: 不要 services 前缀
|
||||
* @param serviceKey `services.list` 或 `services.sub.list`
|
||||
* @returns
|
||||
*/
|
||||
getServiceFunction(serviceKey: string) {
|
||||
const { name, moduleName } = parseServiceVariablePath(serviceKey);
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
moduleName,
|
||||
config: this.serviceModules[moduleName]?.serviceFunctions[name],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务函数的列表
|
||||
* @returns 返回服务函数的列表 { [serviceKey: string]: Dict }
|
||||
*/
|
||||
listServiceFunctions() {
|
||||
const ret: Record<string, Dict> = {};
|
||||
Object.keys(this.serviceModules).forEach((moduleName) => {
|
||||
const module = this.serviceModules[moduleName];
|
||||
Object.keys(module.serviceFunctions).forEach((name) => {
|
||||
const serviceKey = moduleName === 'index' ? name : [moduleName, name].join('.');
|
||||
ret[serviceKey] = module.serviceFunctions[name];
|
||||
});
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新服务函数
|
||||
*/
|
||||
updateServiceFunction(serviceName: string, payload: Dict, moduleName = 'index') {
|
||||
this.serviceModules[moduleName].updateServiceFunction(serviceName, payload).update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增服务函数,支持批量添加
|
||||
*/
|
||||
addServiceFunction(name: string, config: Dict, moduleName = 'index') {
|
||||
this.serviceModules[moduleName]?.addServiceFunction(name, config).update();
|
||||
}
|
||||
|
||||
addServiceFunctions(configs: Dict<Dict>, modName = 'index') {
|
||||
this.serviceModules[modName]?.addServiceFunctions(configs).update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除服务函数
|
||||
* @param name
|
||||
*/
|
||||
removeServiceFunction(serviceKey: string) {
|
||||
const { moduleName, name } = parseServiceVariablePath(serviceKey);
|
||||
this.serviceModules[moduleName]?.deleteServiceFunction(name).update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新服务的基础配置
|
||||
*/
|
||||
updateServiceBaseConfig(config: Dict, moduleName = 'index') {
|
||||
this.serviceModules[moduleName]?.updateBaseConfig(config).update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import type { FileType, IFileConfig } from '../types';
|
||||
|
||||
/**
|
||||
* 普通文件抽象基类,不进行 AST 解析
|
||||
*/
|
||||
export abstract class AbstractFile {
|
||||
readonly workspace: AbstractWorkspace;
|
||||
/**
|
||||
* 文件名
|
||||
*/
|
||||
readonly filename: string;
|
||||
|
||||
/**
|
||||
* 文件类型
|
||||
*/
|
||||
readonly type: FileType;
|
||||
|
||||
/**
|
||||
* 最近修改的时间戳
|
||||
*/
|
||||
lastModified: number;
|
||||
|
||||
/**
|
||||
* 文件解析是否出错
|
||||
*/
|
||||
isError: boolean;
|
||||
|
||||
/**
|
||||
* 文件解析错误消息
|
||||
*/
|
||||
errorMessage: string;
|
||||
|
||||
_code: string;
|
||||
_cleanCode: string;
|
||||
|
||||
get code() {
|
||||
return this._code;
|
||||
}
|
||||
|
||||
// FIXME: cleanCode 是不是只有 viewFile 有 ????
|
||||
get cleanCode() {
|
||||
return this._cleanCode;
|
||||
}
|
||||
|
||||
constructor(workspace: AbstractWorkspace, props: IFileConfig, isSyncCode = true) {
|
||||
this.workspace = workspace;
|
||||
this.filename = props.filename;
|
||||
this.type = props.type;
|
||||
this.lastModified = Date.now();
|
||||
this.isError = false;
|
||||
|
||||
// 这里主要是为了解决 umi ts 编译错误的问题,@see https://github.com/umijs/umi/issues/7594
|
||||
if (isSyncCode) {
|
||||
this.update(props.code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新文件内容
|
||||
*/
|
||||
abstract update(code?: string): void;
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as t from '@babel/types';
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { isNil } from '@music163/tango-helpers';
|
||||
import {
|
||||
code2ast,
|
||||
@@ -9,16 +8,16 @@ import {
|
||||
addImportDeclaration,
|
||||
updateImportDeclaration,
|
||||
} from '../helpers';
|
||||
import { TangoFile } from './file';
|
||||
import { IFileConfig, IImportSpecifierData, ImportDeclarationDataType } from '../types';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import { AbstractFile } from './abstract-file';
|
||||
|
||||
/**
|
||||
* JS 模块实现规范
|
||||
* JS 文件抽象基类
|
||||
* - ast 操纵类方法,统一返回 this,支持外层链式调用
|
||||
* - observable state 统一用 _foo 格式,并提供 getter 方法
|
||||
*/
|
||||
export class TangoModule extends TangoFile {
|
||||
export abstract class AbstractJsFile extends AbstractFile {
|
||||
ast: t.File;
|
||||
|
||||
/**
|
||||
@@ -31,7 +30,7 @@ export class TangoModule extends TangoFile {
|
||||
*/
|
||||
importList: ImportDeclarationDataType;
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig, isSyncCode = true) {
|
||||
constructor(workspace: AbstractWorkspace, props: IFileConfig, isSyncCode = true) {
|
||||
super(workspace, props, isSyncCode);
|
||||
}
|
||||
|
||||
@@ -137,24 +136,3 @@ export class TangoModule extends TangoFile {
|
||||
this.importList = imports;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 普通 JS 文件
|
||||
*/
|
||||
export class TangoJsModule extends TangoModule {
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code, true, false);
|
||||
|
||||
makeObservable(this, {
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
isError: observable,
|
||||
errorMessage: observable,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
update: action,
|
||||
updateAst: action,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { getValue, isNil, logger, setValue } from '@music163/tango-helpers';
|
||||
import type { IFileConfig } from '../types';
|
||||
import { formatCode } from '../helpers';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import { AbstractFile } from './abstract-file';
|
||||
|
||||
export abstract class AbstractJsonFile extends AbstractFile {
|
||||
_object;
|
||||
|
||||
abstract get json(): object;
|
||||
|
||||
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this._object = {};
|
||||
this.update(props.code);
|
||||
}
|
||||
|
||||
update(code?: string) {
|
||||
this.lastModified = Date.now();
|
||||
|
||||
if (isNil(code)) {
|
||||
// 基于最新的 json 同步代码
|
||||
let newCode = JSON.stringify(this._object);
|
||||
try {
|
||||
newCode = formatCode(newCode, 'json');
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return;
|
||||
}
|
||||
this._code = newCode;
|
||||
this._cleanCode = newCode;
|
||||
} else {
|
||||
try {
|
||||
// 基于传入的代码,同步 json 对象
|
||||
code = formatCode(code, 'json');
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return;
|
||||
}
|
||||
this._code = code;
|
||||
this._cleanCode = code;
|
||||
try {
|
||||
const json = JSON.parse(code);
|
||||
this._object = json;
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
}
|
||||
this.workspace.onFilesChange([this.filename]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据路径取值
|
||||
* @param valuePath
|
||||
* @returns
|
||||
*/
|
||||
getValue(valuePath: string) {
|
||||
return getValue(this.json, valuePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据路径设置值
|
||||
* @param valuePath
|
||||
* @param visitor
|
||||
*/
|
||||
setValue(valuePath: string, visitor: (targetValue: any) => any) {
|
||||
const target = this.getValue(valuePath);
|
||||
let next: unknown;
|
||||
if (typeof visitor === 'function') {
|
||||
next = visitor?.(target);
|
||||
} else {
|
||||
next = visitor;
|
||||
}
|
||||
if (next !== undefined) {
|
||||
setValue(this._object, valuePath, next);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据路径删除值
|
||||
* @param valuePath
|
||||
* @param visitor
|
||||
*/
|
||||
deleteValue(valuePath: string) {
|
||||
const pathList = valuePath.split('.');
|
||||
const lastPath = pathList.pop();
|
||||
const parentPath = pathList.join('.');
|
||||
let target;
|
||||
if (parentPath) {
|
||||
target = this.getValue(parentPath);
|
||||
} else {
|
||||
target = this.json;
|
||||
}
|
||||
if (!target) {
|
||||
return this;
|
||||
}
|
||||
delete target[lastPath];
|
||||
if (parentPath) {
|
||||
this.setValue(parentPath, target);
|
||||
} else {
|
||||
this._object = target;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Dict } from '@music163/tango-helpers';
|
||||
import { AbstractFile } from './abstract-file';
|
||||
|
||||
export interface IViewNodeInitConfig<RawNodeType = unknown, ViewFileType = AbstractFile> {
|
||||
id: string;
|
||||
component: string;
|
||||
rawNode: RawNodeType;
|
||||
file: ViewFileType;
|
||||
}
|
||||
|
||||
export abstract class AbstractViewNode<RawNodeType = unknown, ViewFileType = AbstractFile> {
|
||||
/**
|
||||
* 节点 ID
|
||||
*/
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* 节点对应的组件名
|
||||
*/
|
||||
readonly component: string;
|
||||
|
||||
readonly rawNode: RawNodeType;
|
||||
|
||||
/**
|
||||
* 节点所属的文件对象
|
||||
*/
|
||||
file: ViewFileType;
|
||||
|
||||
/**
|
||||
* 节点所属的文件对象
|
||||
*/
|
||||
props: Record<string, any>;
|
||||
|
||||
/**
|
||||
* 节点的位置信息
|
||||
*/
|
||||
abstract get loc(): unknown;
|
||||
|
||||
constructor(props: IViewNodeInitConfig<RawNodeType, ViewFileType>) {
|
||||
this.id = props.id;
|
||||
this.component = props.component;
|
||||
this.rawNode = props.rawNode;
|
||||
this.file = props.file;
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁当前节点,清空文件和节点的关联关系
|
||||
*/
|
||||
destroy() {
|
||||
this.file = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回克隆后的 ast 节点
|
||||
* @param overrideProps 额外设置给克隆节点的属性
|
||||
* @returns 返回克隆的原始节点
|
||||
*/
|
||||
abstract cloneRawNode(overrideProps?: Dict): RawNodeType;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { MenuDataType } from '@music163/tango-helpers';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
|
||||
export type SimulatorNameType = 'desktop' | 'phone';
|
||||
|
||||
@@ -18,7 +18,7 @@ interface IViewportBounding {
|
||||
}
|
||||
|
||||
interface IDesignerOptions {
|
||||
workspace: IWorkspace;
|
||||
workspace: AbstractWorkspace;
|
||||
simulator?: SimulatorNameType | ISimulatorType;
|
||||
/**
|
||||
* 菜单配置
|
||||
@@ -108,7 +108,7 @@ export class Designer {
|
||||
*/
|
||||
_menuData?: MenuDataType = null;
|
||||
|
||||
private readonly workspace: IWorkspace;
|
||||
private readonly workspace: AbstractWorkspace;
|
||||
|
||||
get simulator(): ISimulatorType {
|
||||
return toJS(this._simulator);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { ISelectedItemData } from '@music163/tango-helpers';
|
||||
import { DropTarget } from './drop-target';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
|
||||
/**
|
||||
* 拖拽来源类,被拖拽的物体
|
||||
@@ -22,7 +22,7 @@ export class DragSource {
|
||||
*/
|
||||
dropTarget: DropTarget;
|
||||
|
||||
private readonly workspace: IWorkspace;
|
||||
private readonly workspace: AbstractWorkspace;
|
||||
|
||||
get node() {
|
||||
return this.workspace.getNode(this.data?.id, this.data?.filename);
|
||||
@@ -47,7 +47,7 @@ export class DragSource {
|
||||
return this.data?.bounding;
|
||||
}
|
||||
|
||||
constructor(workspace: IWorkspace) {
|
||||
constructor(workspace: AbstractWorkspace) {
|
||||
this.workspace = workspace;
|
||||
this.data = null;
|
||||
this.isDragging = false;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { ISelectedItemData } from '@music163/tango-helpers';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
|
||||
export enum DropMethod {
|
||||
ReplaceNode = 'replaceNode', // 替换节点
|
||||
@@ -23,7 +23,7 @@ export class DropTarget {
|
||||
*/
|
||||
data: ISelectedItemData;
|
||||
|
||||
private readonly workspace: IWorkspace;
|
||||
private readonly workspace: AbstractWorkspace;
|
||||
|
||||
get node() {
|
||||
return this.workspace.getNode(this.data.id, this.data.filename);
|
||||
@@ -48,7 +48,7 @@ export class DropTarget {
|
||||
return this.data?.display;
|
||||
}
|
||||
|
||||
constructor(workspace: IWorkspace) {
|
||||
constructor(workspace: AbstractWorkspace) {
|
||||
this.workspace = workspace;
|
||||
this.method = DropMethod.InsertAfter;
|
||||
this.data = null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import { Designer } from './designer';
|
||||
import { IWorkspace } from './interfaces';
|
||||
|
||||
/**
|
||||
* 设计器引擎
|
||||
@@ -8,7 +8,7 @@ export class Engine {
|
||||
/**
|
||||
* 工作区状态
|
||||
*/
|
||||
workspace: IWorkspace;
|
||||
workspace: AbstractWorkspace;
|
||||
/**
|
||||
* 设计器状态
|
||||
*/
|
||||
|
||||
@@ -1,66 +1,22 @@
|
||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||
import { getValue, isNil, logger, setValue } from '@music163/tango-helpers';
|
||||
import type { FileType, IFileConfig } from '../types';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { formatCode } from '../helpers';
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { isNil } from '@music163/tango-helpers';
|
||||
import type { IFileConfig } from '../types';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import { AbstractFile } from './abstract-file';
|
||||
|
||||
/**
|
||||
* 普通文件,不进行 AST 解析
|
||||
*/
|
||||
export class TangoFile {
|
||||
readonly workspace: IWorkspace;
|
||||
/**
|
||||
* 文件名
|
||||
*/
|
||||
readonly filename: string;
|
||||
|
||||
/**
|
||||
* 文件类型
|
||||
*/
|
||||
readonly type: FileType;
|
||||
|
||||
/**
|
||||
* 最近修改的时间戳
|
||||
*/
|
||||
lastModified: number;
|
||||
|
||||
/**
|
||||
* 文件解析是否出错
|
||||
*/
|
||||
isError: boolean;
|
||||
|
||||
/**
|
||||
* 文件解析错误消息
|
||||
*/
|
||||
errorMessage: string;
|
||||
|
||||
_code: string;
|
||||
_cleanCode: string;
|
||||
|
||||
get code() {
|
||||
return this._code;
|
||||
export class TangoFile extends AbstractFile {
|
||||
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code);
|
||||
makeObservable(this, {
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
update: action,
|
||||
});
|
||||
}
|
||||
|
||||
get cleanCode() {
|
||||
return this._cleanCode;
|
||||
}
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig, isSyncCode = true) {
|
||||
this.workspace = workspace;
|
||||
this.filename = props.filename;
|
||||
this.type = props.type;
|
||||
this.lastModified = Date.now();
|
||||
this.isError = false;
|
||||
|
||||
// 这里主要是为了解决 umi ts 编译错误的问题,@see https://github.com/umijs/umi/issues/7594
|
||||
if (isSyncCode) {
|
||||
this.update(props.code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新文件内容
|
||||
*/
|
||||
update(code?: string) {
|
||||
if (!isNil(code)) {
|
||||
this.lastModified = Date.now();
|
||||
@@ -70,137 +26,3 @@ export class TangoFile {
|
||||
this.workspace.onFilesChange([this.filename]);
|
||||
}
|
||||
}
|
||||
|
||||
export class TangoLessFile extends TangoFile {
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code);
|
||||
makeObservable(this, {
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
update: action,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class TangoJsonFile extends TangoFile {
|
||||
_object = {};
|
||||
|
||||
/**
|
||||
* @deprecated 使用 file.json 代替
|
||||
*/
|
||||
get object() {
|
||||
return toJS(this._object);
|
||||
}
|
||||
|
||||
get json() {
|
||||
return toJS(this._object);
|
||||
}
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code);
|
||||
makeObservable(this, {
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
_object: observable,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
object: computed,
|
||||
json: computed,
|
||||
update: action,
|
||||
setValue: action,
|
||||
});
|
||||
}
|
||||
|
||||
update(code?: string) {
|
||||
this.lastModified = Date.now();
|
||||
|
||||
if (isNil(code)) {
|
||||
// 基于最新的 json 同步代码
|
||||
let newCode = JSON.stringify(this._object);
|
||||
try {
|
||||
newCode = formatCode(newCode, 'json');
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return;
|
||||
}
|
||||
this._code = newCode;
|
||||
this._cleanCode = newCode;
|
||||
} else {
|
||||
try {
|
||||
// 基于传入的代码,同步 json 对象
|
||||
code = formatCode(code, 'json');
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return;
|
||||
}
|
||||
this._code = code;
|
||||
this._cleanCode = code;
|
||||
try {
|
||||
const json = JSON.parse(code);
|
||||
this._object = json;
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
}
|
||||
this.workspace.onFilesChange([this.filename]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据路径取值
|
||||
* @param valuePath
|
||||
* @returns
|
||||
*/
|
||||
getValue(valuePath: string) {
|
||||
return getValue(this.json, valuePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据路径设置值
|
||||
* @param valuePath
|
||||
* @param visitor
|
||||
*/
|
||||
setValue(valuePath: string, visitor: (targetValue: any) => any) {
|
||||
const target = this.getValue(valuePath);
|
||||
let next: unknown;
|
||||
if (typeof visitor === 'function') {
|
||||
next = visitor?.(target);
|
||||
} else {
|
||||
next = visitor;
|
||||
}
|
||||
if (next !== undefined) {
|
||||
setValue(this._object, valuePath, next);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据路径删除值
|
||||
* @param valuePath
|
||||
* @param visitor
|
||||
*/
|
||||
deleteValue(valuePath: string) {
|
||||
const pathList = valuePath.split('.');
|
||||
const lastPath = pathList.pop();
|
||||
const parentPath = pathList.join('.');
|
||||
let target;
|
||||
if (parentPath) {
|
||||
target = this.getValue(parentPath);
|
||||
} else {
|
||||
target = this.json;
|
||||
}
|
||||
if (!target) {
|
||||
return this;
|
||||
}
|
||||
delete target[lastPath];
|
||||
if (parentPath) {
|
||||
this.setValue(parentPath, target);
|
||||
} else {
|
||||
this._object = target;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
|
||||
export enum HistoryMessage {
|
||||
InitView = 'initView',
|
||||
@@ -43,7 +43,7 @@ export class TangoHistory {
|
||||
// 最多记录数
|
||||
_maxSize = 100;
|
||||
|
||||
private readonly workspace: IWorkspace;
|
||||
private readonly workspace: AbstractWorkspace;
|
||||
|
||||
get index() {
|
||||
return this._index;
|
||||
@@ -65,7 +65,7 @@ export class TangoHistory {
|
||||
return this._records.length > this._index + 1;
|
||||
}
|
||||
|
||||
constructor(workspace: IWorkspace) {
|
||||
constructor(workspace: AbstractWorkspace) {
|
||||
this.workspace = workspace;
|
||||
|
||||
makeObservable(this, {
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
export * from './engine';
|
||||
export * from './workspace';
|
||||
export * from './abstract-workspace';
|
||||
export * from './abstract-code-workspace';
|
||||
export * from './abstract-file';
|
||||
export * from './abstract-js-file';
|
||||
export * from './abstract-json-file';
|
||||
export * from './abstract-view-node';
|
||||
export * from './designer';
|
||||
export * from './drop-target';
|
||||
export * from './select-source';
|
||||
export * from './drag-source';
|
||||
export * from './history';
|
||||
export * from './drop-target';
|
||||
export * from './engine';
|
||||
export * from './file';
|
||||
export * from './module';
|
||||
export * from './entry-module';
|
||||
export * from './route-module';
|
||||
export * from './service-module';
|
||||
export * from './store-module';
|
||||
export * from './view-module';
|
||||
export * from './component-module';
|
||||
export * from './node';
|
||||
export * from './history';
|
||||
export * from './interfaces';
|
||||
export * from './js-app-entry-file';
|
||||
export * from './js-file';
|
||||
export * from './js-local-components-entry-file';
|
||||
export * from './js-route-config-file';
|
||||
export * from './js-service-file';
|
||||
export * from './js-store-entry-file';
|
||||
export * from './js-store-file';
|
||||
export * from './js-view-file';
|
||||
export * from './json-file';
|
||||
export * from './select-source';
|
||||
export * from './view-node';
|
||||
export * from './workspace';
|
||||
|
||||
@@ -1,28 +1,17 @@
|
||||
import { IComponentPrototype, Dict, ITangoConfigJson } from '@music163/tango-helpers';
|
||||
import { TangoHistory } from './history';
|
||||
import { SelectSource } from './select-source';
|
||||
import { DragSource } from './drag-source';
|
||||
import { Dict } from '@music163/tango-helpers';
|
||||
import {
|
||||
IFileConfig,
|
||||
FileType,
|
||||
InsertChildPositionType,
|
||||
ITangoConfigPackages,
|
||||
IPageConfigData,
|
||||
IImportSpecifierSourceData,
|
||||
IImportSpecifierData,
|
||||
IFileError,
|
||||
} from '../types';
|
||||
import { TangoFile, TangoJsonFile } from './file';
|
||||
import { TangoRouteModule } from './route-module';
|
||||
import { TangoStoreModule } from './store-module';
|
||||
import { TangoServiceModule } from './service-module';
|
||||
import { IdGenerator } from '../helpers';
|
||||
import { AppEntryModule } from './entry-module';
|
||||
import { AbstractViewNode } from './abstract-view-node';
|
||||
|
||||
export interface IViewFile {
|
||||
readonly workspace: IWorkspace;
|
||||
readonly filename: string;
|
||||
readonly type: FileType;
|
||||
/**
|
||||
* 文件名
|
||||
*/
|
||||
filename: string;
|
||||
|
||||
/**
|
||||
* ID 生成器
|
||||
@@ -55,267 +44,38 @@ export interface IViewFile {
|
||||
*/
|
||||
addImportSpecifiers: (source: string, newSpecifiers: IImportSpecifierData[]) => IViewFile;
|
||||
|
||||
getNode: (targetNodeId: string) => IViewNode;
|
||||
getNode: (targetNodeId: string) => AbstractViewNode;
|
||||
|
||||
removeNode: (targetNodeId: string) => IViewFile;
|
||||
removeNode: (targetNodeId: string) => this;
|
||||
|
||||
insertChild: (
|
||||
targetNodeId: string,
|
||||
newNode: any,
|
||||
position?: InsertChildPositionType,
|
||||
) => IViewFile;
|
||||
insertChild: (targetNodeId: string, newNode: any, position?: InsertChildPositionType) => this;
|
||||
|
||||
insertAfter: (targetNodeId: string, newNode: any) => IViewFile;
|
||||
insertAfter: (targetNodeId: string, newNode: any) => this;
|
||||
|
||||
insertBefore: (targetNodeId: string, newNode: any) => IViewFile;
|
||||
insertBefore: (targetNodeId: string, newNode: any) => this;
|
||||
|
||||
replaceNode: (targetNodeId: string, newNode: any) => IViewFile;
|
||||
replaceNode: (targetNodeId: string, newNode: any) => this;
|
||||
|
||||
replaceViewChildren: (rawNodes: any[]) => IViewFile;
|
||||
replaceViewChildren: (rawNodes: any[]) => this;
|
||||
|
||||
updateNodeAttribute: (
|
||||
nodeId: string,
|
||||
attrName: string,
|
||||
attrValue?: any,
|
||||
relatedImports?: string[],
|
||||
) => IViewFile;
|
||||
) => this;
|
||||
|
||||
updateNodeAttributes: (
|
||||
nodeId: string,
|
||||
config: Record<string, any>,
|
||||
relatedImports?: string[],
|
||||
) => IViewFile;
|
||||
) => this;
|
||||
|
||||
get code(): string;
|
||||
get nodes(): Map<string, IViewNode>;
|
||||
get nodes(): Map<string, AbstractViewNode>;
|
||||
get nodesTree(): object[];
|
||||
get tree(): any;
|
||||
}
|
||||
|
||||
export interface IViewNode {
|
||||
/**
|
||||
* 所属的文件
|
||||
*/
|
||||
file: IViewFile;
|
||||
|
||||
/**
|
||||
* 节点 ID
|
||||
*/
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* 对应的组件
|
||||
*/
|
||||
readonly component: string;
|
||||
|
||||
/**
|
||||
* 原始节点对象
|
||||
*/
|
||||
readonly rawNode: unknown;
|
||||
|
||||
/**
|
||||
* 属性集合
|
||||
*/
|
||||
readonly props: Record<string, any>;
|
||||
|
||||
/**
|
||||
* 克隆原始节点
|
||||
* @param overrideProps 额外设置给克隆节点的属性
|
||||
* @returns
|
||||
*/
|
||||
cloneRawNode: (overrideProps?: Dict) => unknown;
|
||||
|
||||
/**
|
||||
* 销毁节点
|
||||
* @returns
|
||||
*/
|
||||
destroy: () => void;
|
||||
|
||||
/**
|
||||
* 原始节点的位置信息
|
||||
*/
|
||||
get loc(): unknown;
|
||||
}
|
||||
|
||||
export interface IWorkspace {
|
||||
history: TangoHistory;
|
||||
selectSource: SelectSource;
|
||||
dragSource: DragSource;
|
||||
|
||||
files: Map<string, TangoFile>;
|
||||
componentPrototypes: Map<string, IComponentPrototype>;
|
||||
|
||||
entry: string;
|
||||
activeFile: string;
|
||||
activeViewFile: string;
|
||||
activeRoute: string;
|
||||
|
||||
/**
|
||||
* 解析后的 tango.config.json 文件,如果要获取项目配置,推荐使用 projectConfig 获取
|
||||
*/
|
||||
tangoConfigJson: TangoJsonFile;
|
||||
/**
|
||||
* app.js 入口文件解析后的模块
|
||||
*/
|
||||
appEntryModule: AppEntryModule;
|
||||
/**
|
||||
* 解析后的路由模块
|
||||
*/
|
||||
routeModule?: TangoRouteModule;
|
||||
/**
|
||||
* 解析后的状态管理模块 Map
|
||||
*/
|
||||
storeModules?: Record<string, TangoStoreModule>;
|
||||
/**
|
||||
* 解析后的服务模块 Map
|
||||
*/
|
||||
serviceModules?: Record<string, TangoServiceModule>;
|
||||
|
||||
ready: () => void;
|
||||
refresh: (names: string[]) => void;
|
||||
|
||||
setActiveRoute: (path: string) => void;
|
||||
setActiveFile: (filename: string) => void;
|
||||
|
||||
setComponentPrototypes: (prototypes: Record<string, IComponentPrototype>) => void;
|
||||
getPrototype: (name: string | IComponentPrototype) => IComponentPrototype;
|
||||
|
||||
// ----------------- 文件操作 -----------------
|
||||
addFiles: (files: IFileConfig[]) => void;
|
||||
addFile: (filename: string, code: string, fileType?: FileType) => void;
|
||||
|
||||
addServiceFile: (serviceName: string, code: string) => void;
|
||||
addStoreFile: (storeName: string, code: string) => void;
|
||||
addViewFile: (viewName: string, code: string) => void;
|
||||
|
||||
removeFile: (filename: string) => void;
|
||||
|
||||
renameFile: (oldFilename: string, newFilename: string) => void;
|
||||
renameFolder: (oldFoldername: string, newFoldername: string) => void;
|
||||
|
||||
/**
|
||||
* 更新文件
|
||||
* @param filename 文件名
|
||||
* @param code 代码
|
||||
* @param isSyncAst 是否同步 ast
|
||||
*/
|
||||
updateFile: (filename: string, code: string, isSyncAst?: boolean) => void;
|
||||
|
||||
/**
|
||||
* 检查并同步文件的 ast
|
||||
*/
|
||||
syncFiles: () => void;
|
||||
|
||||
listFiles: () => Record<string, string>;
|
||||
getFile: (filename: string) => TangoFile;
|
||||
|
||||
/**
|
||||
* 文件变化回调
|
||||
* @param filenames 文件名列表
|
||||
*/
|
||||
onFilesChange: (filenames: string[]) => void;
|
||||
|
||||
// ----------------- 节点操作 -----------------
|
||||
|
||||
removeSelectedNode: () => void;
|
||||
cloneSelectedNode: () => void;
|
||||
copySelectedNode: () => void;
|
||||
pasteSelectedNode: () => void;
|
||||
insertToSelectedNode: (childNameOrPrototype: string | IComponentPrototype) => void;
|
||||
insertBeforeSelectedNode: (sourceNameOrPrototype: string | IComponentPrototype) => void;
|
||||
insertAfterSelectedNode: (sourceNameOrPrototype: string | IComponentPrototype) => void;
|
||||
dropNode: () => void;
|
||||
insertToNode: (targetNodeId: string, sourceNameOrPrototype: string | IComponentPrototype) => void;
|
||||
replaceNode: (targetNodeId: string, sourceNameOrPrototype: string | IComponentPrototype) => void;
|
||||
updateSelectedNodeAttributes: (
|
||||
attributes: Record<string, any>,
|
||||
relatedImports?: string[],
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* 查询节点
|
||||
* @param id 节点 ID
|
||||
* @param module 节点所在的模块名
|
||||
* @returns 返回节点对象
|
||||
*/
|
||||
getNode: (id: string, module?: string) => IViewNode;
|
||||
|
||||
// ----------------- 服务函数文件操作 -----------------
|
||||
|
||||
getServiceFunction?: (serviceKey: string) => {
|
||||
name: string;
|
||||
moduleName: string;
|
||||
config: Dict<object>;
|
||||
};
|
||||
listServiceFunctions?: () => Dict<object>;
|
||||
removeServiceFunction?: (serviceKey: string) => void;
|
||||
addServiceFunction?: (serviceName: string, config: Dict, modName?: string) => void;
|
||||
addServiceFunctions?: (configs: Dict<object>, modName?: string) => void;
|
||||
updateServiceFunction?: (serviceName: string, payload: Dict, modName?: string) => void;
|
||||
updateServiceBaseConfig?: (config: Dict, modName?: string) => void;
|
||||
|
||||
// ----------------- 状态管理文件操作 -----------------
|
||||
addStoreState?: (storeName: string, stateName: string, initValue: string) => void;
|
||||
removeStoreModule?: (storeName: string) => void;
|
||||
removeStoreVariable?: (variablePath: string) => void;
|
||||
updateStoreVariable?: (variablePath: string, code: string) => void;
|
||||
|
||||
// ----------------- 视图文件操作 -----------------
|
||||
|
||||
removeViewModule: (routePath: string) => void;
|
||||
copyViewPage: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
|
||||
|
||||
// ----------------- 路由文件操作 -----------------
|
||||
|
||||
updateRoute: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
|
||||
|
||||
// ----------------- 依赖包操作 -----------------
|
||||
|
||||
addDependency?: (data: any) => void;
|
||||
listDependencies?: () => any;
|
||||
getDependency?: (pkgName: string) => object;
|
||||
|
||||
updateDependency?: (
|
||||
name: string,
|
||||
version: string,
|
||||
options?: {
|
||||
package?: ITangoConfigPackages;
|
||||
[x: string]: any;
|
||||
},
|
||||
) => void;
|
||||
|
||||
removeDependency?: (name: string) => void;
|
||||
|
||||
addBizComp?: (
|
||||
name: string,
|
||||
version: string,
|
||||
options?: {
|
||||
package?: ITangoConfigPackages;
|
||||
[x: string]: any;
|
||||
},
|
||||
) => void;
|
||||
|
||||
removeBizComp?: (name: string) => void;
|
||||
|
||||
// ----------------- getter -----------------
|
||||
/**
|
||||
* 解析后的项目配置信息
|
||||
*/
|
||||
get projectConfig(): ITangoConfigJson;
|
||||
/**
|
||||
* 当前活动的视图文件
|
||||
*/
|
||||
get activeViewModule(): IViewFile;
|
||||
get pages(): any[];
|
||||
get bizComps(): string[];
|
||||
get baseComps(): string[];
|
||||
get localComps(): string[];
|
||||
get fileErrors(): IFileError[];
|
||||
/**
|
||||
* 是否是有效的项目
|
||||
* - 包含 tango.config.json
|
||||
* - 包含视图模块
|
||||
* - 没有文件错误
|
||||
*/
|
||||
get isValid(): boolean;
|
||||
/**
|
||||
* 文件中的代码
|
||||
*/
|
||||
get code(): string;
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,12 +1,12 @@
|
||||
import { traverseEntryFile } from '../helpers';
|
||||
import { IFileConfig } from '../types';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { TangoModule } from './module';
|
||||
import { AbstractJsFile } from './abstract-js-file';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
|
||||
export class AppEntryModule extends TangoModule {
|
||||
export class JsAppEntryFile extends AbstractJsFile {
|
||||
routerType: string;
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code, true, false);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { IFileConfig } from '../types';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import { AbstractJsFile } from './abstract-js-file';
|
||||
|
||||
/**
|
||||
* 普通 JS 文件
|
||||
*/
|
||||
export class JsFile extends AbstractJsFile {
|
||||
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code, true, false);
|
||||
|
||||
makeObservable(this, {
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
isError: observable,
|
||||
errorMessage: observable,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
update: action,
|
||||
updateAst: action,
|
||||
});
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -1,17 +1,17 @@
|
||||
import path from 'path';
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { TangoModule } from './module';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { IExportSpecifierData, IFileConfig } from '../types';
|
||||
import { traverseComponentsEntryFile } from '../helpers';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import { AbstractJsFile } from './abstract-js-file';
|
||||
|
||||
/**
|
||||
* 本地组件目录的入口文件,例如 '/components/index.js' 或 `/blocks/index.js`
|
||||
*/
|
||||
export class TangoComponentsEntryModule extends TangoModule {
|
||||
export class JsLocalComponentsEntryFile extends AbstractJsFile {
|
||||
exportList: Record<string, IExportSpecifierData>;
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code, true, false);
|
||||
makeObservable(this, {
|
||||
+4
-4
@@ -7,20 +7,20 @@ import {
|
||||
updateRouteToRouteFile,
|
||||
} from '../helpers';
|
||||
import { IRouteData, IFileConfig } from '../types';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { TangoModule } from './module';
|
||||
import { AbstractJsFile } from './abstract-js-file';
|
||||
import { AbstractCodeWorkspace } from './abstract-code-workspace';
|
||||
|
||||
/**
|
||||
* 路由配置模块
|
||||
*/
|
||||
export class TangoRouteModule extends TangoModule {
|
||||
export class JsRouteConfigFile extends AbstractJsFile {
|
||||
_routes: IRouteData[];
|
||||
|
||||
get routes() {
|
||||
return toJS(this._routes);
|
||||
}
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
constructor(workspace: AbstractCodeWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code, true, false);
|
||||
|
||||
+4
-4
@@ -8,13 +8,13 @@ import {
|
||||
updateBaseConfigToServiceFile,
|
||||
} from '../helpers';
|
||||
import { IFileConfig } from '../types';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { TangoModule } from './module';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import { AbstractJsFile } from './abstract-js-file';
|
||||
|
||||
/**
|
||||
* 数据服务模块
|
||||
*/
|
||||
export class TangoServiceModule extends TangoModule {
|
||||
export class JsServiceFile extends AbstractJsFile {
|
||||
/**
|
||||
* 服务函数的模块名,默认为 index
|
||||
*/
|
||||
@@ -39,7 +39,7 @@ export class TangoServiceModule extends TangoModule {
|
||||
return toJS(this._baseConfig);
|
||||
}
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.name = getModuleNameByFilename(props.filename);
|
||||
this.update(props.code, true, false);
|
||||
@@ -0,0 +1,56 @@
|
||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||
import { traverseStoreEntryFile, addStoreToEntryFile, removeStoreToEntryFile } from '../helpers';
|
||||
import { IFileConfig } from '../types';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import { AbstractJsFile } from './abstract-js-file';
|
||||
|
||||
/**
|
||||
* stores 入口文件
|
||||
*/
|
||||
export class JsStoreEntryFile extends AbstractJsFile {
|
||||
_stores: string[] = [];
|
||||
|
||||
get stores() {
|
||||
return toJS(this._stores);
|
||||
}
|
||||
|
||||
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code, true, false);
|
||||
|
||||
makeObservable(this, {
|
||||
_stores: observable,
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
isError: observable,
|
||||
errorMessage: observable,
|
||||
stores: computed,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
update: action,
|
||||
updateAst: action,
|
||||
});
|
||||
}
|
||||
|
||||
_analysisAst() {
|
||||
this._stores = traverseStoreEntryFile(this.ast);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建模型
|
||||
* @param name
|
||||
*/
|
||||
addStore(name: string) {
|
||||
this.ast = addStoreToEntryFile(this.ast, name);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除模型
|
||||
* @param name
|
||||
*/
|
||||
removeStore(name: string) {
|
||||
this.ast = removeStoreToEntryFile(this.ast, name);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
+5
-59
@@ -1,73 +1,19 @@
|
||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import {
|
||||
traverseStoreFile,
|
||||
traverseStoreEntryFile,
|
||||
addStoreToEntryFile,
|
||||
getModuleNameByFilename,
|
||||
addStoreState,
|
||||
updateStoreState,
|
||||
removeStoreState,
|
||||
removeStoreToEntryFile,
|
||||
} from '../helpers';
|
||||
import { IFileConfig, IStorePropertyData } from '../types';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { TangoModule } from './module';
|
||||
|
||||
/**
|
||||
* 入口配置模块
|
||||
*/
|
||||
export class TangoStoreEntryModule extends TangoModule {
|
||||
_stores: string[] = [];
|
||||
|
||||
get stores() {
|
||||
return toJS(this._stores);
|
||||
}
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code, true, false);
|
||||
|
||||
makeObservable(this, {
|
||||
_stores: observable,
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
isError: observable,
|
||||
errorMessage: observable,
|
||||
stores: computed,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
update: action,
|
||||
updateAst: action,
|
||||
});
|
||||
}
|
||||
|
||||
_analysisAst() {
|
||||
this._stores = traverseStoreEntryFile(this.ast);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建模型
|
||||
* @param name
|
||||
*/
|
||||
addStore(name: string) {
|
||||
this.ast = addStoreToEntryFile(this.ast, name);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除模型
|
||||
* @param name
|
||||
*/
|
||||
removeStore(name: string) {
|
||||
this.ast = removeStoreToEntryFile(this.ast, name);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import { AbstractJsFile } from './abstract-js-file';
|
||||
|
||||
/**
|
||||
* 状态模型模块
|
||||
*/
|
||||
export class TangoStoreModule extends TangoModule {
|
||||
export class JsStoreFile extends AbstractJsFile {
|
||||
/**
|
||||
* 模块名
|
||||
*/
|
||||
@@ -79,7 +25,7 @@ export class TangoStoreModule extends TangoModule {
|
||||
|
||||
actions: IStorePropertyData[];
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.name = getModuleNameByFilename(props.filename);
|
||||
this.update(props.code, true, false);
|
||||
+15
-12
@@ -20,18 +20,19 @@ import {
|
||||
addImportDeclarationLegacy,
|
||||
updateImportDeclarationLegacy,
|
||||
} from '../helpers';
|
||||
import { TangoNode } from './node';
|
||||
import { JsxViewNode } from './view-node';
|
||||
import {
|
||||
IFileConfig,
|
||||
ITangoViewNodeData,
|
||||
IViewNodeData,
|
||||
IImportDeclarationPayload,
|
||||
InsertChildPositionType,
|
||||
IImportSpecifierSourceData,
|
||||
ImportDeclarationDataType,
|
||||
IImportSpecifierData,
|
||||
} from '../types';
|
||||
import { IViewFile, IWorkspace } from './interfaces';
|
||||
import { TangoModule } from './module';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import { IViewFile } from './interfaces';
|
||||
import { AbstractJsFile } from './abstract-js-file';
|
||||
|
||||
/**
|
||||
* 导入信息转为 变量名->来源 的 map 结构
|
||||
@@ -56,8 +57,8 @@ function buildImportMap(importedModules: ImportDeclarationDataType) {
|
||||
* 将节点列表转换为 tree data 嵌套数组
|
||||
* @param list
|
||||
*/
|
||||
function nodeListToTreeData(list: ITangoViewNodeData[]) {
|
||||
const map: Record<string, ITangoViewNodeData> = {};
|
||||
function nodeListToTreeData(list: IViewNodeData[]) {
|
||||
const map: Record<string, IViewNodeData> = {};
|
||||
|
||||
list.forEach((item) => {
|
||||
// 如果不存在,则初始化
|
||||
@@ -82,9 +83,9 @@ function nodeListToTreeData(list: ITangoViewNodeData[]) {
|
||||
/**
|
||||
* 视图模块
|
||||
*/
|
||||
export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
export class JsViewFile extends AbstractJsFile implements IViewFile {
|
||||
// 解析为树结构的 jsxNodes 数组
|
||||
_nodesTree: ITangoViewNodeData[] = [];
|
||||
_nodesTree: IViewNodeData[] = [];
|
||||
/**
|
||||
* 通过导入组件名查找组件来自的包
|
||||
*/
|
||||
@@ -108,7 +109,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
/**
|
||||
* 节点列表 <id, Node>
|
||||
*/
|
||||
private _nodes: Map<string, TangoNode>;
|
||||
private _nodes: Map<string, JsxViewNode>;
|
||||
/**
|
||||
* 导入的模块
|
||||
* @deprecated
|
||||
@@ -127,7 +128,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
return this.ast;
|
||||
}
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this._nodes = new Map();
|
||||
this.idGenerator = new IdGenerator({ prefix: props.filename });
|
||||
@@ -176,8 +177,10 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
this._codeIdList = [];
|
||||
|
||||
nodes.forEach((cur) => {
|
||||
const node = new TangoNode({
|
||||
...cur,
|
||||
const node = new JsxViewNode({
|
||||
id: cur.id,
|
||||
component: cur.component,
|
||||
rawNode: cur.rawNode,
|
||||
file: this,
|
||||
});
|
||||
this._nodes.set(cur.id, node);
|
||||
@@ -0,0 +1,24 @@
|
||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||
import type { IFileConfig } from '../types';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import { AbstractJsonFile } from './abstract-json-file';
|
||||
|
||||
export class JsonFile extends AbstractJsonFile {
|
||||
get json(): object {
|
||||
return toJS(this._object);
|
||||
}
|
||||
|
||||
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
|
||||
super(workspace, props);
|
||||
makeObservable(this, {
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
_object: observable,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
json: computed,
|
||||
update: action,
|
||||
setValue: action,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import { JSXElement, SourceLocation } from '@babel/types';
|
||||
import { Dict } from '@music163/tango-helpers';
|
||||
import { cloneJSXElement, getJSXElementAttributes } from '../helpers';
|
||||
import { ITangoViewNodeData } from '../types';
|
||||
import { TangoViewModule } from './view-module';
|
||||
import { IViewNode } from './interfaces';
|
||||
|
||||
type TangoNodeConstructorPropsType = ITangoViewNodeData & {
|
||||
file: TangoViewModule;
|
||||
};
|
||||
|
||||
/**
|
||||
* 视图节点类
|
||||
*/
|
||||
export class TangoNode implements IViewNode {
|
||||
/**
|
||||
* 节点 ID
|
||||
*/
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* 节点对应的组件名
|
||||
*/
|
||||
readonly component: string;
|
||||
|
||||
readonly rawNode: JSXElement;
|
||||
|
||||
/**
|
||||
* 节点所属的文件对象
|
||||
*/
|
||||
file: TangoViewModule;
|
||||
|
||||
props: Record<string, any>;
|
||||
|
||||
get loc(): SourceLocation {
|
||||
return this.rawNode?.loc;
|
||||
}
|
||||
|
||||
constructor(props: TangoNodeConstructorPropsType) {
|
||||
this.file = props.file;
|
||||
this.id = props.id;
|
||||
this.component = props.component;
|
||||
this.rawNode = props.rawNode;
|
||||
this.props = getJSXElementAttributes(cloneJSXElement(props.rawNode));
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回克隆后的 ast 节点
|
||||
* @param overrideProps 额外设置给克隆节点的属性
|
||||
* @returns
|
||||
*/
|
||||
cloneRawNode(overrideProps?: Dict) {
|
||||
return cloneJSXElement(this.rawNode, overrideProps);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空节点的指向,交给 GC 去回收
|
||||
*/
|
||||
destroy() {
|
||||
this.file = null;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ISelectedItemData, MousePoint } from '@music163/tango-helpers';
|
||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||
import { IViewFile, IWorkspace } from './interfaces';
|
||||
import { AbstractWorkspace } from './abstract-workspace';
|
||||
import { IViewFile } from './interfaces';
|
||||
|
||||
type StartDataType = {
|
||||
point: MousePoint;
|
||||
@@ -24,7 +25,7 @@ export class SelectSource {
|
||||
element: null,
|
||||
};
|
||||
|
||||
private readonly workspace: IWorkspace;
|
||||
private readonly workspace: AbstractWorkspace;
|
||||
|
||||
get start() {
|
||||
return toJS(this._start);
|
||||
@@ -74,7 +75,7 @@ export class SelectSource {
|
||||
.filter((node) => !!node);
|
||||
}
|
||||
|
||||
constructor(workspace: IWorkspace) {
|
||||
constructor(workspace: AbstractWorkspace) {
|
||||
this.workspace = workspace;
|
||||
makeObservable(this, {
|
||||
_items: observable,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { JSXElement } from '@babel/types';
|
||||
import { Dict } from '@music163/tango-helpers';
|
||||
import { cloneJSXElement, getJSXElementAttributes } from '../helpers';
|
||||
import { JsViewFile } from './js-view-file';
|
||||
import { AbstractViewNode, IViewNodeInitConfig } from './abstract-view-node';
|
||||
|
||||
/**
|
||||
* 视图节点类
|
||||
*/
|
||||
export class JsxViewNode extends AbstractViewNode<JSXElement, JsViewFile> {
|
||||
get loc() {
|
||||
return this.rawNode?.loc;
|
||||
}
|
||||
|
||||
constructor(props: IViewNodeInitConfig<JSXElement, JsViewFile>) {
|
||||
super(props);
|
||||
this.props = getJSXElementAttributes(cloneJSXElement(props.rawNode));
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回克隆后的 ast 节点
|
||||
* @param overrideProps 额外设置给克隆节点的属性
|
||||
* @returns
|
||||
*/
|
||||
cloneRawNode(overrideProps?: Dict) {
|
||||
return cloneJSXElement(this.rawNode, overrideProps);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+19
-31
@@ -6,40 +6,28 @@ export type SimulatorMode = 'desktop' | 'tablet' | 'phone';
|
||||
* 文件类型枚举
|
||||
*/
|
||||
export enum FileType {
|
||||
// js 文件
|
||||
Module = 'module',
|
||||
AppEntryModule = 'appEntryModule',
|
||||
StoreEntryModule = 'storeEntryModule',
|
||||
RouteModule = 'routeModule',
|
||||
ServiceModule = 'serviceModule',
|
||||
StoreModule = 'storeModule',
|
||||
File = 'file',
|
||||
|
||||
JsFile = 'jsFile',
|
||||
JsAppEntryFile = 'jsAppEntryFile',
|
||||
JsRouteConfigFile = 'jsRouteConfigFile',
|
||||
JsStoreEntryFile = 'jsStoreEntryFile',
|
||||
JsStoreFile = 'jsStoreFile',
|
||||
JsServiceFile = 'jsServiceFile',
|
||||
JsLocalComponentsEntryFile = 'jsLocalComponentsEntryFile',
|
||||
|
||||
JsViewFile = 'jsViewFile',
|
||||
JsonViewFile = 'jsonViewFile',
|
||||
|
||||
JsonFile = 'jsonFile',
|
||||
PackageJsonFile = 'packageJsonFile',
|
||||
TangoConfigJsonFile = 'tangoConfigJsonFile',
|
||||
AppJsonFile = 'appJsonFile',
|
||||
|
||||
// 组件配置文件
|
||||
ComponentPrototypeModule = 'componentPrototypeModule',
|
||||
// 组件运行调试入口文件,一般为 `/app.js`
|
||||
ComponentDemoEntryModule = 'componentDemoEntryModule',
|
||||
/**
|
||||
* 本地组件目录的入口文件
|
||||
*/
|
||||
ComponentsEntryModule = 'componentsEntryModule',
|
||||
/**
|
||||
* @deprecated 已废弃
|
||||
*/
|
||||
BlockEntryModule = 'blockEntryModule',
|
||||
|
||||
// jsx 类型视图文件
|
||||
JsxViewModule = 'jsxViewModule',
|
||||
// json 类型视图文件
|
||||
JsonViewModule = 'jsonViewModule',
|
||||
|
||||
// 非 js 文件
|
||||
PackageJson = 'packageJson',
|
||||
TangoConfigJson = 'tangoConfigJson',
|
||||
AppJson = 'appJson',
|
||||
File = 'file',
|
||||
Json = 'json',
|
||||
Less = 'less',
|
||||
Scss = 'scss',
|
||||
}
|
||||
|
||||
export interface IFileConfig {
|
||||
@@ -65,7 +53,7 @@ export interface IFileError {
|
||||
/**
|
||||
* 视图节点数据类型
|
||||
*/
|
||||
export interface ITangoViewNodeData<T = JSXElement> {
|
||||
export interface IViewNodeData<T = JSXElement> {
|
||||
/**
|
||||
* 节点 ID
|
||||
*/
|
||||
@@ -97,7 +85,7 @@ export interface ITangoViewNodeData<T = JSXElement> {
|
||||
/**
|
||||
* 子节点列表
|
||||
*/
|
||||
children?: Array<ITangoViewNodeData<T>>;
|
||||
children?: Array<IViewNodeData<T>>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -179,12 +179,12 @@ describe('string helpers', () => {
|
||||
});
|
||||
|
||||
it('inferFileType', () => {
|
||||
expect(inferFileType('/src/pages/template.js')).toBe(FileType.JsxViewModule);
|
||||
expect(inferFileType('/src/pages/template.jsx')).toBe(FileType.JsxViewModule);
|
||||
expect(inferFileType('/src/pages/template.js')).toBe(FileType.JsViewFile);
|
||||
expect(inferFileType('/src/pages/template.jsx')).toBe(FileType.JsViewFile);
|
||||
expect(inferFileType('/src/pages/template.ejs')).toBe(FileType.File);
|
||||
expect(inferFileType('/src/index.scss')).toBe(FileType.Scss);
|
||||
expect(inferFileType('/src/index.less')).toBe(FileType.Less);
|
||||
expect(inferFileType('/src/index.json')).toBe(FileType.Json);
|
||||
expect(inferFileType('/src/index.scss')).toBe(FileType.File);
|
||||
expect(inferFileType('/src/index.less')).toBe(FileType.File);
|
||||
expect(inferFileType('/src/index.json')).toBe(FileType.JsonFile);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,20 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.4.6](https://github.com/netease/tango/compare/@music163/tango-designer@1.4.5...@music163/tango-designer@1.4.6) (2024-09-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add classNameSetter and classNameInput ([#207](https://github.com/netease/tango/issues/207)) ([55602fc](https://github.com/netease/tango/commit/55602fc961b5c20d923891ce13d7a51588bd047f))
|
||||
- renderSetter with template ([#202](https://github.com/netease/tango/issues/202)) ([ea96872](https://github.com/netease/tango/commit/ea9687226123c839f7a9e001b4c01aa8315a135d))
|
||||
- service support set description field ([#206](https://github.com/netease/tango/issues/206)) ([79f9a8c](https://github.com/netease/tango/commit/79f9a8c1791424f35f194ab2a9aa7c263ef39f40))
|
||||
|
||||
## [1.4.5](https://github.com/netease/tango/compare/@music163/tango-designer@1.4.4...@music163/tango-designer@1.4.5) (2024-09-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add prop value template & fix components popover ([#201](https://github.com/netease/tango/issues/201)) ([c4e1ed6](https://github.com/netease/tango/commit/c4e1ed67f55deb17dae740559602cd0adcfc8eb4))
|
||||
|
||||
## [1.4.4](https://github.com/netease/tango/compare/@music163/tango-designer@1.4.3...@music163/tango-designer@1.4.4) (2024-08-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-designer",
|
||||
"version": "1.4.4",
|
||||
"version": "1.4.6",
|
||||
"description": "lowcode designer",
|
||||
"keywords": [
|
||||
"react"
|
||||
@@ -33,12 +33,12 @@
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/request": "^0.2.0",
|
||||
"@music163/tango-context": "^1.1.8",
|
||||
"@music163/tango-core": "^1.4.2",
|
||||
"@music163/tango-helpers": "^1.2.2",
|
||||
"@music163/tango-sandbox": "^1.0.11",
|
||||
"@music163/tango-setting-form": "^1.2.13",
|
||||
"@music163/tango-ui": "^1.4.3",
|
||||
"@music163/tango-context": "^1.1.10",
|
||||
"@music163/tango-core": "^1.4.4",
|
||||
"@music163/tango-helpers": "^1.2.4",
|
||||
"@music163/tango-sandbox": "^1.0.13",
|
||||
"@music163/tango-setting-form": "^1.2.15",
|
||||
"@music163/tango-ui": "^1.4.5",
|
||||
"antd": "^4.24.2",
|
||||
"cash-dom": "^8.1.2",
|
||||
"classnames": "^2.5.1",
|
||||
|
||||
@@ -34,17 +34,17 @@ export const ComponentsPopover = observer(
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
workspace.componentPrototypes.get(selectedNode?.name) ?? ({} as IComponentPrototype);
|
||||
|
||||
// 推荐使用的子组件
|
||||
const insertedList = useMemo(
|
||||
() =>
|
||||
Array.isArray(prototype?.childrenName)
|
||||
? prototype?.childrenName
|
||||
: [prototype?.childrenName].filter(Boolean),
|
||||
[prototype?.childrenName],
|
||||
);
|
||||
|
||||
// 推荐使用的代码片段
|
||||
const siblingList = useMemo(() => prototype?.siblingNames ?? [], [prototype.siblingNames]);
|
||||
const recommendedList = useMemo(() => {
|
||||
if (type === 'inner') {
|
||||
return prototype?.childrenName
|
||||
? Array.isArray(prototype?.childrenName)
|
||||
? prototype.childrenName
|
||||
: [prototype.childrenName]
|
||||
: [];
|
||||
}
|
||||
// 默认推荐使用相同类型的组件作为兄弟节点
|
||||
return prototype.siblingNames || [prototype.name];
|
||||
}, [prototype.childrenName, prototype.siblingNames, prototype.name, type]);
|
||||
|
||||
const tipsTextMap = useMemo(
|
||||
() => ({
|
||||
@@ -82,22 +82,15 @@ export const ComponentsPopover = observer(
|
||||
const menuList = JSON.parse(JSON.stringify(designer.menuData));
|
||||
|
||||
const commonList = menuList['common'] ?? [];
|
||||
if (commonList?.length && siblingList?.length) {
|
||||
commonList.unshift({
|
||||
title: '代码片段',
|
||||
items: siblingList,
|
||||
});
|
||||
}
|
||||
|
||||
if (commonList?.length && insertedList?.length) {
|
||||
if (commonList?.length && recommendedList.length) {
|
||||
commonList.unshift({
|
||||
title: '推荐使用',
|
||||
items: insertedList,
|
||||
items: recommendedList,
|
||||
});
|
||||
}
|
||||
|
||||
return menuList;
|
||||
}, [insertedList, siblingList, designer.menuData]);
|
||||
}, [recommendedList, designer.menuData]);
|
||||
|
||||
const innerTypeProps =
|
||||
// 手动触发 适用于 点击添加组件
|
||||
|
||||
@@ -88,6 +88,10 @@ export function AddServiceForm({
|
||||
>
|
||||
<Input placeholder="请输入数据服务调用名称" disabled={disabled || isModifyMode} />
|
||||
</Form.Item>
|
||||
{/* 备注 */}
|
||||
<Form.Item label="备注" name="description">
|
||||
<Input placeholder="请输入备注信息" />
|
||||
</Form.Item>
|
||||
<Form.Item label="路径" name="url" rules={[{ required: true, type: 'url' }]}>
|
||||
<Input placeholder="请输入数据服务调用名称" disabled={disabled || isModifyMode} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { DropMethod, FileType, Designer, IWorkspace } from '@music163/tango-core';
|
||||
import { DropMethod, FileType, Designer, AbstractCodeWorkspace } from '@music163/tango-core';
|
||||
import { ISelectedItemData, events, getHotkey } from '@music163/tango-helpers';
|
||||
import {
|
||||
setElementStyle,
|
||||
@@ -12,7 +12,7 @@ import { Hotkey } from './hotkey';
|
||||
import { SelectModeType } from '../types';
|
||||
|
||||
interface UseDndProps {
|
||||
workspace: IWorkspace;
|
||||
workspace: AbstractCodeWorkspace;
|
||||
designer: Designer;
|
||||
/**
|
||||
* 沙箱内的 DOM 查询操作
|
||||
@@ -345,7 +345,7 @@ export function useDnd({
|
||||
// 区块不能拖拽到区块中
|
||||
if (
|
||||
workspace.dragSource.prototype.type === 'block' &&
|
||||
closetDropTargetNode.file.type === FileType.BlockEntryModule
|
||||
closetDropTargetNode.file.type === FileType.JsLocalComponentsEntryFile
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ function useSandbox({
|
||||
// 根据当前 workspace 状态与组件传入的状态是否一致,控制是否需要切换到空白路由
|
||||
const display = isActive ? 'block' : 'none';
|
||||
const routePath = isActive ? startRoute || workspace.activeRoute : LANDING_PAGE_PATH;
|
||||
const routerMode = fixRouterMode(workspace.appEntryModule?.routerType);
|
||||
const routerMode = fixRouterMode(workspace.jsAppEntryFile?.routerType);
|
||||
|
||||
const sandboxProps = isPreview
|
||||
? {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Radio, RadioProps, Tooltip } from 'antd';
|
||||
import type { OptionType } from '@music163/tango-helpers';
|
||||
import type { IOptionItem } from '@music163/tango-helpers';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { IconFont } from '@music163/tango-ui';
|
||||
|
||||
interface ChoiceSetterProps {
|
||||
options?: OptionType[];
|
||||
options?: IOptionItem[];
|
||||
}
|
||||
|
||||
export function ChoiceSetter({
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { ClassNameInput } from '@music163/tango-ui';
|
||||
import React from 'react';
|
||||
|
||||
export function ClassNameSetter({ value, onChange }: FormItemComponentProps<string>) {
|
||||
return <ClassNameInput value={value} onChange={onChange} />;
|
||||
}
|
||||
+38
-15
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Box, Text, css } from 'coral-system';
|
||||
import { Dropdown, Button } from 'antd';
|
||||
import { isValidExpressionCode } from '@music163/tango-core';
|
||||
import { getValue, IVariableTreeNode, noop } from '@music163/tango-helpers';
|
||||
import { getValue, interpolate, IVariableTreeNode, noop } from '@music163/tango-helpers';
|
||||
import { CloseCircleFilled, MenuOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Panel,
|
||||
@@ -34,6 +34,19 @@ export const jsonValueValidate = (value: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 拼装回调函数
|
||||
* @param value 回调函数体
|
||||
* @param template 回调函数模板
|
||||
* @returns
|
||||
*/
|
||||
export function getCallbackValue(value: string, template?: string) {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
return template ? interpolate(template, { content: value }) : `() => {\n ${value}\n}`;
|
||||
}
|
||||
|
||||
const suffixStyle = css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -45,7 +58,7 @@ const suffixStyle = css`
|
||||
}
|
||||
`;
|
||||
|
||||
export interface ExpressionSetterProps extends FormItemComponentProps<string> {
|
||||
export interface CodeSetterProps extends FormItemComponentProps<string> {
|
||||
modalTitle?: string;
|
||||
modalTip?: string;
|
||||
autoCompleteOptions?: string[];
|
||||
@@ -57,12 +70,13 @@ export interface ExpressionSetterProps extends FormItemComponentProps<string> {
|
||||
expressionType?: 'cssObject';
|
||||
}
|
||||
|
||||
export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
export function CodeSetter(props: CodeSetterProps) {
|
||||
const {
|
||||
onChange,
|
||||
modalTitle,
|
||||
modalTip,
|
||||
autoCompleteOptions,
|
||||
template,
|
||||
placeholder = '在这里输入代码',
|
||||
value: valueProp,
|
||||
status,
|
||||
@@ -105,7 +119,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
<CloseCircleFilled
|
||||
title="清空"
|
||||
onClick={() => {
|
||||
change('');
|
||||
change(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -134,6 +148,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
subTitle={modalTip}
|
||||
placeholder={placeholder}
|
||||
autoCompleteOptions={autoCompleteOptions}
|
||||
template={template}
|
||||
newStoreTemplate={newStoreTemplate}
|
||||
value={inputValue}
|
||||
expressionType={expressionType}
|
||||
@@ -166,6 +181,10 @@ export interface ExpressionPopoverProps extends InputCodeProps {
|
||||
onOk?: (value: string) => void;
|
||||
dataSource?: IVariableTreeNode[];
|
||||
autoCompleteOptions?: string[];
|
||||
/**
|
||||
* 值的模板,一般用于定义函数模板
|
||||
*/
|
||||
template?: string;
|
||||
/**
|
||||
* 新建 store 的模板代码
|
||||
*/
|
||||
@@ -186,6 +205,7 @@ export function ExpressionPopover({
|
||||
value,
|
||||
dataSource,
|
||||
autoCompleteOptions,
|
||||
template,
|
||||
newStoreTemplate = CODE_TEMPLATES.newStoreTemplate,
|
||||
children,
|
||||
expressionType,
|
||||
@@ -245,19 +265,18 @@ export function ExpressionPopover({
|
||||
autoCompleteOptions={autoCompleteOptions}
|
||||
/>
|
||||
{error ? (
|
||||
<Text color="red" fontSize="12px" as="p">
|
||||
<Text color="red" fontSize="12px" as="div">
|
||||
出错了!输入的表达式存在语法错误,请修改后再提交!
|
||||
</Text>
|
||||
) : null}
|
||||
{subTitle && (
|
||||
<Text fontSize="12px" color="text3" as="p">
|
||||
{subTitle}
|
||||
<Box fontSize="12px" color="text2">
|
||||
<Text display="block">说明:</Text>
|
||||
{subTitle && <Text display="block">{subTitle}</Text>}
|
||||
<Text display="block">
|
||||
你可以在上面的代码输入框里输入常规的 javascript 代码,还可以直接使用 jsx
|
||||
代码,但需要符合该属性的接受值定义。
|
||||
</Text>
|
||||
)}
|
||||
<Text fontSize="12px" color="text3" as="p">
|
||||
说明:你可以在上面的代码输入框里输入常规的 javascript 代码,还可以直接使用 jsx
|
||||
代码,但需要符合该属性的接受值定义。
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Panel
|
||||
title="从变量列表中选中"
|
||||
@@ -275,7 +294,7 @@ export function ExpressionPopover({
|
||||
height="100%"
|
||||
showViewButton
|
||||
dataSource={dataSource || expressionVariables}
|
||||
appContext={evaluateContext['tango']}
|
||||
appContext={evaluateContext?.['tango']}
|
||||
getStoreNames={() => Object.keys(workspace.storeModules)}
|
||||
serviceModules={serviceModules}
|
||||
getServiceData={(serviceKey) => {
|
||||
@@ -296,7 +315,11 @@ export function ExpressionPopover({
|
||||
}
|
||||
let str;
|
||||
if (/^(stores|services)\./.test(node.key)) {
|
||||
str = `tango.${node.key.replaceAll('.', '?.')}`;
|
||||
// 从匹配到的第二个点开始替换为 ?.,因为第一个点是 stores 或 services
|
||||
str = `tango.${node.key.replace(/(?<=\..*?)\./g, '?.')}`;
|
||||
if (node.type === 'function') {
|
||||
str = getCallbackValue(`${str}();`, template);
|
||||
}
|
||||
} else {
|
||||
str = `${node.key}`;
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { css, Box, Text } from 'coral-system';
|
||||
import { AutoComplete } from 'antd';
|
||||
import { AutoComplete, Input } from 'antd';
|
||||
import { ActionSelect } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||
import { Dict, wrapCode } from '@music163/tango-helpers';
|
||||
import { ExpressionPopover } from './expression-setter';
|
||||
import { ExpressionPopover, getCallbackValue } from './code-setter';
|
||||
import { value2code } from '@music163/tango-core';
|
||||
|
||||
enum EventAction {
|
||||
@@ -31,7 +31,7 @@ export type EventSetterProps = FormItemComponentProps<string>;
|
||||
* 事件监听函数绑定器
|
||||
*/
|
||||
export function EventSetter(props: EventSetterProps) {
|
||||
const { value, onChange, modalTitle } = props;
|
||||
const { value, onChange, modalTitle, modalTip, template } = props;
|
||||
const [type, setType] = useState<EventAction>(); // 事件类型
|
||||
const [temp, setTemp] = useState(''); // 二级暂存值
|
||||
const { actionVariables, routeOptions } = useWorkspaceData();
|
||||
@@ -59,7 +59,9 @@ export function EventSetter(props: EventSetterProps) {
|
||||
label: (
|
||||
<ExpressionPopover
|
||||
title={modalTitle}
|
||||
subTitle={modalTip}
|
||||
value={value}
|
||||
template={template}
|
||||
onOk={(nextValue) => {
|
||||
handleChange(nextValue);
|
||||
}}
|
||||
@@ -74,30 +76,34 @@ export function EventSetter(props: EventSetterProps) {
|
||||
{ label: '打开弹窗', value: EventAction.OpenModal },
|
||||
{ label: '关闭弹窗', value: EventAction.CloseModal },
|
||||
],
|
||||
[modalTitle, value, actionVariables, handleChange],
|
||||
[modalTitle, value, actionVariables, template, handleChange],
|
||||
);
|
||||
|
||||
const onAction = (key: string) => {
|
||||
setType(key as EventAction); // 记录事件类型
|
||||
setTemp(''); // 重置二级选项值
|
||||
|
||||
switch (key) {
|
||||
case EventAction.ConsoleLog:
|
||||
handleChange('(...args) => console.log(...args)');
|
||||
break;
|
||||
case EventAction.NoAction:
|
||||
handleChange(undefined);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
if (key === EventAction.NoAction) {
|
||||
handleChange(undefined);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const actionText = getActionText(type, temp, code);
|
||||
|
||||
return (
|
||||
<Box css={wrapperStyle}>
|
||||
<ActionSelect options={options} onSelect={onAction} text={actionText} />
|
||||
<ActionSelect options={options} onSelect={onAction} defaultText="请选择动作类型" />
|
||||
{type === EventAction.ConsoleLog && (
|
||||
<Input
|
||||
placeholder="输入 Console.log 日志内容"
|
||||
value={temp}
|
||||
onChange={(e) => setTemp(e.target.value)}
|
||||
onBlur={() => {
|
||||
if (temp) {
|
||||
handleChange(getExpressionValue(type, temp));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{type === EventAction.NavigateTo && (
|
||||
<AutoComplete
|
||||
placeholder="选择或输入页面路由"
|
||||
@@ -142,25 +148,15 @@ export function EventSetter(props: EventSetterProps) {
|
||||
}
|
||||
|
||||
const handlerMap: Dict = {
|
||||
[EventAction.OpenModal]: 'openModal',
|
||||
[EventAction.CloseModal]: 'closeModal',
|
||||
[EventAction.NavigateTo]: 'navigateTo',
|
||||
[EventAction.ConsoleLog]: 'console.log',
|
||||
[EventAction.OpenModal]: 'tango.openModal',
|
||||
[EventAction.CloseModal]: 'tango.closeModal',
|
||||
[EventAction.NavigateTo]: 'tango.navigateTo',
|
||||
};
|
||||
|
||||
function getActionText(type: EventAction, temp: string, fallbackCode: string) {
|
||||
let text;
|
||||
if (handlerMap[type]) {
|
||||
text = getExpressionValue(type, temp);
|
||||
} else if (fallbackCode) {
|
||||
text = fallbackCode;
|
||||
}
|
||||
text = text || '请选择';
|
||||
return text;
|
||||
}
|
||||
|
||||
function getExpressionValue(type: EventAction, value = '') {
|
||||
const handler = handlerMap[type];
|
||||
if (handler) {
|
||||
return `() => tango.${handler}("${value}")`;
|
||||
return getCallbackValue(`${handler}("${value}");`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@ import { CssSetter } from './css-setter';
|
||||
import { DateRangeSetter, DateSetter, TimeRangeSetter } from './date-setter';
|
||||
import { EnumSetter } from './enum-setter';
|
||||
import { EventSetter } from './event-setter';
|
||||
import { ExpressionSetter } from './expression-setter';
|
||||
import { CodeSetter } from './code-setter';
|
||||
import { JSONSetter } from './json-setter';
|
||||
import { JsxSetter } from './jsx-setter';
|
||||
import { ListSetter } from './list-setter';
|
||||
import { ModelSetter } from './model-setter';
|
||||
import { OptionSetter } from './option-setter';
|
||||
import { PickerSetter } from './picker-setter';
|
||||
import { RenderSetter, TableCellSetter, TableExpandableSetter } from './render-setter';
|
||||
import { RenderSetter } from './render-setter';
|
||||
import { RouterSetter } from './router-setter';
|
||||
import {
|
||||
SpacingSetter,
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
FlexDirectionSetter,
|
||||
} from './style-setter';
|
||||
import { ChoiceSetter } from './choice-setter';
|
||||
import { ClassNameSetter } from './classname-setter';
|
||||
import { isValidExpressionCode } from '@music163/tango-core';
|
||||
|
||||
const codeValidate: IFormItemCreateOptions['validate'] = (value, field) => {
|
||||
@@ -49,10 +50,14 @@ export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
|
||||
{
|
||||
name: 'codeSetter',
|
||||
alias: ['expSetter', 'expressionSetter'],
|
||||
component: ExpressionSetter,
|
||||
component: CodeSetter,
|
||||
type: 'code',
|
||||
validate: codeValidate,
|
||||
},
|
||||
{
|
||||
name: 'classNameSetter',
|
||||
component: ClassNameSetter,
|
||||
},
|
||||
{
|
||||
name: 'radioGroupSetter',
|
||||
alias: ['choiceSetter'],
|
||||
@@ -126,22 +131,12 @@ export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
|
||||
component: PickerSetter,
|
||||
},
|
||||
{
|
||||
name: 'renderPropsSetter',
|
||||
name: 'renderSetter',
|
||||
alias: ['renderPropsSetter'],
|
||||
component: RenderSetter,
|
||||
type: 'code',
|
||||
validate: codeValidate,
|
||||
},
|
||||
{
|
||||
name: 'tableCellSetter',
|
||||
component: TableCellSetter,
|
||||
type: 'code',
|
||||
},
|
||||
{
|
||||
name: 'tableExpandableSetter',
|
||||
component: TableExpandableSetter,
|
||||
type: 'code',
|
||||
validate: codeValidate,
|
||||
},
|
||||
{
|
||||
name: 'routerSetter',
|
||||
component: RouterSetter,
|
||||
|
||||
@@ -2,22 +2,16 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { ActionSelect, InputCode } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { Box } from 'coral-system';
|
||||
import { Dict } from '@music163/tango-helpers';
|
||||
|
||||
interface IRenderOption {
|
||||
label: string;
|
||||
value: string;
|
||||
render?: string;
|
||||
relatedImports?: string[];
|
||||
}
|
||||
import { Dict, IOptionItem } from '@music163/tango-helpers';
|
||||
import { getCallbackValue } from './code-setter';
|
||||
|
||||
export interface RenderSetterProps {
|
||||
text?: string;
|
||||
options?: IRenderOption[];
|
||||
fallbackOption?: IRenderOption;
|
||||
options?: IOptionItem[];
|
||||
fallbackOption?: IOptionItem;
|
||||
}
|
||||
|
||||
const defaultOptions: IRenderOption[] = [
|
||||
const defaultOptions: IOptionItem[] = [
|
||||
{ label: '取消自定义', value: '' },
|
||||
{ label: '自定义渲染', value: 'Box', render: '() => <Box></Box>' },
|
||||
];
|
||||
@@ -30,6 +24,7 @@ export function RenderSetter({
|
||||
onChange,
|
||||
text = '自定义渲染为',
|
||||
options = defaultOptions,
|
||||
template = `() => {{content}}`,
|
||||
fallbackOption,
|
||||
}: FormItemComponentProps & RenderSetterProps) {
|
||||
const [inputValue, setInputValue] = useState(value || '');
|
||||
@@ -47,13 +42,14 @@ export function RenderSetter({
|
||||
const onSelect = useCallback(
|
||||
(key: string) => {
|
||||
const option = optionsMap[key] || fallbackOption;
|
||||
if (option?.render) {
|
||||
onChange(option.render, { relatedImports: option.relatedImports });
|
||||
const next = option?.render || getCallbackValue(option.renderBody, template);
|
||||
if (next) {
|
||||
onChange(next, { relatedImports: option.relatedImports });
|
||||
} else {
|
||||
onChange(undefined);
|
||||
}
|
||||
},
|
||||
[optionsMap, fallbackOption, onChange],
|
||||
[optionsMap, fallbackOption, onChange, template],
|
||||
);
|
||||
return (
|
||||
<Box>
|
||||
@@ -68,69 +64,3 @@ export function RenderSetter({
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const getRender = (content: string, type?: 'tableCell' | 'tableExpandable') => {
|
||||
let code;
|
||||
switch (type) {
|
||||
case 'tableCell':
|
||||
code = `(value, record, index) => ${content}`;
|
||||
break;
|
||||
case 'tableExpandable':
|
||||
code = `{
|
||||
expandedRowRender: (record) => ${content},
|
||||
rowExpandable: (record) => true
|
||||
}`;
|
||||
break;
|
||||
default:
|
||||
code = `() => ${content}`;
|
||||
break;
|
||||
}
|
||||
return code;
|
||||
};
|
||||
|
||||
const tableCellOptions: RenderSetterProps['options'] = [
|
||||
{ label: '取消自定义', value: '' },
|
||||
{
|
||||
label: '自定义区域',
|
||||
value: 'Box',
|
||||
render: getRender('<Box></Box>', 'tableCell'),
|
||||
relatedImports: ['Box'],
|
||||
},
|
||||
{
|
||||
label: '标签',
|
||||
value: 'Tag',
|
||||
render: getRender('<Tag>tag</Tag>', 'tableCell'),
|
||||
relatedImports: ['Tag'],
|
||||
},
|
||||
{
|
||||
label: '按钮',
|
||||
value: 'Button',
|
||||
render: getRender('<Button>button</Button>', 'tableCell'),
|
||||
relatedImports: ['Button'],
|
||||
},
|
||||
{
|
||||
label: '图片',
|
||||
value: 'Image',
|
||||
render: getRender('<Image width={150} src="https://picsum.photos/100" />', 'tableCell'),
|
||||
relatedImports: ['Image'],
|
||||
},
|
||||
];
|
||||
|
||||
const tableExpandableOptions: RenderSetterProps['options'] = [
|
||||
{
|
||||
label: '设置可展开行',
|
||||
value: 'Box',
|
||||
render: getRender('<Box></Box>', 'tableExpandable'),
|
||||
relatedImports: ['Box'],
|
||||
},
|
||||
{ label: '取消可展开行', value: '' },
|
||||
];
|
||||
|
||||
export function TableCellSetter(props: FormItemComponentProps) {
|
||||
return <RenderSetter options={tableCellOptions} {...props} />;
|
||||
}
|
||||
|
||||
// FIXME: 应该直接用 props 嵌套的模式
|
||||
export function TableExpandableSetter(props: FormItemComponentProps) {
|
||||
return <RenderSetter options={tableExpandableOptions} text="配置表格可展开行" {...props} />;
|
||||
}
|
||||
|
||||
@@ -365,7 +365,7 @@ function MaterialGrid({ data }: MaterialProps) {
|
||||
<IconFont className="material-icon" type={data.icon || 'icon-placeholder'} />
|
||||
) : (
|
||||
<Box className="material-icon">
|
||||
<img src={icon} alt={data.name} />
|
||||
<img src={icon} alt={data.name} height="32" />
|
||||
</Box>
|
||||
);
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import { ColorTag, ConfigGroup, ConfigItem } from '@music163/tango-ui';
|
||||
import { MinusCircleOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { useBoolean } from '@music163/tango-helpers';
|
||||
import { isUndefined } from 'lodash-es';
|
||||
import { Workspace } from '@music163/tango-core';
|
||||
import { useSandboxQuery } from '../context';
|
||||
|
||||
enum DependencyItemType {
|
||||
@@ -169,7 +168,7 @@ function RenderItem({
|
||||
}: RenderItemProps) {
|
||||
const [open, { on, off }] = useBoolean(false);
|
||||
|
||||
const workspace = useWorkspace() as Workspace;
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const basePackage = useMemo(() => {
|
||||
if (type !== DependencyItemType.基础包 || !templateBaseDependencies) return undefined;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Box, css } from 'coral-system';
|
||||
import { IconFont } from '@music163/tango-ui';
|
||||
import { EyeOutlined, EyeInvisibleOutlined, EllipsisOutlined } from '@ant-design/icons';
|
||||
import { observer, useWorkspace } from '@music163/tango-context';
|
||||
import { DropMethod, ITangoViewNodeData } from '@music163/tango-core';
|
||||
import { DropMethod, IViewNodeData } from '@music163/tango-core';
|
||||
import { noop, parseDndId } from '@music163/tango-helpers';
|
||||
import { useSandboxQuery } from '../../context';
|
||||
import { buildQueryBySlotId } from '../../helpers';
|
||||
@@ -65,7 +65,7 @@ const filedNames = {
|
||||
children: 'children',
|
||||
};
|
||||
|
||||
const getNodeKeys = (data: ITangoViewNodeData[]) => {
|
||||
const getNodeKeys = (data: IViewNodeData[]) => {
|
||||
const ids: string[] = [];
|
||||
data?.forEach((node) => {
|
||||
ids.push(node.id);
|
||||
@@ -76,7 +76,7 @@ const getNodeKeys = (data: ITangoViewNodeData[]) => {
|
||||
return ids;
|
||||
};
|
||||
|
||||
const OutlineTreeNode: React.FC<{ node: ITangoViewNodeData } & ComponentsTreeProps> = observer(
|
||||
const OutlineTreeNode: React.FC<{ node: IViewNodeData } & ComponentsTreeProps> = observer(
|
||||
({ node, showToggleVisibleIcon, actionItems }) => {
|
||||
const workspace = useWorkspace();
|
||||
const sandboxQuery = useSandboxQuery();
|
||||
@@ -142,7 +142,7 @@ export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
|
||||
workspace.selectSource.selected.map((item) => item.id),
|
||||
);
|
||||
const file = workspace.activeViewModule;
|
||||
const nodesTree = (file?.nodesTree ?? []) as ITangoViewNodeData[];
|
||||
const nodesTree = (file?.nodesTree ?? []) as IViewNodeData[];
|
||||
const [expandedKeys, setExpandedKeys] = useState(getNodeKeys(nodesTree));
|
||||
const [contextMenuOpen, setContextMenuOpen] = useState(false);
|
||||
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.2.4](https://github.com/netease/tango/compare/@music163/tango-helpers@1.2.3...@music163/tango-helpers@1.2.4) (2024-09-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- renderSetter with template ([#202](https://github.com/netease/tango/issues/202)) ([ea96872](https://github.com/netease/tango/commit/ea9687226123c839f7a9e001b4c01aa8315a135d))
|
||||
|
||||
## [1.2.3](https://github.com/netease/tango/compare/@music163/tango-helpers@1.2.2...@music163/tango-helpers@1.2.3) (2024-09-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add prop value template & fix components popover ([#201](https://github.com/netease/tango/issues/201)) ([c4e1ed6](https://github.com/netease/tango/commit/c4e1ed67f55deb17dae740559602cd0adcfc8eb4))
|
||||
|
||||
## [1.2.2](https://github.com/netease/tango/compare/@music163/tango-helpers@1.2.1...@music163/tango-helpers@1.2.2) (2024-08-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-helpers",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.4",
|
||||
"description": "Shared types, helpers, and hooks of tango-apps",
|
||||
"keywords": [
|
||||
"shared",
|
||||
|
||||
@@ -125,3 +125,17 @@ export function parseDndId(str: string): DndIdParsedType {
|
||||
id: str,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换模板中的变量
|
||||
* @example interpolate('hello {{name}}', { name: 'world' }) -> 'hello world'
|
||||
*
|
||||
* @param template 带模板变量的字符串
|
||||
* @param props 变量字典
|
||||
* @returns 返回替换后的字符串
|
||||
*/
|
||||
export function interpolate(template: string, props: Record<string, any>) {
|
||||
return template.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
|
||||
return props[key];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,11 +10,19 @@ export type MousePoint = {
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type OptionType = {
|
||||
/**
|
||||
* @deprecated 使用 IOptionItem 代替
|
||||
*/
|
||||
export type OptionType = IOptionItem<any>;
|
||||
|
||||
/**
|
||||
* 列表项
|
||||
*/
|
||||
export interface IOptionItem<T = string> {
|
||||
/**
|
||||
* 值
|
||||
*/
|
||||
value: any;
|
||||
value: T;
|
||||
/**
|
||||
* 显示的文本
|
||||
*/
|
||||
@@ -28,10 +36,18 @@ export type OptionType = {
|
||||
*/
|
||||
tip?: string;
|
||||
/**
|
||||
* 关联的导入
|
||||
* 自定义渲染函数,仅用于 renderSetter
|
||||
*/
|
||||
render?: string;
|
||||
/**
|
||||
* 自定义渲染函数的 body(可以用来和 template 配合),仅用于 renderSetter
|
||||
*/
|
||||
renderBody?: string;
|
||||
/**
|
||||
* 关联的导入,仅用于 renderSetter
|
||||
*/
|
||||
relatedImports?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 变量树节点类型
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* 物料描述类型
|
||||
*/
|
||||
|
||||
import { OptionType } from './advanced';
|
||||
import { IOptionItem } from './advanced';
|
||||
import { PartialRecord } from './base';
|
||||
|
||||
/**
|
||||
@@ -88,6 +88,11 @@ export interface IComponentProp<T = any> {
|
||||
* 自动补全的提示值,仅对 ExpressionSetter 有效
|
||||
*/
|
||||
autoCompleteOptions?: string[];
|
||||
/**
|
||||
* value 的模板,一般用于函数类型的属性,便于 setter 用来拼装返回值
|
||||
* @example "(arg1, arg2, arg3) => { {{content}}}"
|
||||
*/
|
||||
template?: string;
|
||||
/**
|
||||
* 设置器
|
||||
*/
|
||||
@@ -108,7 +113,7 @@ export interface IComponentProp<T = any> {
|
||||
/**
|
||||
* 配置项的可选值,setterProps.options 的简写
|
||||
*/
|
||||
options?: OptionType[];
|
||||
options?: IOptionItem[];
|
||||
/**
|
||||
* 是否禁用变量绑定
|
||||
*/
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.13](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.12...@music163/tango-sandbox@1.0.13) (2024-09-09)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
## [1.0.12](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.11...@music163/tango-sandbox@1.0.12) (2024-09-02)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
## [1.0.11](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.10...@music163/tango-sandbox@1.0.11) (2024-08-06)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-sandbox",
|
||||
"version": "1.0.11",
|
||||
"version": "1.0.13",
|
||||
"description": "sandbox of tango apps",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"homepage": "",
|
||||
@@ -29,8 +29,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/tango-core": "^1.4.2",
|
||||
"@music163/tango-helpers": "^1.2.2",
|
||||
"@music163/tango-core": "^1.4.4",
|
||||
"@music163/tango-helpers": "^1.2.4",
|
||||
"crypto-js": "^4.1.1",
|
||||
"lodash.isequal": "4.5.0",
|
||||
"react-frame-component": "^5.2.4"
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.2.15](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.14...@music163/tango-setting-form@1.2.15) (2024-09-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- renderSetter with template ([#202](https://github.com/netease/tango/issues/202)) ([ea96872](https://github.com/netease/tango/commit/ea9687226123c839f7a9e001b4c01aa8315a135d))
|
||||
|
||||
## [1.2.14](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.13...@music163/tango-setting-form@1.2.14) (2024-09-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add prop value template & fix components popover ([#201](https://github.com/netease/tango/issues/201)) ([c4e1ed6](https://github.com/netease/tango/commit/c4e1ed67f55deb17dae740559602cd0adcfc8eb4))
|
||||
|
||||
## [1.2.13](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.12...@music163/tango-setting-form@1.2.13) (2024-08-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-setting-form",
|
||||
"version": "1.2.13",
|
||||
"version": "1.2.15",
|
||||
"description": "setting form of tango-apps",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"homepage": "",
|
||||
@@ -28,12 +28,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/tango-core": "^1.4.2",
|
||||
"@music163/tango-helpers": "^1.2.2",
|
||||
"@music163/tango-ui": "^1.4.3",
|
||||
"@music163/tango-core": "^1.4.4",
|
||||
"@music163/tango-helpers": "^1.2.4",
|
||||
"@music163/tango-ui": "^1.4.5",
|
||||
"antd": "^4.24.2",
|
||||
"coral-system": "^1.0.5",
|
||||
"mobx": "6.12.3",
|
||||
"mobx": "6.13.2",
|
||||
"mobx-react-lite": "4.0.7"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -182,6 +182,7 @@ export function createFormItem(options: IFormItemCreateOptions) {
|
||||
placeholder,
|
||||
docs,
|
||||
autoCompleteOptions,
|
||||
template,
|
||||
setter: setterProp,
|
||||
setterProps,
|
||||
defaultValue,
|
||||
@@ -230,14 +231,20 @@ export function createFormItem(options: IFormItemCreateOptions) {
|
||||
|
||||
// FIXME: 重新考虑这段代码的位置,外置这个逻辑
|
||||
if (
|
||||
['codeSetter', 'expressionSetter', 'expSetter', 'actionSetter', 'eventSetter'].includes(
|
||||
setter,
|
||||
)
|
||||
[
|
||||
'codeSetter',
|
||||
'expressionSetter',
|
||||
'expSetter',
|
||||
'actionSetter',
|
||||
'eventSetter',
|
||||
'renderSetter',
|
||||
].includes(setter)
|
||||
) {
|
||||
expProps = {
|
||||
modalTitle: title,
|
||||
modalTip: tip,
|
||||
autoCompleteOptions,
|
||||
template,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ export const SettingFormObject = observer(
|
||||
note={name}
|
||||
tip={tip}
|
||||
docs={docs}
|
||||
checked={!!subModel.values ?? !!defaultValue}
|
||||
checked={!!subModel.values || !!defaultValue}
|
||||
onCheck={(checked) => {
|
||||
const nextValue = checked ? {} : undefined;
|
||||
parent.setValue(name, nextValue);
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.4.5](https://github.com/netease/tango/compare/@music163/tango-ui@1.4.4...@music163/tango-ui@1.4.5) (2024-09-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add classNameSetter and classNameInput ([#207](https://github.com/netease/tango/issues/207)) ([55602fc](https://github.com/netease/tango/commit/55602fc961b5c20d923891ce13d7a51588bd047f))
|
||||
|
||||
## [1.4.4](https://github.com/netease/tango/compare/@music163/tango-ui@1.4.3...@music163/tango-ui@1.4.4) (2024-09-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add prop value template & fix components popover ([#201](https://github.com/netease/tango/issues/201)) ([c4e1ed6](https://github.com/netease/tango/commit/c4e1ed67f55deb17dae740559602cd0adcfc8eb4))
|
||||
|
||||
## [1.4.3](https://github.com/netease/tango/compare/@music163/tango-ui@1.4.2...@music163/tango-ui@1.4.3) (2024-08-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-ui",
|
||||
"version": "1.4.3",
|
||||
"version": "1.4.5",
|
||||
"description": "ui widgets of tango",
|
||||
"keywords": [
|
||||
"react",
|
||||
@@ -38,7 +38,7 @@
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
"@codemirror/lint": "^6.7.1",
|
||||
"@codemirror/search": "^6.5.6",
|
||||
"@music163/tango-helpers": "^1.2.2",
|
||||
"@music163/tango-helpers": "^1.2.4",
|
||||
"@uiw/react-codemirror": "^4.22.0",
|
||||
"antd": "^4.24.2",
|
||||
"classnames": "^2.5.1",
|
||||
@@ -46,7 +46,7 @@
|
||||
"eslint-linter-browserify": "^8.51.0",
|
||||
"react-draggable": "^4.4.5",
|
||||
"react-json-view": "^1.21.3",
|
||||
"react-monaco-editor-lite": "^1.3.11"
|
||||
"react-monaco-editor-lite": "^1.3.16"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Box, css, Text } from 'coral-system';
|
||||
import { Button, Dropdown, Input, Menu } from 'antd';
|
||||
import { DownOutlined, PlusSquareOutlined } from '@ant-design/icons';
|
||||
@@ -23,9 +23,13 @@ interface ActionSelectProps {
|
||||
*/
|
||||
onInputChange?: (value: string) => void;
|
||||
/**
|
||||
* 提示文本
|
||||
* 受控的提示文本
|
||||
*/
|
||||
text?: string;
|
||||
/**
|
||||
* 默认的提示文本
|
||||
*/
|
||||
defaultText?: string;
|
||||
/**
|
||||
* 选择菜单时的回调
|
||||
*/
|
||||
@@ -50,11 +54,6 @@ const actionInputStyle = css`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--tango-colors-text3);
|
||||
|
||||
&:hover {
|
||||
color: var(--tango-colors-text2);
|
||||
}
|
||||
}
|
||||
|
||||
.anticon-down {
|
||||
@@ -71,15 +70,25 @@ const inputStyle = { width: 'calc(100% - 82px)' };
|
||||
export function ActionSelect({
|
||||
showInput = false,
|
||||
defaultInputValue,
|
||||
text,
|
||||
text: textProp,
|
||||
defaultText,
|
||||
options = [],
|
||||
onSelect,
|
||||
onInputChange,
|
||||
}: ActionSelectProps) {
|
||||
const [text, setText] = useState<string>(defaultText);
|
||||
const menu = (
|
||||
<Menu onClick={({ key }) => onSelect(key)}>
|
||||
<Menu>
|
||||
{options.map((item) => (
|
||||
<Menu.Item key={item.value}>
|
||||
<Menu.Item
|
||||
key={item.value}
|
||||
onClick={() => {
|
||||
onSelect?.(item.value);
|
||||
if (!textProp) {
|
||||
setText(item.label as string);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center" minWidth={200} columnGap="m">
|
||||
<Box flex="1">{item.label}</Box>
|
||||
<Text fontSize="12px" color="text.note">
|
||||
@@ -122,8 +131,8 @@ export function ActionSelect({
|
||||
mb="m"
|
||||
css={actionInputStyle}
|
||||
>
|
||||
<Box className="action-input-text" color="text.body">
|
||||
{text}
|
||||
<Box className="action-input-text" color="text2">
|
||||
{textProp ?? text}
|
||||
</Box>
|
||||
<DownOutlined />
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
import React, { useState, useRef, KeyboardEvent, ChangeEvent, useEffect } from 'react';
|
||||
import { Dropdown, Menu, Tag } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const InputWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
padding: 4px 11px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 2px;
|
||||
min-height: 32px;
|
||||
&:hover {
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
&:focus-within {
|
||||
border-color: #40a9ff;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
`;
|
||||
|
||||
const Input = styled.input`
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
min-width: 50px;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
`;
|
||||
|
||||
const StyledTag = styled(Tag)`
|
||||
margin: 2px 4px 2px 0;
|
||||
`;
|
||||
|
||||
// Tailwind CSS 基础类名列表
|
||||
const tailwindClasses = [
|
||||
// 布局
|
||||
'container',
|
||||
'flex',
|
||||
'grid',
|
||||
'block',
|
||||
'inline',
|
||||
'inline-block',
|
||||
'hidden',
|
||||
// 弹性布局
|
||||
'flex-row',
|
||||
'flex-col',
|
||||
'flex-wrap',
|
||||
'flex-nowrap',
|
||||
'justify-start',
|
||||
'justify-end',
|
||||
'justify-center',
|
||||
'justify-between',
|
||||
'justify-around',
|
||||
'items-start',
|
||||
'items-end',
|
||||
'items-center',
|
||||
'items-baseline',
|
||||
'items-stretch',
|
||||
// 网格布局
|
||||
'grid-cols-1',
|
||||
'grid-cols-2',
|
||||
'grid-cols-3',
|
||||
'grid-cols-4',
|
||||
'grid-cols-5',
|
||||
'grid-cols-6',
|
||||
'grid-cols-12',
|
||||
// 间距
|
||||
'p-0',
|
||||
'p-1',
|
||||
'p-2',
|
||||
'p-3',
|
||||
'p-4',
|
||||
'p-5',
|
||||
'p-6',
|
||||
'p-8',
|
||||
'p-10',
|
||||
'p-12',
|
||||
'p-16',
|
||||
'p-20',
|
||||
'm-0',
|
||||
'm-1',
|
||||
'm-2',
|
||||
'm-3',
|
||||
'm-4',
|
||||
'm-5',
|
||||
'm-6',
|
||||
'm-8',
|
||||
'm-10',
|
||||
'm-12',
|
||||
'm-16',
|
||||
'm-20',
|
||||
// 尺寸
|
||||
'w-full',
|
||||
'w-auto',
|
||||
'w-1/2',
|
||||
'w-1/3',
|
||||
'w-2/3',
|
||||
'w-1/4',
|
||||
'w-3/4',
|
||||
'h-full',
|
||||
'h-auto',
|
||||
'h-screen',
|
||||
// 字体
|
||||
'text-xs',
|
||||
'text-sm',
|
||||
'text-base',
|
||||
'text-lg',
|
||||
'text-xl',
|
||||
'text-2xl',
|
||||
'text-3xl',
|
||||
'text-4xl',
|
||||
'text-5xl',
|
||||
'font-thin',
|
||||
'font-light',
|
||||
'font-normal',
|
||||
'font-medium',
|
||||
'font-semibold',
|
||||
'font-bold',
|
||||
'font-extrabold',
|
||||
// 文本颜色
|
||||
'text-black',
|
||||
'text-white',
|
||||
'text-gray-100',
|
||||
'text-gray-200',
|
||||
'text-gray-300',
|
||||
'text-gray-400',
|
||||
'text-gray-500',
|
||||
'text-red-500',
|
||||
'text-blue-500',
|
||||
'text-green-500',
|
||||
'text-yellow-500',
|
||||
'text-purple-500',
|
||||
'text-pink-500',
|
||||
// 背景颜色
|
||||
'bg-transparent',
|
||||
'bg-black',
|
||||
'bg-white',
|
||||
'bg-gray-100',
|
||||
'bg-gray-200',
|
||||
'bg-gray-300',
|
||||
'bg-gray-400',
|
||||
'bg-gray-500',
|
||||
'bg-red-500',
|
||||
'bg-blue-500',
|
||||
'bg-green-500',
|
||||
'bg-yellow-500',
|
||||
'bg-purple-500',
|
||||
'bg-pink-500',
|
||||
// 边框
|
||||
'border',
|
||||
'border-0',
|
||||
'border-2',
|
||||
'border-4',
|
||||
'border-8',
|
||||
'border-black',
|
||||
'border-white',
|
||||
'border-gray-300',
|
||||
'border-gray-400',
|
||||
'border-gray-500',
|
||||
// 圆角
|
||||
'rounded-none',
|
||||
'rounded-sm',
|
||||
'rounded',
|
||||
'rounded-lg',
|
||||
'rounded-full',
|
||||
// 阴影
|
||||
'shadow-sm',
|
||||
'shadow',
|
||||
'shadow-md',
|
||||
'shadow-lg',
|
||||
'shadow-xl',
|
||||
'shadow-2xl',
|
||||
'shadow-none',
|
||||
// 不透明度
|
||||
'opacity-0',
|
||||
'opacity-25',
|
||||
'opacity-50',
|
||||
'opacity-75',
|
||||
'opacity-100',
|
||||
];
|
||||
|
||||
interface ClassNameInputProps {
|
||||
value?: string;
|
||||
defaultValue?: string;
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
export function ClassNameInput({ value, defaultValue, onChange }: ClassNameInputProps) {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [suggestions, setSuggestions] = useState<string[]>([]);
|
||||
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [internalValue, setInternalValue] = useState(defaultValue || '');
|
||||
const isControlled = value !== undefined;
|
||||
const classNames = (isControlled ? value : internalValue).split(' ').filter(Boolean);
|
||||
|
||||
useEffect(() => {
|
||||
if (isControlled) {
|
||||
setInternalValue(value);
|
||||
}
|
||||
}, [isControlled, value]);
|
||||
|
||||
const isValidClassName = (className: string) => {
|
||||
return /^[a-zA-Z0-9_-]+(?::[a-zA-Z0-9_-]+)*$/.test(className);
|
||||
};
|
||||
|
||||
const updateValue = (newClassNames: string[]) => {
|
||||
const newValue = newClassNames.join(' ');
|
||||
if (isControlled) {
|
||||
onChange?.(newValue);
|
||||
} else {
|
||||
setInternalValue(newValue);
|
||||
onChange?.(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
if (highlightedIndex >= 0 && highlightedIndex < suggestions.length) {
|
||||
addClassName(suggestions[highlightedIndex]);
|
||||
} else {
|
||||
addClassName(inputValue.trim());
|
||||
}
|
||||
} else if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
setHighlightedIndex((prev) => (prev < suggestions.length - 1 ? prev + 1 : 0));
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : suggestions.length - 1));
|
||||
} else if (event.key === 'Backspace' && inputValue === '' && classNames.length > 0) {
|
||||
event.preventDefault();
|
||||
const newClassNames = [...classNames];
|
||||
newClassNames.pop();
|
||||
updateValue(newClassNames);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const input = event.target.value;
|
||||
setInputValue(input);
|
||||
if (input) {
|
||||
const matchedSuggestions = tailwindClasses
|
||||
.filter(
|
||||
(className) =>
|
||||
className.toLowerCase().includes(input.toLowerCase()) &&
|
||||
!classNames.includes(className),
|
||||
)
|
||||
.slice(0, 10);
|
||||
setSuggestions(matchedSuggestions);
|
||||
setHighlightedIndex(-1);
|
||||
} else {
|
||||
setSuggestions([]);
|
||||
}
|
||||
};
|
||||
|
||||
const addClassName = (className: string) => {
|
||||
if (className && !classNames.includes(className) && isValidClassName(className)) {
|
||||
updateValue([...classNames, className]);
|
||||
setInputValue('');
|
||||
setSuggestions([]);
|
||||
setHighlightedIndex(-1);
|
||||
}
|
||||
};
|
||||
|
||||
const removeClassName = (removedTag: string) => {
|
||||
const newClassNames = classNames.filter((tag) => tag !== removedTag);
|
||||
updateValue(newClassNames);
|
||||
};
|
||||
|
||||
const handleSuggestionClick = (suggestion: string) => {
|
||||
addClassName(suggestion);
|
||||
};
|
||||
|
||||
const menu = (
|
||||
<Menu>
|
||||
{suggestions.map((suggestion, index) => (
|
||||
<Menu.Item
|
||||
key={index}
|
||||
onClick={() => handleSuggestionClick(suggestion)}
|
||||
className={index === highlightedIndex ? 'ant-dropdown-menu-item-active' : ''}
|
||||
>
|
||||
{suggestion}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown overlay={menu} visible={suggestions.length > 0} placement="bottomLeft">
|
||||
<InputWrapper onClick={() => inputRef.current?.focus()}>
|
||||
{classNames.map((className) => (
|
||||
<StyledTag key={className} closable onClose={() => removeClassName(className)}>
|
||||
{className}
|
||||
</StyledTag>
|
||||
))}
|
||||
<Input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
onChange={handleInputChange}
|
||||
placeholder={classNames.length === 0 ? '输入 class 名称' : ''}
|
||||
/>
|
||||
</InputWrapper>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
@@ -26,3 +26,4 @@ export * from './tag-select';
|
||||
export * from './popover';
|
||||
export * from './drag-panel';
|
||||
export * from './context-action';
|
||||
export * from './classname-input';
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"suppressImplicitAnyIndexErrors": true,
|
||||
"ignoreDeprecations": "5.0",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strictNullChecks": false,
|
||||
|
||||
Reference in New Issue
Block a user