diff --git a/apps/playground/src/pages/index.tsx b/apps/playground/src/pages/index.tsx index 81c8fa0..6786c80 100644 --- a/apps/playground/src/pages/index.tsx +++ b/apps/playground/src/pages/index.tsx @@ -113,8 +113,8 @@ export default function App() { actions={ - - + + setShowNewPageModal(true)} /> - - + + + - diff --git a/packages/core/src/helpers/ast/generate.ts b/packages/core/src/helpers/ast/generate.ts index 8749149..0cab0e0 100644 --- a/packages/core/src/helpers/ast/generate.ts +++ b/packages/core/src/helpers/ast/generate.ts @@ -3,7 +3,7 @@ */ import generator, { GeneratorOptions } from '@babel/generator'; import * as t from '@babel/types'; -import { logger, wrapCode } from '@music163/tango-helpers'; +import { Dict, logger, wrapCode } from '@music163/tango-helpers'; import { formatCode } from '../string'; const defaultGeneratorOptions: GeneratorOptions = { @@ -25,19 +25,7 @@ export function ast2code(ast: t.Node, options: GeneratorOptions = defaultGenerat return code; } -const bracketPattern = /^\(.+\)$/s; - -/** - * 是否被 () 包裹 - * - * @example ({ foo: 'foo' }) -> true - * @example { foo: 'foo' } -> false - * - * @param str 目标字符串 - */ -function isWrappingWithBrackets(str: string) { - return bracketPattern.test(str); -} +const bracketPattern = /^\(.+\)$/; /** * 将表达式生成为块级代码 @@ -54,7 +42,7 @@ export function expression2code(node: t.Expression) { const isWrappingExpression = t.isObjectExpression(node) || t.isFunctionExpression(node); - if (isWrappingExpression && isWrappingWithBrackets(ret)) { + if (isWrappingExpression && bracketPattern.test(ret)) { // 如果是对象,输出包含 ({}),则去掉首尾的括号 ret = ret.slice(1, -1); } @@ -195,7 +183,7 @@ export function node2value(node: t.Node, isWrapCode = true): any { ); if (isSimpleObject) { // simple object: { key1, key2, key3 } - ret = node.properties.reduce((prev, propertyNode) => { + ret = node.properties.reduce((prev, propertyNode) => { if (propertyNode.type === 'ObjectProperty') { const key = keyNode2value(propertyNode.key); const value = node2value(propertyNode.value, isWrapCode); diff --git a/packages/core/src/helpers/ast/parse.ts b/packages/core/src/helpers/ast/parse.ts index 02ec0ad..b847b82 100644 --- a/packages/core/src/helpers/ast/parse.ts +++ b/packages/core/src/helpers/ast/parse.ts @@ -68,11 +68,7 @@ export function isValidExpressionCode(code: string) { * @returns */ export function code2ast(code: string): t.File { - try { - return parse(code, babelParserConfig); - } catch (err) { - logger.error('[code2ast failed!]', err); - } + return parse(code, babelParserConfig); } /** @@ -175,7 +171,7 @@ export function value2node( * 将 js 普通对象解析为 t.Node */ export function object2node( - obj: object, + obj: Dict, getValueNode: (value: any, key?: string) => t.Expression = value2node, ) { if (!isPlainObject(obj)) { diff --git a/packages/core/src/helpers/ast/traverse.ts b/packages/core/src/helpers/ast/traverse.ts index 8eaadc3..2d73595 100644 --- a/packages/core/src/helpers/ast/traverse.ts +++ b/packages/core/src/helpers/ast/traverse.ts @@ -1009,7 +1009,7 @@ export function serviceConfig2Node(payload: object) { }); } -export function updateServiceConfigToServiceFile(ast: t.File, config: Dict) { +export function updateServiceConfigToServiceFile(ast: t.File, config: Dict) { traverse(ast, { CallExpression(path) { const calleeName = keyNode2value(path.node.callee) as string; @@ -1017,7 +1017,7 @@ export function updateServiceConfigToServiceFile(ast: t.File, config: Dict { + const newPropertiesNodeMap = Object.keys(config).reduce((properties, key) => { const serviceConfig = config[key]; const property = t.objectProperty(t.identifier(key), serviceConfig2Node(serviceConfig)); properties[key] = property; diff --git a/packages/core/src/models/component-module.ts b/packages/core/src/models/component-module.ts index ebccdaa..3560010 100644 --- a/packages/core/src/models/component-module.ts +++ b/packages/core/src/models/component-module.ts @@ -13,7 +13,7 @@ export class TangoComponentsEntryModule extends TangoModule { constructor(workspace: IWorkspace, props: IFileConfig) { super(workspace, props, false); - this.update(props.code, false, false); + this.update(props.code, true, false); makeObservable(this, { _code: observable, _cleanCode: observable, diff --git a/packages/core/src/models/file.ts b/packages/core/src/models/file.ts index eadff56..4841675 100644 --- a/packages/core/src/models/file.ts +++ b/packages/core/src/models/file.ts @@ -24,6 +24,16 @@ export class TangoFile { */ lastModified: number; + /** + * 文件解析是否出错 + */ + isError: boolean; + + /** + * 文件解析错误消息 + */ + errorMessage: string; + _code: string; _cleanCode: string; @@ -40,6 +50,7 @@ export class TangoFile { 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) { diff --git a/packages/core/src/models/interfaces.ts b/packages/core/src/models/interfaces.ts index b40371c..92136ea 100644 --- a/packages/core/src/models/interfaces.ts +++ b/packages/core/src/models/interfaces.ts @@ -10,6 +10,7 @@ import { IPageConfigData, IImportSpecifierSourceData, IImportSpecifierData, + IFileError, } from '../types'; import { TangoFile, TangoJsonFile } from './file'; import { TangoRouteModule } from './route-module'; @@ -192,7 +193,18 @@ export interface IWorkspace { renameFile: (oldFilename: string, newFilename: string) => void; renameFolder: (oldFoldername: string, newFoldername: string) => void; - updateFile: (filename: string, code: string, shouldFormatCode?: boolean) => void; + /** + * 更新文件 + * @param filename 文件名 + * @param code 代码 + * @param isSyncAst 是否同步 ast + */ + updateFile: (filename: string, code: string, isSyncAst?: boolean) => void; + + /** + * 检查并同步文件的 ast + */ + syncFiles: () => void; listFiles: () => Record; getFile: (filename: string) => TangoFile; @@ -298,8 +310,12 @@ export interface IWorkspace { get bizComps(): string[]; get baseComps(): string[]; get localComps(): string[]; + get fileErrors(): IFileError[]; /** - * 是否是合法的项目 + * 是否是有效的项目 + * - 包含 tango.config.json + * - 包含视图模块 + * - 没有文件错误 */ get isValid(): boolean; } diff --git a/packages/core/src/models/module.ts b/packages/core/src/models/module.ts index 468c133..7c5c796 100644 --- a/packages/core/src/models/module.ts +++ b/packages/core/src/models/module.ts @@ -21,6 +21,11 @@ import { IWorkspace } from './interfaces'; export class TangoModule extends TangoFile { ast: t.File; + /** + * ast 是否与 code 保持同步 + */ + isAstSynced: boolean; + /** * 导入的依赖列表 */ @@ -33,23 +38,53 @@ export class TangoModule extends TangoFile { /** * 基于最新的 ast 进行同步 * @param code 如果传入 code,则基于 code 进行同步 - * @param isFormatCode 是否格式化代码 - * @param refreshWorkspace 是否刷新 workspace + * @param isSyncAst 是否同步 ast + * @param isRefreshWorkspace 是否刷新 workspace */ - update(code?: string, isFormatCode = true, refreshWorkspace = true) { + update(code?: string, isSyncAst = true, isRefreshWorkspace = true) { this.lastModified = Date.now(); - if (isNil(code)) { - this._syncByAst(); - } else { - this._syncByCode(code, isFormatCode); + + try { + if (isNil(code)) { + this._syncByAst(); + } else { + this._syncByCode(code, isSyncAst); + } + + if (isSyncAst) { + this._analysisAst(); + } + + this.isAstSynced = isSyncAst; + this.isError = false; + this.errorMessage = undefined; + + this.workspace.onFilesChange([this.filename]); + if (isRefreshWorkspace) { + this.workspace.refresh([this.filename]); + } + } catch (err: any) { + this.isError = true; + this.errorMessage = err.message; } + } - this._analysisAst(); - - this.workspace.onFilesChange([this.filename]); - - if (refreshWorkspace) { - this.workspace.refresh([this.filename]); + /** + * 基于当前的代码重新生成 ast + */ + updateAst() { + if (!this.isAstSynced) { + try { + this.ast = code2ast(this._code); + this._analysisAst(); + this.isAstSynced = true; + this.isError = false; + this.errorMessage = undefined; + } catch (err: any) { + this.isAstSynced = false; + this.isError = true; + this.errorMessage = err.message; + } } } @@ -75,22 +110,26 @@ export class TangoModule extends TangoFile { /** * 基于输入的源码进行同步 * @param code 源码 - * @param isFormatCode 是否格式化代码 + * @param isSyncAst 是否同步 ast * @returns */ - _syncByCode(code: string, isFormatCode = true) { + _syncByCode(code: string, isSyncAst = true) { if (code === this._code) { return; } // 提前格式化代码 - if (isFormatCode) { + try { code = formatCode(code); + } catch (err) { + // err ignored, format code failed } this._code = code; this._cleanCode = code; - this.ast = code2ast(code); + if (isSyncAst) { + this.ast = code2ast(code); + } } _analysisAst() { @@ -105,14 +144,17 @@ export class TangoModule extends TangoFile { export class TangoJsModule extends TangoModule { constructor(workspace: IWorkspace, props: IFileConfig) { super(workspace, props, false); - this.update(props.code, false, 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, }); } } diff --git a/packages/core/src/models/route-module.ts b/packages/core/src/models/route-module.ts index fbfac24..78cb132 100644 --- a/packages/core/src/models/route-module.ts +++ b/packages/core/src/models/route-module.ts @@ -1,4 +1,5 @@ import { action, computed, makeObservable, observable, toJS } from 'mobx'; +import { logger } from '@music163/tango-helpers'; import { traverseRouteFile, addRouteToRouteFile, @@ -27,10 +28,13 @@ export class TangoRouteModule extends TangoModule { _routes: observable, _code: observable, _cleanCode: observable, + isError: observable, + errorMessage: observable, routes: computed, code: computed, cleanCode: computed, update: action, + updateAst: action, }); } @@ -74,7 +78,7 @@ export class TangoRouteModule extends TangoModule { */ removeRoute(route: string) { if (route === '/') { - console.warn('index route should not be removed!'); + logger.warn('index route should not be removed!'); return; } const record = this.getRouteByRoutePath(route); diff --git a/packages/core/src/models/service-module.ts b/packages/core/src/models/service-module.ts index 5940462..8d42e7a 100644 --- a/packages/core/src/models/service-module.ts +++ b/packages/core/src/models/service-module.ts @@ -49,11 +49,14 @@ export class TangoServiceModule extends TangoModule { _baseConfig: observable, _code: observable, _cleanCode: observable, + isError: observable, + errorMessage: observable, serviceFunctions: computed, baseConfig: computed, cleanCode: computed, code: computed, update: action, + updateAst: action, }); } diff --git a/packages/core/src/models/store-module.ts b/packages/core/src/models/store-module.ts index ed208da..4b88084 100644 --- a/packages/core/src/models/store-module.ts +++ b/packages/core/src/models/store-module.ts @@ -31,10 +31,13 @@ export class TangoStoreEntryModule extends TangoModule { _stores: observable, _code: observable, _cleanCode: observable, + isError: observable, + errorMessage: observable, stores: computed, code: computed, cleanCode: computed, update: action, + updateAst: action, }); } diff --git a/packages/core/src/models/view-module.ts b/packages/core/src/models/view-module.ts index 305d846..6964ada 100644 --- a/packages/core/src/models/view-module.ts +++ b/packages/core/src/models/view-module.ts @@ -138,10 +138,14 @@ export class TangoViewModule extends TangoModule implements IViewFile { _code: observable, _cleanCode: observable, + isError: observable, + errorMessage: observable, + code: computed, cleanCode: computed, update: action, + updateAst: action, }); } @@ -320,7 +324,7 @@ export class TangoViewModule extends TangoModule implements IViewFile { updateNodeAttributes(nodeId: string, config: Record, relatedImports?: string[]) { if (relatedImports && relatedImports.length) { // 导入依赖的组件 - const newImportData = relatedImports.reduce((prev, name) => { + const newImportData = relatedImports.reduce>((prev, name) => { const proto = this.workspace.getPrototype(name); const { source, specifiers } = prototype2importDeclarationData(proto, this.filename); const existSpecifiers: IImportSpecifierData[] = prev[source]; diff --git a/packages/core/src/models/workspace.ts b/packages/core/src/models/workspace.ts index f39857c..f90a519 100644 --- a/packages/core/src/models/workspace.ts +++ b/packages/core/src/models/workspace.ts @@ -25,10 +25,10 @@ import { import { DropMethod } from './drop-target'; import { HistoryMessage, TangoHistory } from './history'; import { TangoNode } from './node'; -import { TangoJsModule } from './module'; +import { TangoJsModule, TangoModule } from './module'; import { TangoFile, TangoJsonFile, TangoLessFile } from './file'; import { IWorkspace } from './interfaces'; -import { IFileConfig, FileType, ITangoConfigPackages, IPageConfigData } from '../types'; +import { IFileConfig, FileType, ITangoConfigPackages, IPageConfigData, IFileError } from '../types'; import { SelectSource } from './select-source'; import { DragSource } from './drag-source'; import { TangoRouteModule } from './route-module'; @@ -151,11 +151,8 @@ export class Workspace extends EventTarget implements IWorkspace { */ private copyTempNodes: TangoNode[]; - /** - * 是否是合法的项目 - */ get isValid() { - return !!this.tangoConfigJson && !!this.activeViewModule; + return !!this.tangoConfigJson && !!this.activeViewModule && this.fileErrors.length === 0; } /** @@ -223,6 +220,19 @@ export class Workspace extends EventTarget implements IWorkspace { return Object.keys(this.componentsEntryModule?.exportList || {}); } + get fileErrors() { + const errors: IFileError[] = []; + this.files.forEach((file) => { + if (file.isError) { + errors.push({ + filename: file.filename, + message: file.errorMessage, + }); + } + }); + return errors; + } + constructor(options?: IWorkspaceOptions) { super(); this.history = new TangoHistory(this); @@ -256,6 +266,8 @@ export class Workspace extends EventTarget implements IWorkspace { activeViewFile: observable, pages: computed, bizComps: computed, + fileErrors: computed, + isValid: computed, setActiveRoute: action, setActiveFile: action, addFile: action, @@ -439,11 +451,15 @@ export class Workspace extends EventTarget implements IWorkspace { ); } - updateFile(filename: string, code: string, shouldFormatCode = false) { + updateFile(filename: string, code: string, isSyncAst = true) { const file = this.getFile(filename); - file.update(code); + if (file instanceof TangoModule) { + file.update(code, isSyncAst); + } else { + file.update(code); + } - const shouldFormat = shouldFormatCode ?? this.projectConfig?.designerConfig?.autoFormatCode; + const shouldFormat = this.projectConfig?.designerConfig?.autoFormatCode; if (shouldFormat && file instanceof TangoViewModule) { file.removeUnusedImportSpecifiers().update(); } @@ -455,12 +471,15 @@ export class Workspace extends EventTarget implements IWorkspace { }); } - /** - * 删除工作区的文件 - * @param filename - */ + syncFiles() { + this.files.forEach((file) => { + if (file instanceof TangoModule) { + file.updateAst(); + } + }); + } + removeFile(filename: string) { - // TODO: refactor visitFile to share this logic if (this.files.get(filename)) { // 如果是文件,直接删除 this.files.delete(filename); @@ -516,7 +535,7 @@ export class Workspace extends EventTarget implements IWorkspace { * @returns { [filename]: fileCode } */ listFiles() { - const ret = {}; + const ret: Dict = {}; this.files.forEach((file) => { ret[file.filename] = file.cleanCode; }); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 9af6892..94da69a 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -57,6 +57,11 @@ export interface IFileConfig { type?: FileType; } +export interface IFileError { + filename: string; + message: string; +} + /** * 视图节点数据类型 */ diff --git a/packages/designer/src/components/file-errors-overlay.tsx b/packages/designer/src/components/file-errors-overlay.tsx new file mode 100644 index 0000000..0d785d5 --- /dev/null +++ b/packages/designer/src/components/file-errors-overlay.tsx @@ -0,0 +1,68 @@ +import React, { useState } from 'react'; +import { Box, css } from 'coral-system'; +import { observer, useWorkspace } from '@music163/tango-context'; +import { CloseOutlined } from '@ant-design/icons'; + +const errorMessageStyle = css` + padding: 24px; + white-space: pre-wrap; +`; + +/** + * 文件错误提示浮层 + */ +export const FileErrorsOverlay = observer(() => { + const [isVisible, setIsVisible] = useState(true); + const workspace = useWorkspace(); + + const handleClose = () => { + setIsVisible(false); + }; + + if (workspace.fileErrors.length === 0) { + return null; + } + + if (!isVisible) { + return null; + } + + return ( + + + + + + 代码解析错误,请回到源码模式检查并修改错误 + + {workspace.fileErrors.map((fileError) => ( + + {fileError.filename} + + {fileError.message} + + + ))} + + + + ); +}); diff --git a/packages/designer/src/components/index.ts b/packages/designer/src/components/index.ts index 824226b..d12c01b 100644 --- a/packages/designer/src/components/index.ts +++ b/packages/designer/src/components/index.ts @@ -4,3 +4,4 @@ export * from './input-kv'; export * from './variable-tree'; export * from './variable-tree-modal'; export * from './components-popover'; +export * from './file-errors-overlay'; diff --git a/packages/designer/src/editor.tsx b/packages/designer/src/editor.tsx index a6bccd1..49fca49 100644 --- a/packages/designer/src/editor.tsx +++ b/packages/designer/src/editor.tsx @@ -2,8 +2,6 @@ import React, { useRef, useEffect, useCallback } from 'react'; import { Box } from 'coral-system'; import { MultiEditor, MultiEditorProps } from '@music163/tango-ui'; import { observer, useDesigner, useWorkspace } from '@music163/tango-context'; -import { isValidCode } from '@music163/tango-core'; -import { Modal } from 'antd'; const ideConfig = { // disableFileOps: { @@ -21,14 +19,9 @@ const ideConfig = { // disableSetting: true, }; -export interface CodeEditorProps extends Partial { - /** - * 是否自动清除未使用的导入 - */ - autoRemoveUnusedImports?: boolean; -} +export type CodeEditorProps = Partial; -export const CodeEditor = observer(({ autoRemoveUnusedImports, ...rest }: CodeEditorProps) => { +export const CodeEditor = observer((props: CodeEditorProps) => { const editorRef = useRef(null); const workspace = useWorkspace(); const designer = useDesigner(); @@ -47,26 +40,9 @@ export const CodeEditor = observer(({ autoRemoveUnusedImports, ...rest }: CodeEd const fileSave = useCallback( (path: string, value: string) => { - if (!isJsFile(path)) { - // 非 js 文件直接保存 - workspace.updateFile(path, value, autoRemoveUnusedImports); - return; - } - - // js 文件需要先检查语法,只有语法正确才会保存 - if (isValidCode(value)) { - workspace.updateFile(path, value, autoRemoveUnusedImports); - } else { - Modal.confirm({ - title: '检测到代码中存在语法错误,暂时无法将代码同步给设计器,是否回退到安全代码?', - onOk: () => { - editorRef.current?.refresh(files, activeFile); - }, - onCancel: () => {}, - }); - } + workspace.updateFile(path, value, false); }, - [workspace, autoRemoveUnusedImports, activeFile, files], + [workspace], ); const handleFileChange = useCallback( @@ -123,12 +99,8 @@ export const CodeEditor = observer(({ autoRemoveUnusedImports, ...rest }: CodeEd defaultPath={activeFile} defaultTheme="GithubLightDefault" defaultFiles={files} - {...rest} + {...props} /> ); }); - -function isJsFile(path: string) { - return /.jsx?$/.test(path); -} diff --git a/packages/designer/src/sidebar/sidebar.tsx b/packages/designer/src/sidebar/sidebar.tsx index 89e7d7d..5889778 100644 --- a/packages/designer/src/sidebar/sidebar.tsx +++ b/packages/designer/src/sidebar/sidebar.tsx @@ -3,7 +3,7 @@ import cx from 'classnames'; import { Box, Text, css, HTMLCoralProps } from 'coral-system'; import { Badge } from 'antd'; import { ReactComponentProps } from '@music163/tango-helpers'; -import { observer, useDesigner } from '@music163/tango-context'; +import { observer, useDesigner, useWorkspace } from '@music163/tango-context'; import { ResizableBox } from './resizable-box'; import { getWidget } from '../widgets'; @@ -19,15 +19,22 @@ const sidebarStyle = css` list-style: none; user-select: none; transition: all 0.3s ease; + + &.disabled { + pointer-events: none; + } } .SidebarPanelBarListItem { + color: var(--tango-colors-text2); + &.active { - background-color: var(--tango-colors-custom-sidebarItemActiveBg); + color: var(--tango-colors-brand); + background-color: var(--tango-colors-custom-sidebarItemActiveBg, --tango-colors-fill2); } &:hover { - background-color: var(--tango-colors-custom-sidebarItemHoverBg); + background-color: var(--tango-colors-custom-sidebarItemHoverBg, --tango-colors-fill2); } } `; @@ -76,6 +83,7 @@ export interface SidebarPanelItemProps function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 266, footer, children }: SidebarProps) { const designer = useDesigner(); + const workspace = useWorkspace(); const items = useMemo(() => { const ret: Record = {}; @@ -92,10 +100,23 @@ function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 266, footer, childre return ret; }, [children]); + if (designer.activeView !== 'design') { + return null; + } + return ( - + 0, + })} + > {Object.keys(items).map((key) => { const item = items[key]; const isActive = key === designer.activeSidebarPanel; @@ -112,7 +133,6 @@ function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 266, footer, childre icon={item.icon} label={item.label} showBadge={item.showBadge} - isActive={isActive} /> ); @@ -183,10 +203,6 @@ BaseSidebarPanel.Item = function ({ export const Sidebar = observer(BaseSidebarPanel); interface SidebarPanelBarItemProps { - /** - * 是否选中 - */ - isActive?: boolean; /** * 侧边栏图标 */ @@ -212,15 +228,9 @@ interface SidebarPanelBarItemProps { }; } -function SidebarPanelBarItem({ - isActive, - icon: iconProp, - label, - showBadge, -}: SidebarPanelBarItemProps) { - const color = isActive ? 'brand' : 'text.body'; +function SidebarPanelBarItem({ icon: iconProp, label, showBadge }: SidebarPanelBarItemProps) { let icon = ( - + {iconProp} ); @@ -241,7 +251,7 @@ function SidebarPanelBarItem({ py="12px" > {icon} - + {label} diff --git a/packages/designer/src/toolbar/history.tsx b/packages/designer/src/toolbar/history.tsx index b9f239c..0739861 100644 --- a/packages/designer/src/toolbar/history.tsx +++ b/packages/designer/src/toolbar/history.tsx @@ -5,11 +5,12 @@ import { observer, useWorkspace } from '@music163/tango-context'; export const HistoryTool = observer(() => { const workspace = useWorkspace(); + const disabled = !workspace.isValid; return ( { workspace.history.back(); @@ -20,7 +21,7 @@ export const HistoryTool = observer(() => { { workspace.history.forward(); diff --git a/packages/designer/src/toolbar/mode-switch.tsx b/packages/designer/src/toolbar/mode-switch.tsx index 6c458cc..e92342a 100644 --- a/packages/designer/src/toolbar/mode-switch.tsx +++ b/packages/designer/src/toolbar/mode-switch.tsx @@ -1,6 +1,5 @@ -import React, { useCallback } from 'react'; +import React from 'react'; import { Group } from 'coral-system'; -import { Modal } from 'antd'; import { ToggleButton, CodeOutlined, DualOutlined } from '@music163/tango-ui'; import { observer, useDesigner, useWorkspace } from '@music163/tango-context'; import { BorderOutlined } from '@ant-design/icons'; @@ -9,23 +8,13 @@ export const ModeSwitchTool = observer(() => { const workspace = useWorkspace(); const designer = useDesigner(); - const activeFileCheck = useCallback(() => { - if (workspace.activeFile !== workspace.activeViewFile) { - Modal.confirm({ - title: '当前打开的文件与视图不匹配,是否切换到当前视图对应的文件?', - onOk: () => { - workspace.setActiveFile(workspace.activeViewFile); - }, - }); - } - }, [workspace]); - return ( { + workspace.syncFiles(); // 保证 ast 与 code 是同步的 designer.setActiveView('design'); }} tooltip="设计视图" @@ -38,7 +27,6 @@ export const ModeSwitchTool = observer(() => { onClick={() => { designer.setActiveView('code'); // 切换到源码视图 designer.setActiveSidebarPanel(''); // 关闭左侧面板 - activeFileCheck(); }} tooltip="源码视图" > @@ -50,7 +38,6 @@ export const ModeSwitchTool = observer(() => { onClick={() => { designer.setActiveView('dual'); // 切换到双屏视图 designer.setActiveSidebarPanel(''); // 关闭左侧面板 - activeFileCheck(); }} tooltip="双屏视图" > diff --git a/packages/designer/src/toolbar/preview.tsx b/packages/designer/src/toolbar/preview.tsx index 499c4e2..366c8bb 100644 --- a/packages/designer/src/toolbar/preview.tsx +++ b/packages/designer/src/toolbar/preview.tsx @@ -1,13 +1,15 @@ import React from 'react'; import { ToggleButton } from '@music163/tango-ui'; import { EyeOutlined } from '@ant-design/icons'; -import { observer, useDesigner } from '@music163/tango-context'; +import { observer, useDesigner, useWorkspace } from '@music163/tango-context'; export const PreviewTool = observer(() => { const designer = useDesigner(); + const workspace = useWorkspace(); return ( { diff --git a/packages/designer/src/toolbar/toolbar.tsx b/packages/designer/src/toolbar/toolbar.tsx index 114fdd4..8f272c7 100644 --- a/packages/designer/src/toolbar/toolbar.tsx +++ b/packages/designer/src/toolbar/toolbar.tsx @@ -1,6 +1,8 @@ import React, { useMemo } from 'react'; import { Box, Group } from 'coral-system'; import { ReactComponentProps } from '@music163/tango-helpers'; +import { DesignerViewType } from '@music163/tango-core'; +import { observer, useDesigner } from '@music163/tango-context'; import { getWidget } from '../widgets'; export interface ToolbarProps { @@ -15,25 +17,23 @@ export function Toolbar({ children }: ToolbarProps) { let prevPlacement: string; React.Children.forEach(children, (child: React.ReactElement, index) => { - let fallbackNode; + const childProps = child.props; + + let fallbackChildren; if (child.key) { const Widget = getWidget(['toolbar', child.key].join('.')); if (Widget) { - fallbackNode = React.createElement(Widget, child.props.widgetProps); + fallbackChildren = ; } } - - let node = child.props.children ?? fallbackNode ?? null; - if (!node) { - node = child; // separator - } const key = child.key || index; - node = ( -
- {node} -
- ); - const placement = child.props.placement || prevPlacement || 'center'; + const node = React.cloneElement(child, { + key, + 'data-key': key, + children: childProps.children || fallbackChildren, + }); + + const placement = childProps.placement || prevPlacement || 'center'; switch (placement) { case 'left': left.push(node); @@ -68,17 +68,47 @@ export function Toolbar({ children }: ToolbarProps) { } export interface ToolbarItemProps extends ReactComponentProps { + /** + * 类型 + */ + type?: 'divider'; placement?: 'left' | 'center' | 'right'; children?: React.ReactElement; /** * 如果 key 匹配到内置组件的话,传递给子节点的属性 */ widgetProps?: object; + /** + * 工具箱展示的视图 + */ + activeViews?: DesignerViewType[]; } -function ToolbarItem({ placement, widgetProps, children }: ToolbarItemProps) { - return
{children}
; -} +const ToolbarItem = observer( + ({ + activeViews = ['code', 'design', 'dual'], + type, + placement, + widgetProps, + children, + ...rest + }: ToolbarItemProps) => { + const designer = useDesigner(); + + if (!activeViews.includes(designer.activeView)) { + return null; + } + + if (type === 'divider') { + return ; + } + return ( +
+ {children} +
+ ); + }, +); function Separator() { return ; diff --git a/packages/designer/src/widgets.ts b/packages/designer/src/widgets.ts index 109ce45..c468a0d 100644 --- a/packages/designer/src/widgets.ts +++ b/packages/designer/src/widgets.ts @@ -30,7 +30,7 @@ import { const widgets = {}; export function registerWidget(key: string, component: React.ComponentType) { - if (!/^(toolbar|sidebar|selectionMenu|contextMenu)\.[a-zA-z]+$/.test(key)) { + if (!/^(toolbar|sidebar|selectionMenu|contextMenu)\.[a-zA-Z]+$/.test(key)) { throw new Error( `Invalid widget key(${key}), should start with toolbar, sidebar, contextMenu or selectionMenu`, ); diff --git a/packages/designer/src/workspace-view.tsx b/packages/designer/src/workspace-view.tsx index 0218d0f..7e206c3 100644 --- a/packages/designer/src/workspace-view.tsx +++ b/packages/designer/src/workspace-view.tsx @@ -1,35 +1,37 @@ import React from 'react'; -import { Box } from 'coral-system'; +import { Box, HTMLCoralProps } from 'coral-system'; import { observer, useDesigner } from '@music163/tango-context'; import { DesignerViewType } from '@music163/tango-core'; -import { ComponentsPopover } from './components'; +import cx from 'classnames'; +import { ComponentsPopover, FileErrorsOverlay } from './components'; -export interface WorkspaceViewProps { +export interface WorkspaceViewProps extends HTMLCoralProps<'div'> { /** * 视图面板模式,对应 Workspace 的模式 */ mode?: DesignerViewType; - children: React.ReactNode; } export const WorkspaceView = observer((props: WorkspaceViewProps) => { - const { mode = 'design', children } = props; + const { mode = 'design', children, className, ...rest } = props; const designer = useDesigner(); const display = mode !== designer.activeView ? 'none' : 'block'; - // 云音乐移动端模式小屏幕适配,可能会溢出屏幕 + // 移动端模式小屏幕适配,可能会溢出屏幕 const overflow = designer.simulator.name === 'phone' ? 'auto' : designer.isPreview ? 'auto' : 'hidden'; return ( {children} {/* 添加组件弹层 */} {display === 'block' && } + {mode === 'design' && } ); }); diff --git a/packages/ui/src/icons/package-outlined.tsx b/packages/ui/src/icons/package-outlined.tsx index 181dcac..f840c4d 100644 --- a/packages/ui/src/icons/package-outlined.tsx +++ b/packages/ui/src/icons/package-outlined.tsx @@ -8,6 +8,7 @@ const PackageOutlinedSvg = () => ( xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" + fill="currentColor" >