n20-project-component 1.0.3 → 1.0.5

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.
@@ -0,0 +1,627 @@
1
+ <template>
2
+ <div class="ProFilterView">
3
+ <div class="flex-box flex-r flex-v top">
4
+ <slot name="leftContent"></slot>
5
+ <div class="flex-box flex-v">
6
+ <el-popover
7
+ v-if="bussId"
8
+ ref="viewPopover"
9
+ v-model="viewPopoverVisible"
10
+ popper-class="pro-filter-view-popover"
11
+ placement="bottom-start"
12
+ trigger="click"
13
+ class="m-r-s"
14
+ >
15
+ <el-button slot="reference" class="botton" :class="{ activiti: selectedItem !== '无视图' }">
16
+ <div class="flex-box flex-v m-r">
17
+ <i class="v3-icon-inbox"></i>
18
+ <div class="popover-title" style="margin: 0 8px" :title="selectedItem">{{ selectedItem }}</div>
19
+ <i class="n20-icon-xiala-moren"></i>
20
+ </div>
21
+ </el-button>
22
+ <div class="p-s">
23
+ <cl-input-search v-model="searchKeyword" class="w-100p" placeholder="请输入关键字" />
24
+ <div style="color: var(--color-text-secondary); margin: 12px 0 8px 0">{{ $lc('自定义视图') }}</div>
25
+ <div class="filter-view-wrapper">
26
+ <cl-drag-list class="flex-item" :list="filteredList" :hide-delete="true" @change="handleDragSort">
27
+ <template v-slot="{ item }">
28
+ <div class="proFilterView-list-item" :class="{ 'is-selected': selectedItem === item.viewName }">
29
+ <span class="proFilterView-list-item__name" :title="item.viewName" @click="handleSelect(item)">{{
30
+ item.viewName
31
+ }}</span>
32
+ <i v-if="selectedItem === item.viewName" class="el-icon-check selected-icon"></i>
33
+ <el-dropdown
34
+ v-if="selectedItem !== item.viewName && item.viewName !== '无视图'"
35
+ trigger="click"
36
+ class="proFilterView-list-item__more"
37
+ @command="command => handleCommand(command, item)"
38
+ >
39
+ <i class="v3-icon-more"></i>
40
+ <el-dropdown-menu slot="dropdown">
41
+ <el-dropdown-item command="edit">{{ $lc('编辑') }}</el-dropdown-item>
42
+ <el-dropdown-item command="delete">{{ $lc('删除') }}</el-dropdown-item>
43
+ </el-dropdown-menu>
44
+ </el-dropdown>
45
+ </div>
46
+ </template>
47
+ </cl-drag-list>
48
+ </div>
49
+ <div class="ProFilterView_divider"></div>
50
+ <div class="flex-box flex-v flex-l ProFilterView-add">
51
+ <img src="./plus.svg" alt="" />
52
+ <div @click="add">{{ $lc('新建视图') }}</div>
53
+ </div>
54
+ </div>
55
+ </el-popover>
56
+ <el-button v-if="bussId && selectItem" class="activiti m-l-s" @click="edit(selectItem, true)">
57
+ <div class="flex-box flex-v">
58
+ <i class="v3-icon-find"></i>
59
+ <div style="margin-left: 8px">
60
+ {{ `视图数据范围(${selectItem.keyIds && selectItem.keyIds.length})` }}
61
+ </div>
62
+ </div>
63
+ </el-button>
64
+ <el-button icon="v3-icon-filter" class="m-r-s botton" @click="filterExpanded = !filterExpanded">{{
65
+ '筛选' | $lc
66
+ }}</el-button>
67
+ <slot name="rightContent"></slot>
68
+ </div>
69
+ </div>
70
+ <cl-advanced-filter
71
+ ref="filter"
72
+ class="filter"
73
+ only-key="id"
74
+ :class="{ 'is-hidden': !filterExpanded }"
75
+ :visible.sync="filterExpanded"
76
+ :filter-id="filterId"
77
+ :buss-id="bussId"
78
+ :model="searchValue"
79
+ :filter-list="filterList"
80
+ :filter-readonly="!!selectItem"
81
+ v-bind="$attrs"
82
+ @search="filterFn"
83
+ @clean="handleClean"
84
+ @clear="handleClear"
85
+ @enter="filterFn"
86
+ @change="changeFn"
87
+ @refresh="getFilterList"
88
+ >
89
+ <div slot="prefix">
90
+ <slot name="prefix"></slot>
91
+ </div>
92
+ <template v-for="filter in slotFilters" #[filter.slotName]="{ model }">
93
+ <!-- 有 value 的 slot:传递单个值及 input 回调 -->
94
+ <slot
95
+ v-if="filter.value"
96
+ :name="filter.slotName"
97
+ :model="model"
98
+ :item="filter"
99
+ :value="searchValue[filter.value]"
100
+ :input="val => handleSlotInput(filter.value, val)"
101
+ ></slot>
102
+ <!-- 无 value 的 slot(如 startDate/endDate 多值绑定),传递 searchValue 让父组件自行管理 -->
103
+ <slot
104
+ v-else
105
+ :name="filter.slotName"
106
+ :model="model"
107
+ :search-value="searchValue"
108
+ :update-search-value="updates => Object.assign(searchValue, updates)"
109
+ ></slot>
110
+ </template>
111
+ <div slot="suffix">
112
+ <slot name="suffix"></slot>
113
+ </div>
114
+ </cl-advanced-filter>
115
+ <cl-dialog
116
+ v-drag
117
+ width="85%"
118
+ :visible.sync="visible"
119
+ :title="isAdd ? $lc('新建视图') : $lc('编辑视图')"
120
+ :destroy-on-close="true"
121
+ :close-on-click-modal="false"
122
+ >
123
+ <el-form ref="form" :model="form" label-width="80px" :inline="false" size="normal" label-position="top">
124
+ <el-form-item :label="$lc('视图名称')" prop="viewName" :rules="[{ required: true, message: '请输入', trigger: 'blur' }]">
125
+ <el-input v-model="form.viewName" maxlength="30" show-word-limit />
126
+ </el-form-item>
127
+ <div>{{ $lc('数据范围') }}</div>
128
+ <cl-advanced-filter
129
+ ref="filter_box"
130
+ class="filter"
131
+ only-key="id"
132
+ :key="dialogFilterKey"
133
+ :visible="visible"
134
+ :model="searchForm"
135
+ :filter-list="filterList"
136
+ :check-ids="form.keyIds"
137
+ @saveCheckData="saveCheckData"
138
+ >
139
+ <template v-for="filter in slotFilters" #[filter.slotName]="{ model }">
140
+ <!-- 有 value 的 slot:传递单个值及 input 回调 -->
141
+ <slot
142
+ v-if="filter.value"
143
+ :name="filter.slotName"
144
+ :model="model"
145
+ :value="searchForm[filter.value]"
146
+ :input="val => (searchForm[filter.value] = val)"
147
+ ></slot>
148
+ <!-- 无 value 的 slot(如 startDate/endDate 多值绑定),传递 searchForm 让父组件自行管理 -->
149
+ <slot
150
+ v-else
151
+ :name="filter.slotName"
152
+ :model="model"
153
+ :search-value="searchForm"
154
+ :update-search-value="updates => Object.assign(searchForm, updates)"
155
+ ></slot>
156
+ </template>
157
+ </cl-advanced-filter>
158
+ </el-form>
159
+ <div slot="footer" class="flex-box flex-c flex-v page-button-shadow">
160
+ <el-button type="primary" @click="saveSt">{{ $lc('确认') }}</el-button>
161
+ <el-button plain @click="visible = false">{{ $lc('取消') }}</el-button>
162
+ </div>
163
+ </cl-dialog>
164
+ </div>
165
+ </template>
166
+
167
+ <script>
168
+ import ClAdvancedFilter from './AdvancedFilter/index.vue';
169
+ import ClDialog from 'n20-common-lib/src/components/Dialog/index.vue';
170
+ import ClDragList from 'n20-common-lib/src/components/DragList/index.vue';
171
+ import ClInputSearch from 'n20-common-lib/src/components/InputSearch/index.vue';
172
+ import axios from 'n20-common-lib/src/utils/axios.js';
173
+
174
+ export default {
175
+ name: 'ProFilterView',
176
+ components: {
177
+ ClAdvancedFilter,
178
+ ClInputSearch,
179
+ ClDragList,
180
+ ClDialog,
181
+ },
182
+ props: {
183
+ bussId: {
184
+ type: String,
185
+ default: '',
186
+ },
187
+ filterId: {
188
+ type: String,
189
+ default: '',
190
+ },
191
+ filterList: {
192
+ type: Array,
193
+ default: () => [],
194
+ },
195
+ // 初始筛选值,用于设置默认筛选条件,同时也用于同步 slot 类型筛选器的值
196
+ initialValue: {
197
+ type: Object,
198
+ default: () => ({}),
199
+ },
200
+ // 筛选器默认展开/收起状态
201
+ defaultExpanded: {
202
+ type: Boolean,
203
+ default: false,
204
+ },
205
+ },
206
+ data() {
207
+ return {
208
+ searchKeyword: '',
209
+ list: ['无视图'],
210
+ selectedItem: '无视图', // 默认选中第一项
211
+ viewPopoverVisible: false,
212
+ visible: false,
213
+ isAdd: false,
214
+ form: { userNo: sessionStorage.getItem('userNo'), pageNo: this.filterId },
215
+
216
+ filterExpanded: this.defaultExpanded,
217
+ dialogFilterKey: 0,
218
+ selectItem: '',
219
+ // 视图筛选值
220
+ searchForm: {},
221
+ // 普通筛选值
222
+ searchValue: {},
223
+ viewId: undefined,
224
+ isRefresh: false,
225
+ };
226
+ },
227
+ computed: {
228
+ /**
229
+ * 本地搜索过滤视图列表
230
+ * @returns {Array} 根据 searchKeyword 过滤后的视图列表;searchKeyword 为空时返回完整列表
231
+ */
232
+ filteredList() {
233
+ if (!this.searchKeyword) {
234
+ return this.list;
235
+ }
236
+ const keyword = this.searchKeyword.toLowerCase();
237
+ return this.list.filter(item => item.viewName && item.viewName.toLowerCase().includes(keyword));
238
+ },
239
+ // 筛选出类型为 slot 且在父组件提供了对应作用域插槽的筛选项
240
+ slotFilters() {
241
+ return this.filterList.filter(item => item.type === 'slot' && this.$scopedSlots[item.slotName]);
242
+ },
243
+
244
+ getInitialSearchValue() {
245
+ const obj = {};
246
+ // 为 slot 类型筛选项初始化值
247
+ this.filterList
248
+ .filter(item => item.type === 'slot')
249
+ .forEach(item => {
250
+ // 单值绑定
251
+ if (item.value) {
252
+ obj[item.value] = undefined;
253
+ }
254
+ // 多值绑定(slotFields 数组)
255
+ if (item.slotFields && item.slotFields.length) {
256
+ item.slotFields.forEach(field => {
257
+ obj[field] = undefined;
258
+ });
259
+ }
260
+ });
261
+ return obj;
262
+ },
263
+ filterObj() {
264
+ // 合并 slot 类型筛选器的值(来自 initialValue)到 searchValue 中
265
+ const slotValues = {};
266
+ this.filterList
267
+ .filter(item => item.type === 'slot' && item.value)
268
+ .forEach(item => {
269
+ if (this.initialValue && this.initialValue[item.value] !== undefined) {
270
+ slotValues[item.value] = this.initialValue[item.value];
271
+ }
272
+ });
273
+ return {
274
+ // 筛选条件(合并 slot 类型的值)
275
+ searchValue: { ...this.searchValue, ...slotValues },
276
+ // 视图id
277
+ viewId: this.selectItem ? this.selectItem.viewId : null,
278
+ };
279
+ },
280
+ },
281
+ watch: {
282
+ initialValue: {
283
+ handler(newVal) {
284
+ if (!newVal) return;
285
+
286
+ // 浅拷贝,避免修改 searchValue 时污染外部 initialValue
287
+ this.searchValue = { ...newVal };
288
+ },
289
+ deep: true,
290
+ immediate: false,
291
+ },
292
+ // filterId 变化时重新请求视图列表并重置状态
293
+ filterId: {
294
+ handler(newVal, oldVal) {
295
+ if (newVal && newVal !== oldVal) {
296
+ this.resetState();
297
+ this.getFilterList();
298
+ }
299
+ },
300
+ immediate: false,
301
+ },
302
+ },
303
+ mounted() {
304
+ // 只有在有 bussId 时才获取视图列表
305
+ if (this.bussId) {
306
+ this.getFilterList();
307
+ }
308
+ // 合并初始筛选值:slot 字段使用默认值,其他字段使用 initialValue 传入的值
309
+ this.searchValue = { ...this.getInitialSearchValue, ...this.initialValue };
310
+ },
311
+ methods: {
312
+ // 处理 slot 类型字段的输入事件,同时更新 searchValue 和 initialValue
313
+ handleSlotInput(fieldName, val) {
314
+ // 防御性检查:如果 fieldName 无效,不执行任何操作
315
+ if (!fieldName) return;
316
+ // 更新 searchValue
317
+ this.searchValue[fieldName] = val;
318
+ // 同步更新 initialValue,确保双向绑定生效
319
+ this.$set(this.initialValue, fieldName, val);
320
+ },
321
+ handleCommand(command, item) {
322
+ const commandMap = {
323
+ edit: () => this.edit(item),
324
+ delete: () => this.deleteOne(item),
325
+ };
326
+ commandMap[command]();
327
+ },
328
+ setOptions(id, opts) {
329
+ this.$refs.filter.setOptions(id, opts);
330
+ this.filterList.forEach(item => {
331
+ if (item.value === id) {
332
+ item.options = opts;
333
+ }
334
+ });
335
+ },
336
+ changeFn(value) {
337
+ const obj = { ...value };
338
+ const filter = this.filterList.find(item => item.value === value.field);
339
+ if (filter && filter.type === 'daterange') {
340
+ obj.value = {
341
+ [filter.startDate]: this.filterObj.searchValue && this.filterObj.searchValue[filter.startDate],
342
+ [filter.endDate]: this.filterObj.searchValue && this.filterObj.searchValue[filter.endDate],
343
+ };
344
+ }
345
+ if (filter && filter.type === 'numberrange') {
346
+ obj.value = {
347
+ [filter.startValue]: this.filterObj.searchValue && this.filterObj.searchValue[filter.startValue],
348
+ [filter.endValue]: this.filterObj.searchValue && this.filterObj.searchValue[filter.endValue],
349
+ };
350
+ }
351
+ this.$emit('changeFn', obj);
352
+ },
353
+ filterFn() {
354
+ this.$emit('filter', this.filterObj, 'filter');
355
+ },
356
+ handleClean() {
357
+ this.$emit('filter', this.filterObj, 'clean');
358
+ },
359
+ handleClear() {
360
+ // 收集不需要清空的字段 key(required / isNotClose / initialValue 中的额外 key)
361
+ const noClearKeys = new Set();
362
+ const allFilterKeys = new Set();
363
+
364
+ this.filterList.forEach(item => {
365
+ const keys =
366
+ item.type === 'daterange'
367
+ ? [item.startDate, item.endDate]
368
+ : item.type === 'numberrange'
369
+ ? [item.startValue, item.endValue]
370
+ : (item.slotFields && item.slotFields.length)
371
+ ? item.slotFields
372
+ : [item.value];
373
+
374
+ keys.forEach(k => allFilterKeys.add(k));
375
+ if (item.required) {
376
+ keys.forEach(k => noClearKeys.add(k));
377
+ }
378
+ });
379
+
380
+ // initialValue 中不在 filterList 里的 key 也需要保留
381
+ Object.keys(this.initialValue).forEach(key => {
382
+ if (!allFilterKeys.has(key)) {
383
+ noClearKeys.add(key);
384
+ }
385
+ });
386
+
387
+ // 仅保留 noClearKeys 中的值,其余清空
388
+ const preserved = {};
389
+ for (const key in this.searchValue) {
390
+ if (noClearKeys.has(key)) {
391
+ preserved[key] = this.searchValue[key];
392
+ }
393
+ }
394
+
395
+ this.searchValue = preserved;
396
+ this.$emit(
397
+ 'filter',
398
+ {
399
+ searchValue: { ...this.searchValue },
400
+ viewId: this.selectItem ? this.selectItem.viewId : null,
401
+ },
402
+ 'clear',
403
+ );
404
+ },
405
+ // 通过bussId拿取视图列表
406
+ getFilterList(viewId) {
407
+ // 如果没有 bussId,不获取视图列表
408
+ if (!this.bussId) {
409
+ return;
410
+ }
411
+
412
+ axios
413
+ .get(`/bems/query/viewColumn/getViewInfo`, { bussId: this.bussId })
414
+ .then(res => {
415
+ const list = (res.data || []).map(item => {
416
+ item.keyIds = this.safeParse(item.keyIds, []);
417
+ return item;
418
+ });
419
+ this.list = [
420
+ {
421
+ viewName: '无视图',
422
+ },
423
+ ...list,
424
+ ];
425
+ if (this.isRefresh) {
426
+ const obj = this.list.find(item => item.viewId === viewId);
427
+ if (obj) {
428
+ this.handleSelect(obj);
429
+ this.selectItem = obj;
430
+ }
431
+ }
432
+ })
433
+ .catch(err => {
434
+ this.list = [
435
+ {
436
+ viewName: '无视图',
437
+ },
438
+ ];
439
+ });
440
+ },
441
+ // 新增
442
+ add() {
443
+ this.isRefresh = false;
444
+ this.viewPopoverVisible = false;
445
+ this.visible = true;
446
+ this.isAdd = true;
447
+ this.form = {
448
+ userNo: sessionStorage.getItem('userNo'),
449
+ pageNo: this.filterId,
450
+ keyIds: [],
451
+ };
452
+ this.viewId = undefined;
453
+ this.searchForm = {
454
+ ...this.getInitialSearchValue,
455
+ };
456
+ },
457
+ // 修改
458
+ edit(item, isRefresh = false) {
459
+ this.isRefresh = isRefresh;
460
+ this.viewPopoverVisible = false;
461
+ this.visible = true;
462
+ this.isAdd = false;
463
+ this.dialogFilterKey++;
464
+ // 浅拷贝,避免修改 form 时污染原始 item
465
+ this.form = { ...item };
466
+ this.viewId = item.viewId;
467
+ const parsed = this.safeParse(item.viewConfig);
468
+ this.searchForm = { ...this.getInitialSearchValue, ...parsed };
469
+ },
470
+ safeParse(str, fallback = {}) {
471
+ try {
472
+ return JSON.parse(str || '{}');
473
+ } catch {
474
+ return fallback;
475
+ }
476
+ },
477
+ // 保存筛选条件
478
+ saveCheckData(data) {
479
+ this.form.keyIds = data.map(item => {
480
+ return item.id;
481
+ });
482
+ },
483
+ // 保存视图
484
+ saveSt() {
485
+ if (!this.bussId) {
486
+ this.$message.warning('缺少业务ID,无法保存视图');
487
+ return;
488
+ }
489
+
490
+ const viewConfig = JSON.stringify(this.searchForm);
491
+ const keyIds = this.form.keyIds;
492
+ this.form.viewConfig = viewConfig;
493
+
494
+ this.$refs.form.validate(async valid => {
495
+ if (!valid) {
496
+ this.$message.error('请输入视图名称');
497
+ return;
498
+ }
499
+
500
+ if (!keyIds.length) {
501
+ this.$message.warning('至少添加一个条件');
502
+ return;
503
+ }
504
+
505
+ // 保存筛选视图
506
+ const obj = {
507
+ viewName: this.form.viewName,
508
+ viewType: '0',
509
+ bussId: this.bussId,
510
+ viewConfig: this.form.viewConfig,
511
+ keyIds: JSON.stringify(keyIds),
512
+ viewId: this.viewId,
513
+ };
514
+
515
+ axios.post('/bems/query/viewColumn/saveOrUpdateViewInfo', obj).then(res => {
516
+ if (res.code !== 200) {
517
+ return false;
518
+ }
519
+ this.visible = false;
520
+ this.$message.success('保存成功');
521
+ this.getFilterList(this.viewId);
522
+ });
523
+ });
524
+ },
525
+ // 删除
526
+ deleteOne(row) {
527
+ this.$msgboxPor({
528
+ title: '确认要删除吗?',
529
+ message: '内容从页面删除,将不可恢复',
530
+ type: 'error',
531
+ confirmButtonText: '确定',
532
+ cancelButtonText: '取消',
533
+ }).then(async () => {
534
+ const { code } = await axios.post('/bems/query/viewColumn/deleteViewInfo', { viewId: row.viewId });
535
+ if (code !== 200) return;
536
+ this.$message.success('删除成功');
537
+ this.getFilterList();
538
+ });
539
+ },
540
+ // 选中视图
541
+ handleSelect(item) {
542
+ this.viewPopoverVisible = false;
543
+ this.selectItem = item;
544
+ this.selectedItem = item.viewName;
545
+ if (item.viewName === '无视图') {
546
+ this.selectItem = '';
547
+ this.searchValue = { ...this.getInitialSearchValue, ...this.initialValue };
548
+ // 无视图时恢复原来的筛选逻辑
549
+ this.$nextTick(() => {
550
+ const filterRef = this.$refs.filter;
551
+ if (filterRef) {
552
+ filterRef.getFilterList();
553
+ }
554
+ this.$emit('filter', this.filterObj, 'viewChange');
555
+ this.$nextTick(() => {
556
+ this.updatePopover();
557
+ });
558
+ });
559
+ } else {
560
+ const config = this.safeParse(item.viewConfig);
561
+ this.searchForm = config;
562
+ this.searchValue = config;
563
+ // 按视图保存的 keyIds 更新展示字段
564
+ this.$nextTick(() => {
565
+ this.applyFilterFields(item.keyIds);
566
+ });
567
+ }
568
+ },
569
+ /**
570
+ * 根据视图的 keyIds 更新 cl-advanced-filter 的展示字段(仅更新展示,不保存)
571
+ * @param {Array} keyIds - 视图保存的筛选字段 ID 列表
572
+ */
573
+ applyFilterFields(keyIds) {
574
+ const filterRef = this.$refs.filter;
575
+ if (!filterRef) return;
576
+
577
+ // 按 keyIds 顺序过滤 filterList
578
+ const filteredItems = this.filterList
579
+ .filter(item => keyIds.includes(item.id))
580
+ .sort((a, b) => keyIds.indexOf(a.id) - keyIds.indexOf(b.id));
581
+
582
+ // 更新 checkList 和展示字段
583
+ filterRef.check(keyIds);
584
+ filterRef.setModelData(filteredItems);
585
+ filterRef.mackData(filteredItems);
586
+
587
+ this.$nextTick(() => {
588
+ this.$emit('filter', this.filterObj, 'viewChange');
589
+ this.$nextTick(() => {
590
+ this.updatePopover();
591
+ });
592
+ });
593
+ },
594
+ // 强制更新 popover 位置
595
+ updatePopover() {
596
+ const popover = this.$refs.viewPopover;
597
+ if (popover && popover.updatePopper) {
598
+ popover.updatePopper();
599
+ }
600
+ },
601
+ // 拖动保存视图列表
602
+ handleDragSort(list) {
603
+ console.log('当前list:', list);
604
+ },
605
+ /**
606
+ * 重置组件状态到初始值
607
+ * 供下游调用以恢复组件初始状态
608
+ */
609
+ resetState() {
610
+ this.searchKeyword = '';
611
+ this.selectedItem = '无视图';
612
+ this.selectItem = '';
613
+ this.searchValue = { ...this.getInitialSearchValue, ...this.initialValue };
614
+ this.searchForm = { ...this.getInitialSearchValue };
615
+ this.filterExpanded = this.defaultExpanded;
616
+ this.viewId = undefined;
617
+ },
618
+ },
619
+ };
620
+ </script>
621
+
622
+ <style scoped>
623
+ .filter.is-hidden {
624
+ padding: 4px;
625
+ border-bottom: none !important;
626
+ }
627
+ </style>
@@ -0,0 +1,3 @@
1
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M8.66626 7.33398H14.6663V8.66699H8.66626V14.667H7.33325V8.66699H1.33325V7.33398H7.33325V1.33398H8.66626V7.33398Z" fill="#4E5969"/>
3
+ </svg>