@manohub/app-kit 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRACT.md +27 -2
- package/README.md +2 -0
- package/dist/components/app-form.d.ts +42 -3
- package/dist/components/app-form.js +12 -5
- package/dist/components/app-panel.d.ts +12 -14
- package/dist/components/app-panel.js +2 -22
- package/dist/components/part-padding.d.ts +27 -0
- package/dist/components/part-padding.js +23 -0
- package/dist/entry/create-sub-app.d.ts +13 -1
- package/dist/entry/create-sub-app.js +8 -2
- package/dist/entry/index.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/shell/AppShell.d.ts +14 -0
- package/dist/shell/AppShell.js +7 -1
- package/dist/shell/shell.css +13 -0
- package/lint/__tests__/fixtures/clean-src/views/good-thing/components/picker-dialog.tsx +5 -0
- package/lint/__tests__/fixtures/violations-src/views/nested-dir/inner-page.tsx +5 -0
- package/lint/__tests__/fixtures/violations-src/views/thing/components/picker-dialog.tsx +5 -0
- package/lint/__tests__/guardrails.spec.mjs +23 -0
- package/lint/shared.mjs +2 -1
- package/lint/structure-audit.mjs +19 -4
- package/package.json +2 -2
- package/skills/app-kit/references/adoption.md +5 -0
- package/skills/app-kit-migrate/references/migration-map.md +3 -2
- package/skills/app-kit-migrate/references/migration-playbook.md +1 -0
package/CONTRACT.md
CHANGED
|
@@ -19,6 +19,9 @@ export const { mount, unmount } = createSubApp({
|
|
|
19
19
|
// 用 vue-i18n 的应用走插件注入,不要用本包的 i18n 选项(那是 i18next 语义)
|
|
20
20
|
extraPlugins: [i18n],
|
|
21
21
|
rootPathAliases: ['/index.html'], // 见 §9「初始守卫与选择模式」
|
|
22
|
+
// 路由历史模式:缺省 'history'(micro-app baseroute 口径)。宿主不支持深路径 fallback 时改 'hash';
|
|
23
|
+
// 需要自定义 base 等细节时传 vue-router 的 RouterHistory 实例。见 §9 第 13 条
|
|
24
|
+
// history: 'hash',
|
|
22
25
|
})
|
|
23
26
|
```
|
|
24
27
|
|
|
@@ -119,6 +122,10 @@ import { AppShell, AppPanel, AppTable, AppButton, notify } from '@manohub/app-ki
|
|
|
119
122
|
</AppShell>
|
|
120
123
|
```
|
|
121
124
|
|
|
125
|
+
- `AppShell.Header` 的五个位:`title`(页标题)/ `subTitle`(**下一行的次要说明**,不传即不渲染)/
|
|
126
|
+
`icon`(不传用内建图标)/ `toolbar`(**页面级操作**位)/ `tabs`(页头下方页签区)。
|
|
127
|
+
`subTitle` 给的是「一句话说明**什么数据**」(对象名、统计口径),不是第二颗标题,也不是放操作的地方;
|
|
128
|
+
跟随数据变化时直接传值即可(组件只渲染文本)。`title` 不传 = 整块走自定义页头出口,此时 `subTitle` 不生效。
|
|
122
129
|
- 筛选字段 > 3 或跨区域时,在 `AppShell.Header` **之前**加 `<AppShell.Filter>`(页面级筛选的唯一位置)。
|
|
123
130
|
- **分页归属跟「承载表格的容器」走**:表格直接挂在 `AppShell.Body` 下 → 分页放 `AppShell.Footer`(模板 A);
|
|
124
131
|
表格在 `AppPanel` 里 → 分页放 `AppPanel.Footer`(模板 B)。
|
|
@@ -168,6 +175,8 @@ import { AppShell, AppPanel, AppTable, AppButton, notify } from '@manohub/app-ki
|
|
|
168
175
|
- `AppPanel.Header.padding` = 内建头那一行的内距;走 `header` 插槽(整行替换)时会把自定义头
|
|
169
176
|
**包一层**只带内距的 div —— 头内部的几何仍归消费方,组件不碰。
|
|
170
177
|
- 四件缺省都**不落任何行内 style**(DOM 与没有这个维度时一致)。
|
|
178
|
+
- 同一套口径与实现在**表单**上也有一份(§4.7 `AppForm.padding`):弹窗 / 抽屉里的表单自带内距,
|
|
179
|
+
同样不必挂「只写 padding」的应用侧类 —— 两处的实现是同一个 `part-padding` 模块,改一处两处同变。
|
|
171
180
|
- **只给「区域」用**:区块标题(没有区域级筛选/操作、内容也不自己滚)用 `AppSection`(§4.11)。
|
|
172
181
|
拿本件当「带标题的卡片」用,会把区域头的高度与 Body 的滚动契约带进不需要它的地方。
|
|
173
182
|
|
|
@@ -232,7 +241,7 @@ import { AppShell, AppPanel, AppTable, AppButton, notify } from '@manohub/app-ki
|
|
|
232
241
|
</AppForm>
|
|
233
242
|
```
|
|
234
243
|
|
|
235
|
-
- `label` 是字段名;`required` 让星号自动落在 label
|
|
244
|
+
- `label` 是字段名;`required` 让星号自动落在 label 文本**之后**(`服务名称*`);`error` 非空即进入错误态;
|
|
236
245
|
`hint` 是常驻说明。错误与说明排在控件**下方**(文档流),不会遮挡下一行。
|
|
237
246
|
- `label` 与 `required` 都不给时**不渲染 label 列**,控件列占满整行 —— 整块内容(如列编辑区)
|
|
238
247
|
与纯说明行用这个形态,不要另起一套 div 排版。
|
|
@@ -241,6 +250,11 @@ import { AppShell, AppPanel, AppTable, AppButton, notify } from '@manohub/app-ki
|
|
|
241
250
|
label 宽度固定为 `labelWidth`:对齐与宽度都属于组件,应用侧不要写 CSS 去改。
|
|
242
251
|
- `labelWidth`(px,缺省 120)与 `controlWidth`(px,缺省撑满)都可写在 `AppForm` 上(下发给每个 Item),
|
|
243
252
|
Item 上再写则覆盖表单级。
|
|
253
|
+
- **容器内距用 `padding`(px 数字 / `{ y, x }`)**:与 §4.3 `AppPanel` 是**同一口径同一实现** ——
|
|
254
|
+
`padding={16}` 四边同值、`padding={{ y: 16, x: 20 }}` 上下 / 左右分别给(只给 `y` 时不覆盖你 `style` 里的左右内距)。
|
|
255
|
+
**缺省 0**(表单贴容器边):页面里的表单在 `AppPanel.Body` 里,内距由那块给;**弹窗 / 抽屉里没有别的容器**
|
|
256
|
+
给内距,就用表单自带的 `padding={{ y: 16, x: 20 }}` —— 不要为此给表单挂一个「只写 padding」的应用侧类,
|
|
257
|
+
也不要在外面套一层只为加内距的 div。
|
|
244
258
|
- **表单太宽时的两条杠杆(可叠加)**:
|
|
245
259
|
① **限宽** —— 表单级或逐行给 `controlWidth`(如 360)封住控件列,输入框不会被拉成长条;
|
|
246
260
|
② **分列** —— `columns={2}` 让行并排:**上限 2 列**,窄容器自动回落成一列(与 §4.12 的栅格同一公式,
|
|
@@ -672,6 +686,10 @@ const list = useClientPagination(() => filteredRows.value)
|
|
|
672
686
|
`component-audit.mjs`(组件使用:禁底层直连 / 禁底层风格写法)、
|
|
673
687
|
`structure-audit.mjs`(页面结构:骨架必用 / 禁自绘页头面板头 / 字段数上限)。
|
|
674
688
|
**排查单条**时才直接跑它(`pnpm exec appkit lint:style`),不必挂成脚本。
|
|
689
|
+
- **「路由页」按目录判,与目录约定对齐**(S1):`src/views|pages/` 下的 `.tsx` 才按路由页要求套 `AppShell`,
|
|
690
|
+
其中 **`…/components/` 子树内的文件不算路由页** —— 页面内的可复用件(弹窗 / 抽屉 / 表单块 / 子表格)
|
|
691
|
+
放同页面的 `components/` 子目录,用 `AppPanel` / `AppForm` / `AppDialog` 承载即可。
|
|
692
|
+
嵌套的页面目录(`views/<页>/<页>.tsx`)**仍然是路由页**,所以目录别只往深了挪、要把件放进 `components/`。
|
|
675
693
|
- `--changed` 只跑改动文件(pre-commit 用;非 git 环境不裁剪,宁可多查)。
|
|
676
694
|
- 每条违规都会给出 `file:line` + 片段 + **唯一改法**(`correction.summary` / `.example`)与 **`doc` 锚点**
|
|
677
695
|
(指回本文件对应章节)。**照 `correction` 改,不要自己另想一套。**
|
|
@@ -773,7 +791,8 @@ pnpm --filter @manohub/app-kit test:unit # vitest(契约单测)+ 护栏
|
|
|
773
791
|
8. **`AppForm`(表单行)为自建件**:底层对应件 `FDynamicFormGroup` 是**动态表单(元数据驱动)体系**的分组件 ——
|
|
774
792
|
控件要经 `editor` 描述对象交给它渲染、标签宽度与字段限宽写死在 farris 表单类里、校验信息是**绝对定位浮层**。
|
|
775
793
|
与本包三条口径冲突(控件由消费方直接写 / 只收朴素业务值 / 错误走文档流),故按 §7 自建;
|
|
776
|
-
|
|
794
|
+
沿用 farris 的一条约定:label 缺省右对齐(`labelAlign` 可切左);
|
|
795
|
+
**必填星号固定在 label 文本之后**(`服务名称*`)—— 这一条与 farris 的「星号在文本左侧」**有意不同**。
|
|
777
796
|
9. **`AppSteps` 复刻了底层步骤条的节点结构**:底层 `FStep` 的默认模板**只渲染 `title`**
|
|
778
797
|
(`description` / `icon` / `class` / `status` 四个字段压根不被读取),第二行副标题只能自己给 `stepTemplate`;
|
|
779
798
|
本包按底层默认结构复刻 DOM 并沿用其类名(底层配色与连接线样式照旧生效)。
|
|
@@ -803,3 +822,9 @@ pnpm --filter @manohub/app-kit test:unit # vitest(契约单测)+ 护栏
|
|
|
803
822
|
| `Invalid prop: … prop "popupOffsetX"/"popupOffsetY". Expected Object, got Number 0`(来自 `FButtonEdit`,链路里能看到 `FComboList`) | 上游 `button/index.esm.js` 写的是 `{ type: Object, default: 0 }` | **无法在应用侧消除**(`FComboList` 不转发这两个 prop)。每渲染一个 `AppSelect` 会刷 2 条,属上游缺陷,忽略即可 |
|
|
804
823
|
| `inject() can only be used inside setup()` | farris 内部与 `@gsp-sys/utils` 在组件上下文外调用 `inject`(app-kit 自己的 `inject` 只在 `AppForm.Item` 的 `setup` 内,`modalService` 走 `app.runWithContext`) | 平台/上游行为,与本包无关 |
|
|
805
824
|
| `Extraneous non-props attributes (customClass) …`(来自 `FModal`) | 早期 `AppDialog` 曾把 class 当 `customClass` 转发,而 `FModal` 运行时没有该 prop、根节点又是 Teleport ⇒ 类名被丢弃还刷告警 | 已移除转发,**不再出现**;消费方要钩子请包在内容元素上 |
|
|
825
|
+
13. **路由历史模式可配,缺省仍是 `history`**:`createSubApp` 默认 `createWebHistory()`(micro-app 以 baseroute 隔离子应用路由,
|
|
826
|
+
这也是平台既有子应用的口径)。宿主**不支持深路径 fallback**、或门户不允许子应用改写地址栏时,传 `history: 'hash'` ——
|
|
827
|
+
路由落进 hash(URL 形如 `<子应用产物路径>#/x`),既不要求宿主 rewrite,也不占用宿主的 pathname 空间。
|
|
828
|
+
需要自定义 base 等细节(如 `createWebHistory('/subapp/x/')`)时,直接传 vue-router 的 `RouterHistory` 实例,本包原样使用、
|
|
829
|
+
不干预 base 与滚动行为。两种模式下 §9 第 2 条的初始守卫口径不变(按 `to.path` 判,hash 模式下的 path 不含 `#`)。
|
|
830
|
+
用法示例见 §1。
|
package/README.md
CHANGED
|
@@ -75,6 +75,8 @@ export const { mount, unmount } = createSubApp({
|
|
|
75
75
|
// extraPlugins: [i18n],
|
|
76
76
|
// 以 `index.html?xxx=1` 直开的入口(iframe / 选择模式)必须登记,否则首屏守卫会清空 query
|
|
77
77
|
// rootPathAliases: ['/index.html'],
|
|
78
|
+
// 宿主不支持深路径 fallback 时改 hash;需要自定义 base 时传 RouterHistory 实例
|
|
79
|
+
// history: 'hash',
|
|
78
80
|
})
|
|
79
81
|
```
|
|
80
82
|
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { type PropType, type SlotsType, type VNodeChild } from 'vue';
|
|
2
|
+
import { type AppPadding } from './part-padding';
|
|
3
|
+
/** 表单内距:与 `AppPanelPadding` **同一口径同一实现**(`number` = 四边同值;`{ y, x }` = 上下 / 左右分别给) */
|
|
4
|
+
export type AppFormPadding = AppPadding;
|
|
2
5
|
/** label 列的水平对齐:`right`(默认,与 farris / 主流水平表单一致)或 `left` */
|
|
3
6
|
export type AppFormLabelAlign = 'left' | 'right';
|
|
4
7
|
export interface AppFormProps {
|
|
@@ -24,6 +27,14 @@ export interface AppFormProps {
|
|
|
24
27
|
* 在对应 `AppForm.Item` 上给 `fullWidth`,否则会被压成半宽。
|
|
25
28
|
*/
|
|
26
29
|
columns?: 1 | 2;
|
|
30
|
+
/**
|
|
31
|
+
* 表单内距(**px 数字**,与 `AppPanel` 同一口径):`number` = 四边同值;`{ y, x }` = 上下 / 左右分别给。
|
|
32
|
+
*
|
|
33
|
+
* **缺省 0**:表单贴容器边,内距由所在容器给(页面里的表单在 `AppPanel.Body` 里,那块自带内距)。
|
|
34
|
+
* 弹窗 / 抽屉里没有别的容器给内距时用它自带 —— 如 `padding={{ y: 16, x: 20 }}`,
|
|
35
|
+
* 这样就不必再给表单挂一个「只写 padding」的应用侧类。
|
|
36
|
+
*/
|
|
37
|
+
padding?: AppFormPadding;
|
|
27
38
|
}
|
|
28
39
|
export interface AppFormItemProps {
|
|
29
40
|
/**
|
|
@@ -31,7 +42,10 @@ export interface AppFormItemProps {
|
|
|
31
42
|
* —— 用于无字段名的整块内容(如列编辑区)与纯说明行。
|
|
32
43
|
*/
|
|
33
44
|
label?: string;
|
|
34
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* 必填标记:星号渲染在 label 文本**之后**(`服务名称*`)—— 星号是字段名的一部分,
|
|
47
|
+
* 跟着字段名走;与 farris 动态表单「星号在左」的写法**有意不同**(本仓口径)。
|
|
48
|
+
*/
|
|
35
49
|
required?: boolean;
|
|
36
50
|
/** 校验错误文案:非空即进入 invalid 态,文案排布在控件**下方**(不遮挡后续行) */
|
|
37
51
|
error?: string;
|
|
@@ -75,7 +89,8 @@ export interface AppFormItemProps {
|
|
|
75
89
|
* (inline 6rem 右对齐、字段 max-width 26.625rem),校验信息走**绝对定位的浮层提示条**。
|
|
76
90
|
* 与本仓三条口径冲突:① 控件由消费方直接写(不构造底层配置对象);② 数据入参只收朴素业务值;
|
|
77
91
|
* ③ 错误与说明按文档流排布(浮层在弹窗里会压住下一行的控件)。故按「缺件处置流程」自建,
|
|
78
|
-
*
|
|
92
|
+
* 必填星号固定在 label 文本**之后**(`服务名称*`,有意偏离 farris 的「星号在左」);
|
|
93
|
+
* **label 对齐是维度**(`labelAlign`,
|
|
79
94
|
* 默认 `right`(与 farris / 主流水平表单一致),`left` 时所有 label 起点一致)。
|
|
80
95
|
*
|
|
81
96
|
* ## 宽表单怎么处理(两条杠杆,可叠加)
|
|
@@ -85,9 +100,15 @@ export interface AppFormItemProps {
|
|
|
85
100
|
* `--ui-form-column-min`(默认 320),所以窄容器会**自动回落成一列**,不需要消费方写媒体查询。
|
|
86
101
|
* 两列下「整块内容」记得 `fullWidth`。
|
|
87
102
|
*
|
|
88
|
-
* `AppForm.Item` 是唯一入口(`AppForm`
|
|
103
|
+
* `AppForm.Item` 是唯一入口(`AppForm` 只承载表单级设置、行间距/列数与容器内距),
|
|
89
104
|
* 控件从默认插槽进,因此输入框 / 下拉 / 文本域 / 自绘分组卡片共用同一个容器。
|
|
90
105
|
*
|
|
106
|
+
* ## 容器内距(`padding`)
|
|
107
|
+
*
|
|
108
|
+
* 与 `AppPanel` **同一口径同一实现**(`src/components/part-padding.ts`):`number` 四边同值、
|
|
109
|
+
* `{ y, x }` 上下 / 左右分别给,缺省 0(贴容器边)。页面里的表单在 `AppPanel.Body` 里、内距由那块给;
|
|
110
|
+
* **弹窗 / 抽屉里没有别的容器**给内距,就用表单自带的 `padding={{ y: 16, x: 20 }}`。
|
|
111
|
+
*
|
|
91
112
|
* ## 静态表单(摘要 / 详情)
|
|
92
113
|
*
|
|
93
114
|
* `AppForm.Item` 传 `text` 即切成**只读文本行**,三类行可以在同一个表单里混排 ——
|
|
@@ -253,7 +274,13 @@ export declare const AppForm: {
|
|
|
253
274
|
type: PropType<1 | 2>;
|
|
254
275
|
default: number;
|
|
255
276
|
};
|
|
277
|
+
/** 表单内距(px 数字,或 `{ y, x }`);缺省 0 = 贴容器边(弹窗 / 抽屉里常用它自带内距) */
|
|
278
|
+
padding: {
|
|
279
|
+
type: PropType<AppFormPadding>;
|
|
280
|
+
default: undefined;
|
|
281
|
+
};
|
|
256
282
|
}>> & Readonly<{}>, () => import("vue/jsx-runtime").JSX.Element, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, import("vue").PublicProps, {
|
|
283
|
+
padding: AppPadding;
|
|
257
284
|
columns: 1 | 2;
|
|
258
285
|
labelWidth: number;
|
|
259
286
|
labelAlign: AppFormLabelAlign;
|
|
@@ -288,7 +315,13 @@ export declare const AppForm: {
|
|
|
288
315
|
type: PropType<1 | 2>;
|
|
289
316
|
default: number;
|
|
290
317
|
};
|
|
318
|
+
/** 表单内距(px 数字,或 `{ y, x }`);缺省 0 = 贴容器边(弹窗 / 抽屉里常用它自带内距) */
|
|
319
|
+
padding: {
|
|
320
|
+
type: PropType<AppFormPadding>;
|
|
321
|
+
default: undefined;
|
|
322
|
+
};
|
|
291
323
|
}>> & Readonly<{}>, () => import("vue/jsx-runtime").JSX.Element, {}, {}, {}, {
|
|
324
|
+
padding: AppPadding;
|
|
292
325
|
columns: 1 | 2;
|
|
293
326
|
labelWidth: number;
|
|
294
327
|
labelAlign: AppFormLabelAlign;
|
|
@@ -318,7 +351,13 @@ export declare const AppForm: {
|
|
|
318
351
|
type: PropType<1 | 2>;
|
|
319
352
|
default: number;
|
|
320
353
|
};
|
|
354
|
+
/** 表单内距(px 数字,或 `{ y, x }`);缺省 0 = 贴容器边(弹窗 / 抽屉里常用它自带内距) */
|
|
355
|
+
padding: {
|
|
356
|
+
type: PropType<AppFormPadding>;
|
|
357
|
+
default: undefined;
|
|
358
|
+
};
|
|
321
359
|
}>> & Readonly<{}>, () => import("vue/jsx-runtime").JSX.Element, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, {
|
|
360
|
+
padding: AppPadding;
|
|
322
361
|
columns: 1 | 2;
|
|
323
362
|
labelWidth: number;
|
|
324
363
|
labelAlign: AppFormLabelAlign;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineComponent, provide, toRef, createVNode, inject, createTextVNode } from "vue";
|
|
2
2
|
import { AppSection } from "./app-section.js";
|
|
3
|
+
import { mergePartStyle } from "./part-padding.js";
|
|
3
4
|
const FORM_LABEL_WIDTH = /* @__PURE__ */ Symbol("ak-form-label-width");
|
|
4
5
|
const FORM_CONTROL_WIDTH = /* @__PURE__ */ Symbol("ak-form-control-width");
|
|
5
6
|
const FORM_LABEL_ALIGN = /* @__PURE__ */ Symbol("ak-form-label-align");
|
|
@@ -85,11 +86,11 @@ const AppFormItem = /* @__PURE__ */ defineComponent({
|
|
|
85
86
|
"style": styles
|
|
86
87
|
}, [showLabel ? createVNode("label", {
|
|
87
88
|
"class": "ak-form-item__label"
|
|
88
|
-
}, [
|
|
89
|
-
"class": "ak-form-item__required"
|
|
90
|
-
}, [createTextVNode("*")]) : null, createVNode("span", {
|
|
89
|
+
}, [createVNode("span", {
|
|
91
90
|
"class": "ak-form-item__label-text"
|
|
92
|
-
}, [props.label])
|
|
91
|
+
}, [props.label]), props.required ? createVNode("span", {
|
|
92
|
+
"class": "ak-form-item__required"
|
|
93
|
+
}, [createTextVNode("*")]) : null]) : null, createVNode("div", {
|
|
93
94
|
"class": "ak-form-item__control",
|
|
94
95
|
"style": controlWidth ? {
|
|
95
96
|
maxWidth: `${controlWidth}px`
|
|
@@ -134,6 +135,11 @@ const AppFormRoot = /* @__PURE__ */ defineComponent({
|
|
|
134
135
|
columns: {
|
|
135
136
|
type: Number,
|
|
136
137
|
default: 1
|
|
138
|
+
},
|
|
139
|
+
/** 表单内距(px 数字,或 `{ y, x }`);缺省 0 = 贴容器边(弹窗 / 抽屉里常用它自带内距) */
|
|
140
|
+
padding: {
|
|
141
|
+
type: [Number, Object],
|
|
142
|
+
default: void 0
|
|
137
143
|
}
|
|
138
144
|
},
|
|
139
145
|
slots: Object,
|
|
@@ -145,7 +151,8 @@ const AppFormRoot = /* @__PURE__ */ defineComponent({
|
|
|
145
151
|
provide(FORM_LABEL_ALIGN, toRef(props, "labelAlign"));
|
|
146
152
|
provide(FORM_CONTROL_WIDTH, toRef(props, "controlWidth"));
|
|
147
153
|
return () => createVNode("div", {
|
|
148
|
-
"class": ["ak-form", props.columns === 2 ? "ak-form--two-column" : "", attrs.class].filter(Boolean).join(" ")
|
|
154
|
+
"class": ["ak-form", props.columns === 2 ? "ak-form--two-column" : "", attrs.class].filter(Boolean).join(" "),
|
|
155
|
+
"style": mergePartStyle(attrs.style, props.padding)
|
|
149
156
|
}, [slots.default?.()]);
|
|
150
157
|
}
|
|
151
158
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type PropType, type SlotsType, type VNodeChild } from 'vue';
|
|
2
|
+
import { type AppPadding } from './part-padding';
|
|
2
3
|
/**
|
|
3
4
|
* 区域容器(页面内分区)—— 与 `AppShell` 的分工:
|
|
4
5
|
*
|
|
@@ -85,11 +86,8 @@ export interface AppPanelProps {
|
|
|
85
86
|
*/
|
|
86
87
|
padding?: AppPanelPadding;
|
|
87
88
|
}
|
|
88
|
-
/**
|
|
89
|
-
export type AppPanelPadding =
|
|
90
|
-
y?: number;
|
|
91
|
-
x?: number;
|
|
92
|
-
};
|
|
89
|
+
/** 面板(及其三件)的内距:与 `AppForm` / `AppPanel` 共用一套口径(`number` = 四边同值;`{ y, x }` 分别给) */
|
|
90
|
+
export type AppPanelPadding = AppPadding;
|
|
93
91
|
/** 面板标题行(区域级):标题 + 本区域筛选位(toolbar) + 本区域操作位(actions) */
|
|
94
92
|
export declare const AppPanelHeader: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
95
93
|
/**
|
|
@@ -143,7 +141,7 @@ export declare const AppPanelHeader: import("vue").DefineComponent<import("vue")
|
|
|
143
141
|
title: string;
|
|
144
142
|
toolbar: any;
|
|
145
143
|
actions: any;
|
|
146
|
-
padding:
|
|
144
|
+
padding: AppPadding;
|
|
147
145
|
}, SlotsType<{
|
|
148
146
|
header?: () => VNodeChild;
|
|
149
147
|
toolbar?: () => VNodeChild;
|
|
@@ -171,7 +169,7 @@ export declare const AppPanelBody: import("vue").DefineComponent<import("vue").E
|
|
|
171
169
|
default: undefined;
|
|
172
170
|
};
|
|
173
171
|
}>> & Readonly<{}>, {
|
|
174
|
-
padding:
|
|
172
|
+
padding: AppPadding;
|
|
175
173
|
}, SlotsType<{
|
|
176
174
|
default?: () => VNodeChild;
|
|
177
175
|
}>, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
@@ -194,7 +192,7 @@ export declare const AppPanelFooter: import("vue").DefineComponent<import("vue")
|
|
|
194
192
|
default: undefined;
|
|
195
193
|
};
|
|
196
194
|
}>> & Readonly<{}>, {
|
|
197
|
-
padding:
|
|
195
|
+
padding: AppPadding;
|
|
198
196
|
}, SlotsType<{
|
|
199
197
|
default?: () => VNodeChild;
|
|
200
198
|
}>, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
@@ -228,7 +226,7 @@ export declare const AppPanel: {
|
|
|
228
226
|
title: string;
|
|
229
227
|
toolbar: any;
|
|
230
228
|
actions: any;
|
|
231
|
-
padding:
|
|
229
|
+
padding: AppPadding;
|
|
232
230
|
}, true, {}, SlotsType<{
|
|
233
231
|
default?: () => VNodeChild;
|
|
234
232
|
header?: () => VNodeChild;
|
|
@@ -266,7 +264,7 @@ export declare const AppPanel: {
|
|
|
266
264
|
title: string;
|
|
267
265
|
toolbar: any;
|
|
268
266
|
actions: any;
|
|
269
|
-
padding:
|
|
267
|
+
padding: AppPadding;
|
|
270
268
|
}>;
|
|
271
269
|
__isFragment?: never;
|
|
272
270
|
__isTeleport?: never;
|
|
@@ -296,7 +294,7 @@ export declare const AppPanel: {
|
|
|
296
294
|
title: string;
|
|
297
295
|
toolbar: any;
|
|
298
296
|
actions: any;
|
|
299
|
-
padding:
|
|
297
|
+
padding: AppPadding;
|
|
300
298
|
}, {}, string, SlotsType<{
|
|
301
299
|
default?: () => VNodeChild;
|
|
302
300
|
header?: () => VNodeChild;
|
|
@@ -355,7 +353,7 @@ export declare const AppPanel: {
|
|
|
355
353
|
title: string;
|
|
356
354
|
toolbar: any;
|
|
357
355
|
actions: any;
|
|
358
|
-
padding:
|
|
356
|
+
padding: AppPadding;
|
|
359
357
|
}, SlotsType<{
|
|
360
358
|
header?: () => VNodeChild;
|
|
361
359
|
toolbar?: () => VNodeChild;
|
|
@@ -374,7 +372,7 @@ export declare const AppPanel: {
|
|
|
374
372
|
default: undefined;
|
|
375
373
|
};
|
|
376
374
|
}>> & Readonly<{}>, {
|
|
377
|
-
padding:
|
|
375
|
+
padding: AppPadding;
|
|
378
376
|
}, SlotsType<{
|
|
379
377
|
default?: () => VNodeChild;
|
|
380
378
|
}>, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
@@ -391,7 +389,7 @@ export declare const AppPanel: {
|
|
|
391
389
|
default: undefined;
|
|
392
390
|
};
|
|
393
391
|
}>> & Readonly<{}>, {
|
|
394
|
-
padding:
|
|
392
|
+
padding: AppPadding;
|
|
395
393
|
}, SlotsType<{
|
|
396
394
|
default?: () => VNodeChild;
|
|
397
395
|
}>, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
@@ -1,25 +1,5 @@
|
|
|
1
1
|
import { defineComponent, createVNode, h } from "vue";
|
|
2
|
-
|
|
3
|
-
if (padding === void 0) return void 0;
|
|
4
|
-
if (typeof padding === "number") return {
|
|
5
|
-
padding: `${padding}px`
|
|
6
|
-
};
|
|
7
|
-
const style = {};
|
|
8
|
-
if (padding.y !== void 0) {
|
|
9
|
-
style.paddingTop = `${padding.y}px`;
|
|
10
|
-
style.paddingBottom = `${padding.y}px`;
|
|
11
|
-
}
|
|
12
|
-
if (padding.x !== void 0) {
|
|
13
|
-
style.paddingLeft = `${padding.x}px`;
|
|
14
|
-
style.paddingRight = `${padding.x}px`;
|
|
15
|
-
}
|
|
16
|
-
return style;
|
|
17
|
-
}
|
|
18
|
-
function mergePartStyle(attrsStyle, padding) {
|
|
19
|
-
const paddingStyle = panelPaddingStyle(padding);
|
|
20
|
-
if (paddingStyle === void 0) return attrsStyle;
|
|
21
|
-
return [attrsStyle, paddingStyle];
|
|
22
|
-
}
|
|
2
|
+
import { mergePartStyle, partPaddingStyle } from "./part-padding.js";
|
|
23
3
|
const AppPanelHeader = /* @__PURE__ */ defineComponent({
|
|
24
4
|
name: "AppPanelHeader",
|
|
25
5
|
inheritAttrs: false,
|
|
@@ -63,7 +43,7 @@ const AppPanelHeader = /* @__PURE__ */ defineComponent({
|
|
|
63
43
|
const actions = renderSlotValue(slots.actions, props.actions);
|
|
64
44
|
if (slots.header) {
|
|
65
45
|
const custom = slots.header();
|
|
66
|
-
const paddingStyle =
|
|
46
|
+
const paddingStyle = partPaddingStyle(props.padding);
|
|
67
47
|
return paddingStyle === void 0 ? custom : createVNode("div", {
|
|
68
48
|
"style": paddingStyle
|
|
69
49
|
}, [custom]);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { StyleValue } from 'vue';
|
|
2
|
+
/**
|
|
3
|
+
* 骨架件的**内距**口径(`AppPanel` 及其三件、`AppForm` 共用同一套)。
|
|
4
|
+
*
|
|
5
|
+
* - `number` → 四边同值(`padding={16}`);
|
|
6
|
+
* - `{ y, x }` → 上下 / 左右分别给(`{ y: 12, x: 20 }`,弹窗里的容器常用 —— 与 Shell 的页面左右内距同档),
|
|
7
|
+
* **缺哪边就不落哪边的声明**(只给 `y` 时不覆盖消费方在 `style` 里写的左右内距)。
|
|
8
|
+
*
|
|
9
|
+
* 为什么是 px 数字而不是 CSS 字符串:几何值在本仓是**设计基准**,不跟根字号
|
|
10
|
+
* (与 `AppForm.labelWidth` / `AppTable.rowHeight` 同口径);要写自适应内距的场合请走 `class`。
|
|
11
|
+
*/
|
|
12
|
+
export type AppPadding = number | {
|
|
13
|
+
y?: number;
|
|
14
|
+
x?: number;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* 内距 → 行内样式。缺省(`undefined`)时返回 `undefined`:**不额外产生 style 属性**,
|
|
18
|
+
* DOM 与「没有这个维度」时完全一致。
|
|
19
|
+
*/
|
|
20
|
+
export declare function partPaddingStyle(padding: AppPadding | undefined): StyleValue | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* 合并「消费方给的 `style`」与「内距行内样式」:两者都可能有,内距排在后面
|
|
23
|
+
* —— 同一属性以组件的 `padding` 为准(消费方要覆盖就用 `class` 里更具体的规则)。
|
|
24
|
+
*
|
|
25
|
+
* 不给 `padding` 时**原样返回消费方的 style**(含 `undefined`)。
|
|
26
|
+
*/
|
|
27
|
+
export declare function mergePartStyle(attrsStyle: unknown, padding: AppPadding | undefined): StyleValue | undefined;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
function partPaddingStyle(padding) {
|
|
2
|
+
if (padding === void 0) return void 0;
|
|
3
|
+
if (typeof padding === "number") return { padding: `${padding}px` };
|
|
4
|
+
const style = {};
|
|
5
|
+
if (padding.y !== void 0) {
|
|
6
|
+
style.paddingTop = `${padding.y}px`;
|
|
7
|
+
style.paddingBottom = `${padding.y}px`;
|
|
8
|
+
}
|
|
9
|
+
if (padding.x !== void 0) {
|
|
10
|
+
style.paddingLeft = `${padding.x}px`;
|
|
11
|
+
style.paddingRight = `${padding.x}px`;
|
|
12
|
+
}
|
|
13
|
+
return style;
|
|
14
|
+
}
|
|
15
|
+
function mergePartStyle(attrsStyle, padding) {
|
|
16
|
+
const paddingStyle = partPaddingStyle(padding);
|
|
17
|
+
if (paddingStyle === void 0) return attrsStyle;
|
|
18
|
+
return [attrsStyle, paddingStyle];
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
mergePartStyle,
|
|
22
|
+
partPaddingStyle
|
|
23
|
+
};
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { QueryClient } from '@tanstack/vue-query';
|
|
2
2
|
import { type App as VueApp, type Component, type Plugin } from 'vue';
|
|
3
|
-
import { type RouteRecordRaw } from 'vue-router';
|
|
3
|
+
import { type RouterHistory, type RouteRecordRaw } from 'vue-router';
|
|
4
|
+
/**
|
|
5
|
+
* 路由历史模式:
|
|
6
|
+
* - `'history'`(缺省):正常 history,micro-app 以 baseroute 隔离子应用路由;
|
|
7
|
+
* - `'hash'`:宿主**不支持深路径 fallback**(刷新深路径 404)、或门户不允许子应用改写地址栏时用;
|
|
8
|
+
* - 传 `RouterHistory` 实例(如 `createWebHistory('/subapp/x/')`):自行定制 base 等细节。
|
|
9
|
+
*/
|
|
10
|
+
export type CreateSubAppHistory = 'history' | 'hash' | RouterHistory;
|
|
4
11
|
export interface CreateSubAppOptions {
|
|
5
12
|
rootComponent: Component;
|
|
6
13
|
routes: RouteRecordRaw[];
|
|
14
|
+
/** 路由历史模式(缺省 `'history'`)。宿主是否支持深路径 fallback 见 `CONTRACT.md` §9 第 13 条 */
|
|
15
|
+
history?: CreateSubAppHistory;
|
|
7
16
|
i18n?: {
|
|
8
17
|
resources: Record<string, Record<string, unknown>>;
|
|
9
18
|
fallbackLng?: string;
|
|
@@ -32,6 +41,9 @@ export interface CreateSubAppOptions {
|
|
|
32
41
|
* farris 插件(app.use(Farris))注册在本工厂内部 —— farris 只允许存在于 app-kit,
|
|
33
42
|
* 应用侧不得 import '@farris/ui-vue'(oxlint 护栏)。
|
|
34
43
|
*
|
|
44
|
+
* 路由历史模式由 `history` 决定:缺省 `'history'`(micro-app 以 baseroute 隔离),
|
|
45
|
+
* 宿主不支持深路径 fallback 时传 `'hash'`,需要自定义 base 等细节时直接传 `RouterHistory` 实例。
|
|
46
|
+
*
|
|
35
47
|
* @example
|
|
36
48
|
* export const { mount, unmount } = createSubApp({
|
|
37
49
|
* rootComponent: Root,
|
|
@@ -2,7 +2,7 @@ import Farris from "@farris/ui-vue";
|
|
|
2
2
|
import { VueQueryPlugin } from "@tanstack/vue-query";
|
|
3
3
|
import { createPinia } from "pinia";
|
|
4
4
|
import { defineComponent, ref, provide, h, createApp } from "vue";
|
|
5
|
-
import { createRouter, createWebHistory } from "vue-router";
|
|
5
|
+
import { createRouter, createWebHistory, createWebHashHistory } from "vue-router";
|
|
6
6
|
import { installInitialGuard } from "./initial-guard.js";
|
|
7
7
|
import { createQueryClient } from "./create-query-client.js";
|
|
8
8
|
import { setupI18n } from "../providers/setup-i18n.js";
|
|
@@ -11,6 +11,7 @@ function createSubApp(options) {
|
|
|
11
11
|
const {
|
|
12
12
|
rootComponent,
|
|
13
13
|
routes,
|
|
14
|
+
history: historyMode = "history",
|
|
14
15
|
i18n,
|
|
15
16
|
queryClient,
|
|
16
17
|
extraPlugins = [],
|
|
@@ -26,7 +27,7 @@ function createSubApp(options) {
|
|
|
26
27
|
let removeGlobalDataListener = null;
|
|
27
28
|
let i18nInstance = null;
|
|
28
29
|
const mount = () => {
|
|
29
|
-
router = createRouter({ history:
|
|
30
|
+
router = createRouter({ history: resolveRouterHistory(historyMode), routes });
|
|
30
31
|
const ContainerRoot = defineComponent({
|
|
31
32
|
name: "AppKitRoot",
|
|
32
33
|
setup() {
|
|
@@ -95,6 +96,11 @@ function createSubApp(options) {
|
|
|
95
96
|
}
|
|
96
97
|
return { mount, unmount };
|
|
97
98
|
}
|
|
99
|
+
function resolveRouterHistory(value) {
|
|
100
|
+
if (value === "history") return createWebHistory();
|
|
101
|
+
if (value === "hash") return createWebHashHistory();
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
98
104
|
function exposeToHost(mount, unmount) {
|
|
99
105
|
if (typeof window === "undefined") return;
|
|
100
106
|
const w = window;
|
package/dist/entry/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createSubApp, type CreateSubAppOptions } from './create-sub-app';
|
|
1
|
+
export { createSubApp, type CreateSubAppHistory, type CreateSubAppOptions } from './create-sub-app';
|
|
2
2
|
export { installInitialGuard } from './initial-guard';
|
|
3
3
|
export { createQueryClient } from './create-query-client';
|
|
4
4
|
export { setupI18n, i18n, type SetupI18nOptions } from '../providers/setup-i18n';
|
package/dist/index.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export { AppBadge, type AppBadgeTone, type AppBadgeShape, type AppBadgeSize, typ
|
|
|
26
26
|
export { AppDialog } from './components/app-dialog';
|
|
27
27
|
export { AppDrawer } from './components/app-drawer';
|
|
28
28
|
export { AppTabs, type AppTabItem } from './components/app-tabs';
|
|
29
|
-
export { AppForm, type AppFormProps, type AppFormItemProps, type AppFormLabelAlign, type AppFormCompound, } from './components/app-form';
|
|
29
|
+
export { AppForm, type AppFormProps, type AppFormItemProps, type AppFormLabelAlign, type AppFormPadding, type AppFormCompound, } from './components/app-form';
|
|
30
30
|
export { AppSection, type AppSectionProps } from './components/app-section';
|
|
31
31
|
export { AppLayout, type AppLayoutGap, type AppLayoutAlign, type AppLayoutJustify, type AppLayoutColumns, type AppLayoutSpan, } from './atoms/app-layout';
|
|
32
32
|
export { AppSteps, type AppStepItem } from './components/app-steps';
|
package/dist/shell/AppShell.d.ts
CHANGED
|
@@ -2,6 +2,11 @@ import { type PropType, type SlotsType, type VNodeChild } from 'vue';
|
|
|
2
2
|
export interface AppShellHeaderProps {
|
|
3
3
|
/** 传入 title 走内置布局;不传则纯 slot 直出(自定义页头出口) */
|
|
4
4
|
title?: string;
|
|
5
|
+
/**
|
|
6
|
+
* 页头副标题:标题**下一行**的次要说明(对象名、统计口径等),随 `title` 一起进内置布局。
|
|
7
|
+
* 不传即不渲染(页头条高度不变);`title` 不传时本项不生效(整块走自定义页头出口)。
|
|
8
|
+
*/
|
|
9
|
+
subTitle?: string;
|
|
5
10
|
/**
|
|
6
11
|
* 标题左侧图标:**svg 节点或返回节点的函数**皆可。缺省渲染设计稿默认图标
|
|
7
12
|
* (MH后台-0911 node 0:3904:26×26 圆角方块 #2660FF)。
|
|
@@ -55,6 +60,10 @@ export declare const AppShell: {
|
|
|
55
60
|
type: StringConstructor;
|
|
56
61
|
default: undefined;
|
|
57
62
|
};
|
|
63
|
+
subTitle: {
|
|
64
|
+
type: StringConstructor;
|
|
65
|
+
default: undefined;
|
|
66
|
+
};
|
|
58
67
|
icon: {
|
|
59
68
|
type: PropType<any>;
|
|
60
69
|
default: undefined;
|
|
@@ -74,6 +83,10 @@ export declare const AppShell: {
|
|
|
74
83
|
type: StringConstructor;
|
|
75
84
|
default: undefined;
|
|
76
85
|
};
|
|
86
|
+
subTitle: {
|
|
87
|
+
type: StringConstructor;
|
|
88
|
+
default: undefined;
|
|
89
|
+
};
|
|
77
90
|
icon: {
|
|
78
91
|
type: PropType<any>;
|
|
79
92
|
default: undefined;
|
|
@@ -88,6 +101,7 @@ export declare const AppShell: {
|
|
|
88
101
|
};
|
|
89
102
|
}>> & Readonly<{}>, {
|
|
90
103
|
title: string;
|
|
104
|
+
subTitle: string;
|
|
91
105
|
icon: any;
|
|
92
106
|
toolbar: any;
|
|
93
107
|
tabs: any;
|
package/dist/shell/AppShell.js
CHANGED
|
@@ -36,6 +36,10 @@ const AppShellHeader = /* @__PURE__ */ defineComponent({
|
|
|
36
36
|
type: String,
|
|
37
37
|
default: void 0
|
|
38
38
|
},
|
|
39
|
+
subTitle: {
|
|
40
|
+
type: String,
|
|
41
|
+
default: void 0
|
|
42
|
+
},
|
|
39
43
|
icon: {
|
|
40
44
|
type: [Function, Object, Array, String],
|
|
41
45
|
default: void 0
|
|
@@ -81,7 +85,9 @@ const AppShellHeader = /* @__PURE__ */ defineComponent({
|
|
|
81
85
|
"class": "ak-shell-header-icon"
|
|
82
86
|
}, [renderIcon()]), createVNode("h1", {
|
|
83
87
|
"class": "ak-shell-header-title"
|
|
84
|
-
}, [props.title])])
|
|
88
|
+
}, [props.title])]), props.subTitle ? createVNode("div", {
|
|
89
|
+
"class": "ak-shell-header-subtitle"
|
|
90
|
+
}, [props.subTitle]) : null]), toolbar ? createVNode("div", {
|
|
85
91
|
"class": "ak-shell-header-toolbar"
|
|
86
92
|
}, [toolbar]) : null]), tabs ? createVNode("div", {
|
|
87
93
|
"class": "ak-shell-header-tabs"
|
package/dist/shell/shell.css
CHANGED
|
@@ -78,6 +78,19 @@
|
|
|
78
78
|
color: var(--ui-base-content);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/* 页头副标题(Header 的 subTitle):标题下一行的次要说明,取 caption 档字号与次要色。
|
|
82
|
+
间距只用 margin-top 4:页头条的上下留白由 .ak-shell-header 的 padding 提供,
|
|
83
|
+
这里只负责标题行与副标题行之间的距离 —— 有副标题时条目高度自然变高(56 → 80),
|
|
84
|
+
不再另调 padding(避免「有/无副标题」两套页头规格)。
|
|
85
|
+
工具条与文本块**底线对齐**是 .ak-shell-header-inner 的既有口径(flex-end),
|
|
86
|
+
有副标题时即与副标题行对齐,不在本项里改。 */
|
|
87
|
+
.ak-shell-header-subtitle {
|
|
88
|
+
margin-top: var(--ui-space-1);
|
|
89
|
+
font-size: var(--ui-font-caption);
|
|
90
|
+
line-height: var(--ui-line-caption);
|
|
91
|
+
color: var(--ui-base-content-muted);
|
|
92
|
+
}
|
|
93
|
+
|
|
81
94
|
/* 筛选区块(Header 上方的第一子块):query-solution / 筛选条。
|
|
82
95
|
无边框;padding-bottom 恒为 0 —— 与 Header 的间距由 Header 自身的 top padding
|
|
83
96
|
(14px)提供,避免两块 padding 叠加成双倍间距 */
|
|
@@ -150,6 +150,29 @@ test('S4:原生按钮、「无 href 的锚点」、跨行写法的原生输入
|
|
|
150
150
|
}
|
|
151
151
|
})
|
|
152
152
|
|
|
153
|
+
test('S1:路由页判定按目录约定 —— 嵌套页面目录仍算,components/ 子树不算', () => {
|
|
154
|
+
const root = makeConsumer('violations-src')
|
|
155
|
+
try {
|
|
156
|
+
const { out } = runAudit('structure-audit.mjs', root)
|
|
157
|
+
const hits = out.split('\n').filter((line) => line.includes('structure/no-shell'))
|
|
158
|
+
assert.ok(
|
|
159
|
+
hits.some((line) => line.includes('no-shell-page.tsx')),
|
|
160
|
+
`views 直属的路由页应命中,实际:${hits.join(' | ')}`,
|
|
161
|
+
)
|
|
162
|
+
assert.ok(
|
|
163
|
+
hits.some((line) => line.includes('nested-dir/inner-page.tsx')),
|
|
164
|
+
`嵌套页面目录下的路由页不能被漏掉(那正是「只认 views 直属文件」的收窄写法),实际:${hits.join(' | ')}`,
|
|
165
|
+
)
|
|
166
|
+
// 弹窗 / 抽屉这类可复用件放同页面的 components/ 子目录 → 不是路由页,不该套 AppShell(契约 §4.3 / §4.10)
|
|
167
|
+
assert.ok(
|
|
168
|
+
!out.includes('picker-dialog.tsx'),
|
|
169
|
+
`components/ 子树内的可复用件不该报 structure/no-shell,实际输出:\n${out}`,
|
|
170
|
+
)
|
|
171
|
+
} finally {
|
|
172
|
+
rmSync(root, { recursive: true, force: true })
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
|
|
153
176
|
test('合规样本:三条护栏必须全绿(不得误报)', () => {
|
|
154
177
|
const root = makeConsumer('clean-src')
|
|
155
178
|
try {
|
package/lint/shared.mjs
CHANGED
|
@@ -304,7 +304,8 @@ export const CORRECTIONS = {
|
|
|
304
304
|
doc: 'CONTRACT.md#3-统一-prop-词表',
|
|
305
305
|
},
|
|
306
306
|
'structure/no-shell': {
|
|
307
|
-
summary:
|
|
307
|
+
summary:
|
|
308
|
+
'路由级页面必须用 AppShell 承载结构(页面内的可复用件放同页面的 components/ 子目录,不属于路由页、不受本规则约束)。',
|
|
308
309
|
example: '<AppShell><AppShell.Header title="…" /><AppShell.Body mode="table">…</AppShell.Body></AppShell>',
|
|
309
310
|
doc: 'CONTRACT.md#41-三种页面模板',
|
|
310
311
|
},
|
package/lint/structure-audit.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* structure-audit —— 页面结构护栏(S1–S7 + 自绘竖直边框检查)。
|
|
4
4
|
*
|
|
5
|
-
* S1 路由级页面必须用 AppShell
|
|
5
|
+
* S1 路由级页面必须用 AppShell(views|pages 下的 tsx;`components/` 子树内的可复用件除外)
|
|
6
6
|
* S2 AppShell.Header 必须传 title(或登记为自定义页头白名单)
|
|
7
7
|
* S3 AppShell.Body 必须显式声明 mode
|
|
8
8
|
* S4 禁原生 button/input/select/textarea 带 class 承载外观
|
|
@@ -32,8 +32,22 @@ const cwd = args.cwd
|
|
|
32
32
|
const config = loadConfig(cwd)
|
|
33
33
|
const changed = args.changed ? changedFiles(cwd) : null
|
|
34
34
|
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* 路由级页面的判定:**`views|pages` 目录下的 tsx,但 `components/` 子树不算**。
|
|
37
|
+
*
|
|
38
|
+
* 页面内的可复用件(弹窗 / 抽屉 / 表单块 / 子表格)按通行约定放在**同页面的 `components/` 子目录**下,
|
|
39
|
+
* 它们该由 `AppPanel` / `AppForm` / `AppDialog` 承载、不套 `AppShell`(契约 §4.3 / §4.7 / §4.10)。
|
|
40
|
+
*
|
|
41
|
+
* 早期实现只判「路径里有没有 `views|pages/`」,于是 `views/<页>/components/*.tsx` 全被当成路由页报 S1
|
|
42
|
+
* —— 契约要求的目录写法反而被判违规(真实误报:某应用 13 个 tsx 全中,其中 10 个是弹窗/表单/抽屉)。
|
|
43
|
+
* 这里按**目录段**排除,判据与契约的目录约定对齐。
|
|
44
|
+
*
|
|
45
|
+
* 注意:**嵌套的页面目录(`views/<页>/<页>.tsx`)仍然是路由页**,不要收窄成「只认 views 的直属文件」,
|
|
46
|
+
* 那会让嵌套页面目录下的真路由页静默漏报。
|
|
47
|
+
*/
|
|
36
48
|
const ROUTE_PAGE_RE = /(?:^|\/)(?:views|pages)\//
|
|
49
|
+
/** 路由页目录内的可复用件目录:S1 不查这里面的文件(`components-x` 这类同前缀目录不误伤) */
|
|
50
|
+
const PAGE_PARTIAL_DIR_RE = /(?:^|\/)components\//
|
|
37
51
|
/** 表单控件(S7 计数用)。 */
|
|
38
52
|
const FORM_CONTROLS = /<(AppInput|AppSearchBox|AppSelect|AppDatePicker|AppTextarea|AppNumber)\b/g
|
|
39
53
|
/** 自绘页头/面板头类名(S6)。 */
|
|
@@ -85,8 +99,9 @@ for (const app of config.apps) {
|
|
|
85
99
|
const hasShell = /<AppShell\b/.test(source)
|
|
86
100
|
if (/<AppShell\.Split\b/.test(source)) usesSplit = true
|
|
87
101
|
|
|
88
|
-
// S1:路由级页面必须用 AppShell
|
|
89
|
-
|
|
102
|
+
// S1:路由级页面必须用 AppShell(`views|pages` 下、但不在 `components/` 子树内)
|
|
103
|
+
const isRoutePage = ROUTE_PAGE_RE.test(rel) && !PAGE_PARTIAL_DIR_RE.test(rel)
|
|
104
|
+
if (isRoutePage && file.endsWith('.tsx') && !hasShell) {
|
|
90
105
|
violations.push(violation(app.name, file, 'structure/no-shell', 1, '路由级页面必须使用 AppShell 承载结构', rel))
|
|
91
106
|
}
|
|
92
107
|
// S2 / S3
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@manohub/app-kit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "子应用统一骨架层:入口编排(createSubApp)、布局契约(AppShell)、页面组件、原子件、服务、样式底座。farris 被收敛在本包内部,对外只暴露标准 API。",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
65
|
"@farris/ui-vue": "^1.8.4",
|
|
66
|
-
"@manohub/icon": "^0.2.
|
|
66
|
+
"@manohub/icon": "^0.2.9"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@tanstack/vue-query": "catalog:",
|
|
@@ -54,6 +54,8 @@ export const { mount, unmount } = createSubApp({
|
|
|
54
54
|
// extraPlugins: [i18n],
|
|
55
55
|
// 以 `index.html?xxx=1` 直开的入口(iframe / 选择模式)必须登记,否则首屏守卫会清空 query
|
|
56
56
|
// rootPathAliases: ['/index.html'],
|
|
57
|
+
// 宿主不支持深路径 fallback(刷新深路径 404)时改 hash;需要自定义 base 时传 RouterHistory 实例
|
|
58
|
+
// history: 'hash',
|
|
57
59
|
})
|
|
58
60
|
```
|
|
59
61
|
|
|
@@ -62,6 +64,7 @@ export const { mount, unmount } = createSubApp({
|
|
|
62
64
|
| 选项 | 用途 |
|
|
63
65
|
|---|---|
|
|
64
66
|
| `rootComponent` / `routes` | 根组件与路由表(路由由应用维护,骨架层只负责装载与守卫) |
|
|
67
|
+
| `history` | 路由历史模式:缺省 `'history'`(micro-app 以 baseroute 隔离);宿主不支持深路径 fallback(刷新深路径 404)或不允许子应用改写地址栏时传 `'hash'`;要自定义 base 等细节则直接传 vue-router 的 `RouterHistory` 实例 |
|
|
65
68
|
| `extraPlugins` | 额外插件(vue-i18n、埋点等);**i18next 不用它** |
|
|
66
69
|
| `i18n` | i18next 语义:`{ resources: { en: { translation: … }, zh: … }, fallbackLng? }`,随包做宿主语言切换 |
|
|
67
70
|
| `queryClient` | 已有 `@tanstack/vue-query` 实例时传入;不传则骨架层自建(不要传第二份) |
|
|
@@ -146,6 +149,7 @@ pnpm exec appkit install --dry-run # 只预览
|
|
|
146
149
|
| 类型检查报「无法解析 `*.css`」 | 消费方 `types` 未包含 `vite/client`,或引入了其它包的 CSS 声明 | 补齐 `types: ["vite/client"]` |
|
|
147
150
|
| 弹层/下拉落点偏移 | 门户容器不是文档原点 | 应用侧只写变量(`--ibp-popup-shift-*`),位移规则由包内桥接层负责 |
|
|
148
151
|
| 页面被跳到根路径、query 丢了 | 直开入口未登记 `rootPathAliases` | 见 §3 注释与契约 §9 |
|
|
152
|
+
| 刷新深路径 404(或子应用路由与宿主地址栏互相污染) | 宿主没有 SPA fallback、或不允子应用改写地址栏 | 入口传 `history: 'hash'`,路由改落 hash(见 §3 选项表与契约 §9 第 13 条) |
|
|
149
153
|
| 装到的版本低于 0.1.1 | 0.1.0 已作废 | 升级到 0.1.1+ |
|
|
150
154
|
|
|
151
155
|
## 9. 新应用从零搭建(最小骨架)
|
|
@@ -194,6 +198,7 @@ pnpm add vue vue-router pinia @tanstack/vue-query i18next i18next-vue i18next-br
|
|
|
194
198
|
|---|---|
|
|
195
199
|
| 子应用产物路径与挂载方式(如 `/subapp/<group>/<app>/index.html`) | 宿主按约定路径加载,写错等于打不开 |
|
|
196
200
|
| 是否有 `index.html?xxx=1` 这类直开入口(iframe / 选择模式) | 有就必须登记 `rootPathAliases`,否则首屏守卫会清空 query(CONTRACT.md §9.2) |
|
|
201
|
+
| **是否支持深路径 fallback**(`/subapp/.../<深路径>` 回落到 `index.html`) | 支持则入口用缺省的 `history`;不支持就得用 `history: 'hash'`,否则刷新即 404(契约 §9 第 13 条) |
|
|
197
202
|
| dev server 端口与代理规则 | 端口冲突与跨域问题都出在这里 |
|
|
198
203
|
| 宿主下发的语言键与初始路由约定 | 语言切换与首屏落点由宿主导 |
|
|
199
204
|
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
| 旧写法 | 新写法 | 注意事项 |
|
|
17
17
|
|---|---|---|
|
|
18
18
|
| 自绘根容器(`display:flex; flex-direction:column` + `height:100vh` + padding) | `AppShell` + `AppShell.Body`(`mode="plain"\|"scroll"\|"table"`) | **禁 `100vh`**:注入宿主容器后会溢出;高度一律 `100%` |
|
|
19
|
-
| 自绘页头(标题 + 关闭 + 操作按钮) | `AppShell.Header title=… toolbar={…}` |
|
|
19
|
+
| 自绘页头(标题 + **副标题** + 关闭 + 操作按钮) | `AppShell.Header title=… subTitle=… toolbar={…}` | 页头规格(高度、间距、副标题行距、按钮形态)归组件;**副标题走 `subTitle`**(跟随业务值直接传文本),不要自绘标题块;选择模式这类极简页可以不给页头,只留 Body |
|
|
20
20
|
| 自绘面板头 / 工具条 / 搜索位 | `AppPanel toolbar={…} actions={…}` 或 `AppPanel.Header` | `toolbar` 是区域筛选(≤3 字段)、`actions` 是区域操作,**两位不可互换**;弹窗内可以不给 title |
|
|
21
21
|
| 自绘表格外框 / 卡片(描边 + 圆角 + 表头) | `AppTable framed` | 卡片外观由内容件承担;`AppPanel` 本身无框 |
|
|
22
22
|
| 自绘双栏 + 手写 1px 竖直分隔线 | `AppShell.Split sidebar={{ width: 208, content: () => … }}` | 中间分隔线由 Split 统一给;**业务侧不得再写 `border-left/right`**;`width` 是内容宽 |
|
|
@@ -110,7 +110,8 @@
|
|
|
110
110
|
|
|
111
111
|
| 旧写法 | 新写法 | 注意事项 |
|
|
112
112
|
|---|---|---|
|
|
113
|
-
| 手写 `createApp` / `mount` / `window.mount` / 微前端协议 / pinia / 路由守卫 / 宿主语言监听 | `createSubApp({ rootComponent, routes, extraPlugins, rootPathAliases, onReady })` | 工厂内部已完成容器包裹、pinia、路由、守卫、协议、非微前端自动挂载;应用侧**不得重复书写**。vue-i18n 走 `extraPlugins`(骨架层内置的 i18n 选项是另一套语义,不要用) |
|
|
113
|
+
| 手写 `createApp` / `mount` / `window.mount` / 微前端协议 / pinia / 路由守卫 / 宿主语言监听 | `createSubApp({ rootComponent, routes, extraPlugins, rootPathAliases, history, onReady })` | 工厂内部已完成容器包裹、pinia、路由、守卫、协议、非微前端自动挂载;应用侧**不得重复书写**。vue-i18n 走 `extraPlugins`(骨架层内置的 i18n 选项是另一套语义,不要用) |
|
|
114
|
+
| `createWebHashHistory()` / 自定义 base 的 `createWebHistory(base)` | `createSubApp({ history: 'hash' })`,或直接把 `RouterHistory` 实例传给 `history` | 历史模式归工厂统一管理:缺省 `history`(micro-app baseroute 口径),宿主不支持深路径 fallback 时才改 hash(契约 §9 第 13 条) |
|
|
114
115
|
| 手写样式导入顺序 | 固定三行:`reset.css` → `styles.css` → `./app.css` | 顺序即契约,不增不减 |
|
|
115
116
|
| 应用侧各自声明底层组件库 / 图标包依赖 | 只声明骨架层包;其余由骨架层的依赖带入 | 应用侧不得安装或 `import` 底层组件库 |
|
|
116
117
|
| 自建 lint 脚本多条 | 只挂 `lint` / `lint:changed` 两条 | 三条护栏由统一入口并发跑 |
|
|
@@ -163,6 +163,7 @@ export APPKIT_APP_DIR=apps/<x>
|
|
|
163
163
|
| 缺口 | 影响 | 处理 |
|
|
164
164
|
|---|---|---|
|
|
165
165
|
| 结构护栏只扫 `.ts` / `.tsx`,不扫 `.vue` | 未纳入骨架的 `.vue` 页面不会报 `structure/no-shell` | 属已知范围,不要以为漏了;要覆盖需先有页面级豁免能力 |
|
|
166
|
+
| `structure/no-shell` 的「路由页」按目录判:`views|pages` 下、**`components/` 子树除外** | 弹窗 / 抽屉 / 表单块放对了目录就不会被报;放错位置(直接在 `views/` 下)仍会被当路由页 | 把这些件收进同页面的 `components/` 子目录(契约 §8);**不要**用「把页面挪深一层」来躲规则 |
|
|
166
167
|
| 骨架层尚未提供的件 | 某些位置只能用现有件近似 | 走契约缺件处置流程,不在应用侧自绘;缺件清单与替代口径见契约 §7.1 |
|
|
167
168
|
| 底层组件库版本跃迁带来的视觉差异 | 个别间距/圆角可能与改造前有细微不同 | 先确认是否骨架层规范使然;确属缺陷则记为骨架层待修项,不在应用侧写覆盖样式 |
|
|
168
169
|
|