@manohub/kit 0.6.0
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 +597 -0
- package/README.md +158 -0
- package/bin/kit.mjs +118 -0
- package/dist/composables/use-client-pagination.d.ts +43 -0
- package/dist/composables/use-client-pagination.js +28 -0
- package/dist/entry/create-query-client.d.ts +8 -0
- package/dist/entry/create-query-client.js +14 -0
- package/dist/entry/create-sub-app.d.ts +81 -0
- package/dist/entry/create-sub-app.js +111 -0
- package/dist/entry/index.d.ts +4 -0
- package/dist/entry/index.js +13 -0
- package/dist/entry/initial-guard.d.ts +12 -0
- package/dist/entry/initial-guard.js +23 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +7 -0
- package/dist/providers/locale-detection.d.ts +35 -0
- package/dist/providers/locale-detection.js +44 -0
- package/dist/providers/setup-i18n.d.ts +50 -0
- package/dist/providers/setup-i18n.js +42 -0
- package/dist/services/app-container.d.ts +31 -0
- package/dist/services/app-container.js +22 -0
- package/dist/services/app-context.d.ts +4 -0
- package/dist/services/app-context.js +11 -0
- package/dist/services/index.d.ts +7 -0
- package/package.json +68 -0
- package/skills/README.md +78 -0
- package/skills/install.mjs +299 -0
- package/skills/kit/SKILL.md +85 -0
- package/skills/kit/references/adoption.md +170 -0
- package/skills/kit/references/contract-index.md +76 -0
- package/skills/kit-dev/SKILL.md +117 -0
- package/skills/kit-dev/references/page-recipes.md +350 -0
- package/skills/kit-dev/references/style-rules.md +47 -0
- package/skills/kit-migrate/SKILL.md +125 -0
- package/skills/kit-migrate/references/migration-map.md +294 -0
- package/skills/kit-migrate/references/migration-playbook.md +188 -0
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# @manohub/kit
|
|
2
|
+
|
|
3
|
+
AIHub 子应用**入口编排层**:`createSubApp` 统一挂载、i18n 单实例,
|
|
4
|
+
外加随包分发的接入契约(`CONTRACT.md`:五层闭集条款 + §7 自检清单)与三个 AI 技能包(`skills/`)。
|
|
5
|
+
|
|
6
|
+
**组件与命令式服务不在本包** —— 它们来自 [`@manohub/ui`](https://www.npmjs.com/package/@manohub/ui);
|
|
7
|
+
**全局令牌(值)不在本包** —— 它来自 [`@manohub/theme`](https://www.npmjs.com/package/@manohub/theme);
|
|
8
|
+
**本包也不发布任何样式** —— 上面两行由消费方直接引。几个包并列安装。
|
|
9
|
+
|
|
10
|
+
> **0.6.0 是破坏性变更**(上一个已发布版本是 `@manohub/app-kit@0.4.3`):本包不再提供任何组件
|
|
11
|
+
> (`App*` 前缀名与 `.ak-*` 类名全部退场、farris 依赖移除)、**主题层独立为 `@manohub/theme`**
|
|
12
|
+
> (容器锚改名 `data-manohub-ui`)、**本包内的样式全部删除**(`reset.css` 与 `.app-markdown`
|
|
13
|
+
> 富文本预设不再提供)、**消费侧机器规则整批下线**(原 `kit lint` 三条护栏不再发布,
|
|
14
|
+
> 合规改为「契约条款 + §7 自检清单」)。
|
|
15
|
+
> 这些是**同一次重构**,一次性做完 —— 迁移对照与步骤见 `CONTRACT.md` §12。
|
|
16
|
+
|
|
17
|
+
## 新项目接入(快速开始)
|
|
18
|
+
|
|
19
|
+
### 1. 安装
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm add @manohub/kit @manohub/ui @manohub/theme
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
peer(由应用提供,缺一个都装不上):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pnpm add vue vue-router pinia vue-i18n @tanstack/vue-query
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`@manohub/ui` 自带图标包依赖(`@manohub/icon`),不必单独声明。
|
|
32
|
+
|
|
33
|
+
### 2. 样式:两行 + 应用自己一行(顺序即契约)
|
|
34
|
+
|
|
35
|
+
**本包不发布任何样式**:设计令牌(值)在 `@manohub/theme`、组件面(类 + 组件令牌基础值)在
|
|
36
|
+
`@manohub/ui`,两行由消费方直接引;本包不再提供 reset、富文本预设与样式汇总入口。
|
|
37
|
+
|
|
38
|
+
```css
|
|
39
|
+
@import "@manohub/theme/default.css"; /* ① 令牌(值)—— 换主题只换这一行 */
|
|
40
|
+
@import "@manohub/ui/styles.css"; /* ② 组件面(类 + 组件令牌基础值) */
|
|
41
|
+
@import "./app.css"; /* ③ 应用自身(只写布局) */
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**顺序不可换**:先值(令牌)后面(组件面)—— 反过来的话组件面里的 `var(--ui-*)` 全是空值。
|
|
45
|
+
|
|
46
|
+
改用非兜底主题:把 ① 换成对应主题入口 + 给容器属性 ——
|
|
47
|
+
|
|
48
|
+
```css
|
|
49
|
+
@import "@manohub/theme/farris.css"; /* 替换 ① 那一行 */
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
createSubApp({ /* … */ theme: 'farris' }) // 写到容器的 data-theme 上
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
主题套锚在容器属性上(`default` 不带主题属性、其余套锚 `[data-manohub-ui][data-theme='<名>']`),
|
|
57
|
+
所以换皮不动类名、不动组件代码、不重打包。
|
|
58
|
+
|
|
59
|
+
### 3. 入口:统一走 createSubApp
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
// src/main.ts
|
|
63
|
+
import { createSubApp } from '@manohub/kit/entry'
|
|
64
|
+
import Root from './root'
|
|
65
|
+
import { routes } from './router'
|
|
66
|
+
|
|
67
|
+
export const { mount, unmount } = createSubApp({
|
|
68
|
+
rootComponent: Root,
|
|
69
|
+
routes,
|
|
70
|
+
i18n: { messages: { zh, en } }, // vue-i18n 形态:{ zh: {...}, en: {...} },无 translation 包装层
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
它替你做完:`div.app-container` 包裹与 `data-manohub-ui` 锚点、pinia / vue-router / vue-query 装配、
|
|
75
|
+
宿主挂载协议(`window.mount/unmount` + `microApp.mount/unmount`)、宿主语言同步、首帧路由重置。
|
|
76
|
+
|
|
77
|
+
`data-manohub-ui` 是**跨包唯一的作用域锚点**(主题令牌、组件令牌、微前端 scopecss 都锚在它上面,
|
|
78
|
+
组件库服务层也按它解析宿主)—— 别改名;若自建容器(不走 `createSubApp`)必须自己带上。
|
|
79
|
+
类名 `.app-container` 只是本包内部命名(调试自查用),不是契约。
|
|
80
|
+
|
|
81
|
+
### 4. 第一个页面用模板 A
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { Button, Input, Page, Panel, Table } from '@manohub/ui'
|
|
85
|
+
|
|
86
|
+
export default function SkillList() {
|
|
87
|
+
return (
|
|
88
|
+
<Page>
|
|
89
|
+
<Page.Header title="技能列表" extra={<Input placeholder="搜索" />} />
|
|
90
|
+
<Page.Body mode="plain">
|
|
91
|
+
<Panel title="技能列表" actions={<Button variant="primary">新建</Button>}>
|
|
92
|
+
<Table data={rows} columns={columns} />
|
|
93
|
+
<Panel.Footer>分页放这里</Panel.Footer>
|
|
94
|
+
</Panel>
|
|
95
|
+
</Page.Body>
|
|
96
|
+
</Page>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
三种页面模板、四种操作位、两级滚动归属、分页归属 —— 见 `CONTRACT.md` §5(L2 结构)。
|
|
102
|
+
|
|
103
|
+
### 5. 自检(0.6.0 起没有自动扫描)
|
|
104
|
+
|
|
105
|
+
本包**不再发布消费侧机器规则**。合规靠契约条款 + 自检清单在写作与评审时把关:
|
|
106
|
+
|
|
107
|
+
1. 写代码前按契约 §0 权威源表去取值 / 查件名 / 查词表(不要抄一份会过期的副本);
|
|
108
|
+
2. 收工前人工过 `CONTRACT.md` §7 自检清单(24 问,五层各一组);
|
|
109
|
+
3. 类型与构建照常跑:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pnpm exec vue-tsc --noEmit
|
|
113
|
+
pnpm build
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
类名命名空间表放本仓 `docs/kit-namespaces.md`(per-app 文件,格式见技能包的
|
|
117
|
+
`kit-migrate/references/migration-playbook.md`)—— 原 `appkit-guardrails.config.json` 不再被读取。
|
|
118
|
+
|
|
119
|
+
### 6. 技能包落盘(AI 代理用)
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pnpm exec kit install # 把三个技能落到本工程的技能目录
|
|
123
|
+
pnpm exec kit install --dry-run # 先看会写什么
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 7. 验收
|
|
127
|
+
|
|
128
|
+
- 应用能挂载(`window.mount()` / micro-app 容器);
|
|
129
|
+
- 类型检查 0 错、构建成功;
|
|
130
|
+
- 命令式提示落回应用容器(`toast('success', 'ok')` 后检查 DOM 里 `[data-manohub-ui]` 内有 `.mh-toast`);
|
|
131
|
+
- `CONTRACT.md` §7 自检清单逐条过完。
|
|
132
|
+
|
|
133
|
+
## 分发形态
|
|
134
|
+
|
|
135
|
+
预编译产物:`exports` 指向 `dist`。公开入口(改名即破坏性变更):
|
|
136
|
+
|
|
137
|
+
| 入口 | 内容 |
|
|
138
|
+
| --- | --- |
|
|
139
|
+
| `@manohub/kit` | `useClientPagination`、`resolveAppContainer` / `resolveAppContainerSelector` |
|
|
140
|
+
| `@manohub/kit/entry` | `createSubApp`、`installInitialGuard`、`createQueryClient`、`applyLocale` / `getLocale` / `i18n` / `setupI18n` |
|
|
141
|
+
| `@manohub/kit/CONTRACT.md` | 接入契约(权威规范:五层闭集条款 + §7 自检清单) |
|
|
142
|
+
|
|
143
|
+
**本包没有任何样式出口**(0.6.0 起):`./styles.css`、`./reset.css`、`./markdown.css` 都已删除。
|
|
144
|
+
样式请引 `@manohub/theme` 与 `@manohub/ui` 的公开入口(见上面 §2)。
|
|
145
|
+
|
|
146
|
+
## 技能包(AI 代理用,随包分发)
|
|
147
|
+
|
|
148
|
+
| 技能 | 什么时候用 |
|
|
149
|
+
| --- | --- |
|
|
150
|
+
| `kit` | 不确定该用哪个子技能、问骨架层总体规范、要接入步骤与升级口径 |
|
|
151
|
+
| `kit-dev` | 写/改页面:选模板、选组件与 prop、样式纪律、收工前自检 |
|
|
152
|
+
| `kit-migrate` | 存量应用改造:按契约分层盘点、划批次、逐文件替换、逐层收口 |
|
|
153
|
+
|
|
154
|
+
## 来源
|
|
155
|
+
|
|
156
|
+
包内实现由 `gsp-cloud-ds/dip/ibp/aihub/aihub-frontend` 的 `kit` 分支首提交导入
|
|
157
|
+
(commit `315b60f`),再按「组件与服务独立成 `@manohub/ui`、本包只留入口编排」拆分而成。
|
|
158
|
+
迁移来源 commit 只作留档,不要据此回改本包实现。
|
package/bin/kit.mjs
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `kit` —— 接入方(消费方)命令行入口。
|
|
4
|
+
*
|
|
5
|
+
* 存在的理由:接入方不该被要求记住 `node node_modules/@manohub/kit/skills/install.mjs`
|
|
6
|
+
* 这类路径。本命令是**薄壳**:把子命令映射到包内既有脚本,透传参数与退出码。
|
|
7
|
+
*
|
|
8
|
+
* 用法(在**应用包根**执行):
|
|
9
|
+
* pnpm exec kit install # 把随包分发的 AI 技能落到本工程
|
|
10
|
+
* pnpm exec kit install --also-claude
|
|
11
|
+
* pnpm exec kit install --target .x/skills
|
|
12
|
+
* pnpm exec kit install --dry-run
|
|
13
|
+
* pnpm exec kit help
|
|
14
|
+
*
|
|
15
|
+
* npm 消费方把 `pnpm exec` 换成 `npx`(两者都解析本地 node_modules/.bin)。
|
|
16
|
+
* package.json 的 scripts 里可以省略前缀(npm/pnpm 会把 .bin 加进 PATH):
|
|
17
|
+
* "skills:sync": "kit install"
|
|
18
|
+
*
|
|
19
|
+
* 0.6.0 起 `lint` / `lint:style` / `lint:component` / `lint:structure` 四个子命令**已下线**
|
|
20
|
+
* (消费侧机器规则整批退场,合规改为「CONTRACT.md 条款 + §7 自检清单」)。
|
|
21
|
+
* 旧脚本若还写着 `kit lint`,会得到「未知命令」并附帮助 —— 不是静默通过。
|
|
22
|
+
*/
|
|
23
|
+
import { spawnSync } from 'node:child_process'
|
|
24
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
25
|
+
import { dirname, join } from 'node:path'
|
|
26
|
+
import { fileURLToPath } from 'node:url'
|
|
27
|
+
// 复用安装器里已测过的「是否被直接执行」判定:pnpm 把包放在 .pnpm 下、消费方引用的是软链,
|
|
28
|
+
// 直接比较 argv[1] 与 import.meta.url 会字面不等 → CLI 静默不执行(同一个坑踩过两次不值得)。
|
|
29
|
+
import { isDirectRun } from '../skills/install.mjs'
|
|
30
|
+
|
|
31
|
+
/** 包根(本文件在 <pkg>/bin/ 下,发布物里 skills/ 是它的兄弟目录) */
|
|
32
|
+
const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
33
|
+
|
|
34
|
+
/** 子命令表:命令名 → 包内脚本与说明(新增子命令只需在这里加一行) */
|
|
35
|
+
export const COMMANDS = {
|
|
36
|
+
install: {
|
|
37
|
+
script: 'skills/install.mjs',
|
|
38
|
+
summary: '把随包分发的三个 AI 技能落到本工程(--also-claude / --target <dir> / --dry-run)',
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 解析命令行:只认 `COMMANDS` 里的子命令,未知子命令**显式报错**
|
|
44
|
+
* (静默忽略会让人以为命令生效了,而它其实什么都没做)。
|
|
45
|
+
*/
|
|
46
|
+
export function parseCommand(argv = []) {
|
|
47
|
+
const [first = '', ...rest] = argv
|
|
48
|
+
if (!first || first === 'help' || first === '--help' || first === '-h') return { kind: 'help' }
|
|
49
|
+
if (first === '--version' || first === '-v') return { kind: 'version' }
|
|
50
|
+
const command = COMMANDS[first]
|
|
51
|
+
if (!command) return { kind: 'unknown', command: first }
|
|
52
|
+
return { kind: 'run', name: first, script: command.script, args: rest }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function usage(version = readVersion()) {
|
|
56
|
+
const lines = [
|
|
57
|
+
`kit ${version} —— @manohub/kit 命令入口(在应用包根执行;npm 消费方用 npx 代替 pnpm exec)`,
|
|
58
|
+
'',
|
|
59
|
+
'用法:',
|
|
60
|
+
' pnpm exec kit <命令> [参数]',
|
|
61
|
+
'',
|
|
62
|
+
'命令:',
|
|
63
|
+
]
|
|
64
|
+
for (const [name, command] of Object.entries(COMMANDS)) {
|
|
65
|
+
lines.push(` ${name.padEnd(16)}${command.summary}`)
|
|
66
|
+
}
|
|
67
|
+
lines.push(' help 显示本帮助')
|
|
68
|
+
lines.push(' --version 显示版本号')
|
|
69
|
+
lines.push('')
|
|
70
|
+
lines.push('参数会原样透传给对应脚本(如 `kit install --dry-run`);退出码同样是脚本的退出码。')
|
|
71
|
+
return lines.join('\n')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 读包自身版本(`kit --version` 用) */
|
|
75
|
+
export function readVersion() {
|
|
76
|
+
try {
|
|
77
|
+
return JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8')).version ?? 'unknown'
|
|
78
|
+
} catch {
|
|
79
|
+
return 'unknown'
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function main() {
|
|
84
|
+
const parsed = parseCommand(process.argv.slice(2))
|
|
85
|
+
|
|
86
|
+
if (parsed.kind === 'help') {
|
|
87
|
+
console.log(usage())
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
if (parsed.kind === 'version') {
|
|
91
|
+
console.log(readVersion())
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
if (parsed.kind === 'unknown') {
|
|
95
|
+
console.error(`[kit] 未知命令:${parsed.command}\n\n${usage()}`)
|
|
96
|
+
process.exitCode = 1
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const script = join(PKG_ROOT, parsed.script)
|
|
101
|
+
if (!existsSync(script)) {
|
|
102
|
+
console.error(`[kit] 包内缺少脚本 ${parsed.script}(安装不完整?请重新安装 @manohub/kit)`)
|
|
103
|
+
process.exitCode = 1
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// stdio 继承:脚本输出是给人看的(含彩色/实时输出),不要在这里再包一层缓冲
|
|
108
|
+
const result = spawnSync(process.execPath, [script, ...parsed.args], {
|
|
109
|
+
cwd: process.cwd(),
|
|
110
|
+
stdio: 'inherit',
|
|
111
|
+
})
|
|
112
|
+
process.exitCode = result.status ?? 1
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 仅在被直接执行时跑;被测试 import 时只取纯函数
|
|
116
|
+
if (isDirectRun(process.argv[1], import.meta.url)) {
|
|
117
|
+
main()
|
|
118
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type MaybeRefOrGetter, type Ref } from 'vue';
|
|
2
|
+
export interface UseClientPaginationOptions {
|
|
3
|
+
/** 初始页大小;缺省取 `pageSizeOptions[0]` */
|
|
4
|
+
pageSize?: number;
|
|
5
|
+
/** 页大小候选;缺省 `[10, 20, 50]`(与 `Pagination` 的缺省一致) */
|
|
6
|
+
pageSizeOptions?: number[];
|
|
7
|
+
}
|
|
8
|
+
export interface UseClientPagination<T = unknown> {
|
|
9
|
+
/** 当前页码(**1 基**,与 `Pagination.modelValue` 同一口径) */
|
|
10
|
+
page: Ref<number>;
|
|
11
|
+
pageSize: Ref<number>;
|
|
12
|
+
/** 总条数(切片前) */
|
|
13
|
+
total: Ref<number>;
|
|
14
|
+
/** 总页数(至少 1) */
|
|
15
|
+
pageCount: Ref<number>;
|
|
16
|
+
/** 当前页数据(已切片,直接喂给 `Table` 的 `data`) */
|
|
17
|
+
pageRows: Ref<T[]>;
|
|
18
|
+
pageSizeOptions: number[];
|
|
19
|
+
setPage: (page: number) => void;
|
|
20
|
+
setPageSize: (size: number) => void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 客户端分页状态(切片 + 越界回退),与 `Pagination` 同契约(**1 基**页码)。
|
|
24
|
+
*
|
|
25
|
+
* **为什么不在 `Table` 内建切片**:`data` 始终是「当前页数据」——
|
|
26
|
+
* 组件自己切片会把「服务端分页」与「客户端分页」两套口径藏进同一个 prop 里。
|
|
27
|
+
* 本 composable 把「切片 + 越界回退 + 页大小切换回首页」这三件样板事收起来,
|
|
28
|
+
* 页面只负责把 `page` / `pageSize` 交给 `Pagination`、把 `pageRows` 交给 `Table.data`。
|
|
29
|
+
*
|
|
30
|
+
* **1 基是硬口径**:`Pagination.modelValue`、`Table` 的 `pagination.current` 都是 1 基,
|
|
31
|
+
* 本 composable 跟着它们;写成 0 基会让每处调用都差 1(那正是迁移里最容易漏的一类缺陷)。
|
|
32
|
+
*
|
|
33
|
+
* 越界回退:数据变少(过滤 / 删除)导致当前页超出范围时,自动回退到最后一页,
|
|
34
|
+
* 避免出现「空白页但分页器显示有数据」。
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* const list = useClientPagination(() => filtered.value)
|
|
38
|
+
* <Table :data="list.pageRows.value" :columns="columns" row-key="id" />
|
|
39
|
+
* <Pagination v-model="list.page.value" v-model:page-size="list.pageSize.value" :total="list.total.value" />
|
|
40
|
+
*
|
|
41
|
+
* @example 服务端分页时不用本 composable:`page` / `pageSize` 交给请求参数,`data` 给当前页结果
|
|
42
|
+
*/
|
|
43
|
+
export declare function useClientPagination<T>(rows: MaybeRefOrGetter<T[]>, options?: UseClientPaginationOptions): UseClientPagination<T>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ref, computed, toValue, watch } from "vue";
|
|
2
|
+
function useClientPagination(rows, options = {}) {
|
|
3
|
+
const pageSizeOptions = options.pageSizeOptions ?? [10, 20, 50];
|
|
4
|
+
const pageSize = ref(options.pageSize ?? pageSizeOptions[0]);
|
|
5
|
+
const page = ref(1);
|
|
6
|
+
const total = computed(() => toValue(rows).length);
|
|
7
|
+
const pageCount = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)));
|
|
8
|
+
watch([total, pageSize], () => {
|
|
9
|
+
if (page.value > pageCount.value) page.value = pageCount.value;
|
|
10
|
+
if (page.value < 1) page.value = 1;
|
|
11
|
+
});
|
|
12
|
+
const pageRows = computed(() => {
|
|
13
|
+
const start = (page.value - 1) * pageSize.value;
|
|
14
|
+
return toValue(rows).slice(start, start + pageSize.value);
|
|
15
|
+
});
|
|
16
|
+
const setPage = (next) => {
|
|
17
|
+
page.value = Math.min(Math.max(1, Math.trunc(next) || 1), pageCount.value);
|
|
18
|
+
};
|
|
19
|
+
const setPageSize = (size) => {
|
|
20
|
+
if (!(size > 0)) return;
|
|
21
|
+
pageSize.value = size;
|
|
22
|
+
page.value = 1;
|
|
23
|
+
};
|
|
24
|
+
return { page, pageSize, total, pageCount, pageRows, pageSizeOptions, setPage, setPageSize };
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
useClientPagination
|
|
28
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { QueryClient } from '@tanstack/vue-query';
|
|
2
|
+
/**
|
|
3
|
+
* 缺省 QueryClient:createSubApp 未显式传入 queryClient 时使用。
|
|
4
|
+
*
|
|
5
|
+
* 与各子应用现状 providers.tsx 的公共默认一致(staleTime 30s、不因窗口聚焦重取);
|
|
6
|
+
* 带全局错误缓存/自定义 retry 的应用应自建 QueryClient 后经 createSubApp 注入。
|
|
7
|
+
*/
|
|
8
|
+
export declare function createQueryClient(): QueryClient;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { QueryClient } from "@tanstack/vue-query";
|
|
2
|
+
function createQueryClient() {
|
|
3
|
+
return new QueryClient({
|
|
4
|
+
defaultOptions: {
|
|
5
|
+
queries: {
|
|
6
|
+
staleTime: 30 * 1e3,
|
|
7
|
+
refetchOnWindowFocus: false
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
createQueryClient
|
|
14
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { QueryClient } from '@tanstack/vue-query';
|
|
2
|
+
import { type App as VueApp, type Component, type Plugin } from 'vue';
|
|
3
|
+
import { type RouterHistory, type RouteRecordRaw } from 'vue-router';
|
|
4
|
+
import { type SetupI18nOptions } from '../providers/setup-i18n';
|
|
5
|
+
/**
|
|
6
|
+
* 路由历史模式:
|
|
7
|
+
* - `'history'`(缺省):正常 history,micro-app 以 baseroute 隔离子应用路由;
|
|
8
|
+
* - `'hash'`:宿主**不支持深路径 fallback**(刷新深路径 404)、或门户不允许子应用改写地址栏时用;
|
|
9
|
+
* - 传 `RouterHistory` 实例(如 `createWebHistory('/subapp/x/')`):自行定制 base 等细节。
|
|
10
|
+
*/
|
|
11
|
+
export type CreateSubAppHistory = 'history' | 'hash' | RouterHistory;
|
|
12
|
+
export interface CreateSubAppOptions {
|
|
13
|
+
rootComponent: Component;
|
|
14
|
+
routes: RouteRecordRaw[];
|
|
15
|
+
/** 路由历史模式(缺省 `'history'`)。宿主是否支持深路径 fallback 见 `CONTRACT.md` §9 第 13 条 */
|
|
16
|
+
history?: CreateSubAppHistory;
|
|
17
|
+
/**
|
|
18
|
+
* vue-i18n 装配选项:`messages` 形态(**不是** i18next 的 `resources`)——见 `CONTRACT.md` §10。
|
|
19
|
+
*
|
|
20
|
+
* 实例由本包唯一创建,应用侧不得自行 `createI18n`。传本选项才会注册 i18n 插件并开启
|
|
21
|
+
* 宿主语言同步;不传则不注册插件(`@manohub/kit/entry` 的 `i18n` 出口是模块级单例,
|
|
22
|
+
* 与应用是否传本选项无关,组件树外可照常消费)。
|
|
23
|
+
*/
|
|
24
|
+
i18n?: SetupI18nOptions;
|
|
25
|
+
/** 缺省由 createQueryClient() 提供;带全局错误缓存/自定义 retry 的应用自建后注入 */
|
|
26
|
+
queryClient?: QueryClient;
|
|
27
|
+
/** 额外插件(如 ContextMenu) */
|
|
28
|
+
extraPlugins?: Plugin[];
|
|
29
|
+
/** 额外样式(如 highlight.js 主题),mount 前动态加载 */
|
|
30
|
+
extraStyles?: () => Promise<unknown>;
|
|
31
|
+
/**
|
|
32
|
+
* 主题名:写到容器的 `data-theme` 属性上。
|
|
33
|
+
*
|
|
34
|
+
* `@manohub/theme` 的主题套锚在容器的 `[data-manohub-ui]` 属性上(`default` 套不带主题属性、
|
|
35
|
+
* 其余套锚 `[data-manohub-ui][data-theme='<名>']`),故给属性即整包换皮 ——
|
|
36
|
+
* 不动类名、不动组件代码、不重打包。
|
|
37
|
+
* 缺省**不写属性**,于是用主题包的兜底套 `default`。
|
|
38
|
+
*/
|
|
39
|
+
theme?: string;
|
|
40
|
+
/** 首帧路由重置到根路径(默认 true,刷新时不恢复 micro-app 传入的路由状态) */
|
|
41
|
+
resetToRootOnBoot?: boolean;
|
|
42
|
+
/** 允许的初始路径别名(宿主兼容前缀),命中不重置 */
|
|
43
|
+
rootPathAliases?: string[];
|
|
44
|
+
onReady?: (app: VueApp) => void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 统一子应用入口工厂,替代各 app 手写的 main.ts 样板。
|
|
48
|
+
*
|
|
49
|
+
* 固定流程(顺序即契约):
|
|
50
|
+
* createApp(外层自动包裹 `.app-container`)→ pinia → i18n → vue-query
|
|
51
|
+
* → 额外插件 → router → mount(`__MICRO_APP_CONTAINER__` || `#app`) → 初始守卫 → window 挂载
|
|
52
|
+
*
|
|
53
|
+
* ## 它到底管哪四件事
|
|
54
|
+
*
|
|
55
|
+
* 1. **作用域容器**:外层包一层 `div.app-container`(`app-` 是本包自己的命名前缀),
|
|
56
|
+
* 并补两个属性 ——
|
|
57
|
+
* `data-manohub-ui`(**跨包唯一作用域锚**,由本工厂**无条件**写:主题令牌、组件令牌、
|
|
58
|
+
* 以及组件库服务层的宿主解析全锚它一个。取「库品牌属性」而不是本包的
|
|
59
|
+
* `app-` 私有类名,是为了让主题包不欠本包一个命名空间;门户里同时挂着多个子应用时,
|
|
60
|
+
* 服务层据此把命令式浮层落回**本应用**的容器,不串到别人的应用)与
|
|
61
|
+
* 可选的 `data-theme`(换肤入口)。**本包不 import 组件库/主题包**:几个包并列安装,
|
|
62
|
+
* 这两个属性就是它们之间唯一的约定。
|
|
63
|
+
* 2. **i18n**:创建全应用唯一的 vue-i18n 实例并注册(`legacy: false`),随后由宿主语言下发驱动切换。
|
|
64
|
+
* 3. **路由 / 状态 / 请求**:pinia、vue-router、`@tanstack/vue-query` 三件装配,外加首帧路由重置。
|
|
65
|
+
* 4. **宿主挂载协议**:`window.mount/unmount` 与 `window.microApp.mount/unmount` 一并暴露。
|
|
66
|
+
*
|
|
67
|
+
* 路由历史模式由 `history` 决定:缺省 `'history'`(micro-app 以 baseroute 隔离),
|
|
68
|
+
* 宿主不支持深路径 fallback 时传 `'hash'`,需要自定义 base 等细节时直接传 `RouterHistory` 实例。
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* export const { mount, unmount } = createSubApp({
|
|
72
|
+
* rootComponent: Root,
|
|
73
|
+
* routes,
|
|
74
|
+
* i18n: { messages: { en, zh } }, // vue-i18n 形态:{ en: {...}, zh: {...} },无 translation 包装层
|
|
75
|
+
* theme: 'farris', // 可选:写到容器 data-theme 上,整包换肤(缺省用兜底主题)
|
|
76
|
+
* })
|
|
77
|
+
*/
|
|
78
|
+
export declare function createSubApp(options: CreateSubAppOptions): {
|
|
79
|
+
mount: () => void;
|
|
80
|
+
unmount: () => void;
|
|
81
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { VueQueryPlugin } from "@tanstack/vue-query";
|
|
2
|
+
import { createPinia } from "pinia";
|
|
3
|
+
import { defineComponent, ref, provide, h, createApp } from "vue";
|
|
4
|
+
import { createRouter, createWebHistory, createWebHashHistory } from "vue-router";
|
|
5
|
+
import { installInitialGuard } from "./initial-guard.js";
|
|
6
|
+
import { createQueryClient } from "./create-query-client.js";
|
|
7
|
+
import { setupI18n, applyLocale } from "../providers/setup-i18n.js";
|
|
8
|
+
import { setCurrentApp } from "../services/app-context.js";
|
|
9
|
+
function createSubApp(options) {
|
|
10
|
+
const {
|
|
11
|
+
rootComponent,
|
|
12
|
+
routes,
|
|
13
|
+
history: historyMode = "history",
|
|
14
|
+
i18n,
|
|
15
|
+
queryClient,
|
|
16
|
+
extraPlugins = [],
|
|
17
|
+
extraStyles,
|
|
18
|
+
theme,
|
|
19
|
+
resetToRootOnBoot = true,
|
|
20
|
+
rootPathAliases = [],
|
|
21
|
+
onReady
|
|
22
|
+
} = options;
|
|
23
|
+
let app = null;
|
|
24
|
+
let router = null;
|
|
25
|
+
let pinia = null;
|
|
26
|
+
let stopInitialGuard = null;
|
|
27
|
+
let removeGlobalDataListener = null;
|
|
28
|
+
const mount = () => {
|
|
29
|
+
router = createRouter({ history: resolveRouterHistory(historyMode), routes });
|
|
30
|
+
const ContainerRoot = defineComponent({
|
|
31
|
+
name: "KitRoot",
|
|
32
|
+
setup() {
|
|
33
|
+
const subAppContext = ref({});
|
|
34
|
+
provide("subAppContext", subAppContext);
|
|
35
|
+
return () => h("div", { class: "app-container", "data-manohub-ui": "", "data-theme": theme }, [
|
|
36
|
+
h(rootComponent)
|
|
37
|
+
]);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
app = createApp(ContainerRoot);
|
|
41
|
+
pinia = createPinia();
|
|
42
|
+
app.use(pinia);
|
|
43
|
+
if (i18n) setupI18n(app, i18n);
|
|
44
|
+
app.use(VueQueryPlugin, { queryClient: queryClient ?? createQueryClient() });
|
|
45
|
+
for (const plugin of extraPlugins) {
|
|
46
|
+
app.use(plugin);
|
|
47
|
+
}
|
|
48
|
+
app.use(router);
|
|
49
|
+
const container = window.__MICRO_APP_CONTAINER__ || "#app";
|
|
50
|
+
app.mount(container);
|
|
51
|
+
const mountTarget = typeof container === "string" ? document.querySelector(container) : container;
|
|
52
|
+
setCurrentApp(app, mountTarget?.querySelector("[data-manohub-ui]") ?? null);
|
|
53
|
+
if (window.__MICRO_APP_ENVIRONMENT__) {
|
|
54
|
+
const microAppWindow = window;
|
|
55
|
+
const onGlobalData = (data) => {
|
|
56
|
+
if (data.language && i18n) {
|
|
57
|
+
applyLocale(data.language, i18n.storageKey);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
microAppWindow.microApp.addGlobalDataListener(onGlobalData, true);
|
|
61
|
+
removeGlobalDataListener = () => microAppWindow.microApp.removeGlobalDataListener(onGlobalData);
|
|
62
|
+
}
|
|
63
|
+
if (resetToRootOnBoot && router) {
|
|
64
|
+
stopInitialGuard = installInitialGuard(router, rootPathAliases);
|
|
65
|
+
}
|
|
66
|
+
if (extraStyles) {
|
|
67
|
+
void extraStyles();
|
|
68
|
+
}
|
|
69
|
+
onReady?.(app);
|
|
70
|
+
};
|
|
71
|
+
const unmount = () => {
|
|
72
|
+
if (stopInitialGuard) {
|
|
73
|
+
stopInitialGuard();
|
|
74
|
+
stopInitialGuard = null;
|
|
75
|
+
}
|
|
76
|
+
if (removeGlobalDataListener) {
|
|
77
|
+
removeGlobalDataListener();
|
|
78
|
+
removeGlobalDataListener = null;
|
|
79
|
+
}
|
|
80
|
+
if (app) {
|
|
81
|
+
app.unmount();
|
|
82
|
+
app = null;
|
|
83
|
+
}
|
|
84
|
+
setCurrentApp();
|
|
85
|
+
router = null;
|
|
86
|
+
pinia = null;
|
|
87
|
+
};
|
|
88
|
+
exposeToHost(mount, unmount);
|
|
89
|
+
if (!window.__MICRO_APP_ENVIRONMENT__) {
|
|
90
|
+
mount();
|
|
91
|
+
}
|
|
92
|
+
return { mount, unmount };
|
|
93
|
+
}
|
|
94
|
+
function resolveRouterHistory(value) {
|
|
95
|
+
if (value === "history") return createWebHistory();
|
|
96
|
+
if (value === "hash") return createWebHashHistory();
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
function exposeToHost(mount, unmount) {
|
|
100
|
+
if (typeof window === "undefined") return;
|
|
101
|
+
const w = window;
|
|
102
|
+
w.mount = mount;
|
|
103
|
+
w.unmount = unmount;
|
|
104
|
+
w.__MICRO_APP__ = true;
|
|
105
|
+
w.microApp = w.microApp ?? {};
|
|
106
|
+
w.microApp.mount = mount;
|
|
107
|
+
w.microApp.unmount = unmount;
|
|
108
|
+
}
|
|
109
|
+
export {
|
|
110
|
+
createSubApp
|
|
111
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createSubApp, type CreateSubAppHistory, type CreateSubAppOptions } from './create-sub-app';
|
|
2
|
+
export { installInitialGuard } from './initial-guard';
|
|
3
|
+
export { createQueryClient } from './create-query-client';
|
|
4
|
+
export { applyLocale, getLocale, i18n, setupI18n, type AppI18n, type SetupI18nOptions, } from '../providers/setup-i18n';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createSubApp } from "./create-sub-app.js";
|
|
2
|
+
import { installInitialGuard } from "./initial-guard.js";
|
|
3
|
+
import { createQueryClient } from "./create-query-client.js";
|
|
4
|
+
import { applyLocale, getLocale, i18n, setupI18n } from "../providers/setup-i18n.js";
|
|
5
|
+
export {
|
|
6
|
+
applyLocale,
|
|
7
|
+
createQueryClient,
|
|
8
|
+
createSubApp,
|
|
9
|
+
getLocale,
|
|
10
|
+
i18n,
|
|
11
|
+
installInitialGuard,
|
|
12
|
+
setupI18n
|
|
13
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Router } from 'vue-router';
|
|
2
|
+
/**
|
|
3
|
+
* 「首次导航重置」守卫:刷新浏览器时,micro-app 会从 URL 参数恢复子应用路由状态,
|
|
4
|
+
* 该守卫在首次非根导航发生前拦截并重定向到根路径,确保每次打开子应用都是初始状态。
|
|
5
|
+
* 导航在产生历史记录前即被取消,避免 afterEach 事后 replace 在快速连续导航时残留多余历史。
|
|
6
|
+
* 只执行一次,执行后立即移除自身,不影响后续正常导航。
|
|
7
|
+
*
|
|
8
|
+
* @param rootPathAliases 额外允许的初始路径(宿主兼容前缀,如 ['/aihub-skill']);
|
|
9
|
+
* 命中别名的首次导航不重置
|
|
10
|
+
* @returns 清理函数:应用卸载时调用,销毁守卫,防止内存泄漏
|
|
11
|
+
*/
|
|
12
|
+
export declare function installInitialGuard(router: Router, rootPathAliases?: string[]): () => void;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
function installInitialGuard(router, rootPathAliases = []) {
|
|
2
|
+
const allowed = /* @__PURE__ */ new Set(["/", ...rootPathAliases]);
|
|
3
|
+
let hasAppliedInitialReset = false;
|
|
4
|
+
let stop = null;
|
|
5
|
+
stop = router.beforeEach((to) => {
|
|
6
|
+
if (hasAppliedInitialReset) return true;
|
|
7
|
+
hasAppliedInitialReset = true;
|
|
8
|
+
stop?.();
|
|
9
|
+
if (!allowed.has(to.path)) {
|
|
10
|
+
return { path: "/", query: {} };
|
|
11
|
+
}
|
|
12
|
+
return true;
|
|
13
|
+
});
|
|
14
|
+
return () => {
|
|
15
|
+
if (stop) {
|
|
16
|
+
stop();
|
|
17
|
+
stop = null;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
installInitialGuard
|
|
23
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@manohub/kit` 对外导出白名单。
|
|
3
|
+
*
|
|
4
|
+
* 禁止 `export *`;禁止 re-export 任何第三方 UI 库的东西(组件与类型都不行)。
|
|
5
|
+
*
|
|
6
|
+
* ## 0.6.0 起本包**不再提供组件**
|
|
7
|
+
*
|
|
8
|
+
* 组件与命令式服务一律来自 **`@manohub/ui`**(消费方直引,几个包并列安装)——
|
|
9
|
+
* 本包不再转出、也不做 `App*` 前缀的别名,避免「同一件两处名字、两套 prop」长期并存。
|
|
10
|
+
* 回归入口件见子路径 `@manohub/kit/entry`(`createSubApp` 等)。
|
|
11
|
+
*
|
|
12
|
+
* 本包剩下的三件事:
|
|
13
|
+
* 1. **入口编排**(`/entry`):`div.app-container` 包裹与 `data-manohub-ui` 锚点、
|
|
14
|
+
* pinia / vue-query / router 装配、宿主挂载协议、语言探测与切换;
|
|
15
|
+
* 2. **应用容器记账**(下面的 `resolveAppContainer`):回答「本应用自己的容器是哪一个」;
|
|
16
|
+
* 3. **契约分发**:`CONTRACT.md`(五层闭集 + §7 自检清单)与 `skills/` 三个技能包。
|
|
17
|
+
*/
|
|
18
|
+
export { useClientPagination, type UseClientPagination, type UseClientPaginationOptions, } from './composables/use-client-pagination';
|
|
19
|
+
export { resolveAppContainer, resolveAppContainerSelector } from './services/app-container';
|
package/dist/index.js
ADDED