mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-08-01 02:45:49 +00:00
Compare commits
No commits in common. "master" and "v1.8.0-beta.13" have entirely different histories.
master
...
v1.8.0-bet
83
CHANGELOG.md
83
CHANGELOG.md
@ -1,86 +1,3 @@
|
||||
# [1.8.0-beta.20](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.19...v1.8.0-beta.20) (2026-07-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **editor:** 修复删除页面/页面片时画布被误清空的问题 ([aa42144](https://github.com/Tencent/tmagic-editor/commit/aa4214473858200c62b6efab79c0671e06cc3917))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **editor,dep:** collectIdle 使用常驻 Web Worker 做依赖收集 ([4bbda35](https://github.com/Tencent/tmagic-editor/commit/4bbda354a987b0a409a01184e8511822a0384a13))
|
||||
# [1.8.0-beta.19](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.18...v1.8.0-beta.19) (2026-07-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **form:** 新增 validateOnInit,默认关闭初始化时自动校验 ([b7c04ed](https://github.com/Tencent/tmagic-editor/commit/b7c04edcd518554d5117c2212bbfe8ef632a4b37))
|
||||
# [1.8.0-beta.18](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.17...v1.8.0-beta.18) (2026-07-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **editor:** 修复 IdleTask 任务异常或 clearTasks 时队列卡住的问题 ([3fe9cf9](https://github.com/Tencent/tmagic-editor/commit/3fe9cf9b664dc5b39c17b40efe1d3935c89f7553))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **form:** 支持 typeMatch 异步校验与 validator 返回值约定 ([a123747](https://github.com/Tencent/tmagic-editor/commit/a123747e9d250166c2bb0f1d3ce3a39dd5baea9f))
|
||||
# [1.8.0-beta.17](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.16...v1.8.0-beta.17) (2026-07-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **editor:** 修复 collectIdle 批次结算与中断时 Promise 挂起问题 ([b5abf31](https://github.com/Tencent/tmagic-editor/commit/b5abf310663f0bc34bd2c51c2e1a28de0bdf6ab9))
|
||||
* **editor:** 切换动作类型时清空关联配置字段 ([b564c9e](https://github.com/Tencent/tmagic-editor/commit/b564c9e4aacd215dd46c4b839cf0033ceedc8047))
|
||||
* **form,table:** groupList分界线、空状态、actions只存在一个 ([dec4ebf](https://github.com/Tencent/tmagic-editor/commit/dec4ebfa20483f699a676e41e0541d23ebe24d81))
|
||||
* **form:** 完善 mountFormInstance 清理逻辑,避免临时表单实例泄漏 ([bf8df74](https://github.com/Tencent/tmagic-editor/commit/bf8df74864b78891a8820462062624702a1d33d3))
|
||||
* **table:** 列操作点击后关闭popover ([596d86f](https://github.com/Tencent/tmagic-editor/commit/596d86f2ab08d3d4e6548b5e4bd5759d18e4493d))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **editor,stage:** 新增 containerHighlightAddOnly 配置 ([e6cf694](https://github.com/Tencent/tmagic-editor/commit/e6cf694a032652e4f2c05efbab4a07a252e2ffa1))
|
||||
# [1.8.0-beta.16](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.15...v1.8.0-beta.16) (2026-07-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 参考建议可选值最多展示个数由 5 调整为 20 ([0e1986c](https://github.com/Tencent/tmagic-editor/commit/0e1986c81998e50461c294c1a27a8139e8ae9a13))
|
||||
# [1.8.0-beta.15](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.14...v1.8.0-beta.15) (2026-07-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** 为背景图片 URL 添加引号 ([97e0cac](https://github.com/Tencent/tmagic-editor/commit/97e0cac44a2d6ceafaf95fbc4ae716bfe5d47f70))
|
||||
* **editor:** 使用属性面板中的源码删除样式无效 ([cde0805](https://github.com/Tencent/tmagic-editor/commit/cde08052924ddef1c51faeb68ce15d3195827adb))
|
||||
* **editor:** 右键菜单动态变化后修复菜单位置防止截断 ([6919a0a](https://github.com/Tencent/tmagic-editor/commit/6919a0a0b19efff92821fc8d52b61b2f7382a817))
|
||||
* 跳过无字段名称配置的类型校验 ([11bf037](https://github.com/Tencent/tmagic-editor/commit/11bf037054774937132743a40006364e49ee8fd5))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **design:** 优化 FormItem 和 colorPicker 样式 ([2e031cc](https://github.com/Tencent/tmagic-editor/commit/2e031cc1346fb6ea739f0d45a89213b6577dc7ee))
|
||||
* **editor:** 数据源字段选择器校验提示细化并支持 fieldConfig 类型校验 ([fe9e754](https://github.com/Tencent/tmagic-editor/commit/fe9e754b50f14e6034170b2284553ec1c4536150))
|
||||
* **form,design:** 一行多个表单默认右靠齐label ([48abab7](https://github.com/Tencent/tmagic-editor/commit/48abab7051b93b7ed336172c21ab3b908733f2e7))
|
||||
* **form,editor:** submitForm/validateForm 静默挂载时注入静默标记,vs-code 字段跳过 monaco 渲染 ([3b9d5da](https://github.com/Tencent/tmagic-editor/commit/3b9d5da0bc9f323e944c0e2adb4fc88642ce1915))
|
||||
* **form:** text 类型校验允许数字值,兼容数据源字段配置 ([1f389ff](https://github.com/Tencent/tmagic-editor/commit/1f389ff9fd77fb93ad749f3fdbbc8aa0fe8725ac))
|
||||
* 新增 page-structure-change 事件,支持页面增删统一通知 ([745f449](https://github.com/Tencent/tmagic-editor/commit/745f44939605528e8e2ccd0639478db98293a441))
|
||||
# [1.8.0-beta.14](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.13...v1.8.0-beta.14) (2026-07-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **editor:** 保留历史操作来源并补发插入事件 ([89689bb](https://github.com/Tencent/tmagic-editor/commit/89689bb9fd2d2d3edcc027b87932cbdd3a5755ad))
|
||||
* **form:** 统一 extendState 合并逻辑 ([0feada8](https://github.com/Tencent/tmagic-editor/commit/0feada8d1fa4057444da746040b3824df09eef07))
|
||||
* **stage:** 修复选中组件时 stage 外层滚动 ([09eecd5](https://github.com/Tencent/tmagic-editor/commit/09eecd5bedd49e5a002fa5ef122eaa1579dc895b))
|
||||
* 调整编辑器配置项展示 ([8699dc1](https://github.com/Tencent/tmagic-editor/commit/8699dc12af9d52b1c460f61f3d3f48e4cf3fc5f6))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **design,editor:** 新增magic-admin主题样式,优化组件表单配置布局样式 ([b13cd34](https://github.com/Tencent/tmagic-editor/commit/b13cd3425b16eaeac33f897bf51e80f7984db782))
|
||||
* playground 支持主题切换 ([ee5247c](https://github.com/Tencent/tmagic-editor/commit/ee5247ca9436c385a5b634d161726006912fe77c))
|
||||
# [1.8.0-beta.13](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.11...v1.8.0-beta.13) (2026-07-22)
|
||||
|
||||
|
||||
|
||||
@ -162,7 +162,7 @@
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary,#409eff);;
|
||||
color: #409eff;
|
||||
background-color: #f9fafc;
|
||||
}
|
||||
|
||||
|
||||
@ -1020,18 +1020,6 @@ alt: 按住alt键启动识别
|
||||
|
||||
- **类型:** `'default' | 'alt' | ''`
|
||||
|
||||
## containerHighlightAddOnly
|
||||
|
||||
- **详情:**
|
||||
|
||||
是否只有新增组件(从组件列表拖入新组件)时才启用识别容器
|
||||
|
||||
开启后,在画布中拖动已有组件时不再识别容器,即已有组件不能通过拖动加入其他容器;新增组件仍然按 [containerHighlightType](#containerHighlightType) 配置的方式识别
|
||||
|
||||
- **默认值:** `false`
|
||||
|
||||
- **类型:** `boolean`
|
||||
|
||||
## stageRect
|
||||
|
||||
- **详情:**
|
||||
|
||||
@ -69,11 +69,10 @@
|
||||
|
||||
## delayedMarkContainer
|
||||
|
||||
- **类型**:`(event: MouseEvent, excludeElList?: Element[], isAdd?: boolean) => NodeJS.Timeout | undefined`
|
||||
- **类型**:`(event: MouseEvent, excludeElList?: Element[]) => NodeJS.Timeout | undefined`
|
||||
- **参数**:
|
||||
- `event`:鼠标事件
|
||||
- `excludeElList`:计算鼠标所在容器时要排除的元素列表
|
||||
- `isAdd`:当前操作是否为新增组件(从组件列表拖入新组件),`containerHighlightType` 为 `addOnly` 时只有新增才会标记容器
|
||||
- **详情**:
|
||||
|
||||
鼠标拖拽着元素,在容器上方悬停,延迟一段时间后,对容器进行标记,如果悬停时间够长将标记成功,悬停时间短,调用方通过返回的timeoutId取消标记
|
||||
|
||||
@ -69,8 +69,6 @@
|
||||
}
|
||||
```
|
||||
|
||||
`validator` 除了调用 `callback` 外,也兼容 async-validator 的返回值约定:返回 `false` 或 `Error` / 错误数组表示失败,返回 `true` 表示通过,返回 `Promise` 则以 resolve / reject 为结果,内部抛出的异常会转成校验失败。同步与异步 `typeMatch` 下这些约定行为一致,且只会上报一次结果(既调 `callback` 又返回 `Promise` 时以先到的为准)。
|
||||
|
||||
## 扩展自定义 type 规则
|
||||
|
||||
业务可覆盖内置规则,或为自定义字段 type 注册校验。自定义规则优先于内置规则。
|
||||
@ -109,26 +107,7 @@ deleteTypeMatchRule('foo');
|
||||
clearTypeMatchRules();
|
||||
```
|
||||
|
||||
自定义校验器签名:`(value, context) => string | undefined | Promise<string | undefined>`。返回错误文案表示失败,返回 `undefined` 表示通过。`context` 包含 `fieldType`、`mForm`、`props`、`message`。
|
||||
|
||||
### 异步校验
|
||||
|
||||
自定义校验器可以返回 `Promise`,用于需要异步确认取值是否合法的场景(如请求接口校验 id 是否存在)。内置规则均为同步。
|
||||
|
||||
```ts
|
||||
registerTypeMatchRule('mod-select', async (value, { message }) => {
|
||||
const exists = await checkModExists(value);
|
||||
if (!exists) {
|
||||
return message || `模块(${value})不存在`;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
异步校验器通过后才会执行同一条 rule 上的自定义 `validator`。需要注意几点:
|
||||
|
||||
- **校验器自身失败不算校验失败。** Promise 被 reject(如接口异常)时只打印错误、该字段按通过处理,避免网络故障阻塞操作。需要把失败暴露给用户时请在校验器内部 catch 并返回错误文案。
|
||||
- **建议自行缓存请求结果。** 每次校验都会执行校验器,无缓存会导致逐次输入都发请求;也可以给 rule 配 `trigger: 'blur'` 降低触发频率。
|
||||
- **只有最新一轮校验的结论会被采用。** 新一轮校验开始后,上一轮针对旧值、尚未返回的校验不再使用自己的结论,而是等最新一轮出结论后一起结算,因此旧结论不会晚到覆盖新结论,`form.validate()` 也不会对一个尚未校验完的值返回成功。
|
||||
自定义校验器签名:`(value, context) => string | undefined`。返回错误文案表示失败,返回 `undefined` 表示通过。`context` 包含 `fieldType`、`mForm`、`props`、`message`。
|
||||
|
||||
### 安装时注册
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ export default defineConfig([
|
||||
'*/**/public/**/*',
|
||||
'*/**/types/**/*',
|
||||
'*/**/*.config.ts',
|
||||
'./temp/**/*',
|
||||
'./tepm/**/*',
|
||||
'vite-env.d.ts',
|
||||
]),
|
||||
...eslintConfig(path.join(path.dirname(fileURLToPath(import.meta.url)), 'tsconfig.json')),
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.20",
|
||||
"version": "1.8.0-beta.13",
|
||||
"name": "tmagic",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.20",
|
||||
"version": "1.8.0-beta.13",
|
||||
"name": "@tmagic/cli",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.20",
|
||||
"version": "1.8.0-beta.13",
|
||||
"name": "@tmagic/core",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -51,7 +51,7 @@ export const style2Obj = (style: string) => {
|
||||
|
||||
export const fillBackgroundImage = (value: string) => {
|
||||
if (value && !/^url/.test(value) && !/^linear-gradient/.test(value)) {
|
||||
return `url("${value}")`;
|
||||
return `url(${value})`;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
@ -341,20 +341,6 @@ describe('App 配置/方法/组件注册', () => {
|
||||
expect(app.page).toBeUndefined();
|
||||
});
|
||||
|
||||
test('deletePage 销毁当前 page', () => {
|
||||
const app = new App({
|
||||
config: {
|
||||
type: NodeType.ROOT,
|
||||
id: 'app',
|
||||
items: [{ type: NodeType.PAGE, id: 'p1', items: [] }],
|
||||
},
|
||||
});
|
||||
expect(app.page?.data.id).toBe('p1');
|
||||
|
||||
app.deletePage();
|
||||
expect(app.page).toBeUndefined();
|
||||
});
|
||||
|
||||
test('runCode 执行代码块', async () => {
|
||||
const fn = vi.fn();
|
||||
const app = new App({
|
||||
|
||||
@ -50,7 +50,7 @@ describe('style2Obj', () => {
|
||||
|
||||
describe('fillBackgroundImage', () => {
|
||||
test('裸路径会包裹 url()', () => {
|
||||
expect(fillBackgroundImage('a.png')).toBe('url("a.png")');
|
||||
expect(fillBackgroundImage('a.png')).toBe('url(a.png)');
|
||||
});
|
||||
|
||||
test('已经是 url() 不重复包裹', () => {
|
||||
@ -127,7 +127,7 @@ describe('transformStyle', () => {
|
||||
|
||||
test('backgroundImage: browser 下补全 url()', () => {
|
||||
expect(transformStyle({ backgroundImage: 'a.png' }, 'browser')).toEqual({
|
||||
backgroundImage: 'url("a.png")',
|
||||
backgroundImage: 'url(a.png)',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.20",
|
||||
"version": "1.8.0-beta.13",
|
||||
"name": "@tmagic/data-source",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -7,7 +7,8 @@ import {
|
||||
DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX,
|
||||
dataSourceTemplateRegExp,
|
||||
getValueByKeyPath,
|
||||
isPageOrFragment,
|
||||
isPage,
|
||||
isPageFragment,
|
||||
NODE_CONDS_KEY,
|
||||
replaceChildNode,
|
||||
} from '@tmagic/core';
|
||||
@ -67,7 +68,7 @@ export const compliedConditions = (node: { [NODE_CONDS_KEY]?: DisplayCond[] }, d
|
||||
};
|
||||
|
||||
export const updateNode = (node: MNode, dsl: MApp) => {
|
||||
if (isPageOrFragment(node)) {
|
||||
if (isPage(node) || isPageFragment(node)) {
|
||||
const index = dsl.items?.findIndex((child: MNode) => child.id === node.id);
|
||||
dsl.items.splice(index, 1, node as MPage | MPageFragment);
|
||||
} else {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.20",
|
||||
"version": "1.8.0-beta.13",
|
||||
"name": "@tmagic/dep",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { DepData } from '@tmagic/schema';
|
||||
|
||||
import { DepTargetType, type IsTarget, type TargetDescriptor, type TargetOptions } from './types';
|
||||
import { DepTargetType, type IsTarget, type TargetOptions } from './types';
|
||||
|
||||
export interface DepUpdateOptions {
|
||||
id: string | number;
|
||||
@ -40,17 +40,12 @@ export default class Target {
|
||||
* 是否默认收集,默认为true,当值为false时需要传入type参数给collect方法才会被收集
|
||||
*/
|
||||
public isCollectByDefault?: boolean;
|
||||
/**
|
||||
* 可序列化描述,有该描述的 target 可以在 worker 中重建,从而把依赖收集放到子线程执行
|
||||
*/
|
||||
public descriptor?: TargetDescriptor;
|
||||
|
||||
constructor(options: TargetOptions) {
|
||||
this.isTarget = options.isTarget;
|
||||
this.id = options.id;
|
||||
this.name = options.name;
|
||||
this.isCollectByDefault = options.isCollectByDefault ?? true;
|
||||
this.descriptor = options.descriptor;
|
||||
if (options.type) {
|
||||
this.type = options.type;
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { CodeBlockContent, DataSourceSchema, DepData, Id } from '@tmagic/schema';
|
||||
import type { DepData } from '@tmagic/schema';
|
||||
|
||||
import type Target from './Target';
|
||||
|
||||
@ -17,30 +17,6 @@ export enum DepTargetType {
|
||||
|
||||
export type IsTarget = (key: string | number, value: any, data?: Record<string, any>) => boolean;
|
||||
|
||||
/** 创建代码块 target 只需要名称,Record 用于兼容直接传入完整代码块 */
|
||||
export type CodeBlockName = Pick<CodeBlockContent, 'name'> & Record<string, any>;
|
||||
|
||||
/**
|
||||
* target 的可序列化描述
|
||||
*
|
||||
* isTarget 是带闭包的函数,无法跨线程传递,只能在另一端用同样的入参重新创建。
|
||||
* 内置的代码块/数据源 target 由工厂函数写入该描述,因此可以放到 worker 中收集;
|
||||
* 业务自定义的 target 没有该描述,只能在主线程收集。
|
||||
*
|
||||
* 各类型只带 isTarget 真正用到的字段:
|
||||
* - CODE_BLOCK: id + name
|
||||
* - DATA_SOURCE / DATA_SOURCE_COND: id + 完整 fields(含 type / 嵌套,用于判断数组模板)
|
||||
* - DATA_SOURCE_METHOD: id + 方法名/字段名(不序列化 content)
|
||||
*/
|
||||
export type TargetDescriptor =
|
||||
| { type: DepTargetType.CODE_BLOCK; id: Id; codeBlock: CodeBlockName }
|
||||
| { type: DepTargetType.DATA_SOURCE; ds: Pick<DataSourceSchema, 'id' | 'fields'> }
|
||||
| { type: DepTargetType.DATA_SOURCE_COND; ds: Pick<DataSourceSchema, 'id' | 'fields'> }
|
||||
| {
|
||||
type: DepTargetType.DATA_SOURCE_METHOD;
|
||||
ds: { id: Id; methods: Array<{ name?: string }>; fields: Array<{ name?: string }> };
|
||||
};
|
||||
|
||||
export interface TargetOptions {
|
||||
isTarget: IsTarget;
|
||||
id: string | number;
|
||||
@ -50,8 +26,6 @@ export interface TargetOptions {
|
||||
initialDeps?: DepData;
|
||||
/** 是否默认收集,默认为true,当值为false时需要传入type参数给collect方法才会被收集 */
|
||||
isCollectByDefault?: boolean;
|
||||
/** 可序列化描述,用于在 worker 中重建该 target */
|
||||
descriptor?: TargetDescriptor;
|
||||
}
|
||||
|
||||
export interface TargetList {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import {
|
||||
type CodeBlockContent,
|
||||
type DataSchema,
|
||||
type DataSourceSchema,
|
||||
type DepData,
|
||||
@ -15,17 +16,16 @@ import {
|
||||
} from '@tmagic/utils';
|
||||
|
||||
import Target from './Target';
|
||||
import { type CodeBlockName, DepTargetType, type TargetDescriptor, type TargetList } from './types';
|
||||
import { DepTargetType, type TargetList } from './types';
|
||||
|
||||
const INTEGER_REGEXP = /^\d+$/;
|
||||
|
||||
export const createCodeBlockTarget = (id: Id, codeBlock: CodeBlockName, initialDeps: DepData = {}) =>
|
||||
export const createCodeBlockTarget = (id: Id, codeBlock: CodeBlockContent, initialDeps: DepData = {}) =>
|
||||
new Target({
|
||||
type: DepTargetType.CODE_BLOCK,
|
||||
id,
|
||||
initialDeps,
|
||||
name: codeBlock.name,
|
||||
descriptor: { type: DepTargetType.CODE_BLOCK, id, codeBlock: { name: codeBlock.name } },
|
||||
isTarget: (_key: string | number, value: any) => {
|
||||
if (id === value) {
|
||||
return true;
|
||||
@ -238,8 +238,6 @@ export const createDataSourceTarget = (ds: Pick<DataSourceSchema, 'id' | 'fields
|
||||
type: DepTargetType.DATA_SOURCE,
|
||||
id: ds.id,
|
||||
initialDeps,
|
||||
// isTarget 需要完整 fields 结构(含 type / 嵌套 fields),但不必带上 methods 等无关字段
|
||||
descriptor: { type: DepTargetType.DATA_SOURCE, ds: { id: ds.id, fields: ds.fields || [] } },
|
||||
isTarget: (key: string | number, value: any) => isDataSourceTarget(ds, key, value),
|
||||
});
|
||||
|
||||
@ -248,8 +246,6 @@ export const createDataSourceCondTarget = (ds: Pick<DataSourceSchema, 'id' | 'fi
|
||||
type: DepTargetType.DATA_SOURCE_COND,
|
||||
id: ds.id,
|
||||
initialDeps,
|
||||
// 与 DATA_SOURCE 相同,只序列化 isTarget 所需的 id + fields
|
||||
descriptor: { type: DepTargetType.DATA_SOURCE_COND, ds: { id: ds.id, fields: ds.fields || [] } },
|
||||
isTarget: (key: string | number, value: any) => isDataSourceCondTarget(ds, key, value),
|
||||
});
|
||||
|
||||
@ -261,15 +257,6 @@ export const createDataSourceMethodTarget = (
|
||||
type: DepTargetType.DATA_SOURCE_METHOD,
|
||||
id: ds.id,
|
||||
initialDeps,
|
||||
// isTarget 只用方法名/字段名,不序列化 content 等函数,降低 idle 收集通信成本
|
||||
descriptor: {
|
||||
type: DepTargetType.DATA_SOURCE_METHOD,
|
||||
ds: {
|
||||
id: ds.id,
|
||||
methods: (ds.methods || []).map((method) => ({ name: method.name })),
|
||||
fields: (ds.fields || []).map((field) => ({ name: field.name })),
|
||||
},
|
||||
},
|
||||
isTarget: (_key: string | number, value: any) => {
|
||||
// 使用data-source-method-select 可以配置出来
|
||||
if (!Array.isArray(value)) {
|
||||
@ -295,31 +282,6 @@ export const createDataSourceMethodTarget = (
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 用可序列化描述重建 target,使依赖收集可以在 worker 等无法传递函数的环境中进行
|
||||
* @param descriptor 由工厂函数写入 target 的描述
|
||||
* @param initialDeps 初始依赖
|
||||
* @returns Target
|
||||
*/
|
||||
export const createTargetByDescriptor = (descriptor: TargetDescriptor, initialDeps: DepData = {}): Target => {
|
||||
switch (descriptor.type) {
|
||||
case DepTargetType.CODE_BLOCK:
|
||||
return createCodeBlockTarget(descriptor.id, descriptor.codeBlock, initialDeps);
|
||||
case DepTargetType.DATA_SOURCE:
|
||||
return createDataSourceTarget(descriptor.ds, initialDeps);
|
||||
case DepTargetType.DATA_SOURCE_COND:
|
||||
return createDataSourceCondTarget(descriptor.ds, initialDeps);
|
||||
case DepTargetType.DATA_SOURCE_METHOD:
|
||||
// descriptor 只保留名称,对 isTarget 已足够;重建时按同样结构传入
|
||||
return createDataSourceMethodTarget(
|
||||
descriptor.ds as Pick<DataSourceSchema, 'id' | 'methods' | 'fields'>,
|
||||
initialDeps,
|
||||
);
|
||||
default:
|
||||
throw new Error(`unknown target descriptor: ${JSON.stringify(descriptor)}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const traverseTarget = (
|
||||
targetsList: TargetList,
|
||||
cb: (target: Target) => void,
|
||||
|
||||
@ -266,65 +266,6 @@ describe('utils', () => {
|
||||
expect(t3.isTarget('k', ['ds_1', 'unknown'])).toBe(true);
|
||||
});
|
||||
|
||||
test('内置 target 带可序列化描述,可在 worker 中重建', () => {
|
||||
const ds = { id: 'ds_1', fields: [{ name: 'name' }] as DataSchema[] };
|
||||
|
||||
expect(utils.createCodeBlockTarget('code_1', { name: 'fn', content: () => false, params: [] }).descriptor).toEqual({
|
||||
type: DepTargetType.CODE_BLOCK,
|
||||
id: 'code_1',
|
||||
codeBlock: { name: 'fn' },
|
||||
});
|
||||
const dsWithMethods = {
|
||||
...ds,
|
||||
methods: [{ name: 'load', content: () => undefined, params: [] } as any],
|
||||
};
|
||||
// DATA_SOURCE / COND 只带 id + fields,不透传完整数据源对象
|
||||
expect(utils.createDataSourceTarget(dsWithMethods).descriptor).toEqual({
|
||||
type: DepTargetType.DATA_SOURCE,
|
||||
ds: { id: 'ds_1', fields: ds.fields },
|
||||
});
|
||||
expect(utils.createDataSourceCondTarget(dsWithMethods).descriptor).toEqual({
|
||||
type: DepTargetType.DATA_SOURCE_COND,
|
||||
ds: { id: 'ds_1', fields: ds.fields },
|
||||
});
|
||||
// METHOD 描述只保留名称,不带 content 等函数
|
||||
expect(utils.createDataSourceMethodTarget(dsWithMethods).descriptor).toEqual({
|
||||
type: DepTargetType.DATA_SOURCE_METHOD,
|
||||
ds: { id: 'ds_1', methods: [{ name: 'load' }], fields: [{ name: 'name' }] },
|
||||
});
|
||||
// 自定义 target 无法序列化 isTarget,因此没有描述
|
||||
expect(new Target({ id: 'custom', isTarget: () => true }).descriptor).toBeUndefined();
|
||||
});
|
||||
|
||||
test('createTargetByDescriptor 用描述重建出等价的 target', () => {
|
||||
const ds = { id: 'ds_1', fields: [{ name: 'name' }] as DataSchema[] };
|
||||
|
||||
const codeBlockTarget = utils.createTargetByDescriptor({
|
||||
type: DepTargetType.CODE_BLOCK,
|
||||
id: 'code_1',
|
||||
codeBlock: { name: 'fn' },
|
||||
});
|
||||
expect(codeBlockTarget.type).toBe(DepTargetType.CODE_BLOCK);
|
||||
expect(codeBlockTarget.name).toBe('fn');
|
||||
expect(codeBlockTarget.isTarget('created', 'code_1')).toBe(true);
|
||||
|
||||
const dsTarget = utils.createTargetByDescriptor({ type: DepTargetType.DATA_SOURCE, ds });
|
||||
expect(dsTarget.type).toBe(DepTargetType.DATA_SOURCE);
|
||||
expect(dsTarget.isTarget('text', '${ds_1.name}')).toBe(true);
|
||||
|
||||
const condTarget = utils.createTargetByDescriptor({ type: DepTargetType.DATA_SOURCE_COND, ds });
|
||||
expect(condTarget.isTarget(`${NODE_CONDS_KEY}_x`, ['ds_1', 'name'])).toBe(true);
|
||||
|
||||
const methodTarget = utils.createTargetByDescriptor(
|
||||
{ type: DepTargetType.DATA_SOURCE_METHOD, ds: { ...ds, methods: [] } },
|
||||
{ n1: { name: 'n1', keys: ['k'] } },
|
||||
);
|
||||
expect(methodTarget.isTarget('k', ['ds_1', 'load'])).toBe(true);
|
||||
expect(methodTarget.deps.n1.keys).toEqual(['k']);
|
||||
|
||||
expect(() => utils.createTargetByDescriptor({ type: 'unknown' } as any)).toThrow();
|
||||
});
|
||||
|
||||
test('traverseTarget 遍历所有 / 指定 type', () => {
|
||||
const t1 = new Target({ id: '1', isTarget: () => true, type: 'a' });
|
||||
const t2 = new Target({ id: '2', isTarget: () => true, type: 'b' });
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.20",
|
||||
"version": "1.8.0-beta.13",
|
||||
"name": "@tmagic/design",
|
||||
"type": "module",
|
||||
"sideEffects": [
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
<template>
|
||||
<component class="tmagic-design-alert" :is="uiComponent" v-bind="uiProps">
|
||||
<slot></slot>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { getDesignConfig } from './config';
|
||||
import type { AlertProps } from './types';
|
||||
|
||||
defineOptions({
|
||||
name: 'TMAlert',
|
||||
});
|
||||
|
||||
const props = defineProps<AlertProps>();
|
||||
|
||||
const ui = getDesignConfig('components')?.alert;
|
||||
const uiComponent = ui?.component || 'el-alert';
|
||||
|
||||
const uiProps = computed<AlertProps>(() => ui?.props(props) || props);
|
||||
</script>
|
||||
@ -1,9 +1,5 @@
|
||||
<template>
|
||||
<component
|
||||
:is="uiComponent"
|
||||
:class="['tmagic-design-card', { 'tmagic-design-card--flat': isFlat, 'm-flat-hide-body': isHideBody }]"
|
||||
v-bind="uiProps"
|
||||
>
|
||||
<component class="tmagic-design-card" :is="uiComponent" v-bind="uiProps">
|
||||
<template #header v-if="$slots.header">
|
||||
<slot name="header" class="header"></slot>
|
||||
</template>
|
||||
@ -18,7 +14,6 @@
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { getDesignConfig } from './config';
|
||||
import { isGlobalFlat } from './index';
|
||||
import type { CardProps } from './types';
|
||||
|
||||
defineOptions({
|
||||
@ -31,14 +26,5 @@ const ui = getDesignConfig('components')?.card;
|
||||
|
||||
const uiComponent = ui?.component || 'el-card';
|
||||
|
||||
// 当祖先 `<MEditor>` / `<MForm>` 的 theme 是 `magic-admin` 时,整套设计语言默认走「无卡片感」,
|
||||
// 因此这里自动等价于 `flat=true`;调用方仍可显式传 `flat` 强制开启。
|
||||
const isFlat = computed(() => !!props.flat || isGlobalFlat.value);
|
||||
const isHideBody = computed(() => isFlat.value && props.bodyStyle?.display === 'none');
|
||||
// 把 `flat` 从转发给底层 UI 库(el-card / t-card 等)的 props 中剥离:
|
||||
// 它仅作用于本组件的视觉修饰类,不需要也不应该作为属性落到底层组件 / DOM 上。
|
||||
const uiProps = computed<CardProps>(() => {
|
||||
const { flat: _flat, ...rest } = props;
|
||||
return ui?.props(rest as CardProps) || (rest as CardProps);
|
||||
});
|
||||
const uiProps = computed<CardProps>(() => ui?.props(props) || props);
|
||||
</script>
|
||||
|
||||
@ -7,39 +7,18 @@
|
||||
@update:modelValue="updateModelValue"
|
||||
>
|
||||
</component>
|
||||
<MInput
|
||||
v-if="isFlat && !isLargeStageContainer"
|
||||
@update:modelValue="updateModelValue"
|
||||
@change="changeHandler"
|
||||
v-bind="uiProps"
|
||||
class="tmagic-design-color-picker-input"
|
||||
clearable
|
||||
></MInput>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, type ComputedRef, inject } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { getDesignConfig } from './config';
|
||||
import { isGlobalFlat } from './index';
|
||||
import MInput from './Input.vue';
|
||||
import type { ColorPickerProps } from './types';
|
||||
|
||||
defineOptions({
|
||||
name: 'TMColorPicker',
|
||||
});
|
||||
|
||||
const isFlat = computed(() => !!props.flat || isGlobalFlat.value);
|
||||
|
||||
// 祖先 `<MEditor>` 用 computed 包着 provide,值会随 props.isLargeStageContainer 变化。
|
||||
// 类型标成 `ComputedRef<boolean>`,模板里 `<script setup>` 自动解包,仍可直接写
|
||||
// `!isLargeStageContainer`;默认值也得包成 computed,保持形状一致,避免祖先没提供时
|
||||
// 模板里对一个裸 `false` 和 `ComputedRef` 走两条不同的取值路径。
|
||||
const isLargeStageContainer = inject<ComputedRef<boolean>>(
|
||||
'isLargeStageContainer',
|
||||
computed(() => false),
|
||||
);
|
||||
|
||||
const props = withDefaults(defineProps<ColorPickerProps>(), {
|
||||
showAlpha: false,
|
||||
disabled: false,
|
||||
|
||||
@ -1,24 +1,12 @@
|
||||
<template>
|
||||
<component
|
||||
class="tmagic-design-form-item"
|
||||
:class="{ 'has-extra-tips': adapterType === 'element-plus' && extraTips }"
|
||||
:is="uiComponent"
|
||||
v-bind="uiProps"
|
||||
>
|
||||
<component class="tmagic-design-form-item" :is="uiComponent" v-bind="uiProps">
|
||||
<template #label>
|
||||
<slot name="label"> </slot>
|
||||
<slot name="label"></slot>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<slot></slot>
|
||||
<alert
|
||||
v-if="adapterType === 'element-plus' && extraTips"
|
||||
:title="extraTips"
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
></alert>
|
||||
<div v-else-if="adapterType === 'element-plus' && extra" v-html="extra" class="m-form-tip"></div>
|
||||
<div v-if="adapterType === 'element-plus' && extra" v-html="extra" class="m-form-tip"></div>
|
||||
</template>
|
||||
|
||||
<template v-if="adapterType === 'element-plus'" #error="{ error }">
|
||||
@ -28,32 +16,26 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import Alert from './Alert.vue';
|
||||
import { getDesignConfig } from './config';
|
||||
import { stripValidateSuggestion } from './formValidateMessage';
|
||||
import { isGlobalFlat } from './index';
|
||||
import type { FormItemProps } from './types';
|
||||
|
||||
defineOptions({
|
||||
name: 'TMFormItem',
|
||||
});
|
||||
|
||||
const props = defineProps<FormItemProps & { theme?: string }>();
|
||||
const props = defineProps<FormItemProps>();
|
||||
|
||||
const ui = getDesignConfig('components')?.formItem;
|
||||
|
||||
const uiComponent = ui?.component || 'el-form-item';
|
||||
|
||||
const adapterType = getDesignConfig('adapterType');
|
||||
|
||||
const formInline = inject<boolean>('formInline', false);
|
||||
const formInRow = inject<boolean>('isInRow', false);
|
||||
const uiProps = computed<FormItemProps>(() => {
|
||||
const { extra, extraTips, ...rest } = ui?.props(props) || props;
|
||||
if (isGlobalFlat.value && rest.labelPosition === undefined) {
|
||||
return { ...rest, labelPosition: formInline || formInRow ? 'right' : 'left' };
|
||||
}
|
||||
const { extra, ...rest } = ui?.props(props) || props;
|
||||
return rest;
|
||||
});
|
||||
|
||||
|
||||
@ -4,9 +4,10 @@
|
||||
<div
|
||||
v-if="popoverVisible || !destroyOnClose"
|
||||
v-show="popoverVisible"
|
||||
class="tmagic-design-popper"
|
||||
ref="popperElementRef"
|
||||
:tabindex="tabindex"
|
||||
:class="['tmagic-design-popper', popperClass, themeClass]"
|
||||
:class="popperClass"
|
||||
:style="style"
|
||||
@mouseenter.once="popperMouseenterHandler"
|
||||
>
|
||||
@ -21,7 +22,7 @@ import { computed, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, ref
|
||||
import type { Instance } from '@popperjs/core';
|
||||
import { createPopper } from '@popperjs/core';
|
||||
|
||||
import { useThemeClass, useZIndex } from './index';
|
||||
import { useZIndex } from './index';
|
||||
import type { PopoverProps } from './types';
|
||||
|
||||
defineSlots<{
|
||||
@ -45,12 +46,6 @@ const props = withDefaults(defineProps<PopoverProps>(), {
|
||||
closeOnClickOutside: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* 主题修饰类(来自最近的 `<MEditor>` / `<MForm>` 祖先 provide)。
|
||||
* 挂在 `Teleport` 出去的 popper 根节点上,让主题级 CSS 变量(`--el-color-primary` 等)
|
||||
* 在 portal 节点上也能命中。详见 `@tmagic/design/theme.ts`。
|
||||
*/
|
||||
const themeClass = useThemeClass();
|
||||
const emit = defineEmits<{
|
||||
/** 受控模式(传入了 visible)下点击外部收起时触发,便于配合 v-model:visible。 */
|
||||
'update:visible': [_visible: boolean];
|
||||
|
||||
@ -8,7 +8,6 @@ import './theme/index.scss';
|
||||
|
||||
export * from './types';
|
||||
export * from './config';
|
||||
export * from './theme';
|
||||
export * from './formValidateMessage';
|
||||
|
||||
export { default as TMagicAutocomplete } from './Autocomplete.vue';
|
||||
@ -95,10 +94,6 @@ export let useZIndex = (zIndexOverrides?: Ref<number>) => {
|
||||
};
|
||||
};
|
||||
|
||||
// 与 form/utils/config、editor/utils/config 里的同名变量保持一致:用 ref 持有 flat 全局开关,
|
||||
// const 引用本身不可变,规避 `no-mutable-exports` / `naming-convention` 对 `let` 模块变量的限制。
|
||||
export const isGlobalFlat = ref(false);
|
||||
|
||||
export default {
|
||||
install(app: App, options: DesignPluginOptions) {
|
||||
tMagicMessage =
|
||||
@ -134,8 +129,6 @@ export default {
|
||||
useZIndex = options.useZIndex;
|
||||
}
|
||||
|
||||
isGlobalFlat.value = options.flat ?? false;
|
||||
|
||||
if (options.adapterType && globalThis.document?.documentElement) {
|
||||
globalThis.document.documentElement.classList.add(`tmagic-adapter-${options.adapterType}`);
|
||||
}
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
/*
|
||||
* 主题作用域穿透到 portal 节点的基建。
|
||||
*
|
||||
* `<MEditor>` / `<MForm>` 通过 provide 暴露当前的 `theme`,包含 `<Teleport>` 的组件
|
||||
* 通过 `useThemeClass()` inject 得到主题修饰类,挂到传送目标根节点上即可让主题级
|
||||
* CSS 变量(如 `--el-color-primary`)在 body 下的 portal 节点上也命中。
|
||||
*
|
||||
* 命名约定:portal 节点本身既不是 `m-editor` 也不是 `m-form`,因此使用独立的中性类
|
||||
* `m-theme--<theme>`,避免语义混淆。同时把跨组件共享的主题级 CSS 变量统一挂在该类上,
|
||||
* editor 根 / form 根 / portal 根都加上该类即可统一受用,editor / form 各自的内部
|
||||
* 样式仍保留在 `.m-editor.m-editor--<theme>` / `.m-form.m-form--<theme>` 双类选择器
|
||||
* 上,互不污染。
|
||||
*/
|
||||
|
||||
import type { ComputedRef, InjectionKey, Ref } from 'vue';
|
||||
import { computed, inject } from 'vue';
|
||||
|
||||
/** 祖先 `<MEditor>` / `<MForm>` 暴露的当前 theme(一般来自 props.theme,经 MForm 时
|
||||
* 若自身没设 theme,会透传外层 `<MEditor>` 的值,详见 Form.vue 中的 effectiveTheme)。 */
|
||||
export const M_THEME_KEY: InjectionKey<Ref<string>> = Symbol('mTheme');
|
||||
|
||||
/**
|
||||
* 取得来自最近 `<MEditor>` / `<MForm>` 祖先 provide 的主题,拼成可直接套到 class 字符串
|
||||
* 的格式:例如 `"m-theme--magic-admin"`。
|
||||
*
|
||||
* - 不在主题作用域里时返回空串,调用方可直接拼接,无需额外判空。
|
||||
* - 与 `m-editor` / `m-form` 同名修饰类完全解耦:portal 节点只挂这一个中性主题类,
|
||||
* 不会意外命中 editor / form 内部 DOM 的样式作用域。
|
||||
*
|
||||
* 使用场景:组件自身渲染了 `<Teleport>`(如 `TMagicPopover` / `FloatingBox` /
|
||||
* `ContentMenu`),需要在传送目标根节点上挂主题类,让主题 CSS 变量能命中。
|
||||
*/
|
||||
export const useThemeClass = (): ComputedRef<string> => {
|
||||
const theme = inject(M_THEME_KEY, null);
|
||||
return computed(() => {
|
||||
const t = theme?.value;
|
||||
return t ? `m-theme--${t}` : '';
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 在组件内统一解析当前生效的 theme:以 `prop.theme` 为准,缺省时回退到祖先
|
||||
* `<MEditor>` / `<MForm>` provide 的主题。
|
||||
*
|
||||
* 入参 `prop` 是任意结构,约束只看 `theme?: string`。一般直接把 `defineProps()` 的
|
||||
* 返回值传进来即可(reactive proxy,computed 可正确跟踪外部更新);不要传
|
||||
* `toRefs` 解构后的对象。
|
||||
*
|
||||
* 若 `prop.theme` 与祖先都没有,返回空串,调用方可直接拼接。
|
||||
*
|
||||
* @example
|
||||
* const props = defineProps<{ theme?: string }>();
|
||||
* const theme = useTheme(props);
|
||||
* // theme.value -> 'magic-admin' 或 ''
|
||||
*/
|
||||
export const useTheme = <T extends { theme?: string }>(prop: T): ComputedRef<string> => {
|
||||
const injected = inject(M_THEME_KEY, null);
|
||||
return computed(() => prop.theme || injected?.value || '');
|
||||
};
|
||||
@ -1,75 +0,0 @@
|
||||
// `TMagicCard` 的「铺平」模式:去掉卡片自身的视觉表达(背景 / 阴影 / 边框 / 圆角 /
|
||||
// header 分隔线 / header & body 内边距),让卡片在视觉上与外层容器融为一体。
|
||||
//
|
||||
// 不动 body-style、不动折叠相关 prop —— 这些上层用法仍可正常工作。
|
||||
// `is-always-shadow` 是 el-card 在 `shadow="always"` 时附加的类,需要单独再压一次。
|
||||
.tmagic-design-card.tmagic-design-card--flat {
|
||||
background: #fff;
|
||||
box-shadow: none !important;
|
||||
border-top: 0;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
border-radius: 0;
|
||||
border-bottom: 0;
|
||||
&.m-flat-hide-body {
|
||||
.el-card__header {
|
||||
border-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
&.m-fields-group-list-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&.is-always-shadow {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.el-card__header {
|
||||
border-bottom: none;
|
||||
padding: 16px;
|
||||
&:hover {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.m-fields-group-list-footer {
|
||||
margin-bottom: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.el-card__body {
|
||||
padding: 0 16px 0 16px;
|
||||
// 这里每个form-item应该要有一个16px间距,但是除了tbody中的form-item
|
||||
.tmagic-design-form-item:not(tbody .tmagic-design-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
> .m-container-row {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
.tmagic-design-card--flat {
|
||||
// .el-card__body {
|
||||
// padding: 0;
|
||||
// }
|
||||
// .el-card__header {
|
||||
// padding: 0 16px;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
.m-split-title-card-container {
|
||||
> .tmagic-design-card.tmagic-design-card--flat {
|
||||
background-color: transparent;
|
||||
> .el-card__header {
|
||||
margin-bottom: 10px;
|
||||
padding: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
}
|
||||
> .el-card__body {
|
||||
background-color: #fff;
|
||||
padding-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
.tmagic-design-color-picker-input {
|
||||
margin-left: 8px;
|
||||
width: calc(var(--el-input-width, 100%) - 40px);
|
||||
}
|
||||
|
||||
.tmagic-design-color-picker {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
@ -1,3 +1 @@
|
||||
@use "./popover.scss";
|
||||
@use "./card.scss";
|
||||
@use "./colorPicker.scss";
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
@use "../../index.scss";
|
||||
|
||||
.m-form.m-form--magic-admin {
|
||||
.el-collapse-item__header {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
padding-left: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.tmagic-design-form-item {
|
||||
&.has-extra-tips {
|
||||
> .el-form-item__content {
|
||||
gap: 10px;
|
||||
> .tmagic-design-alert {
|
||||
border-radius: 2px;
|
||||
width: fit-content;
|
||||
padding: 0 8px;
|
||||
color: var(--el-color-primary, #0056ea);
|
||||
background-color: var(--el-color-primary-light-9, rgb(236, 245, 255));
|
||||
.el-icon.el-alert__icon {
|
||||
fill: var(--el-color-primary, #0056ea);
|
||||
color: var(--el-color-primary, #0056ea);
|
||||
}
|
||||
.el-alert__title {
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -24,17 +24,6 @@ export interface BadgeProps {
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
export interface AlertProps {
|
||||
title?: string;
|
||||
/** ElAlert 实际支持的 type;与 ButtonProps.type 不同,没有 `primary` / `danger` */
|
||||
type?: 'success' | 'warning' | 'info' | 'error' | 'primary';
|
||||
description?: string;
|
||||
closable?: boolean;
|
||||
center?: boolean;
|
||||
closeText?: string;
|
||||
showIcon?: boolean;
|
||||
effect?: 'light' | 'dark';
|
||||
}
|
||||
export interface ButtonProps {
|
||||
type?: string;
|
||||
size?: FieldSize;
|
||||
@ -50,11 +39,6 @@ export interface CardProps {
|
||||
bodyStyle?: Record<string, any>;
|
||||
shadow?: string;
|
||||
header?: string;
|
||||
/**
|
||||
* 铺平模式:去除卡片自身的视觉(背景 / 阴影 / 边框 / 圆角 / header 分隔线 / header 与 body 内边距)。
|
||||
* 用于已有外层容器或者使用方希望「无卡片感」的场景,TMagicCard 的折叠等 prop 仍可正常使用。
|
||||
*/
|
||||
flat?: boolean;
|
||||
}
|
||||
|
||||
export interface CascaderProps {
|
||||
@ -113,7 +97,6 @@ export interface ColorPickerProps {
|
||||
disabled?: boolean;
|
||||
showAlpha?: boolean;
|
||||
size?: FieldSize;
|
||||
flat?: boolean;
|
||||
}
|
||||
|
||||
export interface DatePickerProps {
|
||||
@ -202,9 +185,7 @@ export interface FormItemProps {
|
||||
labelWidth?: string | number;
|
||||
rules?: any;
|
||||
extra?: string;
|
||||
extraTips?: string;
|
||||
labelPosition?: 'top' | 'left' | 'right';
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export interface InputProps {
|
||||
@ -483,11 +464,6 @@ export interface Components {
|
||||
props: (props: BadgeProps) => BadgeProps;
|
||||
};
|
||||
|
||||
alert: {
|
||||
component: DefineComponent<AlertProps, {}, any> | string;
|
||||
props: (props: AlertProps) => AlertProps;
|
||||
};
|
||||
|
||||
autocomplete: {
|
||||
component: DefineComponent<AutocompleteProps, {}, any> | string;
|
||||
props: (props: AutocompleteProps) => AutocompleteProps;
|
||||
@ -750,7 +726,6 @@ export interface Components {
|
||||
}
|
||||
|
||||
export interface DesignPluginOptions {
|
||||
flat?: boolean;
|
||||
adapterType?: string;
|
||||
message?: TMagicMessage;
|
||||
messageBox?: TMagicMessageBox;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.20",
|
||||
"version": "1.8.0-beta.13",
|
||||
"name": "@tmagic/editor",
|
||||
"type": "module",
|
||||
"sideEffects": [
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
:page-bar-sort-options="pageBarSortOptions"
|
||||
:page-filter-function="pageFilterFunction"
|
||||
:hide-sidebar="hideSidebar"
|
||||
:theme="theme"
|
||||
>
|
||||
<template #header>
|
||||
<slot name="header"></slot>
|
||||
@ -136,10 +135,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { computed, provide, ref } from 'vue';
|
||||
import { provide, ref } from 'vue';
|
||||
|
||||
import type { MApp } from '@tmagic/core';
|
||||
import { M_THEME_KEY } from '@tmagic/design';
|
||||
|
||||
import Framework from './layouts/Framework.vue';
|
||||
import TMagicNavMenu from './layouts/NavMenu.vue';
|
||||
@ -218,7 +216,6 @@ const stageOptions: StageOptions = {
|
||||
containerHighlightClassName: props.containerHighlightClassName,
|
||||
containerHighlightDuration: props.containerHighlightDuration,
|
||||
containerHighlightType: props.containerHighlightType,
|
||||
containerHighlightAddOnly: props.containerHighlightAddOnly,
|
||||
disabledDragStart: props.disabledDragStart,
|
||||
renderType: props.renderType,
|
||||
guidesOptions: props.guidesOptions,
|
||||
@ -244,15 +241,6 @@ provide(ENABLE_PROPS_FORM_VALIDATE, props.enablePropsFormValidate ?? false);
|
||||
* 与 PropsPanel 通过 `:extend-state` 显式传入的方式保持等价。
|
||||
*/
|
||||
provide('extendFormState', props.extendFormState);
|
||||
|
||||
// 用 computed 包一层再 provide,否则传下去的是 provide 那一刻的值快照,
|
||||
// props.isLargeStageContainer 后续变化不会同步到子孙(如 @tmagic/design/ColorPicker)。
|
||||
// 子孙侧 `inject<ComputedRef<boolean>>('isLargeStageContainer', computed(() => false))`
|
||||
// 用 `.value` 或模板自动解包读取即可拿到最新值。
|
||||
provide(
|
||||
'isLargeStageContainer',
|
||||
computed(() => props.isLargeStageContainer),
|
||||
);
|
||||
/**
|
||||
* 提供 PropsPanel 主属性表单的 formState getter,供历史差异弹窗复用,
|
||||
* 让 CompareForm 与 PropsPanel 的 filterFunction 上下文保持一致。
|
||||
@ -268,16 +256,6 @@ provide('historyListExtraTabs', props.historyListExtraTabs);
|
||||
|
||||
provide<EventBus>('eventBus', new EventEmitter());
|
||||
|
||||
/**
|
||||
* 把当前主题以响应式 ref 形式 provide 给后代,供包含 `Teleport` 的组件
|
||||
* (如 `TMagicPopover` / `FloatingBox` / `ContentMenu`)在传送目标上挂 `m-theme--<theme>`
|
||||
* 类,让主题级 CSS 变量在 portal 节点上也能命中。详见 `@tmagic/design/theme.ts`。
|
||||
*/
|
||||
provide(
|
||||
M_THEME_KEY,
|
||||
computed(() => props.theme ?? ''),
|
||||
);
|
||||
|
||||
const propsPanelMountedHandler = (e: InstanceType<typeof FormPanel>) => {
|
||||
propsPanelRef.value = e;
|
||||
emit('props-panel-mounted', e);
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-show="visible"
|
||||
class="magic-editor-content-menu"
|
||||
ref="menu"
|
||||
:class="['magic-editor-content-menu', themeClass]"
|
||||
:style="menuStyle"
|
||||
@mouseenter="mouseenterHandler()"
|
||||
@contextmenu.prevent
|
||||
@ -39,7 +39,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, type Ref, ref, useTemplateRef } from 'vue';
|
||||
|
||||
import { useThemeClass, useZIndex } from '@tmagic/design';
|
||||
import { useZIndex } from '@tmagic/design';
|
||||
|
||||
import { MenuButton, MenuComponent } from '@editor/type';
|
||||
|
||||
@ -73,14 +73,6 @@ const menuEl = useTemplateRef<HTMLDivElement>('menu');
|
||||
const buttonRefs = useTemplateRef<InstanceType<typeof ToolButton>[]>('buttons');
|
||||
const subMenuRef = useTemplateRef<any>('subMenu');
|
||||
const visible = ref(false);
|
||||
|
||||
/**
|
||||
* 主题修饰类(来自最近的 `<MEditor>` / `<MForm>` 祖先 provide)。
|
||||
* - 顶层 `ContentMenu` 渲染在编辑器子树中,加上去是冗余但无害;
|
||||
* - 子菜单(递归 `<content-menu :is-sub-menu="true">` 被 `<teleport to="body">` 送到 body)
|
||||
* 不在编辑器子树中,必须显式挂上主题类,主题级 CSS 变量才能命中。
|
||||
*/
|
||||
const themeClass = useThemeClass();
|
||||
const subMenuData: Ref<(MenuButton | MenuComponent)[]> = ref<(MenuButton | MenuComponent)[]>([]);
|
||||
const zIndex = useZIndex();
|
||||
const curZIndex = ref<number>(0);
|
||||
@ -128,42 +120,20 @@ const outsideClickHideHandler = (e: MouseEvent) => {
|
||||
hide();
|
||||
};
|
||||
|
||||
// 根据菜单实际尺寸修正位置,避免超出可视范围
|
||||
const fixPosition = () => {
|
||||
const menu = menuEl.value;
|
||||
if (!menu || !visible.value) return;
|
||||
|
||||
const menuHeight = menu.clientHeight;
|
||||
const menuWidth = menu.clientWidth;
|
||||
|
||||
let { top, left } = menuPosition.value;
|
||||
|
||||
if (top + menuHeight > document.body.clientHeight) {
|
||||
top = Math.max(0, document.body.clientHeight - menuHeight);
|
||||
}
|
||||
|
||||
if (left + menuWidth > document.body.clientWidth) {
|
||||
left = Math.max(0, document.body.clientWidth - menuWidth);
|
||||
}
|
||||
|
||||
if (top !== menuPosition.value.top || left !== menuPosition.value.left) {
|
||||
menuPosition.value = { top, left };
|
||||
}
|
||||
};
|
||||
|
||||
const setPosition = (e: { clientY: number; clientX: number }) => {
|
||||
const menuHeight = menuEl.value?.clientHeight || 0;
|
||||
|
||||
let top = e.clientY;
|
||||
if (menuHeight + e.clientY > document.body.clientHeight) {
|
||||
top = document.body.clientHeight - menuHeight;
|
||||
}
|
||||
|
||||
menuPosition.value = {
|
||||
top: e.clientY,
|
||||
top,
|
||||
left: e.clientX,
|
||||
};
|
||||
fixPosition();
|
||||
};
|
||||
|
||||
// 菜单大小动态变化(如菜单项更新)后重新修正位置
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
fixPosition();
|
||||
});
|
||||
|
||||
const show = (e?: { clientY: number; clientX: number }) => {
|
||||
visible.value = true;
|
||||
|
||||
@ -208,18 +178,12 @@ const mouseenterHandler = () => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (menuEl.value) {
|
||||
resizeObserver.observe(menuEl.value);
|
||||
}
|
||||
|
||||
if (props.isSubMenu) return;
|
||||
|
||||
globalThis.addEventListener('mousedown', outsideClickHideHandler, true);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resizeObserver.disconnect();
|
||||
|
||||
if (props.isSubMenu) return;
|
||||
|
||||
globalThis.removeEventListener('mousedown', outsideClickHideHandler, true);
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<Teleport to="body" v-if="visible">
|
||||
<div
|
||||
ref="target"
|
||||
:class="['m-editor-float-box', themeClass]"
|
||||
class="m-editor-float-box"
|
||||
v-bind="$attrs"
|
||||
:style="{ ...style, zIndex: curZIndex }"
|
||||
@mousedown="nextZIndex"
|
||||
@ -27,7 +27,7 @@ import { computed, type CSSProperties, nextTick, onBeforeUnmount, provide, ref,
|
||||
import { Close } from '@element-plus/icons-vue';
|
||||
import VanillaMoveable from 'moveable';
|
||||
|
||||
import { TMagicButton, useThemeClass, useZIndex } from '@tmagic/design';
|
||||
import { TMagicButton, useZIndex } from '@tmagic/design';
|
||||
|
||||
import MIcon from '@editor/components/Icon.vue';
|
||||
|
||||
@ -62,12 +62,6 @@ const props = withDefaults(
|
||||
const targetEl = useTemplateRef<HTMLDivElement>('target');
|
||||
const titleEl = useTemplateRef<HTMLDivElement>('title');
|
||||
|
||||
/**
|
||||
* 主题修饰类(来自最近的 `<MEditor>` / `<MForm>` 祖先 provide)。
|
||||
* 挂在 `Teleport` 出去的浮动面板根节点上,让主题级 CSS 变量在 portal 节点上也能命中。
|
||||
*/
|
||||
const themeClass = useThemeClass();
|
||||
|
||||
const zIndex = useZIndex();
|
||||
const curZIndex = ref<number>(0);
|
||||
|
||||
|
||||
@ -35,10 +35,6 @@ import type {
|
||||
export const ENABLE_PROPS_FORM_VALIDATE: InjectionKey<boolean> = Symbol('enablePropsFormValidate');
|
||||
|
||||
export interface EditorProps {
|
||||
/** 是否是大屏模拟器容器,大屏容器时,左侧属性面板会扩展为两列(正常3列),颜色表单不扩展两列等 */
|
||||
isLargeStageContainer?: boolean;
|
||||
|
||||
theme?: string;
|
||||
/** 页面初始值 */
|
||||
modelValue?: MApp;
|
||||
/** 左侧面板中的组件类型列表 */
|
||||
@ -82,11 +78,6 @@ export interface EditorProps {
|
||||
containerHighlightDuration?: number;
|
||||
/** 拖入画布中容器时,识别容器的操作类型 */
|
||||
containerHighlightType?: ContainerHighlightType;
|
||||
/**
|
||||
* 是否仅在新增组件(从组件列表拖入新组件)时才启用识别容器,
|
||||
* 开启后在画布中拖动已有组件不会识别容器,默认 false
|
||||
*/
|
||||
containerHighlightAddOnly?: boolean;
|
||||
/** 画布大小 */
|
||||
stageRect?: StageRect;
|
||||
/** monaco editor 的配置 */
|
||||
@ -168,7 +159,6 @@ export const defaultEditorProps = {
|
||||
containerHighlightClassName: CONTAINER_HIGHLIGHT_CLASS_NAME,
|
||||
containerHighlightDuration: 800,
|
||||
containerHighlightType: ContainerHighlightType.DEFAULT,
|
||||
containerHighlightAddOnly: false,
|
||||
disabledShowSrc: false,
|
||||
disabledDataSource: false,
|
||||
disabledCodeBlock: false,
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<MagicCodeEditor
|
||||
v-if="!silentMode"
|
||||
:height="config.height"
|
||||
:type="diffMode ? 'diff' : undefined"
|
||||
:init-values="diffMode ? (lastValues || {})[name] : model[name]"
|
||||
@ -18,9 +17,9 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { type CodeConfig, type FieldProps, FORM_SILENT_MODE_KEY } from '@tmagic/form';
|
||||
import type { CodeConfig, FieldProps } from '@tmagic/form';
|
||||
|
||||
import MagicCodeEditor from '@editor/layouts/CodeEditor.vue';
|
||||
|
||||
@ -28,13 +27,6 @@ defineOptions({
|
||||
name: 'MFieldsVsCode',
|
||||
});
|
||||
|
||||
/**
|
||||
* 静默模式(submitForm/validateForm 隐藏挂载)下跳过 monaco 渲染:
|
||||
* 校验由 FormItem 针对 model 中的值完成,与编辑器实例无关;本组件挂载无值副作用
|
||||
* (save 仅由用户操作/编辑器内容变更触发),跳过可省去 monaco worker/model 的无谓实例化。
|
||||
*/
|
||||
const silentMode = inject(FORM_SILENT_MODE_KEY, false);
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [value: string | any];
|
||||
}>();
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
<template>
|
||||
<div class="m-fields-code-select" :class="config.className">
|
||||
<TMagicCard :flat="config.flat">
|
||||
<TMagicCard>
|
||||
<MContainer
|
||||
:config="codeConfig"
|
||||
:size="size"
|
||||
class="code-select-content"
|
||||
:prop="prop"
|
||||
:disabled="disabled"
|
||||
:is-compare="isCompareMode"
|
||||
@ -13,20 +12,16 @@
|
||||
@change="changeHandler"
|
||||
>
|
||||
</MContainer>
|
||||
<TMagicButton class="create-button fullWidth" :icon="Plus" :size="size" :disabled="disabled" @click="newHandler()"
|
||||
>添加{{ config.text }}</TMagicButton
|
||||
>
|
||||
</TMagicCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch } from 'vue';
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
|
||||
import { HookCodeType, HookType } from '@tmagic/core';
|
||||
import { TMagicButton, TMagicCard } from '@tmagic/design';
|
||||
import { TMagicCard } from '@tmagic/design';
|
||||
import type { CodeSelectConfig, ContainerChangeEventData, FieldProps, GroupListConfig } from '@tmagic/form';
|
||||
import { MContainer } from '@tmagic/form';
|
||||
|
||||
@ -58,23 +53,12 @@ const props = withDefaults(defineProps<FieldProps<CodeSelectConfig>>(), {});
|
||||
* 仅当存在历史值时才启用对比,避免 lastValues 缺失时退化为「全部新增」的空对比。
|
||||
*/
|
||||
const isCompareMode = computed(() => Boolean(props.isCompare && props.lastValues));
|
||||
const newHandler = () => {
|
||||
const defaultCode = {
|
||||
codeType: HookCodeType.CODE,
|
||||
codeId: '',
|
||||
};
|
||||
const name = props.config.name || '';
|
||||
const hookData = props.model[name]?.hookData || [];
|
||||
emit('change', defaultCode, {
|
||||
modifyKey: `hookData.${hookData.length}`,
|
||||
});
|
||||
};
|
||||
|
||||
const codeConfig = computed<GroupListConfig>(() => ({
|
||||
type: 'group-list',
|
||||
name: 'hookData',
|
||||
enableToggleMode: false,
|
||||
expandAll: true,
|
||||
addable: () => false,
|
||||
title: (mForm, { model, index }: any) => {
|
||||
if (model.codeType === HookCodeType.DATA_SOURCE_METHOD) {
|
||||
if (Array.isArray(model.codeId)) {
|
||||
@ -97,44 +81,49 @@ const codeConfig = computed<GroupListConfig>(() => ({
|
||||
|
||||
return model.codeId || index;
|
||||
},
|
||||
titlePrefix: props.config.name === undefined ? undefined : String(props.config.name),
|
||||
items: [
|
||||
{
|
||||
text: '代码类型',
|
||||
type: 'select',
|
||||
name: 'codeType',
|
||||
labelPosition: 'right',
|
||||
rules: [{ typeMatch: true, trigger: 'change' }],
|
||||
options: [
|
||||
{ value: HookCodeType.CODE, text: '代码块' },
|
||||
{ value: HookCodeType.DATA_SOURCE_METHOD, text: '数据源方法' },
|
||||
],
|
||||
defaultValue: HookCodeType.CODE,
|
||||
onChange: (_mForm, v: HookCodeType, { setModel }) => {
|
||||
if (v === HookCodeType.DATA_SOURCE_METHOD) {
|
||||
setModel('codeId', []);
|
||||
} else {
|
||||
setModel('codeId', '');
|
||||
}
|
||||
return v;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'code-select-col',
|
||||
name: 'codeId',
|
||||
text: '代码块',
|
||||
rules: [{ typeMatch: true, trigger: 'change' }],
|
||||
type: 'row',
|
||||
items: [
|
||||
{
|
||||
type: 'select',
|
||||
name: 'codeType',
|
||||
span: 6,
|
||||
options: [
|
||||
{ value: HookCodeType.CODE, text: '代码块' },
|
||||
{ value: HookCodeType.DATA_SOURCE_METHOD, text: '数据源方法' },
|
||||
],
|
||||
rules: [{ typeMatch: true, trigger: 'change' }],
|
||||
defaultValue: HookCodeType.CODE,
|
||||
onChange: (_mForm, v: HookCodeType, { setModel }) => {
|
||||
if (v === HookCodeType.DATA_SOURCE_METHOD) {
|
||||
setModel('codeId', []);
|
||||
} else {
|
||||
setModel('codeId', '');
|
||||
}
|
||||
|
||||
display: (_mForm, { model }) => model.codeType !== HookCodeType.DATA_SOURCE_METHOD,
|
||||
notEditable: () => !codeBlockService.getEditStatus(),
|
||||
},
|
||||
{
|
||||
type: 'data-source-method-select',
|
||||
name: 'codeId',
|
||||
text: '数据源字段',
|
||||
rules: [{ typeMatch: true, trigger: 'change' }],
|
||||
display: (_mForm, { model }) => model.codeType === HookCodeType.DATA_SOURCE_METHOD,
|
||||
notEditable: () => !dataSourceService.get('editable'),
|
||||
return v;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'code-select-col',
|
||||
name: 'codeId',
|
||||
span: 18,
|
||||
labelWidth: 0,
|
||||
display: (_mForm, { model }) => model.codeType !== HookCodeType.DATA_SOURCE_METHOD,
|
||||
notEditable: () => !codeBlockService.getEditStatus(),
|
||||
rules: [{ typeMatch: true, trigger: 'change' }],
|
||||
},
|
||||
{
|
||||
type: 'data-source-method-select',
|
||||
name: 'codeId',
|
||||
span: 18,
|
||||
labelWidth: 0,
|
||||
display: (_mForm, { model }) => model.codeType === HookCodeType.DATA_SOURCE_METHOD,
|
||||
notEditable: () => !dataSourceService.get('editable'),
|
||||
rules: [{ typeMatch: true, trigger: 'change' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
@ -147,7 +147,6 @@ const selectConfig: SelectConfig = {
|
||||
type: 'select',
|
||||
name: props.name,
|
||||
disabled: props.disabled,
|
||||
text: props.config.text,
|
||||
options: () => {
|
||||
if (codeDsl.value) {
|
||||
return map(codeDsl.value, (value, key) => ({
|
||||
|
||||
@ -1,12 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="m-fields-data-source-field-select"
|
||||
:class="{
|
||||
[`data-source-field-${type}`]: !isSelectValid,
|
||||
}"
|
||||
>
|
||||
<div class="m-fields-data-source-field-select">
|
||||
<FieldSelect
|
||||
v-if="isSelectValid"
|
||||
v-if="!disabledDataSource && (showDataSourceFieldSelect || !config.fieldConfig)"
|
||||
:model-value="model[name]"
|
||||
:disabled="disabled"
|
||||
:size="size"
|
||||
@ -42,14 +37,15 @@
|
||||
:size="size"
|
||||
:disabled="disabled"
|
||||
@click="onToggleDataSourceFieldSelectHandler"
|
||||
><MIcon :icon="dataSourceIcon"></MIcon>
|
||||
</TMagicButton>
|
||||
><MIcon :icon="Coin"></MIcon
|
||||
></TMagicButton>
|
||||
</TMagicTooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref, resolveComponent, watch } from 'vue';
|
||||
import { Coin } from '@element-plus/icons-vue';
|
||||
|
||||
import { DataSchema } from '@tmagic/core';
|
||||
import { TMagicButton, tMagicMessage, TMagicTooltip } from '@tmagic/design';
|
||||
@ -65,8 +61,6 @@ import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX, removeDataSourceFieldPrefix } f
|
||||
import MIcon from '@editor/components/Icon.vue';
|
||||
import { useServices } from '@editor/hooks/use-services';
|
||||
|
||||
import dataSourceIcon from '../../icons/DatasourceIcon.vue';
|
||||
|
||||
import FieldSelect from './FieldSelect.vue';
|
||||
|
||||
defineOptions({
|
||||
@ -81,9 +75,6 @@ const props = withDefaults(defineProps<FieldProps<DataSourceFieldSelectConfig>>(
|
||||
|
||||
const showDataSourceFieldSelect = ref(false);
|
||||
|
||||
const isSelectValid = computed(
|
||||
() => !disabledDataSource.value && (showDataSourceFieldSelect.value || !props.config.fieldConfig),
|
||||
);
|
||||
watch(
|
||||
() => props.model[props.name],
|
||||
(value) => {
|
||||
|
||||
@ -68,15 +68,12 @@ const config = computed<GroupListConfig>(() => ({
|
||||
titlePrefix: props.config.titlePrefix,
|
||||
expandAll: true,
|
||||
enableToggleMode: false,
|
||||
flat: props.config.flat,
|
||||
items: [
|
||||
{
|
||||
type: 'table',
|
||||
name: 'cond',
|
||||
operateColWidth: props.config.operateColWidth,
|
||||
operateColWidth: 80,
|
||||
enableToggleMode: false,
|
||||
fixed: props.config.fixed,
|
||||
flat: props.config.flat,
|
||||
items: [
|
||||
parentFields.value.length
|
||||
? {
|
||||
|
||||
@ -12,21 +12,16 @@
|
||||
@change="onChangeHandler"
|
||||
></MTable>
|
||||
|
||||
<div v-else class="fullWidth event-select-container">
|
||||
<div class="event-select-header">
|
||||
<div class="event-select-title">事件配置</div>
|
||||
<TMagicButton
|
||||
v-if="!isCompareMode && displayList.length > 0"
|
||||
class="create-button"
|
||||
text
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
:size="size"
|
||||
:disabled="disabled"
|
||||
@click="addEvent()"
|
||||
>添加事件</TMagicButton
|
||||
>
|
||||
</div>
|
||||
<div v-else class="fullWidth">
|
||||
<TMagicButton
|
||||
v-if="!isCompareMode"
|
||||
class="create-button"
|
||||
type="primary"
|
||||
:size="size"
|
||||
:disabled="disabled"
|
||||
@click="addEvent()"
|
||||
>添加事件</TMagicButton
|
||||
>
|
||||
<MPanel
|
||||
v-for="entry in displayList"
|
||||
:key="entry.index"
|
||||
@ -37,44 +32,32 @@
|
||||
:model="entry.cardItem"
|
||||
:last-values="entry.lastCardItem"
|
||||
:is-compare="isCompareMode"
|
||||
:hide-expand="false"
|
||||
:label-width="config.labelWidth || '100px'"
|
||||
@change="onChangeHandler"
|
||||
>
|
||||
<template #header>
|
||||
<div class="event-item-header">
|
||||
<MFormContainer
|
||||
class="fullWidth"
|
||||
:config="eventNameConfig"
|
||||
:model="entry.cardItem"
|
||||
:last-values="entry.lastCardItem"
|
||||
:is-compare="isCompareMode"
|
||||
:disabled="disabled"
|
||||
:size="size"
|
||||
:prop="`${prop}.${entry.index}`"
|
||||
@change="eventNameChangeHandler"
|
||||
></MFormContainer>
|
||||
<TMagicButton
|
||||
class="event-item-delete-button"
|
||||
v-if="!isCompareMode"
|
||||
link
|
||||
:icon="Delete"
|
||||
:disabled="disabled"
|
||||
:size="size"
|
||||
@click="removeEvent(Number(entry.index))"
|
||||
></TMagicButton>
|
||||
</div>
|
||||
<MFormContainer
|
||||
class="fullWidth"
|
||||
:config="eventNameConfig"
|
||||
:model="entry.cardItem"
|
||||
:last-values="entry.lastCardItem"
|
||||
:is-compare="isCompareMode"
|
||||
:disabled="disabled"
|
||||
:size="size"
|
||||
:prop="`${prop}.${entry.index}`"
|
||||
@change="eventNameChangeHandler"
|
||||
></MFormContainer>
|
||||
<TMagicButton
|
||||
v-if="!isCompareMode"
|
||||
style="color: #f56c6c"
|
||||
link
|
||||
:icon="Delete"
|
||||
:disabled="disabled"
|
||||
:size="size"
|
||||
@click="removeEvent(Number(entry.index))"
|
||||
></TMagicButton>
|
||||
</template>
|
||||
</MPanel>
|
||||
|
||||
<TMagicButton
|
||||
v-if="!isCompareMode"
|
||||
class="create-button fullWidth"
|
||||
:icon="Plus"
|
||||
:disabled="disabled"
|
||||
@click="addEvent()"
|
||||
>添加事件</TMagicButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -82,7 +65,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { Delete } from '@element-plus/icons-vue';
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import { has } from 'lodash-es';
|
||||
|
||||
import { ActionType } from '@tmagic/core';
|
||||
@ -126,7 +108,7 @@ const { editorService, dataSourceService, eventsService, codeBlockService, props
|
||||
const eventNameConfig = computed(() => {
|
||||
const defaultEventNameConfig = {
|
||||
name: 'name',
|
||||
text: '事件类型',
|
||||
text: '事件',
|
||||
type: (mForm: FormState | undefined, { formValue }: any) => {
|
||||
if (
|
||||
props.config.src !== 'component' ||
|
||||
@ -136,7 +118,7 @@ const eventNameConfig = computed(() => {
|
||||
}
|
||||
return 'select';
|
||||
},
|
||||
labelWidth: '70px',
|
||||
labelWidth: '40px',
|
||||
checkStrictly: () => props.config.src !== 'component',
|
||||
valueSeparator: '.',
|
||||
options: (_mForm: FormState, { formValue }: any) => getEventNameOptions(props.config.src, formValue),
|
||||
@ -196,7 +178,6 @@ const actionTypeConfig = computed(() => {
|
||||
name: 'actionType',
|
||||
text: '联动类型',
|
||||
type: 'select',
|
||||
labelPosition: 'left',
|
||||
defaultValue: ActionType.COMP,
|
||||
options: actionTypeOptions.value,
|
||||
rules: [
|
||||
@ -209,12 +190,6 @@ const actionTypeConfig = computed(() => {
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
onChange: (_mForm: FormState, _v: string, { setModel }: any) => {
|
||||
setModel('to', '');
|
||||
setModel('method', '');
|
||||
setModel('codeId', '');
|
||||
setModel('dataSourceMethod', []);
|
||||
},
|
||||
};
|
||||
return { ...defaultActionTypeConfig, ...props.config.actionTypeConfig };
|
||||
});
|
||||
@ -225,9 +200,8 @@ const targetCompConfig = computed(() => {
|
||||
name: 'to',
|
||||
text: '联动组件',
|
||||
type: 'ui-select',
|
||||
labelPosition: 'left',
|
||||
display: (_mForm, { model }) => model.actionType === ActionType.COMP,
|
||||
onChange: (_mForm, _v, { setModel }) => {
|
||||
onChange: (_MForm, _v, { setModel }) => {
|
||||
setModel('method', '');
|
||||
},
|
||||
rules: [
|
||||
@ -245,7 +219,6 @@ const compActionConfig = computed(() => {
|
||||
const defaultCompActionConfig: DynamicTypeConfig = {
|
||||
name: 'method',
|
||||
text: '动作',
|
||||
labelPosition: 'left',
|
||||
type: (mForm: FormState | undefined, { model }: any) => {
|
||||
const to = editorService.getNodeById(model.to);
|
||||
|
||||
@ -344,7 +317,6 @@ const actionsConfig = computed(
|
||||
() =>
|
||||
defineFormItem({
|
||||
type: 'panel',
|
||||
labelPosition: 'left',
|
||||
items: [
|
||||
{
|
||||
type: 'group-list',
|
||||
@ -352,7 +324,6 @@ const actionsConfig = computed(
|
||||
expandAll: true,
|
||||
enableToggleMode: false,
|
||||
titlePrefix: '动作',
|
||||
labelPosition: 'left',
|
||||
items: [
|
||||
actionTypeConfig.value,
|
||||
targetCompConfig.value,
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<TMagicCollapse class="m-fields-style-setter" v-model="collapseValue">
|
||||
<template v-for="(item, index) in list" :key="index">
|
||||
<TMagicCollapseItem :name="`${index}`">
|
||||
<template #title>{{ item.title }}</template>
|
||||
<template #title><MIcon :icon="Grid"></MIcon>{{ item.title }}</template>
|
||||
<component
|
||||
v-if="item.component"
|
||||
:is="item.component"
|
||||
@ -22,11 +22,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { shallowRef } from 'vue';
|
||||
import { Grid } from '@element-plus/icons-vue';
|
||||
|
||||
import { TMagicCollapse, TMagicCollapseItem } from '@tmagic/design';
|
||||
import type { ContainerChangeEventData, FieldProps } from '@tmagic/form';
|
||||
import type { StyleSchema } from '@tmagic/schema';
|
||||
|
||||
import MIcon from '@editor/components/Icon.vue';
|
||||
|
||||
import { Background, Border, Font, Layout, Position, Transform } from './pro/';
|
||||
|
||||
defineOptions({
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
@click="selectDirection('Left')"
|
||||
></div>
|
||||
<div
|
||||
class="border-icon center"
|
||||
class="border-icon"
|
||||
:class="{ active: direction === '', configured: isConfigured('') }"
|
||||
@click="selectDirection()"
|
||||
></div>
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path
|
||||
d="M12 4C12.5523 4 13 4.44772 13 5V11C13 11.5523 12.5523 12 12 12H4C3.44772 12 3 11.5523 3 11V5C3 4.44772 3.44772 4 4 4H12ZM4.5 5C4.22386 5 4 5.22386 4 5.5V10.5C4 10.7761 4.22386 11 4.5 11H11.5C11.7761 11 12 10.7761 12 10.5V5.5C12 5.22386 11.7761 5 11.5 5H4.5Z"
|
||||
/>
|
||||
<rect
|
||||
x="13.75"
|
||||
y="7.75"
|
||||
width="0.5"
|
||||
height="11.5"
|
||||
rx="0.25"
|
||||
transform="rotate(90 13.75 7.75)"
|
||||
stroke="black"
|
||||
stroke-opacity="0.3"
|
||||
style="stroke: black; stroke-opacity: 0.3"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@ -1,19 +0,0 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path
|
||||
d="M12 3C12.5523 3 13 3.44772 13 4V10C13 10.5523 12.5523 11 12 11H4C3.44772 11 3 10.5523 3 10V4C3 3.44772 3.44772 3 4 3H12ZM4.5 4C4.22386 4 4 4.22386 4 4.5V9.5C4 9.77614 4.22386 10 4.5 10H11.5C11.7761 10 12 9.77614 12 9.5V4.5C12 4.22386 11.7761 4 11.5 4H4.5Z"
|
||||
/>
|
||||
<rect
|
||||
x="13.75"
|
||||
y="12.25"
|
||||
width="0.5"
|
||||
height="11.5"
|
||||
rx="0.25"
|
||||
transform="rotate(90 13.75 12.25)"
|
||||
stroke="black"
|
||||
stroke-opacity="0.3"
|
||||
style="stroke: black; stroke-opacity: 0.3"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@ -1,19 +0,0 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path
|
||||
d="M12 5C12.5523 5 13 5.44772 13 6V12C13 12.5523 12.5523 13 12 13H4C3.44772 13 3 12.5523 3 12V6C3 5.44772 3.44772 5 4 5H12ZM4.5 6C4.22386 6 4 6.22386 4 6.5V11.5C4 11.7761 4.22386 12 4.5 12H11.5C11.7761 12 12 11.7761 12 11.5V6.5C12 6.22386 11.7761 6 11.5 6H4.5Z"
|
||||
/>
|
||||
<rect
|
||||
x="13.75"
|
||||
y="3.25"
|
||||
width="0.5"
|
||||
height="11.5"
|
||||
rx="0.25"
|
||||
transform="rotate(90 13.75 3.25)"
|
||||
stroke="white"
|
||||
stroke-opacity="0.3"
|
||||
style="stroke: white; stroke-opacity: 0.3"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@ -1,34 +0,0 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path
|
||||
d="M12 8C12.5523 8 13 8.44772 13 9V12C13 12.5523 12.5523 13 12 13H4C3.44772 13 3 12.5523 3 12V9C3 8.44772 3.44772 8 4 8H12ZM4.5 9C4.22386 9 4 9.22386 4 9.5V11.5C4 11.7761 4.22386 12 4.5 12H11.5C11.7761 12 12 11.7761 12 11.5V9.5C12 9.22386 11.7761 9 11.5 9H4.5Z"
|
||||
/>
|
||||
<path
|
||||
d="M12 2C12.5523 2 13 2.44772 13 3V6C13 6.55228 12.5523 7 12 7H4C3.44772 7 3 6.55228 3 6V3C3 2.44772 3.44772 2 4 2H12ZM4.5 3C4.22386 3 4 3.22386 4 3.5V5.5C4 5.77614 4.22386 6 4.5 6H11.5C11.7761 6 12 5.77614 12 5.5V3.5C12 3.22386 11.7761 3 11.5 3H4.5Z"
|
||||
/>
|
||||
<rect
|
||||
x="13.75"
|
||||
y="10.25"
|
||||
width="0.5"
|
||||
height="11.5"
|
||||
rx="0.25"
|
||||
transform="rotate(90 13.75 10.25)"
|
||||
stroke="black"
|
||||
stroke-opacity="0.3"
|
||||
style="stroke: black; stroke-opacity: 0.3"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
<rect
|
||||
x="13.75"
|
||||
y="4.25"
|
||||
width="0.5"
|
||||
height="11.5"
|
||||
rx="0.25"
|
||||
transform="rotate(90 13.75 4.25)"
|
||||
stroke="black"
|
||||
stroke-opacity="0.3"
|
||||
style="stroke: black; stroke-opacity: 0.3"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@ -1,34 +0,0 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path
|
||||
d="M12 9C12.5523 9 13 9.44772 13 10V12C13 12.5523 12.5523 13 12 13H4C3.44772 13 3 12.5523 3 12V10C3 9.44772 3.44772 9 4 9H12ZM4.5 10C4.22386 10 4 10.2239 4 10.5V11.5C4 11.7761 4.22386 12 4.5 12H11.5C11.7761 12 12 11.7761 12 11.5V10.5C12 10.2239 11.7761 10 11.5 10H4.5Z"
|
||||
/>
|
||||
<path
|
||||
d="M12 3C12.5523 3 13 3.44772 13 4V6C13 6.55228 12.5523 7 12 7H4C3.44772 7 3 6.55228 3 6V4C3 3.44772 3.44772 3 4 3H12ZM4.5 4C4.22386 4 4 4.22386 4 4.5V5.5C4 5.77614 4.22386 6 4.5 6H11.5C11.7761 6 12 5.77614 12 5.5V4.5C12 4.22386 11.7761 4 11.5 4H4.5Z"
|
||||
/>
|
||||
<rect
|
||||
x="13.75"
|
||||
y="14.25"
|
||||
width="0.5"
|
||||
height="11.5"
|
||||
rx="0.25"
|
||||
transform="rotate(90 13.75 14.25)"
|
||||
stroke="black"
|
||||
stroke-opacity="0.3"
|
||||
style="stroke: black; stroke-opacity: 0.3"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
<rect
|
||||
x="13.75"
|
||||
y="1.25"
|
||||
width="0.5"
|
||||
height="11.5"
|
||||
rx="0.25"
|
||||
transform="rotate(90 13.75 1.25)"
|
||||
stroke="black"
|
||||
stroke-opacity="0.3"
|
||||
style="stroke: black; stroke-opacity: 0.3"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@ -1,5 +0,0 @@
|
||||
export { default as AlignItemsCenter } from './Center.vue';
|
||||
export { default as AlignItemsFlexEnd } from './FlexEnd.vue';
|
||||
export { default as AlignItemsFlexStart } from './FlexStart.vue';
|
||||
export { default as AlignItemsSpaceAround } from './SpaceAround.vue';
|
||||
export { default as AlignItemsSpaceBetween } from './SpaceBetween.vue';
|
||||
@ -1,24 +1,8 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10" fill="currentColor">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path
|
||||
d="M0 0.625C0 0.279822 0.248731 0 0.555556 0H9.44444C9.75127 0 10 0.279822 10 0.625C10 0.970178 9.75127 1.25 9.44444 1.25H0.555556C0.248731 1.25 0 0.970178 0 0.625Z"
|
||||
fill-opacity="0.3"
|
||||
/>
|
||||
<path
|
||||
d="M0 9.375C0 9.02982 0.248731 8.75 0.555556 8.75H9.44444C9.75127 8.75 10 9.02982 10 9.375C10 9.72018 9.75127 10 9.44444 10H0.555556C0.248731 10 0 9.72018 0 9.375Z"
|
||||
fill-opacity="0.3"
|
||||
/>
|
||||
<path
|
||||
d="M0.555556 4.375C0.248731 4.375 0 4.65482 0 5C0 5.34518 0.248731 5.625 0.555556 5.625H2.87307L3.42868 4.99994L2.87318 4.375H0.555556Z"
|
||||
fill-opacity="0.3"
|
||||
/>
|
||||
<path
|
||||
d="M9.44444 5.625H7.12699L6.57138 4.99994L7.12688 4.375H9.44444C9.75127 4.375 10 4.65482 10 5C10 5.34518 9.75127 5.625 9.44444 5.625Z"
|
||||
fill-opacity="0.3"
|
||||
/>
|
||||
<path
|
||||
transform="translate(0 2.6072)"
|
||||
d="M3.30542 0.183058C3.52237 -0.0610194 3.87413 -0.0610194 4.09109 0.183058C4.30805 0.427136 4.30805 0.822864 4.09109 1.06694L2.91256 2.39279L4.0911 3.71865C4.30806 3.96273 4.30806 4.35846 4.0911 4.60253C3.87414 4.84661 3.52239 4.84661 3.30543 4.60253L2.12688 3.27667L0.948393 4.60248C0.731435 4.84655 0.379676 4.84655 0.162719 4.60248C-0.0542395 4.3584 -0.0542395 3.96267 0.162719 3.71859L1.34121 2.39279L0.162731 1.067C-0.0542269 0.822922 -0.0542268 0.427194 0.162731 0.183116C0.379689 -0.0609612 0.731447 -0.0609612 0.948405 0.183116L2.12688 1.50891L3.30542 0.183058Z"
|
||||
/>
|
||||
d="M912.526651 867.741144 555.540144 510.712681l356.986507-357.000833c11.171434-11.18576 11.171434-29.257348 0-40.443108-11.20111-11.18576-29.272697-11.18576-40.444131 0L515.096013 470.267527 158.096203 113.267716c-11.187807-11.159154-29.258371-11.159154-40.444131 0-11.186783 11.186783-11.186783 29.286 0 40.47176L474.623229 510.712681 117.623419 867.741144c-11.159154 11.172457-11.159154 29.216415 0 40.443108 11.18576 11.17348 29.284977 11.17348 40.47176 0l357.000833-357.027439 356.985484 357.027439c11.171434 11.17348 29.243021 11.17348 40.444131 0C923.698085 896.957559 923.725714 878.913601 912.526651 867.741144z"
|
||||
fill="#5D5D5D"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -1,13 +1,31 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<rect x="13" y="3" width="2" height="2" rx="0.5" transform="rotate(90 13 3)" fill-opacity="0.3" />
|
||||
<rect x="13" y="7" width="2" height="2" rx="0.5" transform="rotate(90 13 7)" fill-opacity="0.3" />
|
||||
<rect x="13" y="11" width="2" height="2" rx="0.5" transform="rotate(90 13 11)" fill-opacity="0.3" />
|
||||
<rect x="9" y="3" width="2" height="2" rx="0.5" transform="rotate(90 9 3)" />
|
||||
<rect x="9" y="7" width="2" height="2" rx="0.5" transform="rotate(90 9 7)" />
|
||||
<rect x="9" y="11" width="2" height="2" rx="0.5" transform="rotate(90 9 11)" fill-opacity="0.3" />
|
||||
<rect x="5" y="3" width="2" height="2" rx="0.5" transform="rotate(90 5 3)" />
|
||||
<rect x="5" y="7" width="2" height="2" rx="0.5" transform="rotate(90 5 7)" />
|
||||
<rect x="5" y="11" width="2" height="2" rx="0.5" transform="rotate(90 5 11)" fill-opacity="0.3" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path
|
||||
d="M884.736 102.4l-147.456 0c-20.48 0-36.864 16.384-36.864 36.864l0 147.456C696.32 311.296 712.704 327.68 733.184 327.68l147.456 0c20.48 0 36.864-16.384 36.864-36.864L917.504 139.264C921.6 118.784 905.216 102.4 884.736 102.4zM884.736 290.816l-147.456 0L737.28 139.264l147.456 0L884.736 290.816z"
|
||||
></path>
|
||||
<path
|
||||
d="M884.736 696.32l-147.456 0c-20.48 0-36.864 16.384-36.864 36.864l0 147.456c0 20.48 16.384 36.864 36.864 36.864l147.456 0c20.48 0 36.864-16.384 36.864-36.864l0-147.456C921.6 712.704 905.216 696.32 884.736 696.32zM884.736 884.736l-147.456 0 0-147.456 147.456 0L884.736 884.736z"
|
||||
></path>
|
||||
<path
|
||||
d="M884.736 401.408l-147.456 0c-20.48 0-36.864 16.384-36.864 36.864l0 147.456c0 20.48 16.384 36.864 36.864 36.864l147.456 0c20.48 0 36.864-16.384 36.864-36.864l0-147.456C921.6 417.792 905.216 401.408 884.736 401.408zM884.736 585.728l-147.456 0 0-147.456 147.456 0L884.736 585.728z"
|
||||
></path>
|
||||
<path
|
||||
d="M585.728 401.408l-147.456 0c-20.48 0-36.864 16.384-36.864 36.864l0 147.456c0 20.48 16.384 36.864 36.864 36.864l147.456 0c20.48 0 36.864-16.384 36.864-36.864l0-147.456C622.592 417.792 606.208 401.408 585.728 401.408zM585.728 585.728l-147.456 0 0-147.456 147.456 0L585.728 585.728z"
|
||||
></path>
|
||||
<path
|
||||
d="M585.728 102.4l-147.456 0c-20.48 0-36.864 16.384-36.864 36.864l0 147.456c0 20.48 16.384 36.864 36.864 36.864l147.456 0c20.48 0 36.864-16.384 36.864-36.864L622.592 139.264C622.592 118.784 606.208 102.4 585.728 102.4zM585.728 290.816l-147.456 0L438.272 139.264l147.456 0L585.728 290.816z"
|
||||
></path>
|
||||
<path
|
||||
d="M585.728 696.32l-147.456 0c-20.48 0-36.864 16.384-36.864 36.864l0 147.456c0 20.48 16.384 36.864 36.864 36.864l147.456 0c20.48 0 36.864-16.384 36.864-36.864l0-147.456C622.592 712.704 606.208 696.32 585.728 696.32zM585.728 884.736l-147.456 0 0-147.456 147.456 0L585.728 884.736z"
|
||||
></path>
|
||||
<path
|
||||
d="M290.816 696.32 139.264 696.32c-20.48 0-36.864 16.384-36.864 36.864l0 147.456c0 20.48 16.384 36.864 36.864 36.864l147.456 0c20.48 0 36.864-16.384 36.864-36.864l0-147.456C327.68 712.704 311.296 696.32 290.816 696.32zM290.816 884.736 139.264 884.736l0-147.456 147.456 0L286.72 884.736z"
|
||||
></path>
|
||||
<path
|
||||
d="M290.816 401.408 139.264 401.408c-20.48 0-36.864 16.384-36.864 36.864l0 147.456c0 20.48 16.384 36.864 36.864 36.864l147.456 0c20.48 0 36.864-16.384 36.864-36.864l0-147.456C327.68 417.792 311.296 401.408 290.816 401.408zM290.816 585.728 139.264 585.728l0-147.456 147.456 0L286.72 585.728z"
|
||||
></path>
|
||||
<path
|
||||
d="M290.816 102.4 139.264 102.4c-20.48 0-36.864 16.384-36.864 36.864l0 147.456C102.4 311.296 118.784 327.68 139.264 327.68l147.456 0C311.296 327.68 327.68 311.296 327.68 290.816L327.68 139.264C327.68 118.784 311.296 102.4 290.816 102.4zM290.816 290.816 139.264 290.816 139.264 139.264l147.456 0L286.72 290.816z"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<rect x="3" y="3" width="2" height="10" rx="1" />
|
||||
<rect x="7" y="3" width="2" height="10" rx="1" />
|
||||
<rect x="11" y="3" width="2" height="10" rx="1" fill-opacity="0.3" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path
|
||||
d="M796.444444 170.666667h-227.555555v682.666666h227.555555V170.666667z m-56.888888 625.777777h-113.777778V227.555556h113.777778v568.888888zM455.111111 170.666667H227.555556v682.666666h227.555555V170.666667zM398.222222 796.444444H284.444444V227.555556h113.777778v568.888888zM910.222222 56.888889h56.888889v910.222222h-56.888889zM56.888889 56.888889h56.888889v910.222222H56.888889z"
|
||||
fill="#333333"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<rect x="13" y="3" width="2" height="10" rx="1" transform="rotate(90 13 3)" />
|
||||
<rect x="13" y="7" width="2" height="10" rx="1" transform="rotate(90 13 7)" />
|
||||
<rect x="13" y="11" width="2" height="10" rx="1" transform="rotate(90 13 11)" fill-opacity="0.3" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path
|
||||
d="M170.666667 227.555556v227.555555h682.666666V227.555556H170.666667z m625.777777 56.888888v113.777778H227.555556V284.444444h568.888888zM170.666667 568.888889v227.555555h682.666666v-227.555555H170.666667z m625.777777 56.888889v113.777778H227.555556v-113.777778h568.888888zM56.888889 56.888889h910.222222v56.888889H56.888889zM56.888889 910.222222h910.222222v56.888889H56.888889z"
|
||||
fill="#333333"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path
|
||||
d="M12 3C12.5523 3 13 3.44772 13 4V10C13 10.5523 12.5523 11 12 11H4C3.44772 11 3 10.5523 3 10V4C3 3.44772 3.44772 3 4 3H12ZM4.5 4C4.22386 4 4 4.22386 4 4.5V9.5C4 9.77614 4.22386 10 4.5 10H11.5C11.7761 10 12 9.77614 12 9.5V4.5C12 4.22386 11.7761 4 11.5 4H4.5Z"
|
||||
/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 12L8 15L5 12L11 12Z" fill-opacity="0.3" />
|
||||
d="M896 320H128V0h768v320z m0 320H128v-256h768v256z m-128 192l-256 192-256-192 192-0.032V704h128v128h192z"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path
|
||||
d="M12 5C12.5523 5 13 5.44772 13 6V12C13 12.5523 12.5523 13 12 13H4C3.44772 13 3 12.5523 3 12V6C3 5.44772 3.44772 5 4 5H12ZM4.5 6C4.22386 6 4 6.22386 4 6.5V11.5C4 11.7761 4.22386 12 4.5 12H11.5C11.7761 12 12 11.7761 12 11.5V6.5C12 6.22386 11.7761 6 11.5 6H4.5Z"
|
||||
/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 4L8 1L11 4L5 4Z" fill-opacity="0.3" />
|
||||
d="M896 704H128v320h768V704z m0-320H128v256h768v-256z m-128-192l-256-192-256 192 192 0.032V320h128V192h192z"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path
|
||||
d="M10 4C10.5523 4 11 4.44772 11 5V11C11 11.5523 10.5523 12 10 12H2C1.44772 12 1 11.5523 1 11V5C1 4.44772 1.44772 4 2 4H10ZM2.5 5C2.22386 5 2 5.22386 2 5.5V10.5C2 10.7761 2.22386 11 2.5 11H9.5C9.77614 11 10 10.7761 10 10.5V5.5C10 5.22386 9.77614 5 9.5 5H2.5Z"
|
||||
/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 5L15 8L12 11L12 5Z" fill-opacity="0.3" />
|
||||
d="M320 128v768H0V128h320z m320 0v768h-256V128h256z m192 128l192 256-192 256-0.032-192H704v-128h128V256z"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path
|
||||
d="M14 4C14.5523 4 15 4.44772 15 5V11C15 11.5523 14.5523 12 14 12H6C5.44772 12 5 11.5523 5 11V5C5 4.44772 5.44772 4 6 4H14ZM6.5 5C6.22386 5 6 5.22386 6 5.5V10.5C6 10.7761 6.22386 11 6.5 11H13.5C13.7761 11 14 10.7761 14 10.5V5.5C14 5.22386 13.7761 5 13.5 5H6.5Z"
|
||||
/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 11L1 8L4 5L4 11Z" fill-opacity="0.3" />
|
||||
d="M704 128v768h320V128H704zM384 128v768h256V128h-256zM192 256l-192 256 192 256 0.032-192H320v-128H192V256z"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path
|
||||
d="M13 4C13.5523 4 14 4.44772 14 5V11C14 11.5523 13.5523 12 13 12H3C2.44772 12 2 11.5523 2 11V5C2 4.44772 2.44772 4 3 4H13ZM3.59082 5C3.31472 5.00005 3.09082 5.22389 3.09082 5.5V10.5C3.09082 10.7761 3.31472 11 3.59082 11H12.4092C12.6853 11 12.9092 10.7761 12.9092 10.5V5.5C12.9092 5.22389 12.6853 5.00005 12.4092 5H3.59082Z"
|
||||
/>
|
||||
<rect x="7.5" y="2" width="1" height="12" rx="0.5" fill-opacity="0.3" style="fill: black; fill-opacity: 0.3" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path d="M480 1024V0h64v1024h-64z m128-64V64h320v896H608zM96 960V64h320v896H96z"></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -1,18 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path
|
||||
d="M11 4C11.5523 4 12 4.44772 12 5V11C12 11.5523 11.5523 12 11 12H2C1.44772 12 1 11.5523 1 11V5C1 4.44772 1.44772 4 2 4H11ZM2.5 5C2.22386 5 2 5.22386 2 5.5V10.5C2 10.7761 2.22386 11 2.5 11H10.5C10.7761 11 11 10.7761 11 10.5V5.5C11 5.22386 10.7761 5 10.5 5H2.5Z"
|
||||
/>
|
||||
<rect
|
||||
x="14.25"
|
||||
y="2.25"
|
||||
width="0.5"
|
||||
height="11.5"
|
||||
rx="0.25"
|
||||
stroke="black"
|
||||
stroke-opacity="0.3"
|
||||
style="stroke: black; stroke-opacity: 0.3"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path d="M416 160H96v704h320V160z m384 0H480v704h320V160z m128-160h-64v1024h64V0z"></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -1,18 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path
|
||||
d="M14 4C14.5523 4 15 4.44772 15 5V11C15 11.5523 14.5523 12 14 12H5C4.44772 12 4 11.5523 4 11V5C4 4.44772 4.44772 4 5 4H14ZM5.5 5C5.22386 5 5 5.22386 5 5.5V10.5C5 10.7761 5.22386 11 5.5 11H13.5C13.7761 11 14 10.7761 14 10.5V5.5C14 5.22386 13.7761 5 13.5 5H5.5Z"
|
||||
/>
|
||||
<rect
|
||||
x="1.25"
|
||||
y="2.25"
|
||||
width="0.5"
|
||||
height="11.5"
|
||||
rx="0.25"
|
||||
stroke="white"
|
||||
stroke-opacity="0.3"
|
||||
style="stroke: white; stroke-opacity: 0.3"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path d="M608 160h320v704H608V160zM224 160h320v704H224V160zM96 0h64v1024H96V0z"></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -1,32 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path
|
||||
d="M6 4C6.55228 4 7 4.44772 7 5V11C7 11.5523 6.55228 12 6 12H3C2.44772 12 2 11.5523 2 11V5C2 4.44772 2.44772 4 3 4H6ZM3.5 5C3.22386 5 3 5.22386 3 5.5V10.5C3 10.7761 3.22386 11 3.5 11H5.5C5.77614 11 6 10.7761 6 10.5V5.5C6 5.22386 5.77614 5 5.5 5H3.5Z"
|
||||
/>
|
||||
<path
|
||||
d="M13 4C13.5523 4 14 4.44772 14 5V11C14 11.5523 13.5523 12 13 12H10C9.44772 12 9 11.5523 9 11V5C9 4.44772 9.44772 4 10 4H13ZM10.5 5C10.2239 5 10 5.22386 10 5.5V10.5C10 10.7761 10.2239 11 10.5 11H12.5C12.7761 11 13 10.7761 13 10.5V5.5C13 5.22386 12.7761 5 12.5 5H10.5Z"
|
||||
/>
|
||||
<rect
|
||||
x="11.25"
|
||||
y="2.25"
|
||||
width="0.5"
|
||||
height="11.5"
|
||||
rx="0.25"
|
||||
stroke="black"
|
||||
stroke-opacity="0.3"
|
||||
style="stroke: black; stroke-opacity: 0.3"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
<rect
|
||||
x="4.25"
|
||||
y="2.25"
|
||||
width="0.5"
|
||||
height="11.5"
|
||||
rx="0.25"
|
||||
stroke="black"
|
||||
stroke-opacity="0.3"
|
||||
style="stroke: black; stroke-opacity: 0.3"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
d="M320 864H192v160H128v-160H0V160h128V0h64v160h128v704z m704 0h-128v160h-64v-160h-128V160h128V0h64v160h128v704z"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -1,31 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path
|
||||
d="M6 4C6.55228 4 7 4.44772 7 5V11C7 11.5523 6.55228 12 6 12H4C3.44772 12 3 11.5523 3 11V5C3 4.44772 3.44772 4 4 4H6ZM4.5 5C4.22386 5 4 5.22386 4 5.5V10.5C4 10.7761 4.22386 11 4.5 11H5.5C5.77614 11 6 10.7761 6 10.5V5.5C6 5.22386 5.77614 5 5.5 5H4.5Z"
|
||||
/>
|
||||
<path
|
||||
d="M12 4C12.5523 4 13 4.44772 13 5V11C13 11.5523 12.5523 12 12 12H10C9.44772 12 9 11.5523 9 11V5C9 4.44772 9.44772 4 10 4H12ZM10.5 5C10.2239 5 10 5.22386 10 5.5V10.5C10 10.7761 10.2239 11 10.5 11H11.5C11.7761 11 12 10.7761 12 10.5V5.5C12 5.22386 11.7761 5 11.5 5H10.5Z"
|
||||
/>
|
||||
<rect
|
||||
x="14.25"
|
||||
y="2.25"
|
||||
width="0.5"
|
||||
height="11.5"
|
||||
rx="0.25"
|
||||
stroke="black"
|
||||
stroke-opacity="0.3"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
<rect
|
||||
x="1.25"
|
||||
y="2.25"
|
||||
width="0.5"
|
||||
height="11.5"
|
||||
rx="0.25"
|
||||
stroke="black"
|
||||
stroke-opacity="0.3"
|
||||
style="stroke: black; stroke-opacity: 0.3"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path d="M64 1024H0V0h64v1024z m384-160H128V160h320v704z m448 0H576V160h320v704z m128 160h-64V0h64v1024z"></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -29,7 +29,6 @@ defineProps<{
|
||||
isCompare?: boolean;
|
||||
disabled?: boolean;
|
||||
size?: 'large' | 'default' | 'small';
|
||||
theme?: string;
|
||||
prop?: string;
|
||||
}>();
|
||||
|
||||
@ -113,14 +112,10 @@ const formConfig = defineFormConfig([
|
||||
childType: 'button',
|
||||
labelWidth: '68px',
|
||||
options: [
|
||||
{ value: 'no-repeat', icon: markRaw(BackgroundNoRepeat), tooltip: '不重复 no-repeat' },
|
||||
{ value: 'repeat', icon: markRaw(BackgroundRepeat), tooltip: '垂直和水平方向重复 repeat' },
|
||||
{ value: 'repeat-x', icon: markRaw(BackgroundRepeatX), tooltip: '水平方向重复 repeat-x' },
|
||||
{ value: 'repeat-y', icon: markRaw(BackgroundRepeatY), tooltip: '垂直方向重复 repeat-y' },
|
||||
{
|
||||
value: 'repeat',
|
||||
icon: markRaw(BackgroundRepeat),
|
||||
tooltip: '垂直和水平方向重复 repeat',
|
||||
},
|
||||
{ value: 'no-repeat', icon: markRaw(BackgroundNoRepeat), tooltip: '不重复 no-repeat' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -95,9 +95,9 @@ const formConfig = defineFormConfig([
|
||||
childType: 'button',
|
||||
labelWidth: '68px',
|
||||
options: [
|
||||
{ value: 'left', icon: markRaw(AlignLeft), tooltip: '左对齐 row', text: '左对齐' },
|
||||
{ value: 'center', icon: markRaw(AlignCenter), tooltip: '居中对齐 center', text: '居中对齐' },
|
||||
{ value: 'right', icon: markRaw(AlignRight), tooltip: '右对齐 right', text: '右对齐' },
|
||||
{ value: 'left', icon: markRaw(AlignLeft), tooltip: '左对齐 row' },
|
||||
{ value: 'center', icon: markRaw(AlignCenter), tooltip: '居中对齐 center' },
|
||||
{ value: 'right', icon: markRaw(AlignRight), tooltip: '右对齐 right' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
@ -26,19 +26,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { markRaw } from 'vue';
|
||||
|
||||
import { useTheme } from '@tmagic/design';
|
||||
import type { ContainerChangeEventData } from '@tmagic/form';
|
||||
import { defineFormConfig, MContainer } from '@tmagic/form';
|
||||
import type { StyleSchema } from '@tmagic/schema';
|
||||
|
||||
import Box from '../components/Box.vue';
|
||||
import {
|
||||
AlignItemsCenter,
|
||||
AlignItemsFlexEnd,
|
||||
AlignItemsFlexStart,
|
||||
AlignItemsSpaceAround,
|
||||
AlignItemsSpaceBetween,
|
||||
} from '../icons/align-items';
|
||||
import { DisplayBlock, DisplayFlex, DisplayInline, DisplayInlineBlock, DisplayNone } from '../icons/display';
|
||||
import {
|
||||
FlexDirectionColumn,
|
||||
@ -54,13 +46,12 @@ import {
|
||||
JustifyContentSpaceBetween,
|
||||
} from '../icons/justify-content';
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
values: Partial<StyleSchema>;
|
||||
lastValues?: Partial<StyleSchema>;
|
||||
isCompare?: boolean;
|
||||
disabled?: boolean;
|
||||
size?: 'large' | 'default' | 'small';
|
||||
theme?: string;
|
||||
prop?: string;
|
||||
}>();
|
||||
|
||||
@ -69,42 +60,19 @@ const emit = defineEmits<{
|
||||
addDiffCount: [];
|
||||
}>();
|
||||
|
||||
const displayTheme = useTheme(props);
|
||||
|
||||
const formConfig = defineFormConfig([
|
||||
{
|
||||
name: 'display',
|
||||
text: '模式',
|
||||
type: 'radioGroup',
|
||||
childType: 'button',
|
||||
labelWidth: '90px',
|
||||
iconSize: '24px',
|
||||
labelWidth: '68px',
|
||||
options: [
|
||||
{
|
||||
value: 'inline',
|
||||
icon: markRaw(DisplayInline),
|
||||
tooltip: '内联布局 inline',
|
||||
},
|
||||
{
|
||||
value: 'flex',
|
||||
icon: markRaw(DisplayFlex),
|
||||
tooltip: '弹性布局 flex',
|
||||
},
|
||||
{
|
||||
value: 'block',
|
||||
icon: markRaw(DisplayBlock),
|
||||
tooltip: '块级布局 block',
|
||||
},
|
||||
{
|
||||
value: 'inline-block',
|
||||
icon: markRaw(DisplayInlineBlock),
|
||||
tooltip: '内联块布局 inline-block',
|
||||
},
|
||||
{
|
||||
value: 'none',
|
||||
icon: markRaw(DisplayNone),
|
||||
tooltip: '隐藏 none',
|
||||
},
|
||||
{ value: 'inline', icon: markRaw(DisplayInline), tooltip: '内联布局 inline' },
|
||||
{ value: 'flex', icon: markRaw(DisplayFlex), tooltip: '弹性布局 flex' },
|
||||
{ value: 'block', icon: markRaw(DisplayBlock), tooltip: '块级布局 block' },
|
||||
{ value: 'inline-block', icon: markRaw(DisplayInlineBlock), tooltip: '内联块布局 inline-block' },
|
||||
{ value: 'none', icon: markRaw(DisplayNone), tooltip: '隐藏 none' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -112,20 +80,11 @@ const formConfig = defineFormConfig([
|
||||
text: '主轴方向',
|
||||
type: 'radioGroup',
|
||||
childType: 'button',
|
||||
labelWidth: '90px',
|
||||
iconSize: '24px',
|
||||
labelWidth: '68px',
|
||||
options: [
|
||||
{ value: 'row', icon: markRaw(FlexDirectionRow), tooltip: '水平方向 起点在左侧 row' },
|
||||
{
|
||||
value: 'row-reverse',
|
||||
icon: markRaw(FlexDirectionRowReverse),
|
||||
tooltip: '水平方向 起点在右侧 row-reverse',
|
||||
},
|
||||
{
|
||||
value: 'column',
|
||||
icon: markRaw(FlexDirectionColumn),
|
||||
tooltip: '垂直方向 起点在上沿 column',
|
||||
},
|
||||
{ value: 'row-reverse', icon: markRaw(FlexDirectionRowReverse), tooltip: '水平方向 起点在右侧 row-reverse' },
|
||||
{ value: 'column', icon: markRaw(FlexDirectionColumn), tooltip: '垂直方向 起点在上沿 column' },
|
||||
{
|
||||
value: 'column-reverse',
|
||||
icon: markRaw(FlexDirectionColumnReverse),
|
||||
@ -139,22 +98,13 @@ const formConfig = defineFormConfig([
|
||||
text: '主轴对齐',
|
||||
type: 'radioGroup',
|
||||
childType: 'button',
|
||||
labelWidth: '90px',
|
||||
iconSize: '24px',
|
||||
labelWidth: '68px',
|
||||
options: [
|
||||
{ value: 'flex-start', icon: markRaw(JustifyContentFlexStart), tooltip: '左对齐 flex-start' },
|
||||
{ value: 'flex-end', icon: markRaw(JustifyContentFlexEnd), tooltip: '右对齐 flex-end' },
|
||||
{ value: 'center', icon: markRaw(JustifyContentCenter), tooltip: '居中 center' },
|
||||
{
|
||||
value: 'space-between',
|
||||
icon: markRaw(JustifyContentSpaceBetween),
|
||||
tooltip: '两端对齐 space-between',
|
||||
},
|
||||
{
|
||||
value: 'space-around',
|
||||
icon: markRaw(JustifyContentSpaceAround),
|
||||
tooltip: '横向平分 space-around',
|
||||
},
|
||||
{ value: 'space-between', icon: markRaw(JustifyContentSpaceBetween), tooltip: '两端对齐 space-between' },
|
||||
{ value: 'space-around', icon: markRaw(JustifyContentSpaceAround), tooltip: '横向平分 space-around' },
|
||||
],
|
||||
display: (_mForm, { model }: { model: Record<any, any> }) => model.display === 'flex',
|
||||
},
|
||||
@ -163,22 +113,13 @@ const formConfig = defineFormConfig([
|
||||
text: '辅轴对齐',
|
||||
type: 'radioGroup',
|
||||
childType: 'button',
|
||||
labelWidth: '90px',
|
||||
iconSize: '24px',
|
||||
labelWidth: '68px',
|
||||
options: [
|
||||
{ value: 'flex-start', icon: markRaw(AlignItemsFlexStart), tooltip: '左对齐 flex-start' },
|
||||
{ value: 'flex-end', icon: markRaw(AlignItemsFlexEnd), tooltip: '右对齐 flex-end' },
|
||||
{ value: 'center', icon: markRaw(AlignItemsCenter), tooltip: '居中 center' },
|
||||
{
|
||||
value: 'space-between',
|
||||
icon: markRaw(AlignItemsSpaceBetween),
|
||||
tooltip: '两端对齐 space-between',
|
||||
},
|
||||
{
|
||||
value: 'space-around',
|
||||
icon: markRaw(AlignItemsSpaceAround),
|
||||
tooltip: '横向平分 space-around',
|
||||
},
|
||||
{ value: 'flex-start', icon: markRaw(JustifyContentFlexStart), tooltip: '左对齐 flex-start' },
|
||||
{ value: 'flex-end', icon: markRaw(JustifyContentFlexEnd), tooltip: '右对齐 flex-end' },
|
||||
{ value: 'center', icon: markRaw(JustifyContentCenter), tooltip: '居中 center' },
|
||||
{ value: 'space-between', icon: markRaw(JustifyContentSpaceBetween), tooltip: '两端对齐 space-between' },
|
||||
{ value: 'space-around', icon: markRaw(JustifyContentSpaceAround), tooltip: '横向平分 space-around' },
|
||||
],
|
||||
display: (_mForm, { model }: { model: Record<any, any> }) => model.display === 'flex',
|
||||
},
|
||||
@ -186,9 +127,8 @@ const formConfig = defineFormConfig([
|
||||
name: 'flexWrap',
|
||||
text: '换行',
|
||||
type: 'radioGroup',
|
||||
childType: displayTheme.value !== 'magic-admin' ? 'button' : 'default',
|
||||
labelWidth: '90px',
|
||||
iconSize: '24px',
|
||||
childType: 'button',
|
||||
labelWidth: '68px',
|
||||
options: [
|
||||
{ value: 'nowrap', text: '不换行', tooltip: '不换行 nowrap' },
|
||||
{ value: 'wrap', text: '正换行', tooltip: '第一行在上方 wrap' },
|
||||
@ -201,22 +141,17 @@ const formConfig = defineFormConfig([
|
||||
items: [
|
||||
{
|
||||
name: 'width',
|
||||
text: '宽度(px)',
|
||||
labelWidth: '90px',
|
||||
text: '宽度',
|
||||
labelWidth: '68px',
|
||||
type: 'data-source-field-select',
|
||||
fieldConfig: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
items: [
|
||||
{
|
||||
name: 'height',
|
||||
text: '高度(px)',
|
||||
labelWidth: '90px',
|
||||
text: '高度',
|
||||
labelWidth: '68px',
|
||||
type: 'data-source-field-select',
|
||||
fieldConfig: {
|
||||
type: 'text',
|
||||
@ -231,7 +166,7 @@ const formConfig = defineFormConfig([
|
||||
type: 'data-source-field-select',
|
||||
text: 'overflow',
|
||||
name: 'overflow',
|
||||
labelWidth: '90px',
|
||||
labelWidth: '68px',
|
||||
checkStrictly: false,
|
||||
dataSourceFieldType: ['string'],
|
||||
fieldConfig: {
|
||||
@ -249,16 +184,11 @@ const formConfig = defineFormConfig([
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
items: [
|
||||
{
|
||||
type: 'data-source-field-select',
|
||||
text: '透明度(%)',
|
||||
text: '透明度',
|
||||
name: 'opacity',
|
||||
labelWidth: '90px',
|
||||
labelWidth: '68px',
|
||||
dataSourceFieldType: ['string', 'number'],
|
||||
fieldConfig: {
|
||||
type: 'text',
|
||||
|
||||
@ -28,7 +28,6 @@ export const useStage = (stageOptions: StageOptions) => {
|
||||
containerHighlightClassName: stageOptions.containerHighlightClassName,
|
||||
containerHighlightDuration: stageOptions.containerHighlightDuration,
|
||||
containerHighlightType: stageOptions.containerHighlightType,
|
||||
containerHighlightAddOnly: stageOptions.containerHighlightAddOnly,
|
||||
disabledDragStart: stageOptions.disabledDragStart,
|
||||
renderType: stageOptions.renderType,
|
||||
canSelect: (el, event, stop) => {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -32,9 +32,6 @@ export { DepTargetType } from '@tmagic/core';
|
||||
export * from './type';
|
||||
export * from './hooks';
|
||||
export * from './utils';
|
||||
// `isGlobalFlat` 同时在 `@tmagic/design` 和 `./utils/config` 中存在,
|
||||
// 通过显式重导一次消除 `export *` 带来的歧义,并固定使用 editor 自己那份。
|
||||
export { isGlobalFlat } from './utils/config';
|
||||
export { default as TMagicEditor } from './Editor.vue';
|
||||
export { default as TMagicCodeEditor } from './layouts/CodeEditor.vue';
|
||||
export { default as editorService } from './services/editor';
|
||||
|
||||
@ -333,11 +333,11 @@ export const initServiceEvents = (
|
||||
Promise.all(
|
||||
nodes.map((node) => {
|
||||
if (node.type === NodeType.ROOT) {
|
||||
return Promise.resolve(true);
|
||||
return Promise.resolve();
|
||||
}
|
||||
return depService.collectIdle([node], { pageId: getPageIdByNode(node) }, deep, type);
|
||||
}),
|
||||
).then((results) => results.every(Boolean));
|
||||
);
|
||||
|
||||
watch(
|
||||
() => editorService.get('stage'),
|
||||
@ -351,7 +351,7 @@ export const initServiceEvents = (
|
||||
|
||||
if (!node) return;
|
||||
|
||||
if (!(await collectIdle([node], true, DepTargetType.DATA_SOURCE))) return;
|
||||
await collectIdle([node], true, DepTargetType.DATA_SOURCE);
|
||||
updateStageNode(node);
|
||||
});
|
||||
},
|
||||
@ -494,10 +494,8 @@ export const initServiceEvents = (
|
||||
|
||||
// 新增节点,收集依赖
|
||||
const nodeAddHandler = (nodes: MComponent[]) => {
|
||||
collectIdle(nodes, true).then((completed) => {
|
||||
if (completed) {
|
||||
updateStageNodes(nodes);
|
||||
}
|
||||
collectIdle(nodes, true).then(() => {
|
||||
updateStageNodes(nodes);
|
||||
});
|
||||
};
|
||||
|
||||
@ -551,8 +549,8 @@ export const initServiceEvents = (
|
||||
if (needRecollectNodes.length) {
|
||||
// 有数据源依赖,需要等依赖重新收集完才更新stage
|
||||
const handler = async () => {
|
||||
if (!(await collectIdle(needRecollectNodes, true, DepTargetType.DATA_SOURCE))) return;
|
||||
if (!(await collectIdle(needRecollectNodes, true, DepTargetType.DATA_SOURCE_COND))) return;
|
||||
await collectIdle(needRecollectNodes, true, DepTargetType.DATA_SOURCE);
|
||||
await collectIdle(needRecollectNodes, true, DepTargetType.DATA_SOURCE_COND);
|
||||
updateStageNodes(needRecollectNodes);
|
||||
};
|
||||
handler();
|
||||
@ -574,10 +572,8 @@ export const initServiceEvents = (
|
||||
|
||||
// 历史记录变化时,需要重新收集依赖
|
||||
const historyChangeHandler = (page: MPage | MPageFragment) => {
|
||||
collectIdle([page], true).then((completed) => {
|
||||
if (completed) {
|
||||
updateStageNode(page);
|
||||
}
|
||||
collectIdle([page], true).then(() => {
|
||||
updateStageNode(page);
|
||||
});
|
||||
};
|
||||
|
||||
@ -658,7 +654,7 @@ export const initServiceEvents = (
|
||||
if (Array.isArray(root?.items)) {
|
||||
depService.clearIdleTasks();
|
||||
|
||||
let collectIdlePromises: Promise<boolean>[] = [];
|
||||
let collectIdlePromises: Promise<void[]>[] = [];
|
||||
if (isModifyField) {
|
||||
depService.removeTarget(config.id, DepTargetType.DATA_SOURCE);
|
||||
depService.removeTarget(config.id, DepTargetType.DATA_SOURCE_COND);
|
||||
@ -683,15 +679,10 @@ export const initServiceEvents = (
|
||||
|
||||
collectIdlePromises = [collectIdle(root.items, true, DepTargetType.DATA_SOURCE_METHOD)];
|
||||
}
|
||||
const handler = async () => {
|
||||
const results = await Promise.all(collectIdlePromises);
|
||||
if (!results.every(Boolean)) return;
|
||||
|
||||
updateDataSourceSchema();
|
||||
await updateDsData();
|
||||
updateStageNodes(root.items);
|
||||
};
|
||||
handler();
|
||||
Promise.all(collectIdlePromises)
|
||||
.then(() => updateDataSourceSchema())
|
||||
.then(() => updateDsData())
|
||||
.then(() => updateStageNodes(root.items));
|
||||
}
|
||||
} else if (root?.dataSources) {
|
||||
updateDsData();
|
||||
@ -715,12 +706,11 @@ export const initServiceEvents = (
|
||||
const nodeIds = Object.keys(root.dataSourceDeps?.[id] || {});
|
||||
const nodes = getNodes(nodeIds, root.items);
|
||||
|
||||
const results = await Promise.all([
|
||||
await Promise.all([
|
||||
collectIdle(nodes, false, DepTargetType.DATA_SOURCE),
|
||||
collectIdle(nodes, false, DepTargetType.DATA_SOURCE_COND),
|
||||
collectIdle(nodes, false, DepTargetType.DATA_SOURCE_METHOD),
|
||||
]);
|
||||
if (!results.every(Boolean)) return;
|
||||
|
||||
updateDataSourceSchema();
|
||||
|
||||
|
||||
@ -1,9 +1,5 @@
|
||||
<template>
|
||||
<div
|
||||
ref="content"
|
||||
:class="['m-editor', theme ? `m-editor--${theme}` : '', theme ? `m-theme--${theme}` : '']"
|
||||
style="min-width: 900px"
|
||||
>
|
||||
<div class="m-editor" ref="content" style="min-width: 900px">
|
||||
<slot name="header"></slot>
|
||||
|
||||
<slot name="nav"></slot>
|
||||
@ -101,8 +97,6 @@ const props = defineProps<{
|
||||
pageFilterFunction?: (_page: MPage | MPageFragment, _keyword: string) => boolean;
|
||||
/** 是否隐藏左侧面板 */
|
||||
hideSidebar?: boolean;
|
||||
/** 主题名称,会在根节点追加 `m-editor--<theme>` 修饰类 */
|
||||
theme?: string;
|
||||
}>();
|
||||
|
||||
const codeOptions = inject('codeOptions', {});
|
||||
|
||||
@ -14,7 +14,6 @@
|
||||
:config="config"
|
||||
:type-match-valid="true"
|
||||
:extend-state="extendState"
|
||||
:validate-on-init="true"
|
||||
@change="submit"
|
||||
@error="errorHandler"
|
||||
></MForm>
|
||||
@ -50,13 +49,14 @@ import { Document as DocumentIcon } from '@element-plus/icons-vue';
|
||||
|
||||
import { TMagicButton, tMagicMessage, TMagicScrollbar } from '@tmagic/design';
|
||||
import type { ContainerChangeEventData, FormConfig, FormState, FormValue } from '@tmagic/form';
|
||||
import { MForm, validateForm } from '@tmagic/form';
|
||||
import { MForm } from '@tmagic/form';
|
||||
import { filterXSS } from '@tmagic/utils';
|
||||
|
||||
import MIcon from '@editor/components/Icon.vue';
|
||||
import { ENABLE_PROPS_FORM_VALIDATE } from '@editor/editorProps';
|
||||
import { useEditorContentHeight } from '@editor/hooks/use-editor-content-height';
|
||||
import { useServices } from '@editor/hooks/use-services';
|
||||
import { validatePropsForm } from '@editor/utils/props';
|
||||
|
||||
import CodeEditor from '../CodeEditor.vue';
|
||||
|
||||
@ -162,19 +162,14 @@ const saveCode = async (values: any) => {
|
||||
// 做一次静默校验(不复用、也不污染页面上正在展示的表单),并将校验结果(错误信息)随提交
|
||||
// 一并抛给上层记录,使源码保存的错误状态与表单编辑保持一致。
|
||||
try {
|
||||
const error = await validateForm({
|
||||
const error = await validatePropsForm({
|
||||
config: props.config,
|
||||
values: newValues,
|
||||
appContext: internalInstance?.appContext ?? null,
|
||||
services,
|
||||
stage: stage.value,
|
||||
typeMatchValid: true,
|
||||
initValues: newValues,
|
||||
// 将当前组件实例的 provides(含 Editor 顶层的 services / codeOptions 等组件级 provide)
|
||||
// 合入 appContext,使临时 MForm 中的编辑器字段组件(DataSourceInput 等)能正常 inject
|
||||
appContext: internalInstance?.appContext ? { ...internalInstance?.appContext, provides: { services } } : null,
|
||||
extendState: (state) => {
|
||||
if (configFormRef.value?.formState) {
|
||||
return { ...(configFormRef.value?.formState || {}) };
|
||||
}
|
||||
return props.extendState?.(state) || {};
|
||||
},
|
||||
extendState: props.extendState,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
|
||||
@ -111,31 +111,14 @@ const styleFormConfig = [
|
||||
},
|
||||
];
|
||||
|
||||
// 用单调递增序号标记每次 init 调用,只让"最新一次"的 await 结果落到 ref 上。
|
||||
// 避免节点快速切换时多个 init 并发 + 解析顺序错乱导致 stale config 覆盖最新选中节点,
|
||||
// 以及组件已卸载 / FormPanel 正在 remount 的中间态下写 ref 触发 Vue setRef 把 __vnode
|
||||
// 设到 null 上的报错(TypeError: Cannot set properties of null (setting '__vnode'))。
|
||||
let initSeq = 0;
|
||||
let mounted = true;
|
||||
|
||||
const init = async () => {
|
||||
initSeq += 1;
|
||||
const seq = initSeq;
|
||||
|
||||
if (!node.value) {
|
||||
if (seq !== initSeq || !mounted) return;
|
||||
curFormConfig.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const type = node.value.type || (node.value.items ? 'container' : 'text');
|
||||
const config = await propsService.getPropsConfig(type, { node: node.value });
|
||||
|
||||
// 期间被新一次 init 取代 / 组件已卸载,丢弃本次结果
|
||||
if (seq !== initSeq || !mounted) return;
|
||||
if (!node.value) return;
|
||||
|
||||
curFormConfig.value = config;
|
||||
curFormConfig.value = await propsService.getPropsConfig(type, { node: node.value });
|
||||
values.value = node.value;
|
||||
};
|
||||
|
||||
@ -143,7 +126,6 @@ watchEffect(init);
|
||||
propsService.on('props-configs-change', init);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
mounted = false;
|
||||
propsService.off('props-configs-change', init);
|
||||
});
|
||||
|
||||
@ -164,25 +146,17 @@ const submit = async (
|
||||
};
|
||||
|
||||
if (v.style) {
|
||||
// 空字符串样式值表示「清除该样式」,需保留才能在 doUpdate 的 mergeWith 中覆盖旧值。
|
||||
if (eventData) {
|
||||
// 表单编辑:先过滤掉空字符串(避免表单默认空值污染 DSL),
|
||||
// 再按 changeRecords 恢复被主动清空的字段。
|
||||
Object.entries(v.style).forEach(([key, value]) => {
|
||||
if (value !== '' && newValue.style) {
|
||||
newValue.style[key] = value;
|
||||
}
|
||||
});
|
||||
Object.entries(v.style).forEach(([key, value]) => {
|
||||
if (value !== '' && newValue.style) {
|
||||
newValue.style[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
eventData.changeRecords?.forEach((record) => {
|
||||
if (record.propPath?.startsWith('style') && record.value === '') {
|
||||
setValueByKeyPath(record.propPath, record.value, newValue);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 源码编辑器保存(无 eventData):style 原样保留,其中的空字符串视为用户主动清除该样式。
|
||||
newValue.style = { ...v.style };
|
||||
}
|
||||
eventData?.changeRecords?.forEach((record) => {
|
||||
if (record.propPath?.startsWith('style') && record.value === '') {
|
||||
setValueByKeyPath(record.propPath, record.value, newValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 区分操作途径:表单字段编辑(MForm @change)会带上 eventData(含 changeRecords);
|
||||
|
||||
@ -145,6 +145,6 @@ const dragHandler = (e: DragEvent) => {
|
||||
|
||||
if (timeout || !stage.value) return;
|
||||
|
||||
timeout = stage.value.delayedMarkContainer(e, [], true);
|
||||
timeout = stage.value.delayedMarkContainer(e);
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -155,7 +155,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import { Box, Close, Coin, EditPen, Finished } from '@element-plus/icons-vue';
|
||||
import { Close, Coin, EditPen, Goods, List } from '@element-plus/icons-vue';
|
||||
|
||||
import FloatingBox from '@editor/components/FloatingBox.vue';
|
||||
import MIcon from '@editor/components/Icon.vue';
|
||||
@ -253,7 +253,7 @@ const getItemConfig = (data: SideItem): SideComponent => {
|
||||
[SideItemKey.COMPONENT_LIST]: {
|
||||
$key: SideItemKey.COMPONENT_LIST,
|
||||
type: 'component',
|
||||
icon: Box,
|
||||
icon: Goods,
|
||||
text: '组件',
|
||||
component: ComponentListPanel,
|
||||
slots: {},
|
||||
@ -261,7 +261,7 @@ const getItemConfig = (data: SideItem): SideComponent => {
|
||||
layer: {
|
||||
$key: SideItemKey.LAYER,
|
||||
type: 'component',
|
||||
icon: Finished,
|
||||
icon: List,
|
||||
text: '已选组件',
|
||||
props: {
|
||||
layerContentMenu: props.layerContentMenu,
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
import { computed, markRaw, useTemplateRef } from 'vue';
|
||||
import { Files, Plus } from '@element-plus/icons-vue';
|
||||
|
||||
import { isPageOrFragment } from '@tmagic/utils';
|
||||
import { isPage, isPageFragment } from '@tmagic/utils';
|
||||
|
||||
import ContentMenu from '@editor/components/ContentMenu.vue';
|
||||
import { useServices } from '@editor/hooks/use-services';
|
||||
@ -101,7 +101,7 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
|
||||
type: 'button',
|
||||
text: '全部折叠',
|
||||
icon: FolderMinusIcon,
|
||||
display: () => isPageOrFragment(node.value),
|
||||
display: () => isPage(node.value) || isPageFragment(node.value),
|
||||
handler: () => {
|
||||
emit('collapse-all');
|
||||
},
|
||||
|
||||
@ -2,7 +2,7 @@ import { computed, type ComputedRef, nextTick, type Ref, type ShallowRef } from
|
||||
import { throttle } from 'lodash-es';
|
||||
|
||||
import { Id, MNode } from '@tmagic/core';
|
||||
import { getElById, isPageOrFragment } from '@tmagic/utils';
|
||||
import { getElById, isPage, isPageFragment } from '@tmagic/utils';
|
||||
|
||||
import type { LayerNodeStatus, Services, TreeNodeData } from '@editor/type';
|
||||
import { UI_SELECT_MODE_EVENT_NAME } from '@editor/utils/const';
|
||||
@ -36,7 +36,7 @@ export const useClick = (
|
||||
};
|
||||
|
||||
const multiSelect = async (data: MNode) => {
|
||||
if (isPageOrFragment(data)) {
|
||||
if (isPage(data) || isPageFragment(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -50,7 +50,7 @@ export const useClick = (
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPageOrFragment(node)) {
|
||||
if (isPage(node) || isPageFragment(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
||||
|
||||
import type { Id, MApp, MNode, MPage, MPageFragment } from '@tmagic/core';
|
||||
import { getNodePath, isPageOrFragment, traverseNode } from '@tmagic/utils';
|
||||
import { getNodePath, isPage, isPageFragment, traverseNode } from '@tmagic/utils';
|
||||
|
||||
import type { LayerNodeStatus, Services } from '@editor/type';
|
||||
import { updateStatus } from '@editor/utils/tree';
|
||||
@ -136,7 +136,7 @@ export const useNodeStatus = ({ editorService }: Services) => {
|
||||
|
||||
const addHandler = (newNodes: MNode[]) => {
|
||||
newNodes.forEach((node) => {
|
||||
if (isPageOrFragment(node)) return;
|
||||
if (isPage(node) || isPageFragment(node)) return;
|
||||
|
||||
traverseNode(node, (node: MNode) => {
|
||||
nodeStatusMap.value?.set(node.id, {
|
||||
|
||||
@ -7,7 +7,7 @@ import { computed, markRaw, ref, useTemplateRef, watch } from 'vue';
|
||||
import { Bottom, Top } from '@element-plus/icons-vue';
|
||||
|
||||
import { NodeType } from '@tmagic/core';
|
||||
import { isPageOrFragment } from '@tmagic/utils';
|
||||
import { isPage, isPageFragment } from '@tmagic/utils';
|
||||
|
||||
import ContentMenu from '@editor/components/ContentMenu.vue';
|
||||
import { useServices } from '@editor/hooks/use-services';
|
||||
@ -59,14 +59,14 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
|
||||
direction: 'horizontal',
|
||||
display: () => {
|
||||
if (!node.value) return false;
|
||||
return !isPageOrFragment(node.value);
|
||||
return !isPage(node.value) && !isPageFragment(node.value);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
text: '上移一层',
|
||||
icon: markRaw(Top),
|
||||
display: () => !isPageOrFragment(node.value) && !props.isMultiSelect,
|
||||
display: () => !isPage(node.value) && !isPageFragment(node.value) && !props.isMultiSelect,
|
||||
handler: () => {
|
||||
editorService.moveLayer(1, { historySource: 'stage-contextmenu' });
|
||||
},
|
||||
@ -75,7 +75,7 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
|
||||
type: 'button',
|
||||
text: '下移一层',
|
||||
icon: markRaw(Bottom),
|
||||
display: () => !isPageOrFragment(node.value) && !props.isMultiSelect,
|
||||
display: () => !isPage(node.value) && !isPageFragment(node.value) && !props.isMultiSelect,
|
||||
handler: () => {
|
||||
editorService.moveLayer(-1, { historySource: 'stage-contextmenu' });
|
||||
},
|
||||
@ -84,7 +84,7 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
|
||||
type: 'button',
|
||||
text: '置顶',
|
||||
icon: markRaw(Top),
|
||||
display: () => !isPageOrFragment(node.value) && !props.isMultiSelect,
|
||||
display: () => !isPage(node.value) && !isPageFragment(node.value) && !props.isMultiSelect,
|
||||
handler: () => {
|
||||
editorService.moveLayer(LayerOffset.TOP, { historySource: 'stage-contextmenu' });
|
||||
},
|
||||
@ -93,7 +93,7 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
|
||||
type: 'button',
|
||||
text: '置底',
|
||||
icon: markRaw(Bottom),
|
||||
display: () => !isPageOrFragment(node.value) && !props.isMultiSelect,
|
||||
display: () => !isPage(node.value) && !isPageFragment(node.value) && !props.isMultiSelect,
|
||||
handler: () => {
|
||||
editorService.moveLayer(LayerOffset.BOTTOM, { historySource: 'stage-contextmenu' });
|
||||
},
|
||||
@ -102,7 +102,7 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
|
||||
{
|
||||
type: 'divider',
|
||||
direction: 'horizontal',
|
||||
display: () => !isPageOrFragment(node.value) && !props.isMultiSelect,
|
||||
display: () => !isPage(node.value) && !isPageFragment(node.value) && !props.isMultiSelect,
|
||||
},
|
||||
useDeleteMenu('stage-contextmenu'),
|
||||
{
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type App } from 'vue';
|
||||
import type { App } from 'vue';
|
||||
|
||||
import type { DesignPluginOptions } from '@tmagic/design';
|
||||
import designPlugin from '@tmagic/design';
|
||||
@ -56,8 +56,6 @@ const defaultInstallOpt: EditorInstallOptions = {
|
||||
customCreateMonacoEditor: (monaco, codeEditorEl, options) => monaco.editor.create(codeEditorEl, options),
|
||||
customCreateMonacoDiffEditor: (monaco, codeEditorEl, options) =>
|
||||
monaco.editor.createDiffEditor(codeEditorEl, options),
|
||||
|
||||
flat: false,
|
||||
};
|
||||
|
||||
export default {
|
||||
@ -71,6 +69,7 @@ export default {
|
||||
|
||||
app.config.globalProperties.$TMAGIC_EDITOR = option;
|
||||
setEditorConfig(option);
|
||||
|
||||
app.component(`${Editor.name || 'MEditor'}`, Editor);
|
||||
app.component('magic-code-editor', CodeEditor);
|
||||
app.component('m-fields-ui-select', uiSelect);
|
||||
|
||||
@ -17,14 +17,14 @@
|
||||
*/
|
||||
import { reactive, shallowReactive } from 'vue';
|
||||
import { throttle } from 'lodash-es';
|
||||
import serialize from 'serialize-javascript';
|
||||
|
||||
import type { DepData, DepExtendedData, Id, MApp, MNode, Target } from '@tmagic/core';
|
||||
import type { DepData, DepExtendedData, Id, MApp, MNode, Target, TargetNode } from '@tmagic/core';
|
||||
import { DepTargetType, traverseTarget, Watcher } from '@tmagic/core';
|
||||
import { isPage } from '@tmagic/utils';
|
||||
|
||||
import { CollectWorkerClient } from '@editor/utils/dep/collect-worker-client';
|
||||
import { IdleTask } from '@editor/utils/dep/idle-task';
|
||||
import type { DepsData } from '@editor/utils/dep/worker';
|
||||
import Work from '@editor/utils/dep/worker.ts?worker&inline';
|
||||
|
||||
import BaseService from './BaseService';
|
||||
|
||||
@ -42,42 +42,18 @@ interface State {
|
||||
|
||||
type StateKey = keyof State;
|
||||
|
||||
/**
|
||||
* 一次 collectIdle 调用对应一个批次。批次自行统计入队/完成的任务数,在自身任务全部完成或被中断时
|
||||
* 结算对应的 Promise,避免多次快速触发时共用 idleTask 的 finish 事件造成的串扰、提前 resolve,
|
||||
* 以及 clearTasks 后 Promise 永不 resolve(collecting 卡死、监听器泄漏)。
|
||||
*/
|
||||
interface CollectBatch {
|
||||
nodes: MNode[];
|
||||
deep: boolean;
|
||||
pending: number;
|
||||
dsPending: number;
|
||||
collectedEmitted: boolean;
|
||||
dsSettled: boolean;
|
||||
aborted: boolean;
|
||||
resolve: (completed: boolean) => void;
|
||||
}
|
||||
|
||||
class Dep extends BaseService {
|
||||
private state = shallowReactive<State>({
|
||||
collecting: false,
|
||||
taskLength: 0,
|
||||
});
|
||||
|
||||
private idleTask = new IdleTask<void>();
|
||||
|
||||
private collectWorker = new CollectWorkerClient();
|
||||
private idleTask = new IdleTask<{ node: TargetNode; deep: boolean; target: Target }>();
|
||||
|
||||
private watcher = new Watcher({ initialTargets: reactive({}) });
|
||||
|
||||
private waitingWorker?: Promise<void>;
|
||||
|
||||
private resolveWaitingWorker?: () => void;
|
||||
|
||||
private workerGeneration = 0;
|
||||
|
||||
private activeBatches = new Set<CollectBatch>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@ -156,77 +132,56 @@ class Dep extends BaseService {
|
||||
this.emit('ds-collected', nodes, deep);
|
||||
}
|
||||
|
||||
/**
|
||||
* 空闲收集依赖
|
||||
*
|
||||
* 遍历匹配是整个收集过程中最耗时的部分(要深度遍历节点树并对每个属性做 target 匹配),
|
||||
* 节点/数据源多时在主线程执行会长时间卡死页面,因此优先交给 worker 执行,主线程只负责把结果写回 target。
|
||||
* 自定义 target 的 isTarget 是无法跨线程传递的闭包函数(没有 descriptor),仍走主线程空闲队列;
|
||||
* worker 不可用(如 SSR)或执行失败时也会回退到主线程空闲队列。
|
||||
*/
|
||||
public async collectIdle(nodes: MNode[], depExtendedData: DepExtendedData = {}, deep = false, type?: DepTargetType) {
|
||||
if (this.waitingWorker) {
|
||||
await this.waitingWorker;
|
||||
}
|
||||
|
||||
const batch: CollectBatch = {
|
||||
nodes,
|
||||
deep,
|
||||
pending: 0,
|
||||
dsPending: 0,
|
||||
collectedEmitted: false,
|
||||
dsSettled: false,
|
||||
aborted: false,
|
||||
resolve: () => {},
|
||||
};
|
||||
|
||||
const workerTargets: Target[] = [];
|
||||
|
||||
for (const target of this.watcher.getCollectableTargets(type)) {
|
||||
if (target.descriptor && this.collectWorker.isSupported) {
|
||||
workerTargets.push(target);
|
||||
} else {
|
||||
this.enqueueTasks(nodes, target, depExtendedData, deep, batch);
|
||||
}
|
||||
}
|
||||
|
||||
if (workerTargets.length && nodes.length) {
|
||||
this.collectByCollectWorker(nodes, workerTargets, depExtendedData, deep, batch);
|
||||
}
|
||||
|
||||
// 没有命中任何 target,无需收集,直接完成
|
||||
if (batch.pending === 0) {
|
||||
this.emit('collected', nodes, deep);
|
||||
this.updateCollectingState();
|
||||
return true;
|
||||
}
|
||||
|
||||
this.activeBatches.add(batch);
|
||||
this.set('collecting', true);
|
||||
let startTask = false;
|
||||
this.watcher.collectByCallback(nodes, type, ({ node, target }) => {
|
||||
startTask = true;
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
batch.resolve = resolve;
|
||||
this.enqueueTask(node, target, depExtendedData, deep);
|
||||
});
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
if (!startTask) {
|
||||
this.emit('collected', nodes, deep);
|
||||
this.set('collecting', false);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
this.idleTask.once('finish', () => {
|
||||
this.emit('collected', nodes, deep);
|
||||
this.set('collecting', false);
|
||||
});
|
||||
this.idleTask.once('hight-level-finish', () => {
|
||||
this.emit('ds-collected', nodes, deep);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public collectByWorker(dsl: MApp) {
|
||||
this.set('collecting', true);
|
||||
this.workerGeneration += 1;
|
||||
const generation = this.workerGeneration;
|
||||
|
||||
const { promise, resolve: waitingResolve } = Promise.withResolvers<void>();
|
||||
|
||||
this.waitingWorker = promise;
|
||||
this.resolveWaitingWorker = waitingResolve;
|
||||
|
||||
return this.collectWorker.collectDsl(dsl).then((deps) => {
|
||||
const depsData: DepsData = deps || {};
|
||||
|
||||
if (generation !== this.workerGeneration) {
|
||||
waitingResolve();
|
||||
return depsData;
|
||||
}
|
||||
|
||||
return new Promise<Record<string, Record<string, DepData>>>((resolve) => {
|
||||
const worker = new Work();
|
||||
worker.postMessage({
|
||||
dsl: serialize(dsl),
|
||||
});
|
||||
worker.onmessage = (e) => {
|
||||
resolve(e.data);
|
||||
};
|
||||
worker.onerror = () => {
|
||||
resolve({});
|
||||
};
|
||||
}).then((depsData) => {
|
||||
traverseTarget(this.watcher.getTargetsList(), (target) => {
|
||||
if (depsData[target.type]?.[target.id]) {
|
||||
target.deps = reactive(depsData[target.type][target.id]);
|
||||
@ -245,10 +200,6 @@ class Dep extends BaseService {
|
||||
this.emit('collected', dsl.items, true);
|
||||
this.emit('ds-collected', dsl.items, true);
|
||||
waitingResolve();
|
||||
if (this.waitingWorker === promise) {
|
||||
this.waitingWorker = undefined;
|
||||
this.resolveWaitingWorker = undefined;
|
||||
}
|
||||
|
||||
return depsData;
|
||||
});
|
||||
@ -258,12 +209,8 @@ class Dep extends BaseService {
|
||||
// 先删除原有依赖,重新收集
|
||||
if (isPage(node)) {
|
||||
this.removePageDep(target, depExtendedData);
|
||||
} else if (deep) {
|
||||
// deep 收集会覆盖整棵子树,删除也要同步清掉子孙旧依赖
|
||||
this.watcher.removeTargetDep(target, node);
|
||||
} else {
|
||||
// 非 deep 只动当前节点,避免误清子节点上其他尚未重收的依赖
|
||||
target.removeDep(node.id);
|
||||
this.watcher.removeTargetDep(target, node);
|
||||
}
|
||||
|
||||
this.watcher.collectItem(node, target, depExtendedData, deep);
|
||||
@ -286,10 +233,7 @@ class Dep extends BaseService {
|
||||
}
|
||||
|
||||
public clearIdleTasks() {
|
||||
this.abortActiveBatches();
|
||||
this.idleTask.clearTasks();
|
||||
// 丢掉 worker 在途任务,避免 abort 后的长任务继续占着常驻 worker
|
||||
this.collectWorker.abort();
|
||||
}
|
||||
|
||||
public on<Name extends keyof DepEvents, Param extends DepEvents[Name]>(
|
||||
@ -307,13 +251,7 @@ class Dep extends BaseService {
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.abortActiveBatches();
|
||||
this.workerGeneration += 1;
|
||||
this.resolveWaitingWorker?.();
|
||||
this.resolveWaitingWorker = undefined;
|
||||
this.waitingWorker = undefined;
|
||||
this.idleTask.clearTasks();
|
||||
this.collectWorker.abort();
|
||||
|
||||
for (const type of Object.keys(this.watcher.getTargetsList())) {
|
||||
this.removeTargets(type);
|
||||
@ -329,7 +267,6 @@ class Dep extends BaseService {
|
||||
this.reset();
|
||||
this.removeAllPlugins();
|
||||
this.idleTask.removeAllListeners();
|
||||
this.collectWorker.terminate();
|
||||
}
|
||||
|
||||
public emit<Name extends keyof DepEvents, Param extends DepEvents[Name]>(eventName: Name, ...args: Param) {
|
||||
@ -349,207 +286,25 @@ class Dep extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 把遍历匹配交给 worker,结果回来后按 target 分片写回主线程
|
||||
*/
|
||||
private collectByCollectWorker(
|
||||
nodes: MNode[],
|
||||
targets: Target[],
|
||||
depExtendedData: DepExtendedData,
|
||||
deep: boolean,
|
||||
batch: CollectBatch,
|
||||
) {
|
||||
const hasDataSource = targets.some((target) => target.type === DepTargetType.DATA_SOURCE);
|
||||
|
||||
// 先占位,避免 worker 返回前批次被误判为已完成(提前 resolve、collecting 提前复位)
|
||||
this.beginBatchTask(batch, hasDataSource);
|
||||
|
||||
this.collectWorker
|
||||
.collect({
|
||||
nodes,
|
||||
targets: targets.map((target) => target.descriptor!),
|
||||
depExtendedData,
|
||||
deep,
|
||||
})
|
||||
.then((result) => {
|
||||
if (batch.aborted) return;
|
||||
|
||||
for (const target of targets) {
|
||||
if (result) {
|
||||
// 写回是纯字典操作,但数据量大时同样会占用主线程,因此按 target 拆成空闲任务
|
||||
this.enqueueApplyTask(target, result.deps[target.type]?.[target.id] || {}, result.nodeIds, batch, {
|
||||
nodes,
|
||||
depExtendedData,
|
||||
deep,
|
||||
});
|
||||
} else {
|
||||
this.enqueueTasks(nodes, target, depExtendedData, deep, batch);
|
||||
}
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
// 释放占位:必须在新任务入队之后,否则批次会在中途被判定为完成
|
||||
this.onBatchTaskDone(batch, hasDataSource);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 用 worker 的收集结果替换 target 上这批节点的依赖
|
||||
*/
|
||||
private enqueueApplyTask(
|
||||
target: Target,
|
||||
deps: DepData,
|
||||
nodeIds: Id[],
|
||||
batch: CollectBatch,
|
||||
{ nodes, depExtendedData, deep }: { nodes: MNode[]; depExtendedData: DepExtendedData; deep: boolean },
|
||||
) {
|
||||
const isDataSource = target.type === DepTargetType.DATA_SOURCE;
|
||||
|
||||
this.beginBatchTask(batch, isDataSource);
|
||||
|
||||
private enqueueTask(node: MNode, target: Target, depExtendedData: DepExtendedData, deep: boolean) {
|
||||
this.idleTask.enqueueTask(
|
||||
() => {
|
||||
try {
|
||||
// target 可能在 worker 收集期间被移除或重建(如修改数据源字段)
|
||||
const current = this.watcher.getTarget(target.id, target.type);
|
||||
if (current !== target) {
|
||||
// 已被同 id 的新 target 替换时,用当前 target 在主线程补收,避免依赖空窗
|
||||
if (current) {
|
||||
this.enqueueTasks(nodes, current, depExtendedData, deep, batch);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 页面按 pageId 匹配删除,可清掉页面内已被删除节点的残留依赖
|
||||
if (nodes.some((node) => isPage(node))) {
|
||||
this.removePageDep(target, depExtendedData);
|
||||
}
|
||||
|
||||
for (const id of nodeIds) {
|
||||
target.removeDep(id);
|
||||
}
|
||||
|
||||
for (const [id, dep] of Object.entries(deps)) {
|
||||
target.deps[id] = dep;
|
||||
}
|
||||
} finally {
|
||||
this.onBatchTaskDone(batch, isDataSource);
|
||||
}
|
||||
({ node, deep, target }) => {
|
||||
this.collectNode(node, target, depExtendedData, deep);
|
||||
},
|
||||
undefined,
|
||||
isDataSource,
|
||||
);
|
||||
}
|
||||
|
||||
private enqueueTasks(
|
||||
nodes: MNode[],
|
||||
target: Target,
|
||||
depExtendedData: DepExtendedData,
|
||||
deep: boolean,
|
||||
batch: CollectBatch,
|
||||
) {
|
||||
for (const node of nodes) {
|
||||
this.enqueueTask(node, target, depExtendedData, deep, batch);
|
||||
}
|
||||
}
|
||||
|
||||
private enqueueTask(
|
||||
node: MNode,
|
||||
target: Target,
|
||||
depExtendedData: DepExtendedData,
|
||||
deep: boolean,
|
||||
batch: CollectBatch,
|
||||
) {
|
||||
const isDataSource = target.type === DepTargetType.DATA_SOURCE;
|
||||
|
||||
this.beginBatchTask(batch, isDataSource);
|
||||
|
||||
this.idleTask.enqueueTask(
|
||||
() => {
|
||||
try {
|
||||
this.collectNode(node, target, depExtendedData);
|
||||
} finally {
|
||||
this.onBatchTaskDone(batch, isDataSource);
|
||||
}
|
||||
{
|
||||
node,
|
||||
deep: false,
|
||||
target,
|
||||
},
|
||||
undefined,
|
||||
isDataSource,
|
||||
target.type === DepTargetType.DATA_SOURCE,
|
||||
);
|
||||
|
||||
if (deep && Array.isArray(node.items) && node.items.length) {
|
||||
node.items.forEach((item) => {
|
||||
this.enqueueTask(item, target, depExtendedData, deep, batch);
|
||||
this.enqueueTask(item, target, depExtendedData, deep);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private beginBatchTask(batch: CollectBatch, isDataSource: boolean) {
|
||||
batch.pending += 1;
|
||||
if (isDataSource) {
|
||||
batch.dsPending += 1;
|
||||
}
|
||||
}
|
||||
|
||||
private onBatchTaskDone(batch: CollectBatch, isDataSource: boolean) {
|
||||
if (batch.aborted) return;
|
||||
|
||||
if (isDataSource) {
|
||||
batch.dsPending -= 1;
|
||||
// 数据源依赖收集完先 resolve,让 stage 尽快更新,其余依赖继续在后台收集
|
||||
if (batch.dsPending === 0) {
|
||||
this.settleBatchDs(batch);
|
||||
}
|
||||
}
|
||||
|
||||
batch.pending -= 1;
|
||||
if (batch.pending === 0) {
|
||||
this.finishBatch(batch);
|
||||
}
|
||||
}
|
||||
|
||||
private settleBatchDs(batch: CollectBatch) {
|
||||
if (batch.dsSettled) return;
|
||||
batch.dsSettled = true;
|
||||
this.emit('ds-collected', batch.nodes, batch.deep);
|
||||
batch.resolve(true);
|
||||
}
|
||||
|
||||
private finishBatch(batch: CollectBatch) {
|
||||
if (!batch.collectedEmitted) {
|
||||
batch.collectedEmitted = true;
|
||||
this.emit('collected', batch.nodes, batch.deep);
|
||||
}
|
||||
// 没有数据源任务的批次在此结算 Promise
|
||||
this.settleBatchDs(batch);
|
||||
this.activeBatches.delete(batch);
|
||||
this.updateCollectingState();
|
||||
}
|
||||
|
||||
/**
|
||||
* idleTask 被清空时,在途批次的任务不会再执行,必须主动结算,
|
||||
* 否则对应的 collectIdle Promise 永远不会 resolve(collecting 卡在 true、once 监听器泄漏)。
|
||||
* 收集被中断(通常紧跟一次全量重新收集),因此不再 emit collected/ds-collected,仅结算 Promise。
|
||||
*/
|
||||
private abortActiveBatches() {
|
||||
if (!this.activeBatches.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
const batches = [...this.activeBatches];
|
||||
this.activeBatches.clear();
|
||||
|
||||
for (const batch of batches) {
|
||||
// 标记中断:worker 结果可能在之后才返回,此时不能再写回依赖,也不能再结算批次
|
||||
batch.aborted = true;
|
||||
batch.resolve(false);
|
||||
}
|
||||
|
||||
this.updateCollectingState();
|
||||
}
|
||||
|
||||
private updateCollectingState() {
|
||||
this.set('collecting', this.activeBatches.size > 0);
|
||||
}
|
||||
}
|
||||
|
||||
export type DepService = Dep;
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { nextTick, reactive, toRaw } from 'vue';
|
||||
import { reactive, toRaw } from 'vue';
|
||||
import { cloneDeep, isEmpty, isEqual, isObject, mergeWith, uniq } from 'lodash-es';
|
||||
|
||||
import type { Id, MApp, MContainer, MNode, MPage, MPageFragment, TargetOptions } from '@tmagic/core';
|
||||
@ -30,7 +30,6 @@ import {
|
||||
guid,
|
||||
isPage,
|
||||
isPageFragment,
|
||||
isPageOrFragment,
|
||||
setValueByKeyPath,
|
||||
traverseNode,
|
||||
} from '@tmagic/utils';
|
||||
@ -273,7 +272,7 @@ class Editor extends BaseService {
|
||||
public isOnDifferentPage(node: MNode): boolean {
|
||||
const currentPageId = this.get('page')?.id;
|
||||
if (currentPageId === undefined || currentPageId === null) return false;
|
||||
if (isPageOrFragment(node)) {
|
||||
if (isPage(node) || isPageFragment(node)) {
|
||||
return `${node.id}` !== `${currentPageId}`;
|
||||
}
|
||||
const nodePage = this.getNodeInfo(node.id, false).page;
|
||||
@ -414,11 +413,11 @@ class Editor extends BaseService {
|
||||
|
||||
if (!curNode) throw new Error('当前选中节点为空');
|
||||
|
||||
if ((parent.type === NodeType.ROOT || curNode?.type === NodeType.ROOT) && !isPageOrFragment(node)) {
|
||||
if ((parent.type === NodeType.ROOT || curNode?.type === NodeType.ROOT) && !(isPage(node) || isPageFragment(node))) {
|
||||
throw new Error('app下不能添加组件');
|
||||
}
|
||||
|
||||
if (parent.id !== curNode.id && !isPageOrFragment(node)) {
|
||||
if (parent.id !== curNode.id && !(isPage(node) || isPageFragment(node))) {
|
||||
const index = parent.items.indexOf(curNode);
|
||||
parent.items?.splice(index + 1, 0, node);
|
||||
} else {
|
||||
@ -488,7 +487,7 @@ class Editor extends BaseService {
|
||||
const newNodes = await Promise.all(
|
||||
addNodes.map((node) => {
|
||||
const root = this.get('root');
|
||||
if (isPageOrFragment(node) && root) {
|
||||
if ((isPage(node) || isPageFragment(node)) && root) {
|
||||
return this.doAdd(node, root);
|
||||
}
|
||||
const parentNode = parent ?? getAddParent(node);
|
||||
@ -524,7 +523,7 @@ class Editor extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
if (!isPageOrFragment(newNodes[0])) {
|
||||
if (!(isPage(newNodes[0]) || isPageFragment(newNodes[0]))) {
|
||||
const pageForOp = this.getNodeInfo(newNodes[0].id, false).page;
|
||||
if (!doNotPushHistory) {
|
||||
const parentId = (this.getParentById(newNodes[0].id, false) ?? this.get('root'))!.id;
|
||||
@ -551,16 +550,8 @@ class Editor extends BaseService {
|
||||
this.emit('change', {
|
||||
type: 'add',
|
||||
data: newNodes.map((node) => ({ node, page: this.getPageOfNode(node.id) })),
|
||||
historySource,
|
||||
doNotPushHistory,
|
||||
});
|
||||
|
||||
// 页面 / 页面片新增不入历史栈(见上方 isPageOrFragment 分支),这里合并补发一次结构变更通知
|
||||
const addedPages = newNodes.filter((node) => isPageOrFragment(node)) as (MPage | MPageFragment)[];
|
||||
if (addedPages.length) {
|
||||
historyService.notifyPageStructureChange({ add: addedPages, remove: [] });
|
||||
}
|
||||
|
||||
return Array.isArray(addNode) ? newNodes : newNodes[0];
|
||||
}
|
||||
|
||||
@ -579,14 +570,12 @@ class Editor extends BaseService {
|
||||
|
||||
if (typeof index !== 'number' || index === -1) throw new Error('找不要删除的节点');
|
||||
|
||||
const stage = this.get('stage');
|
||||
const currentPage = this.get('page');
|
||||
const isDeletingCurrentPage = isPageOrFragment(node) && !!currentPage && `${currentPage.id}` === `${node.id}`;
|
||||
|
||||
parent.items?.splice(index, 1);
|
||||
const stage = this.get('stage');
|
||||
stage?.remove({ id: node.id, parentId: parent.id, root: cloneDeep(root) });
|
||||
|
||||
// 始终清理已删除节点在 state 中的残留引用:
|
||||
// - 即使后续会调用 select 覆盖,跳过这些调用(doNotSelect / doNotSwitchPage)时也不能让 state 持有已删除节点
|
||||
// - 即使后续会调用 selectDefault / select(parent) 覆盖,跳过这些调用(doNotSelect / doNotSwitchPage)时也不能让 state 持有已删除节点
|
||||
const selectedNodes = this.get('nodes');
|
||||
const removedSelectedIndex = selectedNodes.findIndex((n: MNode) => `${n.id}` === `${node.id}`);
|
||||
if (removedSelectedIndex !== -1) {
|
||||
@ -594,44 +583,38 @@ class Editor extends BaseService {
|
||||
nextSelected.splice(removedSelectedIndex, 1);
|
||||
this.set('nodes', nextSelected);
|
||||
}
|
||||
|
||||
const removeData = { id: node.id, parentId: parent.id, root: cloneDeep(root) };
|
||||
const rootItems = root.items || [];
|
||||
// 删非当前页时画布不应该变动,所以只有删的是当前页才重新选中
|
||||
const shouldReselect = isDeletingCurrentPage && !doNotSelect && !doNotSwitchPage;
|
||||
// 还有剩余页面才能切过去,否则退回选中 root
|
||||
const shouldSwitchPage = shouldReselect && rootItems.length > 0;
|
||||
|
||||
if (isPageOrFragment(node)) {
|
||||
if (isPage(node)) {
|
||||
this.state.pageLength -= 1;
|
||||
} else {
|
||||
this.state.pageFragmentLength -= 1;
|
||||
if (isPage(node) || isPageFragment(node)) {
|
||||
const currentPage = this.get('page');
|
||||
if (currentPage && `${currentPage.id}` === `${node.id}`) {
|
||||
this.set('page', null);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldSwitchPage) {
|
||||
// runtime 收到页面删除时会销毁当前渲染的 page 实例,必须先把画布切到剩余页面再通知删除,
|
||||
// 否则画布会被清空,且要等下一次 updatePageId 才能恢复。
|
||||
// nextTick 用于等 Stage 的 page watch 把新页面 id 同步给 runtime。
|
||||
await this.select(rootItems[0]);
|
||||
stage?.select(rootItems[0].id);
|
||||
await nextTick();
|
||||
stage?.remove(removeData);
|
||||
const selectDefault = async (pages: MNode[]) => {
|
||||
if (pages[0]) {
|
||||
await this.select(pages[0]);
|
||||
stage?.select(pages[0].id);
|
||||
} else {
|
||||
// page 置空会让 Workspace 卸载 Stage 并销毁 renderer,删除通知必须在卸载前发出,
|
||||
// 且不能 await:runtime 未 ready 时 getRuntime 的监听会随 destroy 一起被移除,永远不会 resolve
|
||||
stage?.remove(removeData);
|
||||
this.selectRoot();
|
||||
}
|
||||
};
|
||||
|
||||
if (shouldReselect) {
|
||||
// 页面已全部删完
|
||||
this.selectRoot();
|
||||
} else if (isDeletingCurrentPage) {
|
||||
this.set('page', null);
|
||||
}
|
||||
const rootItems = root.items || [];
|
||||
|
||||
if (isPage(node)) {
|
||||
this.state.pageLength -= 1;
|
||||
|
||||
// 删除页面后默认会切到首个剩余页面(selectDefault),doNotSwitchPage 时跳过这次自动切换
|
||||
if (!doNotSelect && !doNotSwitchPage) {
|
||||
await selectDefault(rootItems);
|
||||
}
|
||||
} else if (isPageFragment(node)) {
|
||||
this.state.pageFragmentLength -= 1;
|
||||
|
||||
if (!doNotSelect && !doNotSwitchPage) {
|
||||
await selectDefault(rootItems);
|
||||
}
|
||||
} else {
|
||||
stage?.remove(removeData);
|
||||
|
||||
if (!doNotSelect) {
|
||||
await this.select(parent);
|
||||
stage?.select(parent.id);
|
||||
@ -673,7 +656,7 @@ class Editor extends BaseService {
|
||||
|
||||
const removedItems: StepDiffItem<MNode>[] = [];
|
||||
let pageForOp: { name: string; id: Id } | null = null;
|
||||
if (!isPageOrFragment(nodes[0])) {
|
||||
if (!(isPage(nodes[0]) || isPageFragment(nodes[0]))) {
|
||||
for (const n of nodes) {
|
||||
const { parent, node: curNode, page } = this.getNodeInfo(n.id, false);
|
||||
if (parent && curNode) {
|
||||
@ -709,13 +692,7 @@ class Editor extends BaseService {
|
||||
}
|
||||
|
||||
this.emit('remove', nodes);
|
||||
this.emit('change', { type: 'remove', data: changeItems, historySource, doNotPushHistory });
|
||||
|
||||
// 页面 / 页面片删除不入历史栈(见上方 isPageOrFragment 分支),这里合并补发一次结构变更通知
|
||||
const removedPages = nodes.filter((node) => isPageOrFragment(node)) as (MPage | MPageFragment)[];
|
||||
if (removedPages.length) {
|
||||
historyService.notifyPageStructureChange({ add: [], remove: removedPages });
|
||||
}
|
||||
this.emit('change', { type: 'remove', data: changeItems });
|
||||
}
|
||||
|
||||
public async doUpdate(
|
||||
@ -773,7 +750,7 @@ class Editor extends BaseService {
|
||||
}
|
||||
|
||||
// 只有被更新节点正好是当前选中页面时才同步 state.page,避免「更新非当前页」误将编辑器切到该页
|
||||
if (isPageOrFragment(newConfig)) {
|
||||
if (isPage(newConfig) || isPageFragment(newConfig)) {
|
||||
const currentPage = this.get('page');
|
||||
if (currentPage && `${currentPage.id}` === `${newConfig.id}`) {
|
||||
this.set('page', newConfig as MPage | MPageFragment);
|
||||
@ -869,8 +846,6 @@ class Editor extends BaseService {
|
||||
this.emit('change', {
|
||||
type: 'update',
|
||||
data: updateData.map((node) => ({ node, page: this.getPageOfNode(node.newNode.id) })),
|
||||
historySource,
|
||||
doNotPushHistory,
|
||||
});
|
||||
return Array.isArray(config) ? updateData.map((item) => item.newNode) : updateData[0].newNode;
|
||||
}
|
||||
@ -1117,8 +1092,6 @@ class Editor extends BaseService {
|
||||
type: 'move-layer',
|
||||
data: [{ node, page: this.getPageOfNode(node.id) }],
|
||||
offset,
|
||||
historySource,
|
||||
doNotPushHistory,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1148,7 +1121,7 @@ class Editor extends BaseService {
|
||||
}: DslOpOptions = {},
|
||||
): Promise<MNode | MNode[]> {
|
||||
const isBatch = Array.isArray(config);
|
||||
const configs = (isBatch ? config : [config]).filter((item) => !isPageOrFragment(item));
|
||||
const configs = (isBatch ? config : [config]).filter((item) => !(isPage(item) || isPageFragment(item)));
|
||||
|
||||
if (configs.length === 0) {
|
||||
throw new Error('没有可移动的节点');
|
||||
@ -1304,8 +1277,6 @@ class Editor extends BaseService {
|
||||
data: configs.map((node) => ({ node, page: this.getPageOfNode(node.id) })),
|
||||
targetIndex,
|
||||
targetParent,
|
||||
historySource,
|
||||
doNotPushHistory,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1769,7 +1740,7 @@ class Editor extends BaseService {
|
||||
private getPageOfNode(id: Id): MPage | MPageFragment | null {
|
||||
const { node, page } = this.getNodeInfo(id, false);
|
||||
if (page) return page;
|
||||
if (node && isPageOrFragment(node)) return node as MPage | MPageFragment;
|
||||
if (node && (isPage(node) || isPageFragment(node))) return node as MPage | MPageFragment;
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -1797,10 +1768,6 @@ class Editor extends BaseService {
|
||||
const nextMap = new Map(nextPages.map((p) => [`${p.id}`, p]));
|
||||
const indexInItems = (root: MApp, id: Id) => (root.items ?? []).findIndex((item) => `${item.id}` === `${id}`);
|
||||
|
||||
// 收集本次整体替换中增删的页面,循环结束后合并为一次结构变更通知(避免逐页派发多个事件)
|
||||
const addedPages: (MPage | MPageFragment)[] = [];
|
||||
const removedPages: (MPage | MPageFragment)[] = [];
|
||||
|
||||
nextPages.forEach((nextPage) => {
|
||||
const prevPage = prevMap.get(`${nextPage.id}`);
|
||||
if (!prevPage) {
|
||||
@ -1810,7 +1777,6 @@ class Editor extends BaseService {
|
||||
{ newSchema: cloneDeep(toRaw(nextPage)), parentId: nextRoot.id, index: indexInItems(nextRoot, nextPage.id) },
|
||||
source,
|
||||
);
|
||||
addedPages.push(nextPage);
|
||||
} else if (!isEqual(toRaw(prevPage), toRaw(nextPage))) {
|
||||
this.pushPageDiffStep(
|
||||
'update',
|
||||
@ -1829,13 +1795,8 @@ class Editor extends BaseService {
|
||||
{ oldSchema: cloneDeep(toRaw(prevPage)), parentId: preRoot.id, index: indexInItems(preRoot, prevPage.id) },
|
||||
source,
|
||||
);
|
||||
removedPages.push(prevPage);
|
||||
}
|
||||
});
|
||||
|
||||
if (addedPages.length || removedPages.length) {
|
||||
historyService.notifyPageStructureChange({ add: addedPages, remove: removedPages });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1917,12 +1878,10 @@ class Editor extends BaseService {
|
||||
/**
|
||||
* 应用历史操作(撤销 / 重做)
|
||||
*
|
||||
* 删除 / 更新类修改走 `editor.remove / update`,并通过 `doNotPushHistory` 阻止再次入栈、
|
||||
* `doNotSelect / doNotSwitchPage` 让选区由方法末尾的统一逻辑兜底;
|
||||
* 「重新插回节点」(撤销 remove / 重做 add)需按 step 记录的 parentId / index 精确还原,
|
||||
* 不走 this.add,由手工 splice + stage.add 完成后补发等价的 add / change 事件(见 emitHistoryInsertEvents)。
|
||||
* 所有 DSL 修改都走 `editor.add / remove / update`,并通过 `doNotPushHistory` 阻止再次入栈、
|
||||
* `doNotSelect / doNotSwitchPage` 让选区由方法末尾的统一逻辑兜底。
|
||||
*
|
||||
* 注意:这些路径都会发出 add / remove / update / change 事件,业务侧若需要区分"用户操作"与"撤销重做触发",
|
||||
* 注意:这些公开方法会发出 add / remove / update 事件,业务侧若需要区分"用户操作"与"撤销重做触发",
|
||||
* 请监听 `history-change` 事件配合判断。
|
||||
*
|
||||
* @param step 操作记录
|
||||
@ -1935,14 +1894,7 @@ class Editor extends BaseService {
|
||||
const stage = this.get('stage');
|
||||
if (!root) return;
|
||||
|
||||
// 撤销/重做内部复用 add/remove/update,透传被应用 step 上记录的 source,
|
||||
// 使其触发的 change 事件同样携带「历史来源」
|
||||
const commonOpts = {
|
||||
doNotSelect: true,
|
||||
doNotSwitchPage: true,
|
||||
doNotPushHistory: true,
|
||||
historySource: step.source,
|
||||
} as const;
|
||||
const commonOpts = { doNotSelect: true, doNotSwitchPage: true, doNotPushHistory: true } as const;
|
||||
|
||||
switch (step.opType) {
|
||||
case 'add': {
|
||||
@ -1958,22 +1910,17 @@ class Editor extends BaseService {
|
||||
}
|
||||
} else {
|
||||
// 重做 add:按记录的 parentId / index 把节点重新插回父容器。
|
||||
// 按目标 index 升序逐个插入,先小后大避免索引漂移。
|
||||
// 不走 this.add:doAdd 依赖当前选中节点(无选区时直接抛错),且只能插到选中节点之后 / 末尾,
|
||||
// 无法按 step 记录的 parentId / index 精确还原,还会重算 style 破坏已记录的终态位置。
|
||||
// 按目标 index 升序逐个插入,先小后大避免索引漂移
|
||||
const sorted = [...items].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
||||
const addedNodes: MNode[] = [];
|
||||
for (const { newSchema, parentId, index } of sorted) {
|
||||
if (!newSchema || parentId === undefined) continue;
|
||||
const parent = this.getNodeById(parentId, false) as MContainer | null;
|
||||
if (parent?.items) {
|
||||
const addedNode = cloneDeep(newSchema);
|
||||
if (typeof index === 'number' && index >= 0 && index < parent.items.length) {
|
||||
parent.items.splice(index, 0, addedNode);
|
||||
parent.items.splice(index, 0, cloneDeep(newSchema));
|
||||
} else {
|
||||
parent.items.push(addedNode);
|
||||
parent.items.push(cloneDeep(newSchema));
|
||||
}
|
||||
addedNodes.push(addedNode);
|
||||
await stage?.add({
|
||||
config: cloneDeep(newSchema),
|
||||
parent: cloneDeep(parent),
|
||||
@ -1982,23 +1929,19 @@ class Editor extends BaseService {
|
||||
});
|
||||
}
|
||||
}
|
||||
this.emitHistoryInsertEvents(addedNodes, step.source);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'remove': {
|
||||
const items = step.diff ?? [];
|
||||
if (reverse) {
|
||||
// 撤销 remove:按原 index 升序逐个插回(先小后大避免索引漂移);不走 this.add 的原因同上
|
||||
// 撤销 remove:按原 index 升序逐个插回(先小后大避免索引漂移)
|
||||
const sorted = [...items].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
||||
const addedNodes: MNode[] = [];
|
||||
for (const { oldSchema, parentId, index } of sorted) {
|
||||
if (!oldSchema || parentId === undefined) continue;
|
||||
const parent = this.getNodeById(parentId, false) as MContainer | null;
|
||||
if (parent?.items) {
|
||||
const addedNode = cloneDeep(oldSchema);
|
||||
parent.items.splice(index ?? parent.items.length, 0, addedNode);
|
||||
addedNodes.push(addedNode);
|
||||
parent.items.splice(index ?? parent.items.length, 0, cloneDeep(oldSchema));
|
||||
await stage?.add({
|
||||
config: cloneDeep(oldSchema),
|
||||
parent: cloneDeep(parent),
|
||||
@ -2007,7 +1950,6 @@ class Editor extends BaseService {
|
||||
});
|
||||
}
|
||||
}
|
||||
this.emitHistoryInsertEvents(addedNodes, step.source);
|
||||
} else {
|
||||
// 重做 remove:再删一次
|
||||
for (const { oldSchema } of items) {
|
||||
@ -2047,7 +1989,7 @@ class Editor extends BaseService {
|
||||
return cloneDeep(reverse ? oldNode : newNode);
|
||||
});
|
||||
if (configs.length) {
|
||||
await this.update(configs, { doNotPushHistory: true, historySource: step.source });
|
||||
await this.update(configs, { doNotPushHistory: true });
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -2086,22 +2028,6 @@ class Editor extends BaseService {
|
||||
private selectedConfigExceptionHandler(config: MNode | Id): EditorNodeInfo {
|
||||
return resolveSelectedNode(config, (id) => this.getNodeInfo(id), this.state.root?.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销 remove / 重做 add 通过手工 splice 按 step 记录的 parentId / index 精确还原节点(不走 this.add,原因见调用处注释),
|
||||
* 这里补齐与 this.add 等价的 add / change 事件,保证撤销/重做路径的事件行为与正向操作一致。
|
||||
*/
|
||||
private emitHistoryInsertEvents(nodes: MNode[], source?: HistoryOpSource) {
|
||||
if (!nodes.length) return;
|
||||
this.emit('add', nodes);
|
||||
this.emit('change', {
|
||||
type: 'add',
|
||||
data: nodes.map((node) => ({ node, page: this.getPageOfNode(node.id) })),
|
||||
historySource: source,
|
||||
// 撤销/重做本身不再写入历史记录
|
||||
doNotPushHistory: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type EditorService = Editor;
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
import { reactive } from 'vue';
|
||||
import type { Writable } from 'type-fest';
|
||||
|
||||
import type { Id, MPage, MPageFragment } from '@tmagic/core';
|
||||
import type { Id } from '@tmagic/core';
|
||||
import { guid } from '@tmagic/utils';
|
||||
|
||||
import type {
|
||||
@ -353,27 +353,6 @@ class History extends BaseService {
|
||||
this.emit('mark-saved', { kind: stepType, id });
|
||||
}
|
||||
|
||||
/**
|
||||
* 派发「页面 / 页面片结构变更」事件(`page-structure-change`)。
|
||||
*
|
||||
* 常规 `editorService.add` / `remove` 页面节点不会写入 `page` 历史栈(见 editor.add / remove 中
|
||||
* 对 isPageOrFragment 的分支),因此不会产生任何 historyService 事件。该方法用于在这些
|
||||
* 场景(以及 setRoot 整体替换 DSL 增删页面)下,向外统一通知页面结构的增删变化,供业务方感知。
|
||||
*
|
||||
* 一次操作涉及多个页面时,调用方应把本次增删的页面**合并为一个 change 一次性传入**,
|
||||
* 使一次操作只派发一个事件;`add` / `remove` 分别为本次新增与删除的页面列表(其一可为空数组)。
|
||||
*
|
||||
* @param change 本次结构变更:{ add: 新增的页面列表, remove: 删除的页面列表 }
|
||||
*/
|
||||
public notifyPageStructureChange(change: {
|
||||
add: (MPage | MPageFragment)[];
|
||||
remove: (MPage | MPageFragment)[];
|
||||
}): void {
|
||||
// 增删均为空时不派发,避免无意义通知
|
||||
if (!change.add.length && !change.remove.length) return;
|
||||
this.emit('page-structure-change', change);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把当前内存中的全部历史栈(页面 / 代码块 / 数据源 / 扩展类型)序列化后写入本地 IndexedDB。
|
||||
*
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import KeyController, { KeyControllerEvent } from 'keycon';
|
||||
|
||||
import { isPageOrFragment } from '@tmagic/utils';
|
||||
import { isPage, isPageFragment } from '@tmagic/utils';
|
||||
|
||||
import { KeyBindingCacheItem, KeyBindingCommand, KeyBindingItem } from '@editor/type';
|
||||
|
||||
@ -19,7 +19,7 @@ class Keybinding extends BaseService {
|
||||
[KeyBindingCommand.DELETE_NODE]: () => {
|
||||
const nodes = editorService.get('nodes');
|
||||
|
||||
if (!nodes || isPageOrFragment(nodes[0])) return;
|
||||
if (!nodes || isPage(nodes[0]) || isPageFragment(nodes[0])) return;
|
||||
editorService.remove(nodes, { historySource: 'shortcut' });
|
||||
},
|
||||
[KeyBindingCommand.COPY_NODE]: () => {
|
||||
@ -29,7 +29,7 @@ class Keybinding extends BaseService {
|
||||
[KeyBindingCommand.CUT_NODE]: () => {
|
||||
const nodes = editorService.get('nodes');
|
||||
|
||||
if (!nodes || isPageOrFragment(nodes[0])) return;
|
||||
if (!nodes || isPage(nodes[0]) || isPageFragment(nodes[0])) return;
|
||||
editorService.copy(nodes);
|
||||
editorService.remove(nodes, { historySource: 'shortcut' });
|
||||
},
|
||||
|
||||
@ -49,14 +49,13 @@ const state = shallowReactive<UiState>({
|
||||
width: 375,
|
||||
height: 817,
|
||||
},
|
||||
// 先给 columnWidth 留占位,真正的 clamp 逻辑在 state 创建完成后
|
||||
// 用 state.minLeftColumnWidth / state.minRightColumnWidth 作为下限重新赋值 —
|
||||
// 这样以后从外部(比如 uiService.set('minLeftColumnWidth', ...))动态改最小宽度,
|
||||
// 只改一处 state 字段即可,不用同步 MIN_* 常量。
|
||||
columnWidth: {
|
||||
left: 0,
|
||||
left:
|
||||
storageService.getItem(LEFT_COLUMN_WIDTH_STORAGE_KEY, { protocol: Protocol.NUMBER }) || DEFAULT_LEFT_COLUMN_WIDTH,
|
||||
center: 0,
|
||||
right: 0,
|
||||
right:
|
||||
storageService.getItem(RIGHT_COLUMN_WIDTH_STORAGE_KEY, { protocol: Protocol.NUMBER }) ||
|
||||
DEFAULT_RIGHT_COLUMN_WIDTH,
|
||||
},
|
||||
minLeftColumnWidth: MIN_LEFT_COLUMN_WIDTH,
|
||||
minCenterColumnWidth: MIN_CENTER_COLUMN_WIDTH,
|
||||
@ -84,20 +83,6 @@ const state = shallowReactive<UiState>({
|
||||
},
|
||||
});
|
||||
|
||||
// 左右两栏宽度延后初始化:
|
||||
// - 优先取 storage 里用户上次拖到的值;storage 空时回落到 DEFAULT_*;
|
||||
// - 再用 state.min*ColumnWidth 兜底,防止历史 storage 里存的是老版本或用户拖过头
|
||||
// 留下的"比当前 min 还小"的数值。放到 state 之后写是因为初始化时无法在同一个
|
||||
// 对象字面量里读到自己的其它字段。
|
||||
state.columnWidth.left = Math.max(
|
||||
storageService.getItem(LEFT_COLUMN_WIDTH_STORAGE_KEY, { protocol: Protocol.NUMBER }) || DEFAULT_LEFT_COLUMN_WIDTH,
|
||||
state.minLeftColumnWidth,
|
||||
);
|
||||
state.columnWidth.right = Math.max(
|
||||
storageService.getItem(RIGHT_COLUMN_WIDTH_STORAGE_KEY, { protocol: Protocol.NUMBER }) || DEFAULT_RIGHT_COLUMN_WIDTH,
|
||||
state.minRightColumnWidth,
|
||||
);
|
||||
|
||||
const canUsePluginMethods = {
|
||||
async: ['zoom', 'calcZoom'] as const,
|
||||
sync: [] as const,
|
||||
|
||||
@ -1,40 +1,3 @@
|
||||
.m-fields-code-select {
|
||||
width: 100%;
|
||||
> .el-card.tmagic-design-card--flat {
|
||||
background-color: transparent;
|
||||
margin-bottom: 10px;
|
||||
> .el-card__body {
|
||||
padding-left: 0;
|
||||
.code-select-content {
|
||||
> .m-fields-group-list {
|
||||
> .tmagic-design-card--flat.el-card {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
margin-bottom: 10px;
|
||||
border: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-card__body {
|
||||
.tmagic-design-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
.code-select-content {
|
||||
> .m-fields-group-list {
|
||||
> .tmagic-design-card--flat {
|
||||
> .el-card__header {
|
||||
padding: 16px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.create-button {
|
||||
&.fullWidth {
|
||||
width: 100%;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
$theme-color: var(--el-color-primary, #2882e0);
|
||||
$theme-color: #2882e0;
|
||||
|
||||
$font-color: #313a40;
|
||||
$border-color: #d9dbdd;
|
||||
|
||||
@ -77,9 +77,9 @@
|
||||
margin-bottom: 5px;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-color-primary, #2882e0);
|
||||
background: #2882e0;
|
||||
color: #fff;
|
||||
border-color: var(--el-color-primary, #4e8be1);
|
||||
border-color: #4e8be1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,10 +5,6 @@
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&.data-source-field-img-upload,
|
||||
&.data-source-field-big-img-upload {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.m-fields-select-action-button {
|
||||
margin-left: 5px;
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
.m-container-text.display-conds-title {
|
||||
.tmagic-design-form-item {
|
||||
.el-form-item__label {
|
||||
color: #111;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
line-height: 24px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
@ -2,35 +2,8 @@
|
||||
width: 100%;
|
||||
.fullWidth {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
&.create-button {
|
||||
width: 100%;
|
||||
}
|
||||
&.m-container-ui-event {
|
||||
width: calc(100% - 32px);
|
||||
}
|
||||
}
|
||||
.event-select-container {
|
||||
padding: 0 16px;
|
||||
> .el-card.tmagic-design-card--flat {
|
||||
> .el-card__header {
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
> .el-card__body {
|
||||
padding-bottom: 16px;
|
||||
.m-fields-group-list-footer {
|
||||
div {
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
}
|
||||
.el-card.tmagic-design-card--flat {
|
||||
> .el-card__header {
|
||||
padding: 16px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.event-select-code {
|
||||
margin-left: 20px;
|
||||
width: auto;
|
||||
@ -63,44 +36,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.event-select-container {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.event-select-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
.event-select-title {
|
||||
color: #0f1113;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
.event-item-header {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.event-item-title {
|
||||
color: #0f1113;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.event-item-delete-button {
|
||||
color: #0f1113;
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
.el-form-item {
|
||||
.el-form-item {
|
||||
&.is-error {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
&.is-error {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,10 +6,7 @@
|
||||
width: 100%;
|
||||
|
||||
&-content {
|
||||
// 与 nav-menu.scss 的 `--editor-nav-height` 联动:默认 fallback 到 SCSS
|
||||
// `$nav-height`(35px),主题覆盖时两处会同时切成同一个高度,保证 nav 和
|
||||
// 主内容区总高度 = 100%。
|
||||
height: calc(100% - var(--editor-nav-height, #{$nav-height}));
|
||||
height: calc(100% - #{$nav-height});
|
||||
}
|
||||
|
||||
&-framework-center {
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
color: #909399;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary, #409eff);
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,7 +96,7 @@
|
||||
// 故这里仅作用于「非合并组」的单步条目,避免与卡片样式互相干扰。
|
||||
&.is-current:not(.m-editor-history-list-group.is-merged) {
|
||||
background-color: rgba(64, 158, 255, 0.1);
|
||||
box-shadow: inset 2px 0 0 var(--el-color-primary, #409eff);
|
||||
box-shadow: inset 2px 0 0 #409eff;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(64, 158, 255, 0.16);
|
||||
@ -104,7 +104,7 @@
|
||||
|
||||
.m-editor-history-list-item-desc {
|
||||
font-weight: 600;
|
||||
color: var(--el-color-primary, #409eff);
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -177,11 +177,11 @@
|
||||
&.is-current {
|
||||
background-color: rgba(64, 158, 255, 0.08);
|
||||
border-color: rgba(64, 158, 255, 0.3);
|
||||
border-left-color: var(--el-color-primary, #409eff);
|
||||
border-left-color: #409eff;
|
||||
box-shadow: none; // 覆盖 .is-current 公共的 inset 阴影
|
||||
|
||||
.m-editor-history-list-group-head {
|
||||
color: var(--el-color-primary, #409eff);
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -216,7 +216,7 @@
|
||||
}
|
||||
|
||||
&.is-current {
|
||||
color: var(--el-color-primary, #409eff);
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
background-color: rgba(64, 158, 255, 0.08);
|
||||
border-radius: 3px;
|
||||
@ -231,7 +231,7 @@
|
||||
font-size: 10px;
|
||||
line-height: 16px;
|
||||
color: #fff;
|
||||
background-color: var(--el-color-primary, #409eff);
|
||||
background-color: #409eff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@ -395,7 +395,7 @@
|
||||
border-radius: 2px;
|
||||
font-size: 10px;
|
||||
line-height: 16px;
|
||||
color: var(--el-color-primary, #409eff);
|
||||
color: #409eff;
|
||||
background-color: rgba(64, 158, 255, 0.1);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
@ -471,7 +471,7 @@
|
||||
flex: 1 1 auto;
|
||||
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
font-size: 11px;
|
||||
color: var(--el-color-primary, #409eff);
|
||||
color: #409eff;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@ -13,11 +13,7 @@
|
||||
font-weight: 400;
|
||||
box-sizing: border-box;
|
||||
margin: 0px;
|
||||
// 用 CSS 变量做主题化开关:默认没设 `--editor-nav-height` 时回落到
|
||||
// SCSS 变量 `$nav-height`(common/var.scss,35px);主题(如 magic-admin)
|
||||
// 只需在祖先节点声明 `--editor-nav-height: 40px` 即可运行时覆盖,避免
|
||||
// 走 `@use with` 那条对加载顺序极敏感的 SCSS 模块配置路径。
|
||||
flex: 0 0 var(--editor-nav-height, #{$nav-height});
|
||||
flex: 0 0 $nav-height;
|
||||
border-bottom: 1px solid #d8dee8;
|
||||
|
||||
> div {
|
||||
|
||||
@ -129,8 +129,7 @@
|
||||
> .m-container-tab {
|
||||
> .tmagic-design-tabs {
|
||||
> .el-tabs__content {
|
||||
margin-top: var(--el-tabs-header-height);
|
||||
padding-top: 15px;
|
||||
padding-top: 55px;
|
||||
}
|
||||
> .el-tabs__header.is-top {
|
||||
position: absolute;
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
.border-box-container {
|
||||
display: flex;
|
||||
--unuse-border-style: dashed;
|
||||
.border-icon-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -29,32 +28,24 @@
|
||||
border-color: var(--el-color-success, var(--td-success-color, #2ba471));
|
||||
}
|
||||
&.active {
|
||||
border-color: var(--el-color-primary);
|
||||
border-width: 1px;
|
||||
// border-color: var(--el-color-primary, var(--td-brand-color, #0052d9));
|
||||
}
|
||||
&.center {
|
||||
border-width: 1px;
|
||||
border-color: var(--el-color-primary, var(--td-brand-color, #0052d9));
|
||||
}
|
||||
&.border-icon-top {
|
||||
border-top-width: 2px;
|
||||
border-style: solid var(--unuse-border-style) var(--unuse-border-style)
|
||||
var(--unuse-border-style);
|
||||
border-style: solid dashed dashed dashed;
|
||||
}
|
||||
&.border-icon-right {
|
||||
border-right-width: 2px;
|
||||
border-style: var(--unuse-border-style) solid var(--unuse-border-style)
|
||||
var(--unuse-border-style);
|
||||
border-style: dashed solid dashed dashed;
|
||||
}
|
||||
&.border-icon-bottom {
|
||||
border-bottom-width: 2px;
|
||||
border-style: var(--unuse-border-style) var(--unuse-border-style) solid
|
||||
var(--unuse-border-style);
|
||||
border-style: dashed dashed solid dashed;
|
||||
}
|
||||
&.border-icon-left {
|
||||
border-left-width: 2px;
|
||||
border-style: var(--unuse-border-style) var(--unuse-border-style)
|
||||
var(--unuse-border-style) solid;
|
||||
border-style: dashed dashed dashed solid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,7 +27,6 @@
|
||||
@use "./page-fragment-select.scss";
|
||||
@use "./data-source-field.scss";
|
||||
@use "./data-source-field-select.scss";
|
||||
@use "./display-conds.scss";
|
||||
@use "./scroll-bar.scss";
|
||||
@use "./ui-select.scss";
|
||||
@use "./layer-node-content.scss";
|
||||
|
||||
@ -1,214 +0,0 @@
|
||||
// magic-admin 主题:所有规则均以 `.m-editor.m-editor--magic-admin` 为前缀,
|
||||
// 仅在 `<MEditor theme="magic-admin" />` 时生效,避免污染默认样式。
|
||||
|
||||
@use "@tmagic/design/src/theme/themes/magic-admin/index.scss" as tMagicDesign;
|
||||
@use "@tmagic/table/src/theme/index.scss" as tMagicTable;
|
||||
@use "@tmagic/form/src/theme/themes/magic-admin/index.scss" as tMagicForm;
|
||||
@use "../../theme.scss";
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// 仅作用于编辑器内部 DOM 的具体样式仍保留「双类」,避免误中 portal 上只挂 `m-theme--*`
|
||||
// 的节点。
|
||||
.m-editor.m-editor--magic-admin {
|
||||
// 主题级 CSS 变量入口:想覆盖 nav 高度等 token 时,改这里一行即可;
|
||||
// 走 CSS 级联,运行时切主题(换 `.m-editor--magic-admin` class)即可即时生效,
|
||||
// 不受 SCSS `@use` 加载顺序限制。fallback 值定义在 common/var.scss,未启用主题
|
||||
// 时自动回落 35px。
|
||||
--editor-nav-height: 40px;
|
||||
|
||||
.m-editor-sidebar {
|
||||
.m-editor-sidebar-header {
|
||||
// padding: 8px 0;
|
||||
width: 61px;
|
||||
padding-top: 4px;
|
||||
// sidebar 父容器是 `display: flex` (row),同级 `.m-editor-sidebar-content` 的 width
|
||||
// 是 `calc(100% - 40px)`(sidebar.scss:52,40 是老 header 宽度)。当 header 改成 61px
|
||||
// 后,两者之和会溢出 21px 左右,默认 `flex-shrink: 1` 会按权重压缩 header,最终渲染
|
||||
// 出 56.3px 而非 61px。这里强制 `flex-shrink: 0` 让 header 守住宽度,多余的部分由
|
||||
// content 侧承担(content 的 `100% - 40` 会自然让出)。
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid rgba(0, 84, 225, 0.1);
|
||||
display: flex;
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
.m-editor-sidebar-header-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
padding: 4px 2px;
|
||||
width: 52px;
|
||||
margin-bottom: 4px;
|
||||
box-sizing: border-box;
|
||||
background-color: transparent;
|
||||
color: #84909d;
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
> i {
|
||||
color: #84909d;
|
||||
font-size: 20px;
|
||||
}
|
||||
&.is-active {
|
||||
background-color: var(--el-button-hover-bg-color, rgb(230, 238, 253));
|
||||
color: var(--el-color-primary);
|
||||
&:hover {
|
||||
background-color: var(
|
||||
--el-button-hover-bg-color,
|
||||
rgb(230, 238, 253)
|
||||
);
|
||||
}
|
||||
> i {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
.magic-editor-tab-panel-title {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
.magic-editor-tab-panel-title {
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
font-weight: 400;
|
||||
margin: 0 4px;
|
||||
width: 52px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
.m-editor-sidebar-content {
|
||||
width: calc(100% - 61px);
|
||||
}
|
||||
}
|
||||
.m-editor-props-panel .el-input__wrapper {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.border-box-container .border-icon-container .border-icon {
|
||||
border-color: #ccc;
|
||||
--unuse-border-style: solid;
|
||||
&.configured {
|
||||
border-color: var(--el-color-success, var(--td-success-color, #2ba471));
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: var(--el-color-primary);
|
||||
background-color: rgba(0, 84, 225, 0.1);
|
||||
&.center {
|
||||
border-width: 2px;
|
||||
}
|
||||
// 命中 `.border-icon-<side>` 时只保留该侧 2px,其余三边清零。
|
||||
// 用 `@each` 循环避免四面手抄;后续要加禁用、hover 等状态也可以照搬这套结构。
|
||||
@each $side in (top, right, bottom, left) {
|
||||
&.border-icon-#{$side} {
|
||||
border-width: 0;
|
||||
border-#{$side}-width: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.center {
|
||||
border-width: 2px;
|
||||
}
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.m-container-code-select {
|
||||
> .el-form-item:first-child {
|
||||
> .el-form-item__label:first-child > :not(.m-form-tip) {
|
||||
color: #111;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.m-editor.m-theme--magic-admin {
|
||||
.m-editor-props-form-panel-form {
|
||||
> .m-container-tab {
|
||||
> .tmagic-design-tabs {
|
||||
> .el-tabs__content {
|
||||
background-color: #fafafa;
|
||||
border-radius: 8px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.m-editor.m-theme--magic-admin {
|
||||
.m-fields-style-setter {
|
||||
border: 0;
|
||||
.tmagic-design-collapse-item {
|
||||
.el-collapse-item__wrap {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.m-editor.m-theme--magic-admin {
|
||||
.magic-right-panel-tabs-top {
|
||||
> .el-tabs {
|
||||
> .el-tabs__header {
|
||||
.el-tabs__nav-wrap.is-top {
|
||||
.el-tabs__nav.is-top {
|
||||
.el-tabs__active-bar {
|
||||
display: none;
|
||||
}
|
||||
.el-tabs__item {
|
||||
&:not(:last-child)::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 12px;
|
||||
border-right: 1px solid #e6e6e6;
|
||||
}
|
||||
}
|
||||
}
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.m-editor.m-theme--magic-admin {
|
||||
.m-editor-nav-menu {
|
||||
.menu-right,
|
||||
.menu-left {
|
||||
> .menu-item {
|
||||
> button {
|
||||
color: #111;
|
||||
}
|
||||
}
|
||||
}
|
||||
.menu-item {
|
||||
margin-right: 8px;
|
||||
.el-button--small {
|
||||
font-size: 14px;
|
||||
}
|
||||
> button {
|
||||
> i {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,6 @@
|
||||
margin-right: 3px;
|
||||
}
|
||||
span {
|
||||
color: var(--el-color-primary, #2882e0);
|
||||
color: #2882e0;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user