diff --git a/docs/.vitepress/theme/components/demo-block.vue b/docs/.vitepress/theme/components/demo-block.vue
index 4c251545..f2d63d0e 100644
--- a/docs/.vitepress/theme/components/demo-block.vue
+++ b/docs/.vitepress/theme/components/demo-block.vue
@@ -162,7 +162,7 @@
}
&:hover {
- color: #409eff;
+ color: var(--el-color-primary,#409eff);;
background-color: #f9fafc;
}
diff --git a/eslint.config.js b/eslint.config.js
index ce9e0d31..2b5b12ca 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -16,7 +16,7 @@ export default defineConfig([
'*/**/public/**/*',
'*/**/types/**/*',
'*/**/*.config.ts',
- './tepm/**/*',
+ './temp/**/*',
'vite-env.d.ts',
]),
...eslintConfig(path.join(path.dirname(fileURLToPath(import.meta.url)), 'tsconfig.json')),
diff --git a/packages/design/src/Alert.vue b/packages/design/src/Alert.vue
new file mode 100644
index 00000000..0dbac9a3
--- /dev/null
+++ b/packages/design/src/Alert.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/packages/design/src/Card.vue b/packages/design/src/Card.vue
index 27329dc7..dffb1fb3 100644
--- a/packages/design/src/Card.vue
+++ b/packages/design/src/Card.vue
@@ -1,5 +1,5 @@
-
+
@@ -14,6 +14,7 @@
import { computed } from 'vue';
import { getDesignConfig } from './config';
+import { isGlobalFlat } from './index';
import type { CardProps } from './types';
defineOptions({
@@ -26,5 +27,14 @@ const ui = getDesignConfig('components')?.card;
const uiComponent = ui?.component || 'el-card';
-const uiProps = computed(() => ui?.props(props) || props);
+// 当祖先 `` / `` 的 theme 是 `magic-admin` 时,整套设计语言默认走「无卡片感」,
+// 因此这里自动等价于 `flat=true`;调用方仍可显式传 `flat` 强制开启。
+const isFlat = computed(() => !!props.flat || isGlobalFlat.value);
+
+// 把 `flat` 从转发给底层 UI 库(el-card / t-card 等)的 props 中剥离:
+// 它仅作用于本组件的视觉修饰类,不需要也不应该作为属性落到底层组件 / DOM 上。
+const uiProps = computed(() => {
+ const { flat: _flat, ...rest } = props;
+ return ui?.props(rest as CardProps) || (rest as CardProps);
+});
diff --git a/packages/design/src/ColorPicker.vue b/packages/design/src/ColorPicker.vue
index aac60d3a..255e60a3 100644
--- a/packages/design/src/ColorPicker.vue
+++ b/packages/design/src/ColorPicker.vue
@@ -7,18 +7,39 @@
@update:modelValue="updateModelValue"
>
+
diff --git a/packages/form/src/containers/Container.vue b/packages/form/src/containers/Container.vue
index 4c3f6cb8..c04dce7b 100644
--- a/packages/form/src/containers/Container.vue
+++ b/packages/form/src/containers/Container.vue
@@ -35,6 +35,9 @@
:label-title="config.labelTitle"
:text="text"
>
+
@@ -222,17 +225,23 @@
-
-
{{
- expand ? '收起配置' : '展开更多配置'
- }}
+
+ {{ expand ? '收起配置' : '展开更多配置' }}
diff --git a/packages/form/src/fields/Text.vue b/packages/form/src/fields/Text.vue
index 4fa3cc83..d709e903 100644
--- a/packages/form/src/fields/Text.vue
+++ b/packages/form/src/fields/Text.vue
@@ -1,6 +1,31 @@
-
+
+
+
+
+ {{ config.prepend }}
+ {{ value }}
+
+
+ {{ appendConfig.text }}
+
+
+ {{ appendConfig.text }}
+
+
+
{{ appendConfig.text }}
+
{{ appendConfig.text }}
-
+
输入内容前后有空格,是否移除空格?
@@ -80,28 +106,26 @@ watch(
);
const appendConfig = computed(() => {
- if (typeof props.config.append === 'string') {
+ const { append } = props.config;
+
+ // 字符串形态:纯文本 append。
+ if (typeof append === 'string') {
return {
type: 'text',
- text: props.config.append,
+ text: append,
handler: undefined,
};
}
- if (typeof props.config.append === 'object') {
- if (typeof props.config.append?.handler === 'function') {
- return {
- type: 'button',
- text: props.config.append.text,
- handler: props.config.append.handler,
- };
- }
- if (props.config.append) {
- if (props.config.append.value === 0) {
- return false;
- }
- return props.config.append;
- }
+ // 对象形态:按 schema 定义,`type` 可为 `'button' | 'icon'`,`handler` 可选。
+ //
+ // 旧逻辑里只要带 handler 就强制覆盖为 `type: 'button'`,导致 schema 中显式传入的
+ // `type: 'icon'` 在到达模板前被抹掉(参见 `@editor/utils/props.ts` 中 ID 字段
+ // 的复制按钮配置)。这里直接返回原对象,模板按 `type` 分支渲染 button / icon / text。
+ if (typeof append === 'object' && append) {
+ // `value === 0` 沿用旧约定:作为「隐藏 append」的开关。
+ if (append.value === 0) return false;
+ return append;
}
return false;
diff --git a/packages/form/src/plugin.ts b/packages/form/src/plugin.ts
index 4a121745..085d5100 100644
--- a/packages/form/src/plugin.ts
+++ b/packages/form/src/plugin.ts
@@ -50,11 +50,13 @@ import { setConfig } from './utils/config';
import { registerTypeMatchRules, type TypeMatchValidator } from './utils/typeMatch';
import Form from './Form.vue';
import FormDialog from './FormDialog.vue';
+import FormDrawer from './FormDrawer.vue';
import './theme/index.scss';
// #region FormInstallOptions
export interface FormInstallOptions {
+ flat?: boolean;
/** 自定义字段 type 的 typeMatch 校验规则,可覆盖内置规则或扩展业务字段 */
typeMatchRules?: Record
;
[key: string]: any;
@@ -76,6 +78,7 @@ export default {
app.component('m-form', Form);
app.component('m-form-dialog', FormDialog);
+ app.component('m-form-drawer', FormDrawer);
app.component('m-form-container', Container);
app.component('m-form-fieldset', Fieldset);
app.component('m-form-group-list', TableGroupList);
diff --git a/packages/form/src/theme/container.scss b/packages/form/src/theme/container.scss
index 0f185270..332f7897 100644
--- a/packages/form/src/theme/container.scss
+++ b/packages/form/src/theme/container.scss
@@ -22,4 +22,7 @@
background: #f7dadd;
}
}
+ .m-form-title-extra {
+ margin-left: 8px;
+ }
}
diff --git a/packages/form/src/theme/fieldset.scss b/packages/form/src/theme/fieldset.scss
index 3bd8390b..8f46940c 100644
--- a/packages/form/src/theme/fieldset.scss
+++ b/packages/form/src/theme/fieldset.scss
@@ -1,3 +1,54 @@
+.m-container-fieldset {
+ &.no-border-fieldset {
+ &.fieldset-in-row {
+ > fieldset.m-fieldset {
+ legend {
+ left: 0;
+ }
+ }
+ }
+ > fieldset.m-fieldset {
+ border: 0;
+ legend {
+ position: relative;
+ left: -5px;
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 24px;
+ padding: 0;
+ }
+ }
+ }
+ &.fieldset-in-card-fit {
+ border-bottom: 1px solid #e6e6e6;
+ > fieldset.m-fieldset {
+ padding: 16px 0 16px 0 !important;
+ border: 0;
+ margin-top: 0;
+ margin-bottom: 0;
+ legend {
+ position: relative;
+ top: 10px;
+ background-color: transparent;
+ }
+ }
+ &.fieldset-in-card-fit-last {
+ padding-bottom: 0 !important;
+ border-bottom: 0;
+ }
+ &.fieldset-in-card-fit-first {
+ padding-top: 0 !important;
+ }
+ &:last-child {
+ padding-bottom: 0 !important;
+ border-bottom: 0;
+ }
+ &:first {
+ padding-top: 0 !important;
+ }
+ }
+}
+
fieldset.m-fieldset {
position: relative;
border: 1px solid rgb(229, 229, 229);
diff --git a/packages/form/src/theme/form.scss b/packages/form/src/theme/form.scss
index 365357d4..3638b449 100644
--- a/packages/form/src/theme/form.scss
+++ b/packages/form/src/theme/form.scss
@@ -53,3 +53,9 @@
margin-bottom: var(--td-comp-margin-xxl);
}
}
+
+.m-form-container {
+ .m-form-container-expand {
+ text-align: center;
+ }
+}
diff --git a/packages/form/src/theme/group-list.scss b/packages/form/src/theme/group-list.scss
index b8c32f23..778ea5ba 100644
--- a/packages/form/src/theme/group-list.scss
+++ b/packages/form/src/theme/group-list.scss
@@ -1,4 +1,8 @@
.m-fields-group-list {
+ .m-fields-group-list-item.tmagic-design-card--flat:last-child {
+ border: 0;
+ }
+
.el-button--text {
padding: 0;
margin-bottom: 7px;
@@ -19,14 +23,11 @@
&:last-of-type {
border-bottom: 0;
}
- }
- .tmagic-design-card {
- .el-card__header {
- padding: 5px 20px;
- }
- .t-card__header {
- padding: 5px 0;
+ .m-fields-group-list-item-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
}
}
@@ -34,5 +35,6 @@
display: flex;
justify-content: space-between;
margin-top: 10px;
+ margin-bottom: 16px;
}
}
diff --git a/packages/form/src/theme/index.scss b/packages/form/src/theme/index.scss
index 3e87b8d1..3bcd54c1 100644
--- a/packages/form/src/theme/index.scss
+++ b/packages/form/src/theme/index.scss
@@ -13,3 +13,4 @@
@use "./tabs.scss";
@use "./number-range.scss";
@use "./form-box.scss";
+@use "./radio-group.scss";
diff --git a/packages/form/src/theme/link.scss b/packages/form/src/theme/link.scss
index a00d7441..345fe255 100644
--- a/packages/form/src/theme/link.scss
+++ b/packages/form/src/theme/link.scss
@@ -1,3 +1,20 @@
div.m-fields-link {
width: fit-content;
}
+a.magic-admin-link {
+ width: calc(100%);
+ box-sizing: border-box;
+ border-radius: 4px;
+ border: 1px solid #e6e6e6;
+ background-color: #fafafa;
+ padding: 0 8px;
+ height: 28px;
+ display: inline-flex;
+ gap: 4px;
+ align-items: center;
+ text-decoration: none;
+
+ i {
+ margin-right: 4px;
+ }
+}
diff --git a/packages/form/src/theme/panel.scss b/packages/form/src/theme/panel.scss
index d83cd698..56daebe5 100644
--- a/packages/form/src/theme/panel.scss
+++ b/packages/form/src/theme/panel.scss
@@ -4,7 +4,7 @@
background: #f2f6fc;
}
a {
- color: #409eff;
+ color: var(--el-color-primary, #409eff);
}
}
diff --git a/packages/form/src/theme/radio-group.scss b/packages/form/src/theme/radio-group.scss
new file mode 100644
index 00000000..457a9c8d
--- /dev/null
+++ b/packages/form/src/theme/radio-group.scss
@@ -0,0 +1,23 @@
+// 单个 radio option 的内容容器:横向排列 icon + 文本,垂直居中对齐。
+// 这样多行文本(`text: '内联布局\n inline'`)能跟左侧 icon 在视觉中线对齐,
+// 不会出现 icon 顶到第一行的错位。
+.m-fields-radio-group__option {
+ display: flex;
+ align-items: center;
+}
+
+.m-fields-radio-group__text {
+ // `inline-block` 关键:inline span 的 line-height 会被父级 inline formatting
+ // context(如 `.el-radio-button__inner` 的 strut)覆盖,外观高度量出来会比
+ // `line-height` 大;显式声明 `inline-block` 让 span 有独立的格式化上下文,
+ // 自身 line-height 才真正生效。
+ display: inline-block;
+ vertical-align: middle;
+ // `pre-line` 让 `\n` 渲染为换行,同时保留自动折行。
+ white-space: pre-line;
+ line-height: 20px;
+}
+
+.m-fields-radio-group__icon_with_text {
+ margin-right: 8px;
+}
diff --git a/packages/form/src/theme/tabs.scss b/packages/form/src/theme/tabs.scss
index 6805908b..a9fba108 100644
--- a/packages/form/src/theme/tabs.scss
+++ b/packages/form/src/theme/tabs.scss
@@ -5,8 +5,8 @@
margin-right: auto;
min-width: 50px;
outline: none;
- border-color: #409eff;
- color: #409eff;
+ border-color: var(--el-color-primary, #409eff);
+ color: var(--el-color-primary, #409eff);
width: 10px;
&::before {
content: "添加";
diff --git a/packages/form/src/theme/text.scss b/packages/form/src/theme/text.scss
index 6c5ba72a..6bd5510f 100644
--- a/packages/form/src/theme/text.scss
+++ b/packages/form/src/theme/text.scss
@@ -2,6 +2,30 @@
display: flex;
align-items: center;
width: 100%;
+
+ // 纯文本(非输入框)形态:保留 prepend / append 槽位的横向布局。
+ // 配合 `TextConfig.static = true` 使用,常见于 ID 之类「只读 + 操作 icon」场景。
+ &--static {
+ gap: 6px;
+ min-height: 32px;
+
+ .m-fields-text__prepend {
+ color: var(--el-text-color-secondary, #909399);
+ }
+
+ .m-fields-text__value {
+ color: var(--el-text-color-regular, #606266);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .m-fields-text__icon {
+ width: 16px;
+ height: 16px;
+ cursor: pointer;
+ }
+ }
}
.m-form-validate__warning {
@@ -29,6 +53,20 @@
}
}
+.m-container-text {
+ &.m-fields-title-container {
+ .tmagic-design-form-item {
+ .el-form-item__label {
+ color: #0f1113;
+ font-weight: 500;
+ font-size: 16px;
+ }
+ line-height: 24px;
+ margin-bottom: 16px;
+ }
+ }
+}
+
.tmagic-form-text-popper[data-popper-placement^="top"]
> .tmagic-form-text-popper-arrow {
bottom: -4px;
diff --git a/packages/form/src/theme/themes/magic-admin/index.scss b/packages/form/src/theme/themes/magic-admin/index.scss
new file mode 100644
index 00000000..c75982a9
--- /dev/null
+++ b/packages/form/src/theme/themes/magic-admin/index.scss
@@ -0,0 +1,95 @@
+// magic-admin 主题:所有规则均以 `.m-form.m-form--magic-admin` 为前缀,
+// 仅在 `` 时生效,避免污染默认样式。
+@use "../../index.scss";
+
+.m-form.m-form--magic-admin {
+ // 精确匹配"只挂 `m-form-container` + `m-container-fieldset` 这两个 class"的元素:
+ // `.a.b` 只能保证"包含"这两个 class,多余的 class 也会命中;
+ // 用 `[class="..."]` 做全量字符串等值匹配即可排除第三个 class 的情况。
+ // 由于 Vue/框架不保证 class 拼接顺序,两种顺序都列出以覆盖到位。
+ [class="m-form-container m-container-fieldset"],
+ [class="m-container-fieldset m-form-container"] {
+ margin-top: 15px;
+ }
+
+ .m-container-tab:not(.magic-right-panel-tabs-top):not(
+ .magic-form-dynamic-tab
+ ) {
+ > .tmagic-design-tabs:not(.el-tabs--border-card) {
+ > .el-tabs__content {
+ background-color: #fff;
+ padding: 16px 16px 0 16px;
+ border-radius: 4px;
+ }
+ }
+ }
+
+ .m-container-tab.magic-form-dynamic-tab {
+ }
+
+ .m-form-tip {
+ color: rgba(0, 0, 0, 0.55);
+ }
+
+ .tmagic-design-form-item {
+ &.show-after-diff {
+ background: #d4edbc;
+ }
+
+ &.show-before-diff {
+ background: #fad2cf;
+ }
+ }
+ .m-fields-radio-group__text {
+ // `pre-line` 让 `\n` 渲染为换行,同时保留自动折行;线高与默认按钮组高度对齐。
+ white-space: pre-line;
+ line-height: 20px;
+ }
+ .el-form-item__content {
+ .tmagic-design-radio-group {
+ .tmagic-design-radio-button {
+ height: 32px;
+ .el-radio-button__inner {
+ height: 32px;
+ display: flex;
+ padding-top: 0;
+ padding-bottom: 0;
+ align-items: center;
+ }
+ }
+ }
+ }
+ .m-fields-data-source-field-select .tmagic-design-button {
+ margin-left: 8px;
+ span {
+ font-size: 16px;
+ }
+ }
+
+ .m-fields-group-list-item.tmagic-design-card--flat {
+ .el-card__body {
+ .m-fields-table-wrap {
+ .m-fields-table {
+ .el-table__header-wrapper {
+ thead {
+ height: 48px;
+ }
+ }
+ }
+ }
+ }
+ .m-fields-group-list-item-header {
+ position: relative;
+ .delete-button {
+ position: absolute;
+ right: 0;
+ color: #0f1113;
+ }
+ }
+ }
+ .m-form-container {
+ .m-form-container-expand {
+ text-align: left;
+ }
+ }
+}
diff --git a/packages/form/src/utils/config.ts b/packages/form/src/utils/config.ts
index 56cf552e..64aba03f 100644
--- a/packages/form/src/utils/config.ts
+++ b/packages/form/src/utils/config.ts
@@ -17,11 +17,17 @@
*/
import type { Component } from 'vue';
+import { ref } from 'vue';
let $MAGIC_FORM = {} as any;
+// 用 ref 持有 flat 全局开关:const 引用本身不可变,符合 `import/no-mutable-exports`;
+// 通过 `.value` 改值仍保持模块级响应式语义,与 editor/plugin.ts、design/index.ts 里
+// 的同名变量写法对齐。
+const isGlobalFlat = ref(false);
const setConfig = (option: any): void => {
$MAGIC_FORM = option;
+ isGlobalFlat.value = option.flat ?? false;
};
const getConfig = (key: string): T => $MAGIC_FORM[key];
@@ -39,4 +45,4 @@ const getField = (tagName: string): Component | undefined => fieldRegistry.get(t
const deleteField = (tagName: string): boolean => fieldRegistry.delete(tagName);
-export { deleteField, getConfig, getField, registerField, setConfig };
+export { deleteField, getConfig, getField, isGlobalFlat, registerField, setConfig };
diff --git a/packages/form/tests/unit/Form.extra.spec.ts b/packages/form/tests/unit/Form.extra.spec.ts
index 61f178c7..3e7d681b 100644
--- a/packages/form/tests/unit/Form.extra.spec.ts
+++ b/packages/form/tests/unit/Form.extra.spec.ts
@@ -90,6 +90,22 @@ describe('Form.vue —— formState getter 行为', () => {
expect(fs2.parentValues).toEqual({ x: 2 });
});
+ test('formState.popperClass 在传入 theme 时自动追加 m-theme 修饰类', async () => {
+ const wrapper = mountForm({ popperClass: 'pop-x', theme: 'magic-admin' });
+ await nextTick();
+
+ const fs: any = wrapper.vm.formState;
+ expect(fs.popperClass).toBe('pop-x m-theme--magic-admin');
+
+ // 仅 theme 没有用户 popperClass 时不带前导空格
+ await wrapper.setProps({ popperClass: undefined });
+ expect(wrapper.vm.formState.popperClass).toBe('m-theme--magic-admin');
+
+ // 取消 theme 后回退为原始 popperClass
+ await wrapper.setProps({ theme: undefined, popperClass: 'pop-x' });
+ expect(wrapper.vm.formState.popperClass).toBe('pop-x');
+ });
+
test('values / lastValuesProcessed 在 formState 上自动解包为 ref 当前值', async () => {
const wrapper = mountForm({
isCompare: true,
diff --git a/packages/stage/src/moveable-able.css b/packages/stage/src/moveable-able.css
index 03d2545f..46b0031a 100644
--- a/packages/stage/src/moveable-able.css
+++ b/packages/stage/src/moveable-able.css
@@ -1,7 +1,7 @@
.moveable-button {
width: 20px;
height: 20px;
- background: #4af;
+ background: var(--el-color-primary,#4af);
border-radius: 4px;
appearance: none;
border: 0;
diff --git a/playground/src/App.vue b/playground/src/App.vue
index 8a25bb00..0a8df79a 100644
--- a/playground/src/App.vue
+++ b/playground/src/App.vue
@@ -12,6 +12,7 @@ export default {
html,
body,
#app {
+ --el-color-primary: #0054e1;
padding: 0;
margin: 0;
width: 100%;
diff --git a/playground/src/configs/formDsl.ts b/playground/src/configs/formDsl.ts
index c3284621..270718be 100644
--- a/playground/src/configs/formDsl.ts
+++ b/playground/src/configs/formDsl.ts
@@ -8,6 +8,16 @@ export default createForm([
extra: (vm: any, { model }: any) => `${model.text}extra`,
rules: [{ required: true, message: '请输入活动名称', trigger: 'blur' }],
},
+ // `img-upload` 是 playground 自定义业务字段,未在 `FormItemConfig` 联合里注册;
+ // `checkStrictly` 又是 `DataSourceFieldSelectConfig` 的专属字段,会把 TS 收窄到该
+ // 分支再报「type 不匹配」。这里用 `as any` 放过,正式工程里应通过 `createForm`
+ // 的泛型把自定义类型并入。
+ {
+ type: 'img-upload',
+ checkStrictly: false,
+ name: 'backgroundImage',
+ text: '背景图',
+ } as any,
{
type: 'checkbox',
text: 'checkbox',
diff --git a/playground/src/main.ts b/playground/src/main.ts
index f1e52973..ab7a0ec7 100644
--- a/playground/src/main.ts
+++ b/playground/src/main.ts
@@ -28,7 +28,8 @@ import editorPlugin from '@tmagic/editor';
import App from './App.vue';
import router from './route';
-import '@tmagic/editor/dist/style.css';
+// import '@tmagic/editor/dist/style.css';
+import '@tmagic/editor/dist/themes/magic-admin.css';
// @ts-ignore
globalThis.MonacoEnvironment = {
@@ -64,6 +65,6 @@ if (adapter === 'tdesign-vue-next') {
adapterModule.then((module: any) => {
const app = createApp(App);
app.use(router);
- app.use(editorPlugin, module.default);
+ app.use(editorPlugin, { ...module.default, flat: true });
app.mount('#app');
});
diff --git a/playground/src/pages/Editor.vue b/playground/src/pages/Editor.vue
index fa584b16..388d6ce8 100644
--- a/playground/src/pages/Editor.vue
+++ b/playground/src/pages/Editor.vue
@@ -11,6 +11,7 @@
:datasource-event-method-list="datasourceEventMethodList"
:datasource-configs="datasourceConfigs"
:datasource-values="datasourceValues"
+ theme="magic-admin"
:component-group-list="componentGroupList"
:datasource-list="datasourceList"
:default-selected="defaultSelected"
@@ -62,6 +63,7 @@ import {
TMagicDialog,
TMagicEditor,
tMagicMessage,
+ uiService,
} from '@tmagic/editor';
import DeviceGroup from '../components/DeviceGroup.vue';
@@ -75,6 +77,8 @@ import { useEditorRes } from './composables/use-editor-res';
const { VITE_RUNTIME_PATH } = import.meta.env;
+uiService.set('propsPanelSize', 'default');
+
const datasourceList: DatasourceTypeOption[] = [];
const runtimeUrl = `${VITE_RUNTIME_PATH}/playground/index.html`;
diff --git a/playground/vite.config.ts b/playground/vite.config.ts
index 976603fa..4e724b36 100644
--- a/playground/vite.config.ts
+++ b/playground/vite.config.ts
@@ -40,6 +40,32 @@ export default defineConfig({
find: /^@tmagic\/editor\/dist\/style.css/,
replacement: path.join(__dirname, '../packages/editor/src/theme/index.scss'),
},
+ // 开发态:把 `@tmagic/editor/dist/themes/.css` 直接指到对应 SCSS 源码,
+ // 这样调主题样式不用先跑 build。$1 来自 `find` 正则的捕获组。
+ {
+ find: /^@tmagic\/editor\/dist\/themes\/(.+)\.css$/,
+ replacement: path.join(__dirname, '../packages/editor/src/theme/themes/$1/index.scss'),
+ },
+ {
+ find: /^@tmagic\/form\/dist\/themes\/(.+)\.css$/,
+ replacement: path.join(__dirname, '../packages/form/src/theme/themes/$1/index.scss'),
+ },
+ // 主题 SCSS 在内部互相 @use 时(例如 editor 的主题里 @use 了 design / form 的主题),
+ // 写的是 `@tmagic//src/theme/themes//index.scss`。这些 SCSS 子路径必须
+ // 在下方通用的 `^@tmagic/` 规则(指向 `src/index.ts`)之前命中,否则会被
+ // 错误改写成 `.../src/index.ts/src/theme/themes/...` 触发 ENOTDIR。
+ {
+ find: /^@tmagic\/editor\/src\/theme\/themes\/(.+)\/index\.scss$/,
+ replacement: path.join(__dirname, '../packages/editor/src/theme/themes/$1/index.scss'),
+ },
+ {
+ find: /^@tmagic\/form\/src\/theme\/themes\/(.+)\/index\.scss$/,
+ replacement: path.join(__dirname, '../packages/form/src/theme/themes/$1/index.scss'),
+ },
+ {
+ find: /^@tmagic\/design\/src\/theme\/themes\/(.+)\/index\.scss$/,
+ replacement: path.join(__dirname, '../packages/design/src/theme/themes/$1/index.scss'),
+ },
{
find: /^@tmagic\/form\/src\/theme\/index.scss/,
replacement: path.join(__dirname, '../packages/form/src/theme/index.scss'),
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 3a4a5ff0..b9f465fb 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -14,4 +14,4 @@ catalog:
overrides:
element-plus: ^2.14.3
- vue: ^3.5.40
+ vue: ^3.5.40
\ No newline at end of file
diff --git a/scripts/build.mjs b/scripts/build.mjs
index 14f1a3dc..482a5d28 100644
--- a/scripts/build.mjs
+++ b/scripts/build.mjs
@@ -7,6 +7,7 @@ import { build as buildVite } from 'vite';
import vue from '@vitejs/plugin-vue';
import minimist from 'minimist';
import rimraf from 'rimraf';
+import * as sass from 'sass-embedded';
const args = minimist(process.argv.slice(2));
@@ -24,6 +25,7 @@ if (args.package) {
build({ packageName: args.package, format: 'es', pkg, packagesDir });
build({ packageName: args.package, format: 'umd', pkg, packagesDir });
+ buildThemes({ packageName: args.package, packagesDir });
}
} else {
const packages = getPackageNames(packagesDir);
@@ -35,6 +37,7 @@ if (args.package) {
build({ packageName, format: 'es', pkg, packagesDir });
build({ packageName, format: 'umd', pkg, packagesDir });
+ buildThemes({ packageName, packagesDir });
}
for (const packageName of runtimeHelpers) {
@@ -133,6 +136,47 @@ async function build({ packageName, format, pkg, packagesDir }) {
});
}
+/**
+ * 扫描 `/src/theme/themes//index.scss`,
+ * 为每个主题单独编译输出 `/dist/themes/.css`。
+ *
+ * 主题样式独立于主样式包(dist/style.css),消费者通过
+ * `import '@tmagic//dist/themes/.css'` 按需引入,
+ * 并在 `` / ``
+ * 上挂载对应的 `m-editor--` / `m-form--` 修饰类后生效。
+ */
+function buildThemes({ packageName, packagesDir }) {
+ const themesDir = path.resolve(packagesDir, `./${packageName}/src/theme/themes`);
+ if (!fs.existsSync(themesDir)) return;
+
+ const themes = fs.readdirSync(themesDir).filter((name) => {
+ const themePath = path.resolve(themesDir, name);
+ return fs.statSync(themePath).isDirectory() && fs.existsSync(path.resolve(themePath, 'index.scss'));
+ });
+ if (themes.length === 0) return;
+
+ const outputDir = path.resolve(packagesDir, `./${packageName}/dist/themes`);
+ fs.mkdirSync(outputDir, { recursive: true });
+
+ // 把 `/node_modules` 也加到 loadPaths,使主题 SCSS 内
+ // `@use "@tmagic//src/.../index.scss"` 能通过 pnpm 在 node_modules
+ // 下的 symlink 解析到对应的源码包。
+ const nodeModulesDir = path.resolve(packagesDir, `./${packageName}/node_modules`);
+ const loadPaths = [path.resolve(packagesDir, `./${packageName}/src`)];
+ if (fs.existsSync(nodeModulesDir)) loadPaths.push(nodeModulesDir);
+
+ for (const theme of themes) {
+ const input = path.resolve(themesDir, theme, 'index.scss');
+ const result = sass.compile(input, {
+ style: 'expanded',
+ sourceMap: false,
+ loadPaths: [path.resolve(themesDir, theme), ...loadPaths],
+ });
+ fs.writeFileSync(path.resolve(outputDir, `${theme}.css`), result.css);
+ console.log(`[themes] ${packageName}: ${theme}.css`);
+ }
+}
+
function getPackageNames(packagesDir) {
return fs.readdirSync(packagesDir).filter((p) => {
const pkgRoot = path.resolve(packagesDir, p);