@suzhou-lab/page-components 1.0.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/README.md ADDED
@@ -0,0 +1,499 @@
1
+ # @suzhou-lab/page-components
2
+
3
+ 苏州实验室 UMC 微应用统一列表页面组件。解决各微应用列表页面**样式不统一、重复造轮子、AI 辅助改造门槛高**的问题。
4
+
5
+ 源码仓库:http://git.inspur.com/sbg/gih/biz-36/suzhou/umc-shared.git
6
+
7
+ ```bash
8
+ npm install @suzhou-lab/page-components # 装组件
9
+ npx install-skills # 部署 AI 技能
10
+ ```
11
+
12
+ ---
13
+
14
+ ## 1. 它能做什么
15
+
16
+ ```
17
+ npm install 装组件 → npx install-skills 部署技能 → 组件和AI技能就位 → 让AI帮你改造列表页面
18
+ ```
19
+
20
+ 核心交付物:
21
+
22
+ | 交付物 | 类型 | 位置 | 作用 |
23
+ |--------|------|------|------|
24
+ | `list-page.vue` | Vue 组件 | `node_modules/` | 统一列表页面布局(筛选区 + 表格 + 分页),自动计算表格高度 |
25
+ | `filter-form.vue` | Vue 组件 | `node_modules/` | 配置驱动筛选表单,只写 JSON 配置不写模板 |
26
+ | `micro-app-setup` | Claude Code 技能 | `.claude/skills/`(npx 部署) | 教 AI 如何配置新项目(端口、CORS、qiankun 等) |
27
+ | `list-page-refactor` | Claude Code 技能 | `.claude/skills/`(npx 部署) | 教 AI 如何把旧列表页改造为 `<list-page>` + `<filter-form>` |
28
+
29
+ ---
30
+
31
+ ## 2. 组件原理
32
+
33
+ ### 2.1 组件自包含,不依赖项目样式变量
34
+
35
+ 传统做法是每个组件 `@import` 项目的 `_var.scss` / `_theme.scss`,导致组件散落在各项目后**样式跟着项目走、无法脱离项目独立运行**。
36
+
37
+ 这两个组件使用了**自包含**策略:
38
+
39
+ - **SCSS 变量 → 硬编码值**:组件不 `@import` 任何项目变量,`font-size`、`color` 等全部写死
40
+ - **可定制属性 → CSS 变量**:暴露 `--list-page-spacing`、`--list-page-thead-bg` 等 CSS 变量,项目可在 `:root` 中覆盖
41
+ - **Element UI 覆盖 → scoped `::v-deep`**:所有对 `el-table`、`el-pagination` 的样式覆盖都在组件 scoped 内完成,不污染全局
42
+
43
+ **这意味着组件拷到任何 Vue 2 + Element UI 项目中都能直接用,不挑项目。**
44
+
45
+ ### 2.2 list-page:布局容器
46
+
47
+ ```
48
+ ┌──────────────────────────────────────────┐
49
+ │ #aside(可选) │ 主区域 │
50
+ │ 左侧面板 │ ┌────────────────┐ │
51
+ │ │ │ #filter 筛选区 │ │
52
+ │ │ ├────────────────┤ │
53
+ │ │ │ #toolbar 工具栏 │ │
54
+ │ │ ├────────────────┤ │
55
+ │ │ │ default slot │ │
56
+ │ │ │ <el-table> │ │
57
+ │ │ ├────────────────┤ │
58
+ │ │ │ #pagination │ │
59
+ │ │ └────────────────┘ │
60
+ └──────────────────────────────────────────┘
61
+ ```
62
+
63
+ 核心能力:
64
+ - **自动计算表格高度**:通过 `ResizeObserver` 监听容器变化,提供 `tableHeight` 给表格 `:height` 绑定
65
+ - **插槽驱动**:`#filter`、`#toolbar`、`#pagination`、`#aside` 四个插槽,按需使用
66
+
67
+ ### 2.3 filter-form:配置驱动筛选
68
+
69
+ 不用写 `<el-input>`、`<el-select>` 模板,只写一个 JSON 配置数组:
70
+
71
+ ```js
72
+ filterItems() {
73
+ return [
74
+ { label: '项目名称', prop: 'name', type: 'input', span: 8 },
75
+ { label: '状态', prop: 'status', type: 'select', span: 8,
76
+ options: [{ value: '1', label: '待审核' }, { value: '2', label: '已通过' }]
77
+ },
78
+ { label: '年度', prop: 'year', type: 'yearpicker', span: 8 },
79
+ ]
80
+ }
81
+ ```
82
+
83
+ 支持的 `type`:`input`、`select`、`yearpicker`、`monthpicker`、`datepicker`、`daterange`、`cascader`、`custom`。
84
+
85
+ 高级特性:条件显隐(`visible`)、联动回调(`onChange`)、折叠展开(超过一行自动折叠)。
86
+
87
+ ---
88
+
89
+ ## 3. 安装
90
+
91
+ ### 3.1 安装组件
92
+
93
+ ```bash
94
+ npm install @suzhou-lab/page-components
95
+ ```
96
+
97
+ ### 3.2 部署 AI 技能
98
+
99
+ ```bash
100
+ npx install-skills
101
+ ```
102
+
103
+ 技能文件会写入 `.claude/skills/` 目录。如果你的 AI 工具使用其他目录名(如 `.codex`、`.github/copilot` 等),手动复制过去即可。
104
+
105
+ ### 3.3 注册组件
106
+
107
+ 在 `src/main.js` 中添加:
108
+
109
+ ```js
110
+ import PageComponents from '@suzhou-lab/page-components'
111
+ Vue.use(PageComponents)
112
+ ```
113
+
114
+ 替代旧的 `import '@/components/page'` 方式。
115
+
116
+ ### 3.4 安装后检查
117
+
118
+ | 检查项 | 说明 |
119
+ |--------|------|
120
+ | 组件注册 | `main.js` 中是否有 `import PageComponents from '@suzhou-lab/page-components'` |
121
+ | `.container` 样式 | `Home.vue` 中 `<router-view>` 是否有 `class="container"`(qiankun 子应用必配) |
122
+ | **zoom 缩放** | 项目中是否有 `document.body.style.zoom`(会破坏 list-page 高度计算,必须移除) |
123
+
124
+ **关于 zoom 缩放:** 部分项目通过 `document.body.style.zoom` 缩放 body 来适配屏幕。`style.zoom` 是非标准 CSS 属性,只在 Chrome 有效,list-page 使用 `ResizeObserver` 计算表格高度,body 缩放后容器尺寸与实际可视区域不一致,会导致**表格高度塌陷或双滚动条**。此外还会导致**popover弹窗出现位置偏移**等问题。
125
+
126
+ 可使用 `micro-app-setup` 技能让 AI 解决:在 Claude Code 中说"帮我配置这个微应用项目"即可。详细说明见 `.claude/skills/micro-app-setup/SKILL.md`。
127
+
128
+ ---
129
+
130
+ ## 4. 组件用法
131
+
132
+ ### 4.1 标准列表页模板
133
+
134
+ ```html
135
+ <template>
136
+ <div class="my-page">
137
+ <list-page>
138
+ <template #filter>
139
+ <filter-form :items="filterItems" v-model="filters"
140
+ @search="onSearch" @reset="onReset" />
141
+ </template>
142
+
143
+ <template #toolbar>
144
+ <el-button type="primary" icon="el-icon-plus" @click="handleAdd">新增</el-button>
145
+ </template>
146
+
147
+ <el-table :data="list" :height="tableHeight" v-loading="loading" border stripe>
148
+ <el-table-column type="selection" width="55" />
149
+ <el-table-column type="index" label="序号" width="60" />
150
+ <el-table-column prop="name" label="名称" />
151
+ </el-table>
152
+
153
+ <template #pagination>
154
+ <el-pagination background :total="total"
155
+ :current-page="pageParam.page" :page-size="pageParam.pageSize"
156
+ layout="total, prev, pager, next, sizes"
157
+ @current-change="page => { pageParam.page = page; fetchList() }"
158
+ @size-change="size => { pageParam.pageSize = size; pageParam.page = 1; fetchList() }" />
159
+ </template>
160
+ </list-page>
161
+ </div>
162
+ </template>
163
+
164
+ <script>
165
+ export default {
166
+ inject: {
167
+ listPage: { from: 'listPage', default() { return { tableHeight: 300 } } }
168
+ },
169
+ data() {
170
+ return {
171
+ filters: { name: '', status: '', year: '' },
172
+ pageParam: { page: 1, pageSize: 10 },
173
+ list: [],
174
+ total: 0,
175
+ loading: false,
176
+ }
177
+ },
178
+ computed: {
179
+ tableHeight() { return this.listPage.tableHeight },
180
+ filterItems() {
181
+ return [
182
+ { label: '名称', prop: 'name', type: 'input', span: 8 },
183
+ { label: '状态', prop: 'status', type: 'select', span: 8,
184
+ options: [{ value: '1', label: '待审核' }, { value: '2', label: '已通过' }]
185
+ },
186
+ { label: '年度', prop: 'year', type: 'yearpicker', span: 8 },
187
+ ]
188
+ },
189
+ },
190
+ mounted() { this.fetchList() },
191
+ methods: {
192
+ onSearch(model) {
193
+ this.pageParam = { ...this.pageParam, ...model, page: 1 }
194
+ this.fetchList()
195
+ },
196
+ onReset(model) {
197
+ this.pageParam = { page: 1, pageSize: 10, ...model }
198
+ this.fetchList()
199
+ },
200
+ fetchList() {
201
+ this.loading = true
202
+ api.getList(this.pageParam).then(res => {
203
+ this.list = res.data
204
+ this.total = res.total
205
+ }).finally(() => { this.loading = false })
206
+ },
207
+ }
208
+ }
209
+ </script>
210
+ ```
211
+
212
+ ### 4.2 关键约定
213
+
214
+ | 约定 | 说明 |
215
+ |------|------|
216
+ | `inject: listPage` | 每个使用 `<list-page>` 的页面必须 inject,用于获取 `tableHeight` |
217
+ | `:height="tableHeight"` | 表格必须绑定动态高度,否则会出现双滚动条或空白 |
218
+ | `filters` 与 `pageParam` 分开 | `filters` 绑定到 `filter-form`;`pageParam` 用于 API 调用,在 `onSearch` 中合并 |
219
+ | options 格式 | 必须 `[{ value, label }]`,原始字段名不管是什么都要 map |
220
+
221
+ ---
222
+
223
+ ## 5. 手动改造页面(人工教程)
224
+
225
+ 如果你不使用 AI 技能,也可以手工将旧列表页改造为新组件。以下是一个完整示例,从旧代码逐步改到新代码。
226
+
227
+ ### 5.1 改造前:旧列表页长什么样
228
+
229
+ 旧页面通常包含 `queryCondition` 对象管理筛选条件、`isshowbutton` 控制折叠展开、`tableheight()` 计算表格高度,外加一堆样板式的 `<el-form>` 筛选区模板:
230
+
231
+ ```html
232
+ <!-- 旧代码:改造前 -->
233
+ <template>
234
+ <div class="my-page">
235
+ <!-- 筛选区:一堆 el-form-item -->
236
+ <el-form :model="queryCondition" label-position="right" inline>
237
+ <el-row>
238
+ <el-col :span="8">
239
+ <el-form-item label="名称">
240
+ <el-input v-model="queryCondition.name" />
241
+ </el-form-item>
242
+ </el-col>
243
+ <el-col :span="8">
244
+ <el-form-item label="状态">
245
+ <el-select v-model="queryCondition.status">
246
+ <el-option label="待审核" value="1" />
247
+ </el-select>
248
+ </el-form-item>
249
+ </el-col>
250
+ <!-- ... 更多字段 -->
251
+ </el-row>
252
+ <div>
253
+ <el-button type="primary" @click="queryListByCriteria">查询</el-button>
254
+ <el-button @click="resetQuery">重置</el-button>
255
+ </div>
256
+ </el-form>
257
+
258
+ <!-- 表格 -->
259
+ <el-table :data="tableData" :height="tableHeight" border stripe>
260
+ <el-table-column prop="name" label="名称" />
261
+ </el-table>
262
+
263
+ <!-- 分页 -->
264
+ <el-pagination
265
+ :total="total"
266
+ :current-page="queryCondition.page"
267
+ :page-size="queryCondition.limit"
268
+ @current-change="page => { queryCondition.page = page; queryListByCriteria() }"
269
+ />
270
+ </div>
271
+ </template>
272
+
273
+ <script>
274
+ export default {
275
+ data() {
276
+ return {
277
+ isshowbutton: true, // 删
278
+ isfolid: true, // 删
279
+ screenHeight: 0, // 删
280
+ tableHeight: 400,
281
+ queryCondition: {
282
+ name: '', status: '', // → 迁移到 filters
283
+ page: 1, limit: 10, // → 迁移到 pageParam
284
+ },
285
+ tableData: [], total: 0,
286
+ }
287
+ },
288
+ mounted() {
289
+ this.tableheight() // 删
290
+ this.queryListByCriteria()
291
+ },
292
+ methods: {
293
+ unfoldandfold() { /* 删 */ },
294
+ tableheight() { /* 删,改为 inject tableHeight */ },
295
+ resetQuery() {
296
+ this.queryCondition = { name: '', status: '', page: 1, limit: 10 }
297
+ this.queryListByCriteria()
298
+ },
299
+ queryListByCriteria() {
300
+ api.getList(this.queryCondition).then(res => {
301
+ this.tableData = res.data; this.total = res.total
302
+ })
303
+ },
304
+ }
305
+ }
306
+ </script>
307
+ ```
308
+
309
+ ### 5.2 改造步骤
310
+
311
+ #### 第 1 步:备份原文件
312
+
313
+ ```bash
314
+ cp src/views/my-page.vue /tmp/my-page.vue.bak
315
+ ```
316
+
317
+ #### 第 2 步:替换模板
318
+
319
+ 把整个 `<template>` 替换为 `<list-page>` + `<filter-form>` 结构:
320
+
321
+ ```html
322
+ <template>
323
+ <div class="my-page">
324
+ <list-page>
325
+ <template #filter>
326
+ <filter-form :items="filterItems" v-model="filters"
327
+ @search="onSearch" @reset="onReset" />
328
+ </template>
329
+ <el-table :data="tableData" :height="tableHeight" border stripe>
330
+ <!-- 列定义原样保留 -->
331
+ </el-table>
332
+ <template #pagination>
333
+ <el-pagination background :total="total"
334
+ :current-page="pageParam.page" :page-size="pageParam.pageSize"
335
+ layout="total, prev, pager, next, sizes"
336
+ @current-change="page => { pageParam.page = page; fetchList() }"
337
+ @size-change="size => { pageParam.pageSize = size; pageParam.page = 1; fetchList() }"
338
+ />
339
+ </template>
340
+ </list-page>
341
+ </div>
342
+ </template>
343
+ ```
344
+
345
+ #### 第 3 步:改造 `<script>`
346
+
347
+ 逐项替换:
348
+
349
+ ```js
350
+ export default {
351
+ // 新增:注入 tableHeight
352
+ inject: {
353
+ listPage: { from: 'listPage', default() { return { tableHeight: 300 } } }
354
+ },
355
+
356
+ data() {
357
+ return {
358
+ // 删:isshowbutton, isfolid, screenHeight
359
+ // 新增:filters(替代 queryCondition 中的筛选字段)
360
+ filters: { name: '', status: '' },
361
+ // 新增:pageParam(替代 queryCondition 中的分页字段)
362
+ pageParam: { page: 1, pageSize: 10 },
363
+ // 保留
364
+ tableData: [], total: 0, loading: false,
365
+ // 删:queryCondition(拆分为 filters + pageParam)
366
+ }
367
+ },
368
+
369
+ computed: {
370
+ // 新增:从 listPage 获取动态高度
371
+ tableHeight() { return this.listPage.tableHeight },
372
+ // 新增:filter-form 配置
373
+ filterItems() {
374
+ return [
375
+ { label: '名称', prop: 'name', type: 'input', span: 8 },
376
+ { label: '状态', prop: 'status', type: 'select', span: 8,
377
+ options: [{ value: '1', label: '待审核' }, { value: '2', label: '已通过' }]
378
+ },
379
+ // ... 把旧模板中每个 <el-form-item> 变成一行配置
380
+ ]
381
+ },
382
+ },
383
+
384
+ mounted() {
385
+ // 删:this.tableheight()
386
+ this.fetchList()
387
+ },
388
+
389
+ methods: {
390
+ // 删:unfoldandfold(), tableheight()
391
+ // 新增
392
+ onSearch(model) {
393
+ this.pageParam = { ...this.pageParam, ...model, page: 1 }
394
+ this.fetchList()
395
+ },
396
+ onReset(model) {
397
+ this.pageParam = { page: 1, pageSize: 10, ...model }
398
+ this.fetchList()
399
+ },
400
+ // 重命名:queryListByCriteria → fetchList
401
+ fetchList() {
402
+ this.loading = true
403
+ const params = { ...this.filters, ...this.pageParam }
404
+ api.getList(params).then(res => {
405
+ this.tableData = res.data; this.total = res.total
406
+ }).finally(() => { this.loading = false })
407
+ },
408
+ }
409
+ }
410
+ ```
411
+
412
+ #### 第 4 步:逐项清理对照表
413
+
414
+ | 删掉的旧代码 | 替换成 |
415
+ |-------------|--------|
416
+ | `queryCondition: { name, status, page, limit }` | `filters: { name, status }` + `pageParam: { page, pageSize }` |
417
+ | `<el-form>` 筛选区整块模板 | `<filter-form :items="filterItems" v-model="filters" />` |
418
+ | `tableheight()` 方法 | `inject: { listPage }` + computed `tableHeight` |
419
+ | `unfoldandfold()` / `unfoldandfold1()` | filter-form 内置折叠,无需方法 |
420
+ | `isshowbutton` / `isfolid` | filter-form 内置,无需 data |
421
+ | `resetQuery()` | `onReset()`,用 filter-form 的 @reset 事件 |
422
+ | `queryListByCriteria()` | `fetchList()`,用 filter-form 的 @search 事件 |
423
+
424
+ #### 第 5 步:构建验证
425
+
426
+ ```bash
427
+ npm run build 2>&1 | tail -20
428
+ ```
429
+
430
+ ### 5.3 常见错误
431
+
432
+ | 错误 | 原因 | 解决 |
433
+ |------|------|------|
434
+ | template "no matching end tag" | `<list-page>` 和 `<el-dialog>` 没被同一 `<div>` 包裹 | 用 `<div class="pageName">` 包裹两者 |
435
+ | 表格高度塌陷或双滚动条 | 没 inject 或没绑定 `:height="tableHeight"` | 确认 inject 和 `:height` 都已加 |
436
+ | 筛选条件不生效 | onSearch 里把 filters 合并到 pageParam 时漏了字段 | 用 `{ ...this.pageParam, ...model }` |
437
+ | options 下拉为空 | 数据未 map 为 `[{ value, label }]` | 检查 `.map()` 字段名映射 |
438
+ | 联动下拉不刷新 | 联动回调中用了 `this.pageParam.xxx` | 改为 `this.filters.xxx` |
439
+ | 可选链 `?.` 编译失败 | Webpack 4 不认 `?.` 语法 | 改为 `if (x) x.method()` |
440
+ | 页码重置不生效 | `onSearch` 忘记设 `page: 1` | 确保 `{ ...model, page: 1 }` |
441
+
442
+ ---
443
+
444
+ ## 6. 使用 AI 技能改造页面
445
+
446
+ `npx install-skills` 会把两个 Claude Code 技能文件部署到 `.claude/skills/`。
447
+
448
+ ### 6.1 前提条件
449
+
450
+ 使用技能改造页面,需要:
451
+ - 项目已安装 Claude Code CLI (或支持读取.claude目录的ai工具,也可以改名文件夹为ai工具支持的名字)
452
+ - 在项目目录下打开终端,运行 `claude` 进入交互模式
453
+
454
+ ### 6.2 列表页面改造
455
+
456
+ 在 Claude Code 对话中直接说:
457
+
458
+ > "改造 xxx 列表页面"
459
+ > "把这个页面改成 list-page + filter-form"
460
+
461
+ AI 会自动激活 `list-page-refactor` 技能,按照规范进行改造:读取页面 → 判断模式 → 改造模板 → 注入 tableHeight → 写 filterItems → 替换 onSearch/onReset。
462
+
463
+ 改造过程中 AI 会:
464
+ - 自动备份原文件
465
+ - 一次只改一个页面,改完等你确认
466
+ - 保留所有业务逻辑(API 调用、对话框、抽屉等)
467
+
468
+ ### 6.3 新项目接入
469
+
470
+ > "帮我配置这个微应用项目"
471
+
472
+ AI 会激活 `micro-app-setup` 技能,自动检查:端口冲突、CORS 配置、qiankun 布局隐藏、`.container` 满高样式等。
473
+
474
+ ---
475
+
476
+ ## 7. 更新
477
+
478
+ 组件有更新时,升级 npm 包即可:
479
+
480
+ ```bash
481
+ npm update @suzhou-lab/page-components
482
+ ```
483
+
484
+ 如需更新技能文件:
485
+
486
+ ```bash
487
+ npx install-skills
488
+ ```
489
+
490
+ 组件完全自包含,**不需要改项目原有的任何文件**。
491
+
492
+ ---
493
+
494
+ ## 8. 适用项目
495
+
496
+ - `establish-ui`(项目申报)
497
+ - `collect-ui`(采集)
498
+ - `review-ui`(评审)
499
+ - 其他基于 Vue 2 + Vue CLI 3 + Element UI + qiankun 的微应用
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs')
3
+ const path = require('path')
4
+
5
+ // skills/ 相对于本脚本所在目录(bin/)
6
+ const src = path.join(__dirname, '..', 'skills')
7
+ const dest = path.join(process.cwd(), '.claude', 'skills')
8
+
9
+ if (!fs.existsSync(src)) {
10
+ console.error('❌ skills 目录不存在,请检查包是否完整')
11
+ process.exit(1)
12
+ }
13
+
14
+ fs.cpSync(src, dest, { recursive: true })
15
+ console.log('✅ AI 技能已部署到 .claude/skills/')