@manohub/app-kit 0.2.1 → 0.2.4
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 +208 -9
- package/README.md +9 -5
- package/bin/appkit.mjs +121 -0
- package/dist/components/app-drawer.d.ts +139 -0
- package/dist/components/app-drawer.js +104 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/styles/farris-bridge.css +8 -0
- package/lint/__tests__/guardrails.spec.mjs +154 -0
- package/lint/component-audit.mjs +1 -1
- package/lint/guardrails.config.schema.json +42 -0
- package/lint/pre-commit.sample +38 -26
- package/lint/run-all.mjs +63 -59
- package/lint/shared.mjs +462 -347
- package/lint/structure-audit.mjs +1 -1
- package/lint/style-audit.mjs +1 -1
- package/package.json +7 -3
- package/skills/README.md +32 -10
- package/skills/app-kit/SKILL.md +19 -11
- package/skills/app-kit/references/adoption.md +76 -6
- package/skills/app-kit/references/contract-index.md +7 -2
- package/skills/app-kit-dev/SKILL.md +40 -8
- package/skills/app-kit-dev/references/page-recipes.md +53 -5
- package/skills/app-kit-dev/references/style-rules.md +2 -2
- package/skills/app-kit-migrate/SKILL.md +30 -11
- package/skills/app-kit-migrate/references/migration-playbook.md +53 -4
- package/skills/install.mjs +298 -222
package/CONTRACT.md
CHANGED
|
@@ -102,7 +102,7 @@ import { AppShell, AppPanel, AppTable, AppButton, notify } from '@manohub/app-ki
|
|
|
102
102
|
<AppPanel
|
|
103
103
|
title="值映射列表"
|
|
104
104
|
toolbar={<><AppSearchBox … /><AppSelect … /></>}
|
|
105
|
-
actions={<><AppButton tone="primary">新建</AppButton><AppButton>刷新</AppButton></>}>
|
|
105
|
+
actions={<><AppButton tone="primary">新建</AppButton><AppButton tone="secondary">刷新</AppButton></>}>
|
|
106
106
|
<AppTable framed rows={rows} columns={cols} rowKey="id" />
|
|
107
107
|
|
|
108
108
|
{/* 分页跟**承载表格的容器**走:表格在本面板内 → AppPanel.Footer(不要提到 AppShell.Footer) */}
|
|
@@ -348,6 +348,7 @@ import { AppShell, AppPanel, AppTable, AppButton, notify } from '@manohub/app-ki
|
|
|
348
348
|
- 关闭拦截用 `beforeClose`(返回 `false` 阻止关闭,可用来弹「确认退出」)。
|
|
349
349
|
- 内容固定渲染进 `.app-container`(令牌与应用侧样式的作用域锚点),渲染到 body 会「样式全丢」。
|
|
350
350
|
- 需要「固定高度 + 区域内滚动」时传 `fitContent={false}` + `height`。
|
|
351
|
+
- 内容**高且窄**(配置详情、逐项设置、多页签)用**抽屉**(§4.18),不要硬塞进弹窗。
|
|
351
352
|
|
|
352
353
|
### 4.11 `AppSection`(区块:标题 + 内容)
|
|
353
354
|
|
|
@@ -447,6 +448,139 @@ import { AppShell, AppPanel, AppTable, AppButton, notify } from '@manohub/app-ki
|
|
|
447
448
|
右上角 ✕ / Esc 只关窗**不触发回调**(farris 行为),Promise 不落定 —— 调用方按"未确认"处理即可。
|
|
448
449
|
- 轻量反馈(保存成功/失败)用 `notify`,长任务用 `loading`;两者都不带确认语义。
|
|
449
450
|
|
|
451
|
+
### 4.14 页签(`AppTabs`)
|
|
452
|
+
|
|
453
|
+
```tsx
|
|
454
|
+
<AppTabs modelValue={activeTab}
|
|
455
|
+
items={[{ key: 'overview', label: '概览' }, { key: 'tools', label: '工具' }]}
|
|
456
|
+
v-slots={{ overview: () => <Overview />, tools: () => <Tools /> }}
|
|
457
|
+
onChange={(key) => (activeTab = key)} />
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
- `items` 是唯一的页签定义入口:`{ key, label }`(`key` 同时是**内容插槽名**)。
|
|
461
|
+
- `modelValue` + `onChange(key)` 走上层统一词表;**点击只上报、消费方必须回写 `modelValue`**(受控语义,不回写界面不动)。
|
|
462
|
+
- `fill` 缺省 `true`(撑满容器高度,抽屉/面板内需要);不需要撑满时显式 `fill={false}`。
|
|
463
|
+
- 页签条本身的视觉与滚动归组件;页面里不要自绘标签行,也不要用 `AppLayout` 拼一个「像页签」的按钮组。
|
|
464
|
+
|
|
465
|
+
### 4.15 页面级筛选(`AppFilter` / `appFilterSelect` / `appFilterInput`)
|
|
466
|
+
|
|
467
|
+
**位置**:只出现在 `AppShell.Filter`(§4.5:字段 > 3、跨区域、或已是组合查询方案)。区域级 ≤3 字段的筛选仍在
|
|
468
|
+
`AppPanel.toolbar`,用普通表单控件。
|
|
469
|
+
|
|
470
|
+
```tsx
|
|
471
|
+
const fields: AppFilterField[] = [
|
|
472
|
+
{ id: 'field-repoId', code: 'repoId', name: '所属仓库', editor: appFilterSelect(repoOptions) },
|
|
473
|
+
{ id: 'field-state', code: 'state', name: '状态', editor: appFilterSelect(stateOptions) },
|
|
474
|
+
{ id: 'field-owner', code: 'owner', name: '负责人', editor: appFilterInput('输入负责人') },
|
|
475
|
+
]
|
|
476
|
+
|
|
477
|
+
<AppShell.Filter>
|
|
478
|
+
<AppFilter fields={fields}
|
|
479
|
+
defaults={{ 'field-repoId': repoId, 'field-state': 'all' }}
|
|
480
|
+
searchFields="name" searchPlaceholder={t('filter.keywordPlaceholder')}
|
|
481
|
+
onChange={(values, meta) => applyFilter(values)}
|
|
482
|
+
onQuery={(values, meta) => applyFilter({ ...values, force: true })} />
|
|
483
|
+
</AppShell.Filter>
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
- `fields` 必填,元素是 `{ id, code, labelCode?, name, controlType?, visible?, editor? }`。
|
|
487
|
+
`id` 是**字段标识**(默认值按它索引),`code` 是**业务条件名**(事件值与后端参数按它走);两者不要混用。
|
|
488
|
+
- **编辑器一律用两个工厂生成**,不要手写 farris 编辑器配置:
|
|
489
|
+
- `appFilterSelect([{ label, value }])` —— 下拉,应用侧只给标准选项;
|
|
490
|
+
- `appFilterInput(placeholder)` —— 单行文本(内部固定实时回传)。
|
|
491
|
+
工厂之外的 farris 字段(`valueField` / `enumValueType` / `combo-list` …)属于包内实现,应用侧写不出来。
|
|
492
|
+
- `defaults` 用**字段 id → 业务值**;哨兵 `'all'` 表示「全部」,事件里会被翻译成空串(后端不传该条件即不过滤)。
|
|
493
|
+
- 事件:`onChange(values, meta)` 条件变化(值已归一化为 `code → 字符串`)、`onQuery(values, meta)` 查询按钮/回车
|
|
494
|
+
(`meta.query === true`)、`onReady()` 条件区就绪。**默认值注入时机、payload 三形态归一、就绪轮询都由组件吃掉**,
|
|
495
|
+
应用侧不要再写 `solutionRef` 直调或 `setTimeout` 补时序。
|
|
496
|
+
- `ignoreInitialEmpty`:按 key 重建条件区时挂载即触发一次空条件查询,传 `true` 忽略该次。
|
|
497
|
+
- `keywordCodes` 决定哪些 code 算「关键字字段」(缺省 `fuzzysearch` / `keyword` / `name` / `displayname`)。
|
|
498
|
+
|
|
499
|
+
### 4.16 树(`AppTree`)
|
|
500
|
+
|
|
501
|
+
`AppTree` 为**自建件**(不封装底层树组件,理由见 §9.6),受控展开、三态内建。
|
|
502
|
+
|
|
503
|
+
```tsx
|
|
504
|
+
<AppTree nodes={nodes} rowKey="id" labelKey="name"
|
|
505
|
+
selected={selectedId} onSelect={(node) => (selectedId = node.id)}
|
|
506
|
+
expandedKeys={expandedKeys} onExpandChange={(keys) => (expandedKeys = keys)}
|
|
507
|
+
loading={loading} error={error} empty="暂无业务域"
|
|
508
|
+
renderNode={(node) => <AppBadge tone="info">{countOf(node.id)}</AppBadge>} />
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
| prop | 说明 |
|
|
512
|
+
|---|---|
|
|
513
|
+
| `nodes` / `rowKey` | **都必填**;`rowKey` 是字段名或 `(node) => key` |
|
|
514
|
+
| `labelKey` / `childrenKey` | 标签字段名(默认 `label`)/ 子节点字段名(默认 `children`) |
|
|
515
|
+
| `selected` + `onSelect(node)` | 选中是**行主键值**,回调给的是**节点对象**(不是 key),要 key 自己取 |
|
|
516
|
+
| `expandedKeys` / `defaultExpandedKeys` / `onExpandChange(keys)` | 传了 `expandedKeys` 即完全受控;不传则内部自持,`defaultExpandedKeys` 给初值 |
|
|
517
|
+
| `renderNode(node)` | 节点附加内容(计数徽标等),渲染在标签之后 |
|
|
518
|
+
| `size` | `md`(行高 30,默认)/ `sm`(紧凑 28) |
|
|
519
|
+
| `loading` / `error` / `empty` / `emptyActionText` + `onEmptyAction` / `errorTitle` / `errorActionText` + `onErrorAction` | 三态与动作,口径同 §4.6(**错误不要塞进 `empty`**) |
|
|
520
|
+
| `treeKey` | 强制重挂逃生舱:正常数据刷新**不需要**它(展开态不会丢) |
|
|
521
|
+
|
|
522
|
+
- **交互分工**:点箭头展开/收拢、**点行选中**(`onSelect`)—— 不要给行挂自绘点击展开逻辑。
|
|
523
|
+
- **缩进步进由组件算**(`data-level` + CSS 变量),应用侧不要改行内 padding 或写死缩进像素(§9.6)。
|
|
524
|
+
- 受控展开是「数据刷新不丢展开态」的实现前提:不要用「数据变了就换 `key` 重挂」的写法绕过。
|
|
525
|
+
|
|
526
|
+
### 4.17 分页(`AppPagination` / `useClientPagination`)
|
|
527
|
+
|
|
528
|
+
```tsx
|
|
529
|
+
// 服务端分页:页面持有 page / pageSize
|
|
530
|
+
<AppPagination page={page} pageSize={pageSize} total={total}
|
|
531
|
+
onPageChange={(p) => (page = p)}
|
|
532
|
+
onPageSizeChange={(s) => { pageSize = s; page = 0 }} />
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
- `page` / `pageSize` / `total` **必填**;`page` 是 **0 基**(与后端分页参数一致,1 基换算由组件内做)。
|
|
536
|
+
- 事件是 **`onPageChange(page)` 与 `onPageSizeChange(pageSize)`** —— 没有 `onChange`;切页大小后回第几页属消费方语义
|
|
537
|
+
(现状惯例:回第 0 页)。
|
|
538
|
+
- `pageSizeOptions`(缺省 `[10, 20, 50]`)、`showInfo`(缺省 `true`,总条数信息自带,**不要自绘「共 N 条」**)。
|
|
539
|
+
- **位置**跟承载表格的容器走(§4.4):表格在 `AppPanel` 内 → `AppPanel.Footer`;表格直接挂 `AppShell.Body` 下 →
|
|
540
|
+
`AppShell.Footer`;无数据 / 加载失败时不渲染分页。
|
|
541
|
+
|
|
542
|
+
**客户端分页**(数据整批在前端)用 `useClientPagination`,不要自己写 `slice` + 越界处理:
|
|
543
|
+
|
|
544
|
+
```tsx
|
|
545
|
+
const list = useClientPagination(() => filteredRows.value)
|
|
546
|
+
|
|
547
|
+
<AppTable framed rows={list.pageRows} columns={cols} rowKey="id" />
|
|
548
|
+
<AppPagination page={list.page} pageSize={list.pageSize} total={list.total}
|
|
549
|
+
pageSizeOptions={list.pageSizeOptions}
|
|
550
|
+
onPageChange={list.setPage} onPageSizeChange={list.setPageSize} />
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
- 返回 `page` / `pageSize` / `total` / `pageCount` / `pageRows`(已切片,直接喂 `AppTable.rows`)/ `pageSizeOptions` /
|
|
554
|
+
`setPage` / `setPageSize`;页码同样是 0 基。
|
|
555
|
+
- 数据变少(过滤 / 删除)导致当前页越界时**自动回退到最后一页**,不会出现「空白页但分页器显示有数据」。
|
|
556
|
+
|
|
557
|
+
### 4.18 抽屉(`AppDrawer`)
|
|
558
|
+
|
|
559
|
+
```tsx
|
|
560
|
+
<AppDrawer open={visible} title="MCP 配置" width={640}
|
|
561
|
+
onUpdate:open={(v) => (visible = v)}
|
|
562
|
+
footer={() => (
|
|
563
|
+
<>
|
|
564
|
+
<AppButton tone="secondary" onClick={close}>取消</AppButton>
|
|
565
|
+
<AppButton tone="primary" loading={saving} onClick={save}>保存</AppButton>
|
|
566
|
+
</>
|
|
567
|
+
)}>
|
|
568
|
+
…内容…
|
|
569
|
+
</AppDrawer>
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
- **判据**:内容「高且窄 / 逐项配置 / 多页签」→ 抽屉;只需一眼确认的短内容 → 弹窗(§4.10)。
|
|
573
|
+
二者都是侧边浮层**只在移动端才退化**的语义,桌面端不要把弹窗当抽屉用。
|
|
574
|
+
- `open` 受控 + `onUpdate:open`;关闭时同发 `close`。`beforeClose` 返回 `false` 可拦截关闭(支持 Promise)。
|
|
575
|
+
- **`width` 传数字(px)**:上游把宽度原样塞进行内 style,字符串 `"300"` 生成的是非法 CSS(宽度会退化成内容宽),
|
|
576
|
+
本件默认 `480`。`position` 只支持 `'left' | 'right'`(上游没有 top/bottom 的定位样式)。
|
|
577
|
+
- 页脚用 `footer`(函数或节点)或 `footer` 插槽;**容器(右对齐 / 常驻不滚)由上游提供**
|
|
578
|
+
(`footerTemplate` 与内建按钮两条路都被包进同一容器),应用侧**不要自绘页脚容器**。
|
|
579
|
+
不给页脚时本件会关掉上游那条恒定 60px 的空白页脚区。
|
|
580
|
+
- 标题栏走内建件(`title` + 右上角关闭);`showHeader={false}` 时关闭入口只剩遮罩点击,
|
|
581
|
+
需要自定义头部就得自行承担关闭入口。
|
|
582
|
+
- 渲染宿主与包含块见 §9 第 11 条(本件已把宿主固定到 `.app-container` 并把定位基准压回视口)。
|
|
583
|
+
|
|
450
584
|
---
|
|
451
585
|
|
|
452
586
|
## 5. 反例库(这些写法一律违规)
|
|
@@ -500,10 +634,23 @@ import { AppShell, AppPanel, AppTable, AppButton, notify } from '@manohub/app-ki
|
|
|
500
634
|
|
|
501
635
|
需要的能力本包没有时,**不要**在页面里自绘外观,也不要直接引底层组件库:
|
|
502
636
|
|
|
503
|
-
1.
|
|
504
|
-
2.
|
|
637
|
+
1. 先查本文件已列能力(含 §7.1 缺件清单)与包导出面(`@manohub/app-kit` 的导出列表);
|
|
638
|
+
2. 仍缺 → 走**包维护侧建件**:在包内新建封装件(命名锚定通用规范名,props 查 §3 词表),补导出面与单测。
|
|
639
|
+
消费方**不要**在自己仓里仿制一个近似件(仿制品一建就与骨架层两套口径,后续还得拆);
|
|
505
640
|
3. 建件前先确认底层库是否已有对应件:有则封装(吃掉其坑),无则自建(原生元素 + `--ui-*` 令牌);
|
|
506
|
-
4.
|
|
641
|
+
4. 页面侧只用建好的件;缺件期间按 §7.1 给的替代口径顶住,不要改护栏放过违规。
|
|
642
|
+
|
|
643
|
+
### 7.1 当前缺件清单(判断「是否漏改 / 能不能实现」时看这里)
|
|
644
|
+
|
|
645
|
+
| 能力 | 现状 | 缺件期间的替代口径 |
|
|
646
|
+
|---|---|---|
|
|
647
|
+
| 日期 / 日期时间选择 | **无**(无 `AppDatePicker`) | 暂用 `AppInput` 并标明格式;需真日期控件走 §7 建件 |
|
|
648
|
+
| 数字 / 步进输入 | **无**(无 `AppNumber`) | 暂用 `AppInput` + 提交侧校验;需步进控件走 §7 建件 |
|
|
649
|
+
| 加载骨架屏 | **无**(`AppSkeleton` 属后续批次) | 用 §4.6 三态的 `loading`(表格/树内建,块级用 `AppQueryState`) |
|
|
650
|
+
| 抽屉(Drawer) | **已有 `AppDrawer`**(§4.18) | —— |
|
|
651
|
+
| 描述列表(Descriptions) | **不再需要** | 摘要 / 详情走 §4.7 的 `AppForm.Item text={…}` 只读文本行 |
|
|
652
|
+
|
|
653
|
+
> 本清单随版本变动;**不存在的能力一律不要在应用侧自绘**,也不要用别的件"拼一个像的"当替代品长期留着。
|
|
507
654
|
|
|
508
655
|
---
|
|
509
656
|
|
|
@@ -513,20 +660,66 @@ import { AppShell, AppPanel, AppTable, AppButton, notify } from '@manohub/app-ki
|
|
|
513
660
|
`package.json` 里**只挂两条**(三条护栏由统一入口并发跑,不要各挂一条):
|
|
514
661
|
|
|
515
662
|
```json
|
|
516
|
-
"lint": "
|
|
517
|
-
"lint:changed": "
|
|
663
|
+
"lint": "appkit lint",
|
|
664
|
+
"lint:changed": "appkit lint --changed",
|
|
665
|
+
"lint:strict": "appkit lint --strict"
|
|
518
666
|
```
|
|
519
667
|
|
|
668
|
+
(scripts 里可以省略 `pnpm exec`:npm/pnpm 会把 `node_modules/.bin` 加进 PATH。终端手动跑时写
|
|
669
|
+
`pnpm exec appkit lint`,npm 消费方写 `npx appkit lint`。)
|
|
670
|
+
|
|
520
671
|
- 统一入口并发跑三条:`style-audit.mjs`(样式:只允许布局属性 / 禁 `!important` / 禁自写 rem)、
|
|
521
672
|
`component-audit.mjs`(组件使用:禁底层直连 / 禁底层风格写法)、
|
|
522
673
|
`structure-audit.mjs`(页面结构:骨架必用 / 禁自绘页头面板头 / 字段数上限)。
|
|
523
|
-
**排查单条**时才直接跑它(`
|
|
674
|
+
**排查单条**时才直接跑它(`pnpm exec appkit lint:style`),不必挂成脚本。
|
|
524
675
|
- `--changed` 只跑改动文件(pre-commit 用;非 git 环境不裁剪,宁可多查)。
|
|
525
676
|
- 每条违规都会给出 `file:line` + 片段 + **唯一改法**(`correction.summary` / `.example`)与 **`doc` 锚点**
|
|
526
677
|
(指回本文件对应章节)。**照 `correction` 改,不要自己另想一套。**
|
|
527
678
|
- 存量文件的口径:先产基线 → 挂起(`pending` + 豁免登记)→ **新增与改动过的文件必须归零**。
|
|
528
679
|
|
|
529
|
-
### 8.1
|
|
680
|
+
### 8.1 分阶段迁移:规则级豁免(`waivedRules`)
|
|
681
|
+
|
|
682
|
+
只做**一部分**迁移是常态(最典型的是「**只迁骨架**」:先把页面结构换成 `AppShell` / `AppPanel`,组件与样式
|
|
683
|
+
下一批次再收)。这一层用 `apps[].waivedRules` 表达:**已迁到的那一层即刻纳入门禁,没迁的登记豁免**。
|
|
684
|
+
|
|
685
|
+
```jsonc
|
|
686
|
+
// appkit-guardrails.config.json —— Shell-only 应用
|
|
687
|
+
{
|
|
688
|
+
"apps": [{
|
|
689
|
+
"dir": ".", "name": "my-app", "prefixes": ["my-"], "pending": false,
|
|
690
|
+
"waivedRules": [
|
|
691
|
+
{ "rule": "api/*", "reason": "只迁骨架(Shell-only):组件替换在下一批次", "since": "2026-09-18" },
|
|
692
|
+
{ "rule": "style/*", "reason": "同上:样式收口等骨架稳定后再做", "since": "2026-09-18" }
|
|
693
|
+
]
|
|
694
|
+
}]
|
|
695
|
+
}
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
- 规则名写 `域/规则`(如 `api/farris-import`)或整类通配(`api/*` / `style/*` / `structure/*`);
|
|
699
|
+
**每条必须写 `reason`**(配置校验会拦),缺理由的豁免等于给护栏装静音键。
|
|
700
|
+
- 豁免项**照常出现在报告里**(标成 `error/waived`),只是不计入退出码:
|
|
701
|
+
```
|
|
702
|
+
component-audit(组件使用护栏):0 个 error / 0 个 warn(其中 3 条属已登记豁免,不计入退出码)
|
|
703
|
+
[error/waived] src/views/x.tsx:2 api/farris-import
|
|
704
|
+
豁免:已登记(不参与门禁;用 --strict 可验证「收口后会怎样」)
|
|
705
|
+
```
|
|
706
|
+
技术债必须可见 —— 这是「豁免」与「关掉规则」的区别。
|
|
707
|
+
- 某条豁免**当前 0 条命中**时,报告会提示「可以撤销了」:迁移往前走了就该撤,否则它会静默地一直关着门。
|
|
708
|
+
- **两条不拦断的机制**(应用级 `pending` = 整应用挂起产基线;规则级 `waivedRules` = 分阶段)都只是
|
|
709
|
+
「暂时不计入退出码」,不是「不检查」。
|
|
710
|
+
|
|
711
|
+
### 8.2 收口验收:`--strict`
|
|
712
|
+
|
|
713
|
+
```bash
|
|
714
|
+
pnpm exec appkit lint --strict
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
`--strict` 下 **`pending` 与 `waivedRules` 一律失效**,退出码反映「真实剩下多少违规」。用于两个时点:
|
|
718
|
+
|
|
719
|
+
1. **里程碑验收**:Shell-only 阶段收尾时跑一次,确认结构类确实归零(`structure/*` 本就没有豁免);
|
|
720
|
+
2. **全量收口**:所有豁免撤销、`pending` 置 `false` 后跑一次,全绿才算迁完。
|
|
721
|
+
|
|
722
|
+
### 8.3 提交前检查(可选,目标 < 3 秒)
|
|
530
723
|
|
|
531
724
|
包内提供钩子样本,放进默认 hooks 目录即可启用(**不需要改 git 配置**):
|
|
532
725
|
|
|
@@ -539,7 +732,7 @@ export APPKIT_APP_DIR=apps/sub-app
|
|
|
539
732
|
|
|
540
733
|
钩子只跑 `run-all.mjs --changed`(改动文件),所以秒级返回;`--changed` 在非 git 环境不裁剪(宁可多查)。
|
|
541
734
|
|
|
542
|
-
### 8.
|
|
735
|
+
### 8.4 包自身(`@manohub/app-kit`)的门禁
|
|
543
736
|
|
|
544
737
|
```bash
|
|
545
738
|
pnpm --filter @manohub/app-kit type-check # 包内 vue-tsc:0 错
|
|
@@ -590,3 +783,9 @@ pnpm --filter @manohub/app-kit test:unit # vitest(契约单测)+ 护栏
|
|
|
590
783
|
而令牌(`--ui-*`)与主题桥接(`--f-theme-*`)都锚在 `.app-container` 上、微前端下应用侧 CSS 还被
|
|
591
784
|
scopecss 限定在容器内 —— 落到 body 会让弹窗**内容里所有组件与应用样式一起失效**。
|
|
592
785
|
`AppDialog` / `modalService` 内部已改投 `.app-container`,消费方无需也不应干预渲染宿主。
|
|
786
|
+
11. **抽屉的宿主与包含块**:`AppDrawer`(§4.18)同样把内容投到 `.app-container`(否则样式全丢),
|
|
787
|
+
而上游在「宿主不是 `body`」时会顺手给抽屉根节点加 `f-drawer-inline`(把 `position: fixed` 改成 `absolute`)——
|
|
788
|
+
微前端里绝对定位的包含块不可控(`.app-container` 自身未定位、宿主层级随门户而变),抽屉与遮罩会错位。
|
|
789
|
+
包内桥接层因此把 `.f-drawer.f-drawer-inline` 的定位压回 `fixed`:**覆盖范围与改造前 `host: "body"` 完全一致**。
|
|
790
|
+
另:上游 `showFooter` 默认 `true` 且高度取 `footerHeight`(默认 60px),不给内容也留一条空白 ——
|
|
791
|
+
`AppDrawer` 在无页脚时直接关掉该区块,应用侧不必再写「隐藏空 footer」的覆盖样式。
|
package/README.md
CHANGED
|
@@ -9,8 +9,12 @@
|
|
|
9
9
|
- **入口**:`@manohub/app-kit/entry` → `createSubApp`
|
|
10
10
|
- **组件/服务**:`@manohub/app-kit` → `AppShell` / `AppPanel` / `AppTree` / `AppTable` / `AppButton` / `notify` …
|
|
11
11
|
- **样式**:`@manohub/app-kit/reset.css` + `@manohub/app-kit/styles.css`(应用侧 `style.css` 固定三行,见 CONTRACT.md §1)
|
|
12
|
-
-
|
|
13
|
-
|
|
12
|
+
- **命令入口**:`appkit`(`package.json` 的 `bin`,在应用包根执行)
|
|
13
|
+
- `pnpm exec appkit lint [--changed|--strict|--json]` —— 三条护栏;单条排查用 `appkit lint:style` / `lint:component` / `lint:structure`
|
|
14
|
+
- `pnpm exec appkit install [--also-claude|--dry-run]` —— 落 AI 技能到本工程
|
|
15
|
+
- npm 消费方把 `pnpm exec` 换成 `npx`;scripts 里可直接写 `appkit lint`(`.bin` 在 PATH 里)
|
|
16
|
+
- 等价写法(老脚本/钩子可用):`node node_modules/@manohub/app-kit/lint/run-all.mjs`、`node node_modules/@manohub/app-kit/skills/install.mjs`
|
|
17
|
+
- 各护栏脚本仍单独可跑(用法见 CONTRACT.md §8)
|
|
14
18
|
|
|
15
19
|
## 分发形态
|
|
16
20
|
|
|
@@ -28,9 +32,9 @@
|
|
|
28
32
|
在消费方工程根执行即可落到自己的技能目录(幂等,包升级后重跑即刷新):
|
|
29
33
|
|
|
30
34
|
```bash
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
pnpm exec appkit install # → .codebuddy/skills/
|
|
36
|
+
pnpm exec appkit install --also-claude # 同时 → .claude/skills/
|
|
37
|
+
pnpm exec appkit install --dry-run # 只预览
|
|
34
38
|
```
|
|
35
39
|
|
|
36
40
|
装了之后,代理在做接入、迁移或页面开发时会命中本包规范与流程;技能职责边界与维护约定见 `skills/README.md`。
|
package/bin/appkit.mjs
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `appkit` —— 接入方(消费方)命令行入口。
|
|
4
|
+
*
|
|
5
|
+
* 存在的理由:接入方不该被要求记住 `node node_modules/@manohub/app-kit/lint/run-all.mjs --changed`
|
|
6
|
+
* 这类路径。本命令是**薄壳**:把子命令映射到包内既有脚本,透传参数与退出码 —— 所以
|
|
7
|
+
* `appkit lint` 与「直接跑那个脚本」是同一件事,脚本仍可单独执行(已有钩子 / 脚本不用改)。
|
|
8
|
+
*
|
|
9
|
+
* 用法(在**应用包根**执行):
|
|
10
|
+
* pnpm exec appkit install # 把随包分发的 AI 技能落到本工程
|
|
11
|
+
* pnpm exec appkit install --also-claude
|
|
12
|
+
* pnpm exec appkit lint # 三条护栏(style / component / structure)
|
|
13
|
+
* pnpm exec appkit lint --changed # 只查改动文件(提交前,含未 git add 的新增)
|
|
14
|
+
* pnpm exec appkit lint --strict # 收口验收:存量挂起与规则豁免一律失效
|
|
15
|
+
* pnpm exec appkit lint:style # 单条排查:style / component / structure
|
|
16
|
+
* pnpm exec appkit help
|
|
17
|
+
*
|
|
18
|
+
* npm 消费方把 `pnpm exec` 换成 `npx`(两者都解析本地 node_modules/.bin)。
|
|
19
|
+
* package.json 的 scripts 里可以省略前缀(npm/pnpm 会把 .bin 加进 PATH):
|
|
20
|
+
* "lint": "appkit lint", "lint:changed": "appkit lint --changed"
|
|
21
|
+
*/
|
|
22
|
+
import { spawnSync } from 'node:child_process'
|
|
23
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
24
|
+
import { dirname, join } from 'node:path'
|
|
25
|
+
import { fileURLToPath } from 'node:url'
|
|
26
|
+
// 复用安装器里已测过的「是否被直接执行」判定:pnpm 把包放在 .pnpm 下、消费方引用的是软链,
|
|
27
|
+
// 直接比较 argv[1] 与 import.meta.url 会字面不等 → CLI 静默不执行(同一个坑踩过两次不值得)。
|
|
28
|
+
import { isDirectRun } from '../skills/install.mjs'
|
|
29
|
+
|
|
30
|
+
/** 包根(本文件在 <pkg>/bin/ 下,发布物里 lint/ 与 skills/ 都是它的兄弟目录) */
|
|
31
|
+
const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
32
|
+
|
|
33
|
+
/** 子命令表:命令名 → 包内脚本与说明(新增子命令只需在这里加一行) */
|
|
34
|
+
export const COMMANDS = {
|
|
35
|
+
install: {
|
|
36
|
+
script: 'skills/install.mjs',
|
|
37
|
+
summary: '把随包分发的三个 AI 技能落到本工程(--also-claude / --target <dir> / --dry-run)',
|
|
38
|
+
},
|
|
39
|
+
lint: { script: 'lint/run-all.mjs', summary: '三条护栏一起跑(--changed / --strict / --json / --app=<name> / --cwd=<dir>)' },
|
|
40
|
+
'lint:style': { script: 'lint/style-audit.mjs', summary: '只跑样式护栏(排查单条规则时用)' },
|
|
41
|
+
'lint:component': { script: 'lint/component-audit.mjs', summary: '只跑组件使用护栏' },
|
|
42
|
+
'lint:structure': { script: 'lint/structure-audit.mjs', summary: '只跑页面结构护栏' },
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 解析命令行:只认 `COMMANDS` 里的子命令,未知子命令**显式报错**
|
|
47
|
+
* (静默忽略会让人以为命令生效了,而护栏其实没跑)。
|
|
48
|
+
*/
|
|
49
|
+
export function parseCommand(argv = []) {
|
|
50
|
+
const [first = '', ...rest] = argv
|
|
51
|
+
if (!first || first === 'help' || first === '--help' || first === '-h') return { kind: 'help' }
|
|
52
|
+
if (first === '--version' || first === '-v') return { kind: 'version' }
|
|
53
|
+
const command = COMMANDS[first]
|
|
54
|
+
if (!command) return { kind: 'unknown', command: first }
|
|
55
|
+
return { kind: 'run', name: first, script: command.script, args: rest }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function usage(version = readVersion()) {
|
|
59
|
+
const lines = [
|
|
60
|
+
`appkit ${version} —— @manohub/app-kit 命令入口(在应用包根执行;npm 消费方用 npx 代替 pnpm exec)`,
|
|
61
|
+
'',
|
|
62
|
+
'用法:',
|
|
63
|
+
' pnpm exec appkit <命令> [参数]',
|
|
64
|
+
'',
|
|
65
|
+
'命令:',
|
|
66
|
+
]
|
|
67
|
+
for (const [name, command] of Object.entries(COMMANDS)) {
|
|
68
|
+
lines.push(` ${name.padEnd(16)}${command.summary}`)
|
|
69
|
+
}
|
|
70
|
+
lines.push(' help 显示本帮助')
|
|
71
|
+
lines.push(' --version 显示版本号')
|
|
72
|
+
lines.push('')
|
|
73
|
+
lines.push('参数会原样透传给对应脚本(如 `appkit lint --changed --strict`);退出码同样是脚本的退出码。')
|
|
74
|
+
return lines.join('\n')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 读包自身版本(`appkit --version` 用) */
|
|
78
|
+
export function readVersion() {
|
|
79
|
+
try {
|
|
80
|
+
return JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8')).version ?? 'unknown'
|
|
81
|
+
} catch {
|
|
82
|
+
return 'unknown'
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function main() {
|
|
87
|
+
const parsed = parseCommand(process.argv.slice(2))
|
|
88
|
+
|
|
89
|
+
if (parsed.kind === 'help') {
|
|
90
|
+
console.log(usage())
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
if (parsed.kind === 'version') {
|
|
94
|
+
console.log(readVersion())
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
if (parsed.kind === 'unknown') {
|
|
98
|
+
console.error(`[appkit] 未知命令:${parsed.command}\n\n${usage()}`)
|
|
99
|
+
process.exitCode = 1
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const script = join(PKG_ROOT, parsed.script)
|
|
104
|
+
if (!existsSync(script)) {
|
|
105
|
+
console.error(`[appkit] 包内缺少脚本 ${parsed.script}(安装不完整?请重新安装 @manohub/app-kit)`)
|
|
106
|
+
process.exitCode = 1
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// stdio 继承:护栏是给人看的(含彩色/实时输出),不要在这里再包一层缓冲
|
|
111
|
+
const result = spawnSync(process.execPath, [script, ...parsed.args], {
|
|
112
|
+
cwd: process.cwd(),
|
|
113
|
+
stdio: 'inherit',
|
|
114
|
+
})
|
|
115
|
+
process.exitCode = result.status ?? 1
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 仅在被直接执行时跑;被测试 import 时只取纯函数
|
|
119
|
+
if (isDirectRun(process.argv[1], import.meta.url)) {
|
|
120
|
+
main()
|
|
121
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { type PropType, type SlotsType, type VNodeChild } from 'vue';
|
|
2
|
+
/**
|
|
3
|
+
* 抽屉(封装 farris FDrawer)。
|
|
4
|
+
*
|
|
5
|
+
* 契约:`open` 受控 + `onUpdate:open`(关闭时同发 `close`);`position` 只支持 `left` / `right`
|
|
6
|
+
* (上游 CSS 只给了 `f-drawer-wrapper-left/right` 的定位规则,`top` / `bottom` 无样式);
|
|
7
|
+
* 页脚用 `footer`(函数或节点)给,**不给页脚时不留空档**。
|
|
8
|
+
*
|
|
9
|
+
* 吃掉的上游坑:
|
|
10
|
+
* 1. **被当 DOM 容器的宽度**:上游 `width` 声明成 `String`、默认 `"300"`,值会原样落进行内 style
|
|
11
|
+
* ⇒ `width:300` 不是合法 CSS,抽屉宽度会退化成「内容宽度」。本件默认 `width: 480`(数值 ⇒ `480px`)。
|
|
12
|
+
* 2. **渲染宿主固定为 `.app-container`**:上游 `host` 默认 `"body"`,抽屉落容器外会同时丢令牌
|
|
13
|
+
* (`--ui-*`)与应用侧样式(微前端下被 scopecss 限定在容器内)—— 与 AppDialog 同一件事。
|
|
14
|
+
* 随之而来的第二个坑:上游在 host ≠ body 时给根节点加 `f-drawer-inline`(`position: absolute`),
|
|
15
|
+
* 而绝对定位的包含块在微前端里不可控,包内桥接层已把它改回 `fixed`
|
|
16
|
+
* (覆盖范围与改造前 `host: "body"` 一致)。
|
|
17
|
+
* 3. **页脚容器由上游统一提供**:`footerTemplate` 与内建按钮两条路都被包进 `div.f-drawer-footer px-2`
|
|
18
|
+
* (与 AppDialog 那条不同:modal 的模板路径是裸渲染,容器由本包补)。所以这里只透传内容,
|
|
19
|
+
* 应用侧不要再自绘页脚容器。
|
|
20
|
+
* 4. **恒定渲染的空页脚**:上游 `showFooter` 默认 `true`、高度取 `footerHeight`(默认 60px),
|
|
21
|
+
* 不给内容也会留一条 60px 空白(mcp 原先用应用侧 CSS `#mcp-config-drawer .f-drawer-footer{display:none!important}`
|
|
22
|
+
* 打补丁)。本件在无页脚时直接关掉该区块。
|
|
23
|
+
* 5. 上游没有 `close` 事件(只有 `update:modelValue` / `afterClose`),本件统一成 `onClose`。
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* <AppDrawer open={visible} title="MCP 配置" width={640}
|
|
27
|
+
* onUpdate:open={(v) => (visible = v)}
|
|
28
|
+
* footer={() => <AppButton tone="primary" onClick={save}>保存</AppButton>}>
|
|
29
|
+
* …抽屉内容…
|
|
30
|
+
* </AppDrawer>
|
|
31
|
+
*/
|
|
32
|
+
export declare const AppDrawer: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
33
|
+
/** 受控开合 */
|
|
34
|
+
open: {
|
|
35
|
+
type: BooleanConstructor;
|
|
36
|
+
default: boolean;
|
|
37
|
+
};
|
|
38
|
+
/** 抽屉根节点 id(上游必填)。不传时本件补一个稳定值 */
|
|
39
|
+
id: {
|
|
40
|
+
type: StringConstructor;
|
|
41
|
+
default: undefined;
|
|
42
|
+
};
|
|
43
|
+
title: {
|
|
44
|
+
type: StringConstructor;
|
|
45
|
+
default: string;
|
|
46
|
+
};
|
|
47
|
+
/** 抽屉宽度(px)。给数值——上游默认值是字符串 `"300"`,会生成非法 CSS */
|
|
48
|
+
width: {
|
|
49
|
+
type: NumberConstructor;
|
|
50
|
+
default: number;
|
|
51
|
+
};
|
|
52
|
+
position: {
|
|
53
|
+
type: PropType<"left" | "right">;
|
|
54
|
+
default: string;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* 是否渲染上游自带标题栏。传 false 时关闭入口只剩遮罩点击
|
|
58
|
+
* (上游的关闭按钮长在标题栏里),需要自定义头部时自行承担关闭入口。
|
|
59
|
+
*/
|
|
60
|
+
showHeader: {
|
|
61
|
+
type: BooleanConstructor;
|
|
62
|
+
default: boolean;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* 关闭前钩子(点遮罩 / 点关闭按钮时触发)。
|
|
66
|
+
* 返回 `false` 可阻止关闭(如「有未保存改动,请确认」),支持返回 Promise。
|
|
67
|
+
*/
|
|
68
|
+
beforeClose: {
|
|
69
|
+
type: PropType<() => boolean | void | Promise<boolean | void>>;
|
|
70
|
+
default: undefined;
|
|
71
|
+
};
|
|
72
|
+
/** 页脚(等价于 `footer` 插槽)。上游负责容器几何,本件只在「有内容」时才开页脚区块 */
|
|
73
|
+
footer: {
|
|
74
|
+
type: PropType<any>;
|
|
75
|
+
default: undefined;
|
|
76
|
+
};
|
|
77
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
78
|
+
[key: string]: any;
|
|
79
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("close" | "update:open")[], "close" | "update:open", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
80
|
+
/** 受控开合 */
|
|
81
|
+
open: {
|
|
82
|
+
type: BooleanConstructor;
|
|
83
|
+
default: boolean;
|
|
84
|
+
};
|
|
85
|
+
/** 抽屉根节点 id(上游必填)。不传时本件补一个稳定值 */
|
|
86
|
+
id: {
|
|
87
|
+
type: StringConstructor;
|
|
88
|
+
default: undefined;
|
|
89
|
+
};
|
|
90
|
+
title: {
|
|
91
|
+
type: StringConstructor;
|
|
92
|
+
default: string;
|
|
93
|
+
};
|
|
94
|
+
/** 抽屉宽度(px)。给数值——上游默认值是字符串 `"300"`,会生成非法 CSS */
|
|
95
|
+
width: {
|
|
96
|
+
type: NumberConstructor;
|
|
97
|
+
default: number;
|
|
98
|
+
};
|
|
99
|
+
position: {
|
|
100
|
+
type: PropType<"left" | "right">;
|
|
101
|
+
default: string;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* 是否渲染上游自带标题栏。传 false 时关闭入口只剩遮罩点击
|
|
105
|
+
* (上游的关闭按钮长在标题栏里),需要自定义头部时自行承担关闭入口。
|
|
106
|
+
*/
|
|
107
|
+
showHeader: {
|
|
108
|
+
type: BooleanConstructor;
|
|
109
|
+
default: boolean;
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* 关闭前钩子(点遮罩 / 点关闭按钮时触发)。
|
|
113
|
+
* 返回 `false` 可阻止关闭(如「有未保存改动,请确认」),支持返回 Promise。
|
|
114
|
+
*/
|
|
115
|
+
beforeClose: {
|
|
116
|
+
type: PropType<() => boolean | void | Promise<boolean | void>>;
|
|
117
|
+
default: undefined;
|
|
118
|
+
};
|
|
119
|
+
/** 页脚(等价于 `footer` 插槽)。上游负责容器几何,本件只在「有内容」时才开页脚区块 */
|
|
120
|
+
footer: {
|
|
121
|
+
type: PropType<any>;
|
|
122
|
+
default: undefined;
|
|
123
|
+
};
|
|
124
|
+
}>> & Readonly<{
|
|
125
|
+
onClose?: ((...args: any[]) => any) | undefined;
|
|
126
|
+
"onUpdate:open"?: ((...args: any[]) => any) | undefined;
|
|
127
|
+
}>, {
|
|
128
|
+
title: string;
|
|
129
|
+
footer: any;
|
|
130
|
+
width: number;
|
|
131
|
+
id: string;
|
|
132
|
+
position: "left" | "right";
|
|
133
|
+
open: boolean;
|
|
134
|
+
showHeader: boolean;
|
|
135
|
+
beforeClose: () => boolean | void | Promise<boolean | void>;
|
|
136
|
+
}, SlotsType<{
|
|
137
|
+
default?: () => VNodeChild;
|
|
138
|
+
footer?: () => VNodeChild;
|
|
139
|
+
}>, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { FDrawer } from "@farris/ui-vue";
|
|
2
|
+
import { defineComponent, computed, h } from "vue";
|
|
3
|
+
import { resolveAppContainer } from "../services/app-container.js";
|
|
4
|
+
const FarrisDrawer = FDrawer;
|
|
5
|
+
let drawerSeq = 0;
|
|
6
|
+
const AppDrawer = /* @__PURE__ */ defineComponent({
|
|
7
|
+
name: "AppDrawer",
|
|
8
|
+
inheritAttrs: false,
|
|
9
|
+
emits: ["update:open", "close"],
|
|
10
|
+
props: {
|
|
11
|
+
/** 受控开合 */
|
|
12
|
+
open: {
|
|
13
|
+
type: Boolean,
|
|
14
|
+
default: false
|
|
15
|
+
},
|
|
16
|
+
/** 抽屉根节点 id(上游必填)。不传时本件补一个稳定值 */
|
|
17
|
+
id: {
|
|
18
|
+
type: String,
|
|
19
|
+
default: void 0
|
|
20
|
+
},
|
|
21
|
+
title: {
|
|
22
|
+
type: String,
|
|
23
|
+
default: ""
|
|
24
|
+
},
|
|
25
|
+
/** 抽屉宽度(px)。给数值——上游默认值是字符串 `"300"`,会生成非法 CSS */
|
|
26
|
+
width: {
|
|
27
|
+
type: Number,
|
|
28
|
+
default: 480
|
|
29
|
+
},
|
|
30
|
+
position: {
|
|
31
|
+
type: String,
|
|
32
|
+
default: "right"
|
|
33
|
+
},
|
|
34
|
+
/**
|
|
35
|
+
* 是否渲染上游自带标题栏。传 false 时关闭入口只剩遮罩点击
|
|
36
|
+
* (上游的关闭按钮长在标题栏里),需要自定义头部时自行承担关闭入口。
|
|
37
|
+
*/
|
|
38
|
+
showHeader: {
|
|
39
|
+
type: Boolean,
|
|
40
|
+
default: true
|
|
41
|
+
},
|
|
42
|
+
/**
|
|
43
|
+
* 关闭前钩子(点遮罩 / 点关闭按钮时触发)。
|
|
44
|
+
* 返回 `false` 可阻止关闭(如「有未保存改动,请确认」),支持返回 Promise。
|
|
45
|
+
*/
|
|
46
|
+
beforeClose: {
|
|
47
|
+
type: Function,
|
|
48
|
+
default: void 0
|
|
49
|
+
},
|
|
50
|
+
/** 页脚(等价于 `footer` 插槽)。上游负责容器几何,本件只在「有内容」时才开页脚区块 */
|
|
51
|
+
footer: {
|
|
52
|
+
type: [Function, Object, Array, String],
|
|
53
|
+
default: void 0
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
slots: Object,
|
|
57
|
+
setup(props, {
|
|
58
|
+
slots,
|
|
59
|
+
attrs,
|
|
60
|
+
emit
|
|
61
|
+
}) {
|
|
62
|
+
const fallbackId = `ak-drawer-${drawerSeq += 1}`;
|
|
63
|
+
const drawerId = computed(() => props.id ?? fallbackId);
|
|
64
|
+
const handleUpdate = (value) => {
|
|
65
|
+
emit("update:open", value);
|
|
66
|
+
if (!value) emit("close");
|
|
67
|
+
};
|
|
68
|
+
const renderFooter = () => slots.footer ? slots.footer() : typeof props.footer === "function" ? props.footer() : props.footer;
|
|
69
|
+
return () => {
|
|
70
|
+
const hasFooter = !!slots.footer || props.footer !== void 0;
|
|
71
|
+
return h(FarrisDrawer, {
|
|
72
|
+
id: drawerId.value,
|
|
73
|
+
modelValue: props.open,
|
|
74
|
+
title: props.title,
|
|
75
|
+
width: props.width,
|
|
76
|
+
position: props.position,
|
|
77
|
+
showHeader: props.showHeader,
|
|
78
|
+
beforeClose: props.beforeClose,
|
|
79
|
+
/**
|
|
80
|
+
* 无内容时关掉上游页脚区块 —— 否则恒定渲染一条 `footerHeight`(默认 60px)的空白。
|
|
81
|
+
*/
|
|
82
|
+
showFooter: hasFooter,
|
|
83
|
+
/** 渲染宿主:见头注释第 2 条(farris 默认 `"body"` 会让抽屉样式全丢) */
|
|
84
|
+
host: resolveAppContainer(),
|
|
85
|
+
customClass: ["ak-drawer", attrs.class].filter(Boolean).join(" "),
|
|
86
|
+
customStyle: attrs.style,
|
|
87
|
+
"onUpdate:modelValue": handleUpdate
|
|
88
|
+
}, {
|
|
89
|
+
default: () => slots.default?.(),
|
|
90
|
+
/**
|
|
91
|
+
* 页脚走 `footerTemplate` **插槽**而不是同名 prop:上游把它声明成 `Object`,
|
|
92
|
+
* 传函数会多刷一条 prop 类型告警;而渲染逻辑是「prop 优先,其次插槽」,
|
|
93
|
+
* 不传 prop 时插槽这条路完全等价。
|
|
94
|
+
*/
|
|
95
|
+
...hasFooter ? {
|
|
96
|
+
footerTemplate: () => renderFooter()
|
|
97
|
+
} : {}
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
export {
|
|
103
|
+
AppDrawer
|
|
104
|
+
};
|