npmapps 1.0.24 → 1.0.26
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/app/.codegraph/daemon.pid +6 -0
- package/app/.eslintrc.js +19 -0
- package/app/README.md +24 -0
- package/app/babel.config.js +5 -0
- package/app/devtool-windows-amd64.zip +0 -0
- package/app/docs/superpowers/plans/2026-05-29-quill-editor.md +836 -0
- package/app/docs/superpowers/specs/2026-05-29-quill-editor-design.md +210 -0
- package/app/docs/superpowers/specs/2026-06-06-lazy-cascader-design.md +400 -0
- package/app/jsconfig.json +19 -0
- package/app/package-lock.json +21347 -0
- package/app/package.json +63 -0
- package/app/postcss.config.js +10 -0
- package/app/public/favicon.ico +0 -0
- package/app/public/index.html +17 -0
- package/app/public//344/270/200/351/224/256/351/273/221/346/232/227.html +136 -0
- package/app/src/App.vue +110 -0
- package/app/src/assets/bpmn-camunda.jpg +0 -0
- package/app/src/assets/css/diagram.less +17 -0
- package/app/src/assets/icon/Icon.less +31 -0
- package/app/src/assets/icon/font/app-codes.css +26 -0
- package/app/src/assets/icon/font/app.eot +0 -0
- package/app/src/assets/icon/font/app.svg +60 -0
- package/app/src/assets/icon/font/app.ttf +0 -0
- package/app/src/assets/icon/font/app.woff +0 -0
- package/app/src/assets/icon/font/app.woff2 +0 -0
- package/app/src/assets/icon/font/config.json +248 -0
- package/app/src/assets/icon/font/source/raw/align-bottom-tool.svg +30 -0
- package/app/src/assets/icon/font/source/raw/align-horizontal-center-tool.svg +85 -0
- package/app/src/assets/icon/font/source/raw/align-left-tool.svg +84 -0
- package/app/src/assets/icon/font/source/raw/align-right-tool.svg +80 -0
- package/app/src/assets/icon/font/source/raw/align-top-tool.svg +84 -0
- package/app/src/assets/icon/font/source/raw/align-vertical-center-tool.svg +89 -0
- package/app/src/assets/icon/font/source/raw/distribute-horizontally-tool.svg +95 -0
- package/app/src/assets/icon/font/source/raw/distribute-vertically-tool.svg +99 -0
- package/app/src/assets/icon/font/source/raw/set-color-tool.svg +111 -0
- package/app/src/assets/icon/font/source/symbols/align-bottom-tool.svg +30 -0
- package/app/src/assets/icon/font/source/symbols/align-horizontal-center-tool.svg +30 -0
- package/app/src/assets/icon/font/source/symbols/align-left-tool.svg +30 -0
- package/app/src/assets/icon/font/source/symbols/align-right-tool.svg +30 -0
- package/app/src/assets/icon/font/source/symbols/align-top-tool.svg +30 -0
- package/app/src/assets/icon/font/source/symbols/align-vertical-center-tool.svg +30 -0
- package/app/src/assets/icon/font/source/symbols/distribute-horizontally-tool.svg +30 -0
- package/app/src/assets/icon/font/source/symbols/distribute-vertically-tool.svg +30 -0
- package/app/src/assets/icon/font/source/symbols/set-color-tool.svg +63 -0
- package/app/src/assets/logo.png +0 -0
- package/app/src/components/EllTable/README.md +70 -0
- package/app/src/components/EllTable/article.md +184 -0
- package/app/src/components/EllTable/index.js +213 -0
- package/app/src/components/FormulaEditor/FunctionSelector.vue +123 -0
- package/app/src/components/FormulaEditor/OperatorSelector.vue +184 -0
- package/app/src/components/FormulaEditor/ParameterSelector.vue +123 -0
- package/app/src/components/FormulaEditor/api.js +69 -0
- package/app/src/components/FormulaEditor/index.vue +435 -0
- package/app/src/components/HelloWorld.vue +58 -0
- package/app/src/components/PageHeader/index.vue +158 -0
- package/app/src/components/Splitter/README.md +144 -0
- package/app/src/components/Splitter/example.vue +88 -0
- package/app/src/components/Splitter/index.vue +203 -0
- package/app/src/components/diagram/ToolBar.vue +357 -0
- package/app/src/components/diagram/customTranslate/customTranslate.js +12 -0
- package/app/src/components/diagram/customTranslate/translationsGerman.js +241 -0
- package/app/src/components/diagram/index.vue +261 -0
- package/app/src/components/diagram/xmlData.js +29 -0
- package/app/src/directives/filldown.js +155 -0
- package/app/src/directives/filldownTable.js +291 -0
- package/app/src/main.js +40 -0
- package/app/src/router/index.js +63 -0
- package/app/src/store/index.js +23 -0
- package/app/src/utils/winBox.js +23 -0
- package/app/src/views/Extend/A.vue +12 -0
- package/app/src/views/Extend/B.vue +10 -0
- package/app/src/views/Extend/MagicalComponentsForELFormItem.vue +87 -0
- package/app/src/views/Extend/index.vue +59 -0
- package/app/src/views/Extend/tableMouseHorizontalWheel.vue +193 -0
- package/app/src/views/Home.vue +37 -0
- package/app/src/views/RouterJump.vue +155 -0
- package/app/src/views/css.vue +57 -0
- package/app/src/views/cssComponents/EllipsisText.vue +83 -0
- package/app/src/views/cssComponents/HoverCard.vue +79 -0
- package/app/src/views/cssComponents/TableHover.vue +140 -0
- package/app/src/views/cssComponents/inputSlo.vue +52 -0
- package/app/src/views/cssComponents/tableFixed.vue +158 -0
- package/app/src/views/echarts/echart-dome.vue +82 -0
- package/app/src/views/echarts/index.vue +118 -0
- package/app/src/views/echarts/pei3d.vue +667 -0
- package/app/src/views/element/bpmn/index.vue +18 -0
- package/app/src/views/element/components/attendanceCycle/index.vue +131 -0
- package/app/src/views/element/components/attendanceGroup/index.vue +147 -0
- package/app/src/views/element/components/attendancePersonnel/index.vue +158 -0
- package/app/src/views/element/components/companyCalendar/index.vue +147 -0
- package/app/src/views/element/components/shift/index.vue +147 -0
- package/app/src/views/element/components/shiftRotationSystem/index.vue +147 -0
- package/app/src/views/element/elTableJsx/columnManagement.vue +340 -0
- package/app/src/views/element/elTableJsx/dialogInput.vue +71 -0
- package/app/src/views/element/elTableJsx/elTableJsx.vue +1826 -0
- package/app/src/views/element/elTableJsx/formTable.vue +598 -0
- package/app/src/views/element/elTableJsx/index.vue +29 -0
- package/app/src/views/element/elTableJsx/simpleTable.vue +192 -0
- package/app/src/views/element/elTableJsx.zip +0 -0
- package/app/src/views/element/index.vue +44 -0
- package/app/src/views/element/lazyCascader/LazyCascader.vue +302 -0
- package/app/src/views/element/lazyCascader/data.js +205 -0
- package/app/src/views/element/lazyCascader/index.vue +315 -0
- package/app/src/views/element/quillEditor/README.md +163 -0
- package/app/src/views/element/quillEditor/example.vue +314 -0
- package/app/src/views/element/quillEditor/index.vue +409 -0
- package/app/src/views/element/quillEditor/toolbar.js +122 -0
- package/app/vue.config.js +15 -0
- package/package.json +1 -1
- package/app/wujie-vue3-child/.claude/settings.local.json +0 -8
- package/app/wujie-vue3-child/.vscode/extensions.json +0 -3
- package/app/wujie-vue3-child/PROJECT_MEMORY.md +0 -427
- package/app/wujie-vue3-child/README.md +0 -5
- package/app/wujie-vue3-child/index.html +0 -13
- package/app/wujie-vue3-child/package-lock.json +0 -5744
- package/app/wujie-vue3-child/package.json +0 -28
- package/app/wujie-vue3-child/public/vite.svg +0 -1
- package/app/wujie-vue3-child/src/App.vue +0 -130
- package/app/wujie-vue3-child/src/assets/vue.svg +0 -1
- package/app/wujie-vue3-child/src/components/HelloWorld.vue +0 -43
- package/app/wujie-vue3-child/src/components/tags-view.vue +0 -193
- package/app/wujie-vue3-child/src/components/tags-view1.vue +0 -131
- package/app/wujie-vue3-child/src/directives/aiLoading.js +0 -182
- package/app/wujie-vue3-child/src/hooks/useClickOutside.js +0 -11
- package/app/wujie-vue3-child/src/hooks/useTableDragSort.js +0 -28
- package/app/wujie-vue3-child/src/main.js +0 -18
- package/app/wujie-vue3-child/src/router/index.js +0 -104
- package/app/wujie-vue3-child/src/store/tagsViewStroe.js +0 -34
- package/app/wujie-vue3-child/src/style.css +0 -171
- package/app/wujie-vue3-child/src/views/aiCoach/collapseExpand/index.jsx +0 -108
- package/app/wujie-vue3-child/src/views/aiCoach/collapseExpand/index.module.scss +0 -97
- package/app/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/README.md +0 -836
- package/app/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/REFLEX_EXAMPLES.md +0 -728
- package/app/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/DepartmentPersonnelSelector.jsx +0 -687
- package/app/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/DepartmentPersonnelSelector.module.scss +0 -560
- package/app/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/DepartmentSelector.jsx +0 -570
- package/app/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/DepartmentSelector.module.scss +0 -330
- package/app/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/DepartmentSelectorV2.jsx +0 -378
- package/app/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/DepartmentSelectorV2.module.scss +0 -228
- package/app/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/OptionsSelector.jsx +0 -399
- package/app/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/OptionsSelector.module.scss +0 -252
- package/app/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/PersonnelSelector.jsx +0 -585
- package/app/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/PersonnelSelector.module.scss +0 -331
- package/app/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/PopoverSelector.jsx +0 -392
- package/app/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/PopoverSelector.module.scss +0 -39
- package/app/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/README.md +0 -248
- package/app/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/SelectorTrigger.jsx +0 -194
- package/app/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/index.jsx +0 -1459
- package/app/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/mockData.js +0 -301
- package/app/wujie-vue3-child/src/views/aiCoach/dialogueSegment/index.jsx +0 -182
- package/app/wujie-vue3-child/src/views/aiCoach/dialogueSegment/index.module.scss +0 -28
- package/app/wujie-vue3-child/src/views/aiCoach/index.jsx +0 -293
- package/app/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/ChartsPanel/index.jsx +0 -121
- package/app/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/ChartsPanel/index.module.scss +0 -76
- package/app/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/DonutChart/index.jsx +0 -104
- package/app/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/PracticeTable/index.jsx +0 -75
- package/app/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/PracticeTable/index.module.scss +0 -12
- package/app/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/RankBarChart/index.jsx +0 -62
- package/app/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/RankBarChart/index.module.scss +0 -43
- package/app/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/RankingGroup/index.jsx +0 -29
- package/app/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/RankingGroup/index.module.scss +0 -5
- package/app/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/RankingList/index.jsx +0 -58
- package/app/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/RankingList/index.module.scss +0 -85
- package/app/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/ScriptStatsPanel/index.jsx +0 -92
- package/app/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/ScriptStatsPanel/index.module.scss +0 -56
- package/app/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/StatCardsRow/index.jsx +0 -40
- package/app/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/StatCardsRow/index.module.scss +0 -53
- package/app/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/echarts/EchartsDonut.jsx +0 -106
- package/app/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/echarts/EchartsRankBar.jsx +0 -132
- package/app/wujie-vue3-child/src/views/aiCoach/practiceStatus/index.jsx +0 -176
- package/app/wujie-vue3-child/src/views/aiCoach/practiceStatus/index.module.scss +0 -96
- package/app/wujie-vue3-child/src/views/aiCoach/reportDetail/components/CoachReport/index.jsx +0 -162
- package/app/wujie-vue3-child/src/views/aiCoach/reportDetail/components/CoachReport/index.module.scss +0 -16
- package/app/wujie-vue3-child/src/views/aiCoach/reportDetail/components/ComprehensiveEvaluation/index.jsx +0 -29
- package/app/wujie-vue3-child/src/views/aiCoach/reportDetail/components/ComprehensiveEvaluation/index.module.scss +0 -25
- package/app/wujie-vue3-child/src/views/aiCoach/reportDetail/components/DialogueBubble/index.jsx +0 -106
- package/app/wujie-vue3-child/src/views/aiCoach/reportDetail/components/DialogueBubble/index.module.scss +0 -164
- package/app/wujie-vue3-child/src/views/aiCoach/reportDetail/components/DialogueRecord/index.jsx +0 -182
- package/app/wujie-vue3-child/src/views/aiCoach/reportDetail/components/DialogueRecord/index.module.scss +0 -203
- package/app/wujie-vue3-child/src/views/aiCoach/reportDetail/components/DimensionDetail/index.jsx +0 -145
- package/app/wujie-vue3-child/src/views/aiCoach/reportDetail/components/DimensionDetail/index.module.scss +0 -126
- package/app/wujie-vue3-child/src/views/aiCoach/reportDetail/components/DimensionScores/index.jsx +0 -67
- package/app/wujie-vue3-child/src/views/aiCoach/reportDetail/components/DimensionScores/index.module.scss +0 -105
- package/app/wujie-vue3-child/src/views/aiCoach/reportDetail/components/ReportHeader/index.jsx +0 -81
- package/app/wujie-vue3-child/src/views/aiCoach/reportDetail/components/ReportHeader/index.module.scss +0 -47
- package/app/wujie-vue3-child/src/views/aiCoach/reportDetail/components/RoleInfo/index.jsx +0 -64
- package/app/wujie-vue3-child/src/views/aiCoach/reportDetail/components/RoleInfo/index.module.scss +0 -85
- package/app/wujie-vue3-child/src/views/aiCoach/reportDetail/components/ScoreBadge/index.jsx +0 -39
- package/app/wujie-vue3-child/src/views/aiCoach/reportDetail/components/ScoreBadge/index.module.scss +0 -44
- package/app/wujie-vue3-child/src/views/aiCoach/reportDetail/components/SubDimensionItem/index.jsx +0 -83
- package/app/wujie-vue3-child/src/views/aiCoach/reportDetail/components/SubDimensionItem/index.module.scss +0 -101
- package/app/wujie-vue3-child/src/views/aiCoach/reportDetail/index.jsx +0 -50
- package/app/wujie-vue3-child/src/views/aiCoach/reportDetail/index.module.scss +0 -25
- package/app/wujie-vue3-child/src/views/aiCoach/scriptTable/index.jsx +0 -196
- package/app/wujie-vue3-child/src/views/aiCoach/scriptTable/index.module.scss +0 -41
- package/app/wujie-vue3-child/src/views/aiCoach/scriptTable/inputColumn/index.jsx +0 -183
- package/app/wujie-vue3-child/src/views/aiCoach/scriptTable/inputColumn/index.module.scss +0 -115
- package/app/wujie-vue3-child/src/views/child-to-parent.vue +0 -117
- package/app/wujie-vue3-child/src/views/home.vue +0 -53
- package/app/wujie-vue3-child/src/views/jsx/btnSelect/btnSelect.vue +0 -169
- package/app/wujie-vue3-child/src/views/jsx/btnSelect/index.vue +0 -69
- package/app/wujie-vue3-child/src/views/jsx/com.vue +0 -44
- package/app/wujie-vue3-child/src/views/jsx/dialog.jsx +0 -66
- package/app/wujie-vue3-child/src/views/jsx/index.vue +0 -72
- package/app/wujie-vue3-child/src/views/jsx/props.vue +0 -33
- package/app/wujie-vue3-child/src/views/parent-to-child.vue +0 -225
- package/app/wujie-vue3-child/src/views/phone-code.vue +0 -318
- package/app/wujie-vue3-child/src/views/router-jump.vue +0 -123
- package/app/wujie-vue3-child/src/views/test.vue +0 -192
- package/app/wujie-vue3-child/vite.config.js +0 -15
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# QuillEditor组件封装设计
|
|
2
|
+
|
|
3
|
+
**日期**:2026-05-29
|
|
4
|
+
**作者**: wk-7500f
|
|
5
|
+
**状态**: Approved
|
|
6
|
+
|
|
7
|
+
##背景
|
|
8
|
+
|
|
9
|
+
`src/views/element/index.vue` 已经预留了 "Quill编辑器" tab 并 import 了
|
|
10
|
+
`./quillEditor/example.vue`,但 `src/views/element/quillEditor/`目录为空,
|
|
11
|
+
example.vue也不存在。需要:
|
|
12
|
+
|
|
13
|
+
1.封装一个功能齐全的 Quill 富文本编辑器 Vue2组件
|
|
14
|
+
2. 必须包含表格(table)功能
|
|
15
|
+
3. 在 element tab 下展示多实例对比案例
|
|
16
|
+
|
|
17
|
+
##决策记录
|
|
18
|
+
|
|
19
|
+
|维度 | 选择 |理由 |
|
|
20
|
+
|------|------|------|
|
|
21
|
+
| Quill 版本 |2.0.2 | quill-table-better@1.2.3明确要求 quill.js >= v2.0.0;Quill 是框架无关的,Vue2也能用2.x;生态最新最活跃 |
|
|
22
|
+
|表格模块 | quill-table-better1.2.3 |唯一活跃维护的 Quill表格模块,功能齐全(增删/合并/拆分/样式),官方要求 Quill2.x |
|
|
23
|
+
| 图片上传 | base64 内联 | 无后端依赖、零配置、适合 demo场景 |
|
|
24
|
+
|主题 | snow | Quill 默认浅色主题,和现有 element tab风格一致 |
|
|
25
|
+
|放置位置 | `src/views/element/quillEditor/` | 仅给 element tab 用,不放全局 components |
|
|
26
|
+
|案例形式 | 多实例对比 | 同时展示基础/只读/禁用三种配置 |
|
|
27
|
+
|
|
28
|
+
##架构
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
src/views/element/index.vue (el-tabs)
|
|
32
|
+
└─ <el-tab-pane name="quillEditor">
|
|
33
|
+
└─ <QuillEditorDemo /> (example.vue)
|
|
34
|
+
├─ <QuillEditor v-model="..." /> 实例1:基础
|
|
35
|
+
├─ <QuillEditor v-model="..." :readonly="true"/> 实例2:只读
|
|
36
|
+
└─ <QuillEditor v-model="..." :disabled="true"/> 实例3:禁用
|
|
37
|
+
|
|
38
|
+
src/views/element/quillEditor/
|
|
39
|
+
├── index.vue QuillEditor 主组件
|
|
40
|
+
├── example.vue Demo页面(已存在 tab引用)
|
|
41
|
+
├── toolbar.js 默认工具栏配置
|
|
42
|
+
└── README.md 使用文档
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
外部依赖:
|
|
46
|
+
|
|
47
|
+
- `quill@2.0.2` 富文本核心
|
|
48
|
+
- `quill-table-better@1.2.3`表格模块(明确要求 Quill >=2.0.0)
|
|
49
|
+
- `quill-image-drop-module` 图片拖拽上传(兼容 Quill2.x)
|
|
50
|
+
- `quill-image-resize-module` (兼容 Quill2.x 版本) 图片缩放
|
|
51
|
+
- `quill/dist/quill.snow.css` snow主题样式
|
|
52
|
+
- `quill-table-better/dist/quill-table-better.css`表格样式
|
|
53
|
+
|
|
54
|
+
> **修订记录**:2026-05-29实施前发现 quill-table-better实际依赖 Quill2.x,
|
|
55
|
+
>升级方案由 "Quill1.x + quill-table-better"改为 "Quill2.x + quill-table-better1.2.3"。
|
|
56
|
+
|
|
57
|
+
##组件 API
|
|
58
|
+
|
|
59
|
+
### Props
|
|
60
|
+
|
|
61
|
+
| Prop | 类型 | 默认值 | 说明 |
|
|
62
|
+
|------|------|--------|------|
|
|
63
|
+
| `value` | String | `''` | 编辑器 HTML 内容,支持 v-model |
|
|
64
|
+
| `placeholder` | String | `'请输入内容...'` | 占位文字 |
|
|
65
|
+
| `readonly` | Boolean | `false` | 是否只读 |
|
|
66
|
+
| `disabled` | Boolean | `false` | 是否禁用工具栏 |
|
|
67
|
+
| `height` | String \| Number | `300` | 编辑区高度,数字按 px,字符串按原值 |
|
|
68
|
+
| `toolbar` | Array \| Object | 见下方"默认工具栏" | 自定义工具栏配置;不传则用默认全量 |
|
|
69
|
+
| `imageHandler` | Function | `null` | 自定义图片处理;不传则走 base64 内联 |
|
|
70
|
+
| `theme` | String | `'snow'` |主题 |
|
|
71
|
+
|
|
72
|
+
### Events
|
|
73
|
+
|
|
74
|
+
|事件 |触发时机 | 参数 |
|
|
75
|
+
|------|---------|------|
|
|
76
|
+
| `input` | 内容变化 | `(html: String)` |
|
|
77
|
+
| `change` | 内容变化(去抖300ms) | `(html: String)` |
|
|
78
|
+
| `ready` | Quill初始化完成 | `(quill: Quill)` |
|
|
79
|
+
| `focus` | 编辑器获得焦点 | `()` |
|
|
80
|
+
| `blur` | 编辑器失去焦点 | `()` |
|
|
81
|
+
|
|
82
|
+
### Methods(ref 调用)
|
|
83
|
+
|
|
84
|
+
| 方法 | 返回值 | 说明 |
|
|
85
|
+
|------|--------|------|
|
|
86
|
+
| `getHtml()` | String | 获取当前 HTML |
|
|
87
|
+
| `getText()` | String | 获取纯文本 |
|
|
88
|
+
| `setHtml(html)` | - | 设置内容 |
|
|
89
|
+
| `clear()` | - | 清空内容 |
|
|
90
|
+
| `focus()` | - |聚焦 |
|
|
91
|
+
| `getQuill()` | Quill | 获取底层 Quill 实例 |
|
|
92
|
+
|
|
93
|
+
### 默认工具栏(全量)
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
[
|
|
97
|
+
[{ header: [1,2,3,4,5,6, false] }],
|
|
98
|
+
[{ font: [] }],
|
|
99
|
+
[{ size: ['small', false, 'large', 'huge'] }],
|
|
100
|
+
['bold', 'italic', 'underline', 'strike'],
|
|
101
|
+
[{ color: [] }, { background: [] }],
|
|
102
|
+
[{ script: 'sub' }, { script: 'super' }],
|
|
103
|
+
['blockquote', 'code-block'],
|
|
104
|
+
[{ list: 'ordered' }, { list: 'bullet' }],
|
|
105
|
+
[{ indent: '-1' }, { indent: '+1' }],
|
|
106
|
+
[{ align: [] }],
|
|
107
|
+
['link', 'image', 'video'],
|
|
108
|
+
['table-better'],
|
|
109
|
+
['clean'],
|
|
110
|
+
['undo', 'redo'],
|
|
111
|
+
]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## 文件内部实现要点
|
|
115
|
+
|
|
116
|
+
### toolbar.js
|
|
117
|
+
|
|
118
|
+
导出:
|
|
119
|
+
|
|
120
|
+
- `defaultToolbar`:默认工具栏数组(上述全量配置)
|
|
121
|
+
- `defaultHandlers`:`{ image, 'table-better' }`,image handler触发组件内的 file input;
|
|
122
|
+
table-better 由 quill-table-better 模块自身注册
|
|
123
|
+
|
|
124
|
+
### index.vue关键实现
|
|
125
|
+
|
|
126
|
+
1. **生命周期**
|
|
127
|
+
- `mounted`(nextTick):创建 Quill 实例,挂载到 `ref="editor"` div
|
|
128
|
+
- `beforeDestroy`:销毁 Quill 实例、清理事件、移除 file input
|
|
129
|
+
- 用 `this._quill`缓存实例,避免 hot reload重复挂载
|
|
130
|
+
|
|
131
|
+
2. **v-model双向同步**
|
|
132
|
+
- `watch.value`(immediate):外部值变化 → `quill.clipboard.dangerouslyPasteHTML(value)`
|
|
133
|
+
- 用 `isInternalChange`标志位避免 watch循环
|
|
134
|
+
- Quill `text-change`事件 →触发 `input` +300ms 去抖触发 `change`
|
|
135
|
+
|
|
136
|
+
3. **图片处理(base64 内联)**
|
|
137
|
+
- 自定义 image handler:创建隐藏 `<input type="file" accept="image/*">`,点击触发选择
|
|
138
|
+
- FileReader读 File → onload 转 base64 → `quill.insertEmbed(range.index, 'image', dataUrl)`
|
|
139
|
+
- 若 `imageHandler` prop传入,优先调用 prop;失败回退 base64
|
|
140
|
+
|
|
141
|
+
4. **表格模块**
|
|
142
|
+
- 通过 Quill `theme.include`机制注册 quill-table-better
|
|
143
|
+
- 在 `index.vue` 中 import quill-table-better CSS
|
|
144
|
+
|
|
145
|
+
5. **样式**
|
|
146
|
+
- scoped容器,只控制外层高度和 disabled态样式
|
|
147
|
+
- Quill内部样式通过 `index.vue` 中 `import 'quill/dist/quill.snow.css'`
|
|
148
|
+
|
|
149
|
+
### example.vue 内容
|
|
150
|
+
|
|
151
|
+
```vue
|
|
152
|
+
<template>
|
|
153
|
+
<div class="quill-demo">
|
|
154
|
+
<h3>实例1:基础编辑器(v-model双向绑定)</h3>
|
|
155
|
+
<QuillEditor v-model="content1" @change="onChange" />
|
|
156
|
+
|
|
157
|
+
<h3>实例2:只读模式(readonly)</h3>
|
|
158
|
+
<QuillEditor v-model="content2" :readonly="true" />
|
|
159
|
+
|
|
160
|
+
<h3>实例3:禁用模式(disabled,工具栏灰掉)</h3>
|
|
161
|
+
<QuillEditor v-model="content3" :disabled="true" :height="200" />
|
|
162
|
+
|
|
163
|
+
<h3>当前实例1 的 v-model 内容:</h3>
|
|
164
|
+
<pre>{{ content1 }}</pre>
|
|
165
|
+
</div>
|
|
166
|
+
</template>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
实例1预填一段带表格的 HTML,演示表格功能;实例2、3共享同一份初始内容,
|
|
170
|
+
便于对比 readonly 和 disabled 的视觉差异。
|
|
171
|
+
|
|
172
|
+
##错误处理与边界
|
|
173
|
+
|
|
174
|
+
|场景 | 处理 |
|
|
175
|
+
|------|------|
|
|
176
|
+
|依赖未安装(window.Quill 不存在) |渲染 `<div class="quill-editor-error">请先安装 quill依赖</div>` + console.error |
|
|
177
|
+
| value 非字符串 |强制 `String(value)` |
|
|
178
|
+
|重复创建 | 用 `this._quill`缓存,hot reload 不重复挂载 |
|
|
179
|
+
| watch循环 | `isInternalChange`标志位 |
|
|
180
|
+
|组件销毁 |销毁 Quill 实例、清理事件、移除 file input |
|
|
181
|
+
| 图片 >5MB | ElMessage.warning提示但仍允许插入(base64 内联策略) |
|
|
182
|
+
|
|
183
|
+
## 测试策略
|
|
184
|
+
|
|
185
|
+
项目无现成测试框架(jest/vitest 未安装),通过 example演示 +人工验证:
|
|
186
|
+
|
|
187
|
+
-三个实例能否正常渲染
|
|
188
|
+
- v-model双向同步是否正确
|
|
189
|
+
-工具栏按钮(尤其 table-better、image)是否可用
|
|
190
|
+
- readonly / disabled切换是否生效
|
|
191
|
+
-组件销毁后无控制台报错
|
|
192
|
+
|
|
193
|
+
## 实现步骤
|
|
194
|
+
|
|
195
|
+
1. 安装依赖:`quill@2.0.2`、`quill-table-better@1.2.3`、`quill-image-drop-module`、
|
|
196
|
+
`quill-image-resize-module`(注意选兼容 Quill2.x 的版本)
|
|
197
|
+
2.写 `toolbar.js`(默认工具栏 + handlers)
|
|
198
|
+
3.写 `index.vue`(QuillEditor 主组件,实现上述所有要点)
|
|
199
|
+
4.写 `example.vue`(三实例对比 demo)
|
|
200
|
+
5.写 `README.md`(用法 + Props/Events/Methods 说明)
|
|
201
|
+
6. `index.vue` (element tab) 已经 import 该组件,无需修改
|
|
202
|
+
|
|
203
|
+
##范围之外(本期不做)
|
|
204
|
+
|
|
205
|
+
- 图片上传到后端 API(默认 base64,后续如需可加 `imageHandler` prop)
|
|
206
|
+
- 自定义主题(只用 snow)
|
|
207
|
+
-暗黑模式
|
|
208
|
+
-协同编辑
|
|
209
|
+
- 字数限制(可在调用方通过 `change`事件自行截断)
|
|
210
|
+
-移动端适配
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
# 懒加载级联选择器(LazyCascader)设计
|
|
2
|
+
|
|
3
|
+
**日期**:2026-06-06
|
|
4
|
+
**作者**: wk-7500f
|
|
5
|
+
**状态**: Approved
|
|
6
|
+
|
|
7
|
+
## 背景
|
|
8
|
+
|
|
9
|
+
`el-cascader`(Element UI 2.x)自带的懒加载模式(`lazy` + `lazyLoad`)是按需向
|
|
10
|
+
后端请求子节点的。但在某些场景下,后端会**一次性**返回所有数据(扁平数组或
|
|
11
|
+
树形结构),数据量较大(数千条),如果直接拼成整棵 options 树一次性渲染会
|
|
12
|
+
导致卡顿。本设计要解决:
|
|
13
|
+
|
|
14
|
+
1. 把"一次性拿到的大数据"在**前端**拆成"按需懒加载"的体验
|
|
15
|
+
2. 支持**默认值回显**(虽然数据全在前端,但 el-cascader lazy 模式初始化时
|
|
16
|
+
不会主动拉祖先节点,需要手动喂数据)
|
|
17
|
+
3. 支持**只选最后一级**且输入框只显示最后一级(`emitPath: false`)
|
|
18
|
+
4. 支持 `v-if` 销毁/重建组件后仍然能正确回显
|
|
19
|
+
5. 支持"先有默认值、后到数据"和"后端给的是树形结构"的兼容
|
|
20
|
+
6. 提供**树→扁平**工具函数,把后端返的树形数据转扁平后走同一套逻辑
|
|
21
|
+
|
|
22
|
+
## 决策记录
|
|
23
|
+
|
|
24
|
+
| 维度 | 选择 | 理由 |
|
|
25
|
+
|------|------|------|
|
|
26
|
+
| 组件放置 | `src/views/element/lazyCascader/` | 与 quillEditor / bpmn 等已有 Type 同级;该组件本就是给 element tab 演示用,不上全局 components |
|
|
27
|
+
| 数据契约 | 扁平数组 `{ id, parentId, label }[]` | 与"后端一次性返扁平数组"对齐;`parentId` 为空/null 即根 |
|
|
28
|
+
| 公共组件 | `LazyCascader.vue`(支持 props) | 用户已确认;真实工程做法,可复用 |
|
|
29
|
+
| 懒加载底层 | `el-cascader` 自带 `lazy` + `lazyLoad` | 原生支持,无需 hack |
|
|
30
|
+
| 默认值回显 | 组件内 `mounted` / `watch value` 时主动预 resolve 祖先链并缓存 | 解决 lazy 模式首屏不自动拉祖先的问题 |
|
|
31
|
+
| 树→扁平工具 | `flattenTree(tree)` 递归遍历 children | 放在 `data.js`,与 mock 数据同文件 |
|
|
32
|
+
| Mock 规模 | 3 省 × 3~4 市 × 2~3 区 ≈ 30 节点 | 够演示又不卡 |
|
|
33
|
+
|
|
34
|
+
## 架构
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
src/views/element/index.vue (el-tabs)
|
|
38
|
+
└─ <el-tab-pane label="级联懒加载" name="lazyCascader">
|
|
39
|
+
└─ <LazyCascaderDemo /> (lazyCascader/index.vue)
|
|
40
|
+
|
|
41
|
+
src/views/element/lazyCascader/
|
|
42
|
+
├── index.vue # Demo 页面:展示 6 个 case
|
|
43
|
+
├── LazyCascader.vue # 公共组件:可复用,接受 props
|
|
44
|
+
└── data.js # 模拟扁平数据 + 模拟树形数据 + 工具函数
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 公共组件 API (`LazyCascader.vue`)
|
|
48
|
+
|
|
49
|
+
### Props
|
|
50
|
+
|
|
51
|
+
| Prop | 类型 | 默认值 | 说明 |
|
|
52
|
+
|------|------|--------|------|
|
|
53
|
+
| `value` | `Array \| String` | `[]` | `v-model` 绑定值;`emitPath=true`(默认)时为 id 路径数组;`emitPath=false` 时为叶子 id 字符串(单值) |
|
|
54
|
+
| `data` | `Array` | `[]` | 扁平原始数据 `[{ id, parentId, label }]`;**必传**(若不传则空) |
|
|
55
|
+
| `placeholder` | `String` | `'请选择'` | 输入框占位 |
|
|
56
|
+
| `checkStrictly` | `Boolean` | `false` | `true` 可选任意级;`false` 只能选叶子级 |
|
|
57
|
+
| `emitPath` | `Boolean` | `true` | `true` v-model 接 id 路径数组;`false` 只接叶子 id,输入框只显示最后一级 |
|
|
58
|
+
| `disabled` | `Boolean` | `false` | 是否禁用 |
|
|
59
|
+
| `clearable` | `Boolean` | `true` | 是否可清空 |
|
|
60
|
+
|
|
61
|
+
### Events
|
|
62
|
+
|
|
63
|
+
| 事件 | 触发时机 | 参数 |
|
|
64
|
+
|------|---------|------|
|
|
65
|
+
| `input` | 选择变化 | `(value: Array\|String)` |
|
|
66
|
+
| `change` | 选择变化 | `(value: Array\|String)` |
|
|
67
|
+
| `ready` | 首次挂载完成(已处理默认值回显) | `()` |
|
|
68
|
+
|
|
69
|
+
### Methods(ref 调用)
|
|
70
|
+
|
|
71
|
+
| 方法 | 说明 |
|
|
72
|
+
|------|------|
|
|
73
|
+
| `reload()` | 清空内部缓存并重置,用于"先有默认值、后到数据"场景主动重新拉取 |
|
|
74
|
+
| `getFlatData()` | 返回当前传入的扁平数据(只读引用) |
|
|
75
|
+
|
|
76
|
+
## 核心实现要点 (`LazyCascader.vue`)
|
|
77
|
+
|
|
78
|
+
### 1. 内部缓存
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
data() {
|
|
82
|
+
return {
|
|
83
|
+
_flatList: [], // 扁平数据快照(props.data 的引用,组件内方便用)
|
|
84
|
+
_resolvedCache: new Map(), // parentId -> 已被 lazyLoad 取过的子节点(避免重复 resolve)
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
watch: {
|
|
88
|
+
data: {
|
|
89
|
+
immediate: true,
|
|
90
|
+
handler(v) { this._flatList = v || [] }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 2. `lazyLoad(node, resolve)`
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
lazyLoad(node, resolve) {
|
|
99
|
+
// 模拟接口延时(0~50ms),让 loading 状态可见
|
|
100
|
+
const delay = 0
|
|
101
|
+
setTimeout(() => {
|
|
102
|
+
const parentId = node.level === 0 ? '' : node.data.id
|
|
103
|
+
// 优先用缓存(默认值回显可能预先塞过)
|
|
104
|
+
if (this._resolvedCache.has(parentId)) {
|
|
105
|
+
return resolve(this._resolvedCache.get(parentId))
|
|
106
|
+
}
|
|
107
|
+
const children = this._flatList
|
|
108
|
+
.filter(item => (item.parentId || '') === parentId)
|
|
109
|
+
.map(item => ({
|
|
110
|
+
...item,
|
|
111
|
+
leaf: !this._flatList.some(c => (c.parentId || '') === item.id),
|
|
112
|
+
children: [],
|
|
113
|
+
}))
|
|
114
|
+
this._resolvedCache.set(parentId, children)
|
|
115
|
+
resolve(children)
|
|
116
|
+
}, delay)
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
注意:lazy 模式下,每个被返回的节点必须有 `leaf` 或 `children` 字段,
|
|
121
|
+
Element UI 据此判断是否还能继续展开。
|
|
122
|
+
|
|
123
|
+
### 3. 默认值回显(关键)
|
|
124
|
+
|
|
125
|
+
`el-cascader` 在 `lazy=true` 时,首次渲染**不会**主动调 `lazyLoad` 拉
|
|
126
|
+
`value` 的祖先链(它假设懒加载就是按需,默认值由调用方负责准备好)。
|
|
127
|
+
解决方案:**在 `mounted` / `value` 变化时,提前把"链上每一层"的**所有兄弟**
|
|
128
|
+
都预 resolve 进 cache**。这样 Element UI 渲染首屏时触发 `lazyLoad(根)`,
|
|
129
|
+
我们直接从 cache 返,链上祖先能正常显示,用户也能看到该层所有兄弟
|
|
130
|
+
(未涉及的层不会被提前渲染,lazy 仍然成立)。
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
mounted() {
|
|
134
|
+
this._primeDefaultValue()
|
|
135
|
+
},
|
|
136
|
+
watch: {
|
|
137
|
+
value: {
|
|
138
|
+
immediate: true,
|
|
139
|
+
handler() { this._primeDefaultValue() }
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
methods: {
|
|
143
|
+
_primeDefaultValue() {
|
|
144
|
+
if (!this.value || (Array.isArray(this.value) && this.value.length === 0)) return
|
|
145
|
+
if (!this._flatList || this._flatList.length === 0) return
|
|
146
|
+
// 从 value 提取要回显的叶子 id
|
|
147
|
+
const leafId = this.emitPath
|
|
148
|
+
? this.value[this.value.length - 1]
|
|
149
|
+
: (Array.isArray(this.value) ? this.value[0] : this.value)
|
|
150
|
+
if (!leafId) return
|
|
151
|
+
// 反向追溯整条祖先链
|
|
152
|
+
const chain = []
|
|
153
|
+
let cur = this._flatList.find(i => i.id === leafId)
|
|
154
|
+
while (cur) {
|
|
155
|
+
chain.unshift(cur)
|
|
156
|
+
cur = cur.parentId ? this._flatList.find(i => i.id === cur.parentId) : null
|
|
157
|
+
}
|
|
158
|
+
if (!chain.length) return
|
|
159
|
+
// ⚠️ 关键:对链上每一层,把"该 parent 的所有子"全部预 resolve
|
|
160
|
+
// 不能只缓存链上节点本身,否则兄弟节点展开时 lazyLoad 仍要走
|
|
161
|
+
// 一次同步查表(虽然能 work,但首屏会有"先空再填"的闪烁)
|
|
162
|
+
const parentIds = ['', ...chain.slice(0, -1).map(n => n.id)]
|
|
163
|
+
parentIds.forEach(pid => {
|
|
164
|
+
if (!this._resolvedCache.has(pid)) {
|
|
165
|
+
this._resolvedCache.set(pid, this._buildChildren(pid))
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
},
|
|
169
|
+
_buildChildren(parentId) {
|
|
170
|
+
return this._flatList
|
|
171
|
+
.filter(item => (item.parentId || '') === (parentId || ''))
|
|
172
|
+
.map(item => ({
|
|
173
|
+
...item,
|
|
174
|
+
leaf: !this._flatList.some(c => (c.parentId || '') === item.id),
|
|
175
|
+
children: [],
|
|
176
|
+
}))
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### 4. `props` 透传给 el-cascader
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
computed: {
|
|
185
|
+
cascaderProps() {
|
|
186
|
+
return {
|
|
187
|
+
lazy: true,
|
|
188
|
+
lazyLoad: this.lazyLoad,
|
|
189
|
+
value: 'id',
|
|
190
|
+
label: 'label',
|
|
191
|
+
children: 'children',
|
|
192
|
+
leaf: 'leaf',
|
|
193
|
+
checkStrictly: this.checkStrictly,
|
|
194
|
+
emitPath: this.emitPath,
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Demo 页面 (`lazyCascader/index.vue`)
|
|
201
|
+
|
|
202
|
+
### 案例清单
|
|
203
|
+
|
|
204
|
+
| # | 案例 | 关键 props | 默认值 | 验证点 |
|
|
205
|
+
|---|------|------------|--------|--------|
|
|
206
|
+
| 1 | 基础:可选任意级 | `checkStrictly=true` | 无 | 展开三级、可选任意级关掉面板 |
|
|
207
|
+
| 2 | 基础:只选最后一级 + 输入框只显示最后一级 | `checkStrictly=false`, `emitPath=false` | 无 | 选叶子后输入框只显示叶子 label;v-model 是叶子 id |
|
|
208
|
+
| 3 | 默认值回显 | `emitPath=true` | 预设一个三级路径 | 组件挂载即显示完整路径;可继续点开重选 |
|
|
209
|
+
| 4 | v-if 销毁/重建 | `emitPath=true` | 预设一个三级路径 | 按钮 toggle v-if;再次显示时回显正确 |
|
|
210
|
+
| 5 | 先有默认值、后到数据 | `emitPath=true` | 预设一个三级路径 | 初始 `data=[]` 渲染,1s 后模拟接口返回 `data`;同时触发组件 `reload()` |
|
|
211
|
+
| 6 | 后端返的是树形结构 | `emitPath=true` | 预设一个三级路径 | `tree` 经 `flattenTree` 转扁平后传给组件;正常回显 |
|
|
212
|
+
|
|
213
|
+
每个 case 下有辅助按钮:
|
|
214
|
+
- **查看当前值** — `<pre>` 打印 v-model 和 emitPath 影响
|
|
215
|
+
- **重置** — 清空选择
|
|
216
|
+
- **重置为默认** — 把 v-model 重新设回该 case 的默认
|
|
217
|
+
|
|
218
|
+
### 关键代码骨架
|
|
219
|
+
|
|
220
|
+
```vue
|
|
221
|
+
<template>
|
|
222
|
+
<div class="lazy-cascader-demo">
|
|
223
|
+
<PageHeader title="级联懒加载(前端拆分)" :description="..." :usage="..." />
|
|
224
|
+
|
|
225
|
+
<el-card class="case" header="案例 1: 基础 - 可选任意级">
|
|
226
|
+
<LazyCascader
|
|
227
|
+
v-model="case1.value"
|
|
228
|
+
:data="flatRegions"
|
|
229
|
+
:check-strictly="true"
|
|
230
|
+
placeholder="请选择(任意级)"
|
|
231
|
+
/>
|
|
232
|
+
<div class="ops">
|
|
233
|
+
<el-button size="mini" @click="showValue('case1')">查看当前值</el-button>
|
|
234
|
+
<el-button size="mini" @click="case1.value = []">重置</el-button>
|
|
235
|
+
</div>
|
|
236
|
+
<pre v-if="case1.display">{{ JSON.stringify(case1.value, null, 2) }}</pre>
|
|
237
|
+
</el-card>
|
|
238
|
+
|
|
239
|
+
<el-card class="case" header="案例 2: 基础 - 只选最后一级 + 输入框只显示最后一级">
|
|
240
|
+
<LazyCascader
|
|
241
|
+
v-model="case2.value"
|
|
242
|
+
:data="flatRegions"
|
|
243
|
+
:check-strictly="false"
|
|
244
|
+
:emit-path="false"
|
|
245
|
+
placeholder="请选择(只选叶子)"
|
|
246
|
+
/>
|
|
247
|
+
<!-- ... -->
|
|
248
|
+
</el-card>
|
|
249
|
+
|
|
250
|
+
<el-card class="case" header="案例 3: 默认值回显">
|
|
251
|
+
<LazyCascader
|
|
252
|
+
v-model="case3.value"
|
|
253
|
+
:data="flatRegions"
|
|
254
|
+
:check-strictly="false"
|
|
255
|
+
:emit-path="true"
|
|
256
|
+
/>
|
|
257
|
+
</el-card>
|
|
258
|
+
|
|
259
|
+
<el-card class="case" header="案例 4: v-if 销毁/重建">
|
|
260
|
+
<el-button size="mini" @click="case4.visible = !case4.visible">
|
|
261
|
+
{{ case4.visible ? '隐藏' : '显示' }}
|
|
262
|
+
</el-button>
|
|
263
|
+
<LazyCascader
|
|
264
|
+
v-if="case4.visible"
|
|
265
|
+
v-model="case4.value"
|
|
266
|
+
:data="flatRegions"
|
|
267
|
+
/>
|
|
268
|
+
</el-card>
|
|
269
|
+
|
|
270
|
+
<el-card class="case" header="案例 5: 先有默认值,后到数据">
|
|
271
|
+
<LazyCascader
|
|
272
|
+
:ref="'case5Ref'"
|
|
273
|
+
v-model="case5.value"
|
|
274
|
+
:data="case5.data"
|
|
275
|
+
/>
|
|
276
|
+
<el-button size="mini" @click="loadCase5Data">模拟接口返回(1s 后)</el-button>
|
|
277
|
+
</el-card>
|
|
278
|
+
|
|
279
|
+
<el-card class="case" header="案例 6: 后端返回的是树形结构">
|
|
280
|
+
<LazyCascader
|
|
281
|
+
v-model="case6.value"
|
|
282
|
+
:data="case6.flatData"
|
|
283
|
+
/>
|
|
284
|
+
<div class="ops">
|
|
285
|
+
<el-button size="mini" @click="case6.flatData = flattenTree(treeRegions)">
|
|
286
|
+
重新调用 flattenTree
|
|
287
|
+
</el-button>
|
|
288
|
+
</div>
|
|
289
|
+
</el-card>
|
|
290
|
+
</div>
|
|
291
|
+
</template>
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
`loadCase5Data`:
|
|
295
|
+
|
|
296
|
+
```js
|
|
297
|
+
loadCase5Data() {
|
|
298
|
+
this.$message.info('1s 后模拟接口返回…')
|
|
299
|
+
setTimeout(() => {
|
|
300
|
+
this.case5.data = flatRegions
|
|
301
|
+
this.$nextTick(() => {
|
|
302
|
+
this.$refs.case5Ref?.reload?.() // 触发组件内部 _primeDefaultValue 重跑
|
|
303
|
+
})
|
|
304
|
+
}, 1000)
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Mock 数据 (`data.js`)
|
|
309
|
+
|
|
310
|
+
```js
|
|
311
|
+
// 1. 扁平:3 省 × 3~4 市 × 2~3 区 ≈ 30 节点
|
|
312
|
+
export const flatRegions = [
|
|
313
|
+
// 河北省
|
|
314
|
+
{ id: 'hebei', parentId: '', label: '河北省' },
|
|
315
|
+
{ id: 'sjz', parentId: 'hebei', label: '石家庄市' },
|
|
316
|
+
{ id: 'sjz-cd', parentId: 'sjz', label: '长安区' },
|
|
317
|
+
{ id: 'sjz-qx', parentId: 'sjz', label: '桥西区' },
|
|
318
|
+
// ... (展开 30+ 节点,覆盖 2~3 级)
|
|
319
|
+
]
|
|
320
|
+
|
|
321
|
+
// 2. 树形:同一份数据,只是 children 嵌套
|
|
322
|
+
export const treeRegions = [
|
|
323
|
+
{
|
|
324
|
+
id: 'hebei', label: '河北省', children: [
|
|
325
|
+
{ id: 'sjz', label: '石家庄市', children: [
|
|
326
|
+
{ id: 'sjz-cd', label: '长安区' },
|
|
327
|
+
...
|
|
328
|
+
]},
|
|
329
|
+
...
|
|
330
|
+
]
|
|
331
|
+
},
|
|
332
|
+
...
|
|
333
|
+
]
|
|
334
|
+
|
|
335
|
+
// 3. 工具:树 → 扁平
|
|
336
|
+
export function flattenTree(tree) {
|
|
337
|
+
const out = []
|
|
338
|
+
const walk = (nodes, parentId) => {
|
|
339
|
+
nodes.forEach(n => {
|
|
340
|
+
const { children, ...rest } = n
|
|
341
|
+
out.push({ ...rest, parentId: parentId || '' })
|
|
342
|
+
if (Array.isArray(children) && children.length) walk(children, n.id)
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
walk(tree, '')
|
|
346
|
+
return out
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 4. 模拟接口(返回 Promise,带 0~50ms 延时)
|
|
350
|
+
export function fetchChildren(parentId, flat = flatRegions) {
|
|
351
|
+
return new Promise(resolve => {
|
|
352
|
+
setTimeout(() => {
|
|
353
|
+
resolve(flat.filter(i => (i.parentId || '') === (parentId || '')))
|
|
354
|
+
}, Math.random() * 50)
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
> 真实工程里 `fetchChildren` 可以走 axios;这里用 `flatRegions` 当数据源
|
|
360
|
+
> 模拟,演示"后端一次返完"的语义。
|
|
361
|
+
|
|
362
|
+
## 错误处理与边界
|
|
363
|
+
|
|
364
|
+
| 场景 | 处理 |
|
|
365
|
+
|------|------|
|
|
366
|
+
| `data` 为空数组 | 组件渲染空面板,不报错 |
|
|
367
|
+
| `value` 对应的 id 在 `data` 中找不到 | `_primeDefaultValue` 静默跳过,清空缓存,组件正常显示空 |
|
|
368
|
+
| `value` 是叶子 id 但 `emitPath=true` | 触发整条链追溯,缓存每一层兄弟 |
|
|
369
|
+
| `value` 在 `data` 变化(案例 5)后变更 | `watch data` 触发 `reload()` 由外部调用;组件内部 `watch value` 重新 prime |
|
|
370
|
+
| `flattenTree` 节点无 `children` 字段 | 视为叶子,跳过递归 |
|
|
371
|
+
| 重复创建(_hot reload_) | 用 `this._resolvedCache = new Map()` 在 `created` 初始化,无副作用 |
|
|
372
|
+
|
|
373
|
+
## 测试策略
|
|
374
|
+
|
|
375
|
+
项目无现成测试框架,采用 demo 页面 + 人工验证:
|
|
376
|
+
|
|
377
|
+
- 6 个 case 全部能正常渲染
|
|
378
|
+
- 案例 1:可选任意级,选完关闭面板
|
|
379
|
+
- 案例 2:只能选叶子级;选完输入框只显示叶子 label;v-model 是叶子 id(字符串)
|
|
380
|
+
- 案例 3:挂载即回显默认值,可继续重选
|
|
381
|
+
- 案例 4:v-if 切换后默认值仍然正确
|
|
382
|
+
- 案例 5:`data` 从 `[]` 变 `flatRegions` 后,默认值自动回显
|
|
383
|
+
- 案例 6:`flattenTree(treeRegions)` 出来的扁平数据能正确驱动回显
|
|
384
|
+
|
|
385
|
+
## 实现步骤
|
|
386
|
+
|
|
387
|
+
1. 写 `data.js`(扁平 + 树形 + flattenTree + fetchChildren)
|
|
388
|
+
2. 写 `LazyCascader.vue`(组件实现,带默认值回显逻辑)
|
|
389
|
+
3. 写 `index.vue`(Demo 页面,6 个 case)
|
|
390
|
+
4. 修改 `src/views/element/index.vue`,新增 tab 并 import Demo
|
|
391
|
+
5. 启动 dev server,人工验证 6 个 case
|
|
392
|
+
|
|
393
|
+
## 范围之外(本期不做)
|
|
394
|
+
|
|
395
|
+
- 多选(只做单选)
|
|
396
|
+
- 搜索/过滤
|
|
397
|
+
- 自定义节点渲染(slot)
|
|
398
|
+
- 虚拟滚动(数据量仅 30 节点,无需)
|
|
399
|
+
- 异步获取整棵 data(本期假设 data 同步传入;真要异步可走案例 5 的
|
|
400
|
+
`data=[]` → `data=...` 模式 + `reload()`)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es5",
|
|
4
|
+
"module": "esnext",
|
|
5
|
+
"baseUrl": "./",
|
|
6
|
+
"moduleResolution": "node",
|
|
7
|
+
"paths": {
|
|
8
|
+
"@/*": [
|
|
9
|
+
"src/*"
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
"lib": [
|
|
13
|
+
"esnext",
|
|
14
|
+
"dom",
|
|
15
|
+
"dom.iterable",
|
|
16
|
+
"scripthost"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
}
|