@html-zj/demo 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/OrgTree.vue ADDED
@@ -0,0 +1,57 @@
1
+ <template>
2
+ <div class="org-tree-page">
3
+ <h3>机构树公共组件示例</h3>
4
+
5
+ <el-card shadow="never">
6
+ <el-form label-width="110px" size="mini">
7
+ <el-form-item label="是否有默认值">
8
+ <el-switch v-model="useDefault"></el-switch>
9
+ </el-form-item>
10
+ <el-form-item label="机构选择">
11
+ <OrgTreeSelect v-model="selectedOrgId" />
12
+ </el-form-item>
13
+ </el-form>
14
+ <div class="selected-value">当前值:{{ selectedOrgId || '-' }}</div>
15
+ <div class="selected-value">当前模式:{{ useDefault ? '有默认值' : '无默认值' }}</div>
16
+ </el-card>
17
+ </div>
18
+ </template>
19
+
20
+ <script>
21
+ import OrgTreeSelect from '@/components/OrgTreeSelect.vue'
22
+
23
+ const DEFAULT_ORG_ID = '1320'
24
+
25
+ export default {
26
+ name: 'OrgTree',
27
+ components: {
28
+ OrgTreeSelect
29
+ },
30
+ data () {
31
+ return {
32
+ useDefault: false,
33
+ selectedOrgId: ''
34
+ }
35
+ },
36
+ watch: {
37
+ useDefault: {
38
+ immediate: true,
39
+ handler (value) {
40
+ this.selectedOrgId = value ? DEFAULT_ORG_ID : ''
41
+ }
42
+ }
43
+ }
44
+ }
45
+ </script>
46
+
47
+ <style scoped>
48
+ .org-tree-page {
49
+ padding: 16px;
50
+ }
51
+
52
+ .selected-value {
53
+ margin-top: 8px;
54
+ color: #606266;
55
+ font-size: 13px;
56
+ }
57
+ </style>
@@ -0,0 +1,278 @@
1
+ <template>
2
+ <el-popover
3
+ placement="bottom-start"
4
+ :width="width"
5
+ trigger="click"
6
+ v-model="visible"
7
+ @show="onPopoverShow"
8
+ >
9
+ <div class="tree-panel">
10
+ <el-input
11
+ size="mini"
12
+ clearable
13
+ :placeholder="searchPlaceholder"
14
+ v-model="searchKeyword"
15
+ @input="onSearchInput"
16
+ />
17
+ <el-tree
18
+ v-if="!isSearchMode"
19
+ ref="lazyTree"
20
+ class="org-tree"
21
+ node-key="id"
22
+ lazy
23
+ :load="loadLazyTree"
24
+ :props="treeProps"
25
+ :expand-on-click-node="false"
26
+ @node-click="onNodeClick"
27
+ />
28
+ <el-tree
29
+ v-else
30
+ :key="searchTreeKey"
31
+ class="org-tree"
32
+ node-key="id"
33
+ :data="searchTreeData"
34
+ :props="treeProps"
35
+ :default-expanded-keys="searchExpandedKeys"
36
+ :auto-expand-parent="true"
37
+ :expand-on-click-node="false"
38
+ @node-click="onNodeClick"
39
+ />
40
+ <!-- <div v-show="loading" class="panel-tip">加载中...</div>
41
+ <div v-show="searchNoResult" class="panel-tip">没有匹配到机构</div> -->
42
+ </div>
43
+ <el-input
44
+ slot="reference"
45
+ class="reference-input"
46
+ size="mini"
47
+ readonly
48
+ clearable
49
+ :placeholder="placeholder"
50
+ v-model="selectedName"
51
+ @clear="clearSelection"
52
+ >
53
+ <i slot="suffix" class="el-input__icon el-icon-arrow-down"></i>
54
+ </el-input>
55
+ </el-popover>
56
+ </template>
57
+
58
+ <script>
59
+ import { fetchOrgById, fetchOrgChildren, searchOrgTreeByName } from '@/mock/orgTreeApi'
60
+
61
+ export default {
62
+ name: 'OrgTreeSelect',
63
+ // 组件对外参数说明:
64
+ // 1) value: 当前选中的机构 id(配合 v-model 使用)
65
+ // 2) placeholder/searchPlaceholder: 输入框提示文案
66
+ // 3) width: 下拉面板宽度
67
+ // 4) fetchChildrenApi/searchTreeApi/fetchByIdApi: 可注入的接口函数,默认使用 mock
68
+ props: {
69
+ value: {
70
+ type: String,
71
+ default: ''
72
+ },
73
+ placeholder: {
74
+ type: String,
75
+ default: '请选择机构'
76
+ },
77
+ searchPlaceholder: {
78
+ type: String,
79
+ default: '输入机构名称搜索'
80
+ },
81
+ width: {
82
+ type: Number,
83
+ default: 420
84
+ },
85
+ fetchChildrenApi: {
86
+ type: Function,
87
+ default: fetchOrgChildren
88
+ },
89
+ searchTreeApi: {
90
+ type: Function,
91
+ default: searchOrgTreeByName
92
+ },
93
+ fetchByIdApi: {
94
+ type: Function,
95
+ default: fetchOrgById
96
+ }
97
+ },
98
+ data () {
99
+ return {
100
+ // 控制 popover 展开/关闭
101
+ visible: false,
102
+ // 面板加载状态(懒加载和搜索共用)
103
+ loading: false,
104
+ // 输入框展示的机构名称(与 value 对应)
105
+ selectedName: '',
106
+ // 搜索关键词
107
+ searchKeyword: '',
108
+ // 搜索模式下的树数据
109
+ searchTreeData: [],
110
+ // 搜索模式下需要默认展开的节点 id 列表(用于展开根到目标路径)
111
+ searchExpandedKeys: [],
112
+ // 强制重建搜索树,保证 default-expanded-keys 每次搜索都生效
113
+ searchTreeKey: 0,
114
+ // 是否处于搜索模式:false=懒加载树,true=搜索结果树
115
+ isSearchMode: false,
116
+ searchNoResult: false,
117
+ // 搜索防抖定时器
118
+ searchTimer: null,
119
+ // el-tree 字段映射
120
+ treeProps: {
121
+ label: 'name',
122
+ children: 'children',
123
+ isLeaf: 'isLeaf'
124
+ }
125
+ }
126
+ },
127
+ watch: {
128
+ value: {
129
+ immediate: true,
130
+ // 外部 v-model 变化时,同步回显机构名
131
+ handler (id) {
132
+ this.syncSelectedName(id)
133
+ }
134
+ }
135
+ },
136
+ beforeDestroy () {
137
+ // 组件销毁前清理定时器,避免内存泄漏
138
+ this.clearSearchTimer()
139
+ },
140
+ methods: {
141
+ // 根据机构 id 拉取详情,用于默认值和回显
142
+ async syncSelectedName (id) {
143
+ if (!id) {
144
+ this.selectedName = ''
145
+ return
146
+ }
147
+ const node = await this.fetchByIdApi(id)
148
+ this.selectedName = node ? node.name : ''
149
+ },
150
+ async onPopoverShow () {
151
+ // 每次打开面板,如果当前无搜索词,切回懒加载模式
152
+ if (!this.searchKeyword) {
153
+ this.isSearchMode = false
154
+ this.searchTreeData = []
155
+ this.searchNoResult = false
156
+ }
157
+ if (this.isSearchMode) {
158
+ return
159
+ }
160
+ const treeRef = this.$refs.lazyTree
161
+ if (treeRef && treeRef.store) {
162
+ // 打开面板时确保根节点被加载出来
163
+ await treeRef.store.load(null, () => {})
164
+ }
165
+ },
166
+ // el-tree 懒加载函数:按父节点 id 加载直接子节点
167
+ async loadLazyTree (node, resolve) {
168
+ this.loading = true
169
+ try {
170
+ const parentId = node.level === 0 ? null : node.data.id
171
+ const children = await this.fetchChildrenApi(parentId)
172
+ resolve(
173
+ children.map(item => ({
174
+ id: item.id,
175
+ name: item.name,
176
+ isLeaf: !item.hasChildren
177
+ }))
178
+ )
179
+ } finally {
180
+ this.loading = false
181
+ }
182
+ },
183
+ // 输入搜索词时做防抖,减少接口调用频率
184
+ onSearchInput () {
185
+ this.clearSearchTimer()
186
+ this.searchTimer = setTimeout(() => {
187
+ this.searchTree()
188
+ }, 350)
189
+ },
190
+ clearSearchTimer () {
191
+ if (this.searchTimer) {
192
+ clearTimeout(this.searchTimer)
193
+ }
194
+ this.searchTimer = null
195
+ },
196
+ // 搜索机构:返回“根 -> 命中机构”的树路径结构并展开显示
197
+ async searchTree () {
198
+ const keyword = this.searchKeyword.trim()
199
+ if (!keyword) {
200
+ this.isSearchMode = false
201
+ this.searchTreeData = []
202
+ this.searchExpandedKeys = []
203
+ this.searchNoResult = false
204
+ return
205
+ }
206
+ this.loading = true
207
+ try {
208
+ const treeData = await this.searchTreeApi(keyword)
209
+ this.searchTreeData = treeData
210
+ this.searchExpandedKeys = this.collectExpandableNodeIds(treeData)
211
+ this.searchTreeKey += 1
212
+ this.isSearchMode = true
213
+ this.searchNoResult = treeData.length === 0
214
+ } finally {
215
+ this.loading = false
216
+ }
217
+ },
218
+ // 提取所有非叶子节点 id,让树从根级逐层展开
219
+ collectExpandableNodeIds (treeData) {
220
+ const keys = []
221
+ const loop = (nodes) => {
222
+ nodes.forEach(node => {
223
+ if (node.children && node.children.length > 0) {
224
+ keys.push(node.id)
225
+ loop(node.children)
226
+ }
227
+ })
228
+ }
229
+ loop(treeData || [])
230
+ return keys
231
+ },
232
+ // 点击任意节点即选中,向外抛出 input/change
233
+ onNodeClick (nodeData) {
234
+ this.selectedName = nodeData.name
235
+ this.visible = false
236
+ this.$emit('input', nodeData.id)
237
+ this.$emit('change', { id: nodeData.id, name: nodeData.name })
238
+ },
239
+ // 清空当前选中
240
+ clearSelection () {
241
+ this.selectedName = ''
242
+ this.$emit('input', '')
243
+ this.$emit('change', { id: '', name: '' })
244
+ }
245
+ }
246
+ }
247
+ </script>
248
+
249
+ <style scoped>
250
+ .reference-input {
251
+ cursor: pointer;
252
+ }
253
+
254
+ /* reference 插槽的输入框及图标统一显示手型 */
255
+ .reference-input ::v-deep .el-input__inner,
256
+ .reference-input ::v-deep .el-input__suffix {
257
+ cursor: pointer;
258
+ }
259
+
260
+ .tree-panel {
261
+ display: flex;
262
+ flex-direction: column;
263
+ gap: 8px;
264
+ }
265
+
266
+ .org-tree {
267
+ max-height: 280px;
268
+ overflow: auto;
269
+ border: 1px solid #ebeef5;
270
+ padding: 8px;
271
+ border-radius: 4px;
272
+ }
273
+
274
+ .panel-tip {
275
+ color: #909399;
276
+ font-size: 12px;
277
+ }
278
+ </style>
package/orgTreeApi.js ADDED
@@ -0,0 +1,90 @@
1
+ const ORG_LIST = [
2
+ { id: '1000', name: '集团总部', parentId: null },
3
+ { id: '1100', name: '华北大区', parentId: '1000' },
4
+ { id: '1200', name: '华东大区', parentId: '1000' },
5
+ { id: '1300', name: '华南大区', parentId: '1000' },
6
+ { id: '1110', name: '北京分公司', parentId: '1100' },
7
+ { id: '1120', name: '天津分公司', parentId: '1100' },
8
+ { id: '1210', name: '上海分公司', parentId: '1200' },
9
+ { id: '1220', name: '杭州分公司', parentId: '1200' },
10
+ { id: '1310', name: '广州分公司', parentId: '1300' },
11
+ { id: '1320', name: '深圳分公司', parentId: '1300' },
12
+ { id: '1111', name: '北京一部', parentId: '1110' },
13
+ { id: '1112', name: '北京二部', parentId: '1110' },
14
+ { id: '1211', name: '上海浦东事业部', parentId: '1210' },
15
+ { id: '1212', name: '上海闵行事业部', parentId: '1210' },
16
+ { id: '1321', name: '深圳南山事业部', parentId: '1320' }
17
+ ]
18
+
19
+ function delay (ms = 260) {
20
+ return new Promise(resolve => setTimeout(resolve, ms))
21
+ }
22
+
23
+ function getChildrenByParentId (parentId) {
24
+ const children = ORG_LIST.filter(item => item.parentId === parentId)
25
+ return children.map(item => ({
26
+ id: item.id,
27
+ name: item.name,
28
+ hasChildren: ORG_LIST.some(candidate => candidate.parentId === item.id)
29
+ }))
30
+ }
31
+
32
+ function getNodeById (id) {
33
+ return ORG_LIST.find(item => item.id === id) || null
34
+ }
35
+
36
+ function getAncestorPathIds (id) {
37
+ const ids = []
38
+ let cursor = getNodeById(id)
39
+ while (cursor) {
40
+ ids.unshift(cursor.id)
41
+ cursor = cursor.parentId ? getNodeById(cursor.parentId) : null
42
+ }
43
+ return ids
44
+ }
45
+
46
+ function mergePathToTree (tree, pathIds) {
47
+ let currentLevel = tree
48
+ for (let i = 0; i < pathIds.length; i++) {
49
+ const id = pathIds[i]
50
+ const source = getNodeById(id)
51
+ if (!source) {
52
+ continue
53
+ }
54
+ let currentNode = currentLevel.find(item => item.id === id)
55
+ if (!currentNode) {
56
+ currentNode = {
57
+ id: source.id,
58
+ name: source.name,
59
+ hasChildren: ORG_LIST.some(candidate => candidate.parentId === source.id),
60
+ children: []
61
+ }
62
+ currentLevel.push(currentNode)
63
+ }
64
+ currentLevel = currentNode.children
65
+ }
66
+ }
67
+
68
+ export async function fetchOrgChildren (parentId) {
69
+ await delay()
70
+ return getChildrenByParentId(parentId || null)
71
+ }
72
+
73
+ export async function searchOrgTreeByName (keyword) {
74
+ await delay(320)
75
+ const trimmed = (keyword || '').trim()
76
+ if (!trimmed) {
77
+ return []
78
+ }
79
+ const matchedNodes = ORG_LIST.filter(item => item.name.includes(trimmed))
80
+ const tree = []
81
+ matchedNodes.forEach(node => {
82
+ mergePathToTree(tree, getAncestorPathIds(node.id))
83
+ })
84
+ return tree
85
+ }
86
+
87
+ export async function fetchOrgById (id) {
88
+ await delay(120)
89
+ return getNodeById(id)
90
+ }
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@html-zj/demo",
3
+ "version": "1.0.0",
4
+ "description": "demo",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "author": "",
10
+ "license": "ISC",
11
+ "publishConfig": {
12
+ "access": "public"
13
+ }
14
+ }