@bytenew/bn-bus-ui 1.1.462 → 1.1.463

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.
Files changed (167) hide show
  1. package/install.js +134 -0
  2. package/package.json +20 -4
  3. package/packages/BnAddress/index.vue +141 -0
  4. package/packages/BnAddressParse/index.vue +75 -0
  5. package/packages/BnAdvancedFilter/BnFilterView.vue +145 -0
  6. package/packages/BnAdvancedFilter/index.vue +345 -0
  7. package/packages/BnAdvancedFilter/util.js +171 -0
  8. package/packages/BnCascader/BnCascader.vue +543 -0
  9. package/packages/BnCascader/BnCascaderPanel.vue +1331 -0
  10. package/packages/BnCascader/cascaderDisplay.js +102 -0
  11. package/packages/BnCheck/BnCheck.vue +253 -0
  12. package/packages/BnColumnConfig/columnConfigDialog.vue +134 -0
  13. package/packages/BnColumnConfig/component/childrenFormSetting/assocTable.vue +143 -0
  14. package/packages/BnColumnConfig/component/childrenFormSetting/assocTableSearch.vue +314 -0
  15. package/packages/BnColumnConfig/component/childrenFormSetting/index.vue +243 -0
  16. package/packages/BnColumnConfig/component/defaultSet/defaultSet.vue +114 -0
  17. package/packages/BnColumnConfig/component/defaultSet/stringDefault.vue +210 -0
  18. package/packages/BnColumnConfig/component/funcExprDialog/funcExprDialog.vue +126 -0
  19. package/packages/BnColumnConfig/component/packageFilter/index.vue +141 -0
  20. package/packages/BnColumnConfig/component/paraExplainSet/index.vue +156 -0
  21. package/packages/BnColumnConfig/component/setOption/setOption.vue +321 -0
  22. package/packages/BnColumnConfig/component/setRelationOption/ConditionList.vue +277 -0
  23. package/packages/BnColumnConfig/component/setRelationOption/LevelHeader.vue +144 -0
  24. package/packages/BnColumnConfig/component/setRelationOption/setRelationOption.vue +642 -0
  25. package/packages/BnColumnConfig/data.js +129 -0
  26. package/packages/BnColumnConfig/index.scss +23 -0
  27. package/packages/BnColumnConfig/index.vue +309 -0
  28. package/packages/BnCondVal/BnCondVal.vue +112 -0
  29. package/packages/BnCondVal/itemVal/dateRegVal.vue +39 -0
  30. package/packages/BnCondVal/itemVal/dateVal.vue +39 -0
  31. package/packages/BnCondVal/itemVal/numRegVal.vue +67 -0
  32. package/packages/BnCondVal/itemVal/numVal.vue +44 -0
  33. package/packages/BnCondVal/itemVal/relationVal.vue +73 -0
  34. package/packages/BnCondVal/itemVal/selectColumnVal.vue +58 -0
  35. package/packages/BnCondVal/itemVal/selectVal.vue +59 -0
  36. package/packages/BnCondVal/itemVal/stringVal.vue +156 -0
  37. package/packages/BnCondition/index.vue +551 -0
  38. package/packages/BnCondition/util.js +232 -0
  39. package/packages/BnDatePicker/BnDatePicker.vue +194 -0
  40. package/packages/BnDrop/BnDrop.vue +917 -0
  41. package/packages/BnEmptyTip/BnEmptyTip.vue +107 -0
  42. package/packages/BnExportOrder/BnExportOrder.vue +241 -0
  43. package/packages/BnExportOrder/validatorCode.vue +94 -0
  44. package/packages/BnFileView/BnFileView.vue +271 -0
  45. package/packages/BnFileView/FileDetail.vue +125 -0
  46. package/packages/BnFileView/videoPreview.vue +70 -0
  47. package/packages/BnFilterOption/BnFilterOption.vue +160 -0
  48. package/packages/BnFunExpr/BnFunExpr.vue +608 -0
  49. package/packages/BnImportOrder/BnImportOrder.vue +323 -0
  50. package/packages/BnImportTemplate/BnImportTemplate.vue +866 -0
  51. package/packages/BnImportTemplate/assets/merge_tip_img.png +0 -0
  52. package/packages/BnImportTemplate/assets/sheet_tip.png +0 -0
  53. package/packages/BnImportTemplate/assets/xls.png +0 -0
  54. package/packages/BnImportTemplate/components/ImportTemplateTrigger.vue +198 -0
  55. package/packages/BnImportTemplate/components/TemplateEditDialog.vue +340 -0
  56. package/packages/BnImportTemplate/components/TemplateFieldsConfig.vue +401 -0
  57. package/packages/BnImportTemplate/components/TemplateListDialog.vue +297 -0
  58. package/packages/BnInpNum/BnInpNum.vue +165 -0
  59. package/packages/BnMenus/BnMenus.vue +502 -0
  60. package/packages/BnMenus/MenuItem.vue +321 -0
  61. package/packages/BnOrderCard/BnOrderCard.vue +389 -0
  62. package/packages/BnOrderCard/orderRemark.vue +178 -0
  63. package/packages/BnOrderCard/util.js +94 -0
  64. package/packages/BnOrderDetail/BnOrderDetail.vue +338 -0
  65. package/packages/BnOrderDetail/filterLogByUser.vue +108 -0
  66. package/packages/BnOrderDetail/orderLogs.vue +53 -0
  67. package/packages/BnOrderDetail/showSubForm/index.vue +41 -0
  68. package/packages/BnOrderDetail/showSubForm/showChildTable.vue +61 -0
  69. package/packages/BnOrderDetail/showSubForm/showTimeLine.vue +131 -0
  70. package/packages/BnOrderDetail/showSubForm/tableCellCopy.vue +84 -0
  71. package/packages/BnOrderShare/index.vue +378 -0
  72. package/packages/BnOrderShare/shareUrlInp.vue +54 -0
  73. package/packages/BnRadio/BnRadio.vue +181 -0
  74. package/packages/BnRelation/BnRelation.vue +120 -0
  75. package/packages/BnSelect/BnSelect.vue +485 -0
  76. package/packages/BnSmartForm/BnBatchForm.vue +334 -0
  77. package/packages/BnSmartForm/BnBatchFormDialog.vue +423 -0
  78. package/packages/BnSmartForm/BnSmartElf/elfLog/index.vue +57 -0
  79. package/packages/BnSmartForm/BnSmartElf/index.vue +95 -0
  80. package/packages/BnSmartForm/BnSmartFormPsychic.js +629 -0
  81. package/packages/BnSmartForm/BnSmartFormUtil.js +524 -0
  82. package/packages/BnSmartForm/addressForm/index.vue +73 -0
  83. package/packages/BnSmartForm/checkboxForm/index.vue +99 -0
  84. package/packages/BnSmartForm/childrenForm/childFormFile.vue +237 -0
  85. package/packages/BnSmartForm/childrenForm/childrenFormBody.vue +260 -0
  86. package/packages/BnSmartForm/childrenForm/index.vue +110 -0
  87. package/packages/BnSmartForm/childrenForm/searchInventory.vue +115 -0
  88. package/packages/BnSmartForm/chooseAssocTable/chooseAssocTable.vue +451 -0
  89. package/packages/BnSmartForm/chooseAssocTable/chooseChildGood.vue +229 -0
  90. package/packages/BnSmartForm/chooseAssocTable/tableCellCopy.vue +84 -0
  91. package/packages/BnSmartForm/chooseChildOrder/chooseChildOrder.vue +158 -0
  92. package/packages/BnSmartForm/chooseIPaasData/chooseIPaasData.vue +307 -0
  93. package/packages/BnSmartForm/chooseIPaasData/tableCellCopy.vue +84 -0
  94. package/packages/BnSmartForm/choosePackage/choosePackage.vue +361 -0
  95. package/packages/BnSmartForm/dateDualForm/index.vue +37 -0
  96. package/packages/BnSmartForm/dateForm/index.vue +38 -0
  97. package/packages/BnSmartForm/fileForm/fileFormData.js +50 -0
  98. package/packages/BnSmartForm/fileForm/index.vue +240 -0
  99. package/packages/BnSmartForm/formItemFooter/formItemFooter.vue +38 -0
  100. package/packages/BnSmartForm/formItemTop/formItemTop.vue +42 -0
  101. package/packages/BnSmartForm/index.vue +1201 -0
  102. package/packages/BnSmartForm/numberForm/index.vue +63 -0
  103. package/packages/BnSmartForm/radioForm/index.vue +72 -0
  104. package/packages/BnSmartForm/receiveForm/index.vue +112 -0
  105. package/packages/BnSmartForm/relationForm/index.vue +82 -0
  106. package/packages/BnSmartForm/scoreForm/index.vue +39 -0
  107. package/packages/BnSmartForm/selectForm/index.vue +55 -0
  108. package/packages/BnSmartForm/showValForm/index.vue +42 -0
  109. package/packages/BnSmartForm/showValForm/showValFormTable.vue +104 -0
  110. package/packages/BnSmartForm/smartFormsCommon.scss +43 -0
  111. package/packages/BnSmartForm/stringForm/index.vue +170 -0
  112. package/packages/BnVirtPopover/index.vue +319 -0
  113. package/packages/BnWebLog/BnWebLog.vue +140 -0
  114. package/packages/BnWorkSettingDrop/advancedOptions.js +112 -0
  115. package/packages/BnWorkSettingDrop/benBenAIUtil.js +89 -0
  116. package/packages/BnWorkSettingDrop/index.vue +292 -0
  117. package/packages/font/iconfont.css +243 -0
  118. package/packages/font/iconfont.ttf +0 -0
  119. package/packages/font/iconfont.woff +0 -0
  120. package/packages/font/iconfont.woff2 +0 -0
  121. package/utils/addressParse-old.js +645 -0
  122. package/utils/addressParse.js +735 -0
  123. package/utils/config.js +68 -0
  124. package/utils/index.js +1171 -0
  125. package/dist/assocTableSearch-CmzOXI5w.js +0 -349
  126. package/dist/assocTableSearch.css +0 -1
  127. package/dist/bn-bus-ui.es.js +0 -65
  128. package/dist/bn-bus-ui.umd.js +0 -49
  129. package/dist/chooseIPaasData-jkTJEMKT.js +0 -391
  130. package/dist/chooseIPaasData.css +0 -1
  131. package/dist/defaultSet-CiYuSOzT.js +0 -297
  132. package/dist/defaultSet.css +0 -1
  133. package/dist/fileFormData-D_Q1vylE.js +0 -53
  134. package/dist/formItemFooter-BpnVUUgV.js +0 -108
  135. package/dist/formItemFooter.css +0 -1
  136. package/dist/funcExprDialog-BMc-n5FL.js +0 -139
  137. package/dist/funcExprDialog.css +0 -1
  138. package/dist/index-8GQJARcF.js +0 -44
  139. package/dist/index-8apT0r8m.js +0 -66
  140. package/dist/index-BOFu3qAH.js +0 -183
  141. package/dist/index-BSKXuSid.js +0 -89
  142. package/dist/index-BhYvDd_Q.js +0 -45
  143. package/dist/index-BhqkeVFc.js +0 -45
  144. package/dist/index-CK7_Ou7F.js +0 -89
  145. package/dist/index-Cfb8D3Fg.js +0 -58
  146. package/dist/index-D1fGD0mX.js +0 -51
  147. package/dist/index-DE_eQGbT.js +0 -746
  148. package/dist/index-DfgiBqzS.js +0 -221
  149. package/dist/index-Dobi0ms9.js +0 -66
  150. package/dist/index-DuyQNxe0.js +0 -118
  151. package/dist/index-DxpJkrN7.js +0 -64
  152. package/dist/index-FGHeG46m.js +0 -109
  153. package/dist/index-P2M7CUTe.js +0 -178
  154. package/dist/index-xCz2zysE.js +0 -135
  155. package/dist/index.css +0 -1
  156. package/dist/index2.css +0 -1
  157. package/dist/index3.css +0 -1
  158. package/dist/index4.css +0 -1
  159. package/dist/index5.css +0 -1
  160. package/dist/index6.css +0 -1
  161. package/dist/index7.css +0 -1
  162. package/dist/install--7IU60oV.js +0 -23103
  163. package/dist/install.css +0 -1
  164. package/dist/setOption-DehYZpZr.js +0 -329
  165. package/dist/setOption.css +0 -1
  166. package/dist/setRelationOption-BL-AFUoF.js +0 -5217
  167. package/dist/setRelationOption.css +0 -1
@@ -0,0 +1,165 @@
1
+ <template>
2
+ <el-input :placeholder="placeholder" v-model="val" :disabled="disabled" :clearable="clearable"
3
+ :class="[underline ?'bnInput-underline':'']"
4
+ @input="valInput" @change="valChange" @blur="inputBlur" :size="size" :maxlength="maxlength">
5
+ <template #prefix>
6
+ <slot name="prefix"></slot>
7
+ </template>
8
+ <template #suffix>
9
+ <slot></slot>
10
+ </template>
11
+ </el-input>
12
+ </template>
13
+
14
+ <script>
15
+ import {debounce} from "lodash";
16
+
17
+ export default {
18
+ name: "BnInpNum",
19
+ props: {
20
+ modelValue: [String, Number],
21
+ placeholder: {
22
+ type: String,
23
+ default: ''
24
+ },
25
+ size: String,
26
+ underline: {
27
+ type: Boolean,
28
+ default: false
29
+ },
30
+ disabled: {
31
+ type: Boolean,
32
+ default: false
33
+ },
34
+ clearable: {
35
+ type: Boolean,
36
+ default: true
37
+ },
38
+ max: {
39
+ type: [String, Number],
40
+ },
41
+ min: {
42
+ type: [String, Number],
43
+ },
44
+ maxlength: {
45
+ type: [String, Number],
46
+ },
47
+ precision: {
48
+ type: [String, Number],
49
+ },
50
+ },
51
+ computed: {
52
+ val: {
53
+ get() {
54
+ return this.modelValue;
55
+ },
56
+ set(val) {
57
+ this.$emit("update:modelValue", val);
58
+ },
59
+ },
60
+ },
61
+ methods: {
62
+ /**
63
+ * input事件 实时变化
64
+ * */
65
+ valInput:debounce(function(val) {
66
+ // 快速处理空值
67
+ if (!val) {
68
+ this.val = '';
69
+ return;
70
+ }
71
+
72
+ let value = val;
73
+
74
+ // 移除无效字符(一次性处理)
75
+ value = value.replace(/[^0-9.-]/g, '');
76
+
77
+ // 处理负号:只保留开头的第一个负号
78
+ if (value.startsWith('-')) {
79
+ value = '-' + value.slice(1).replace(/-/g, '');
80
+ } else {
81
+ value = value.replace(/-/g, '');
82
+ }
83
+
84
+ // 处理小数点:移除开头的点,保留第一个点
85
+ if (value.startsWith('.')) {
86
+ value = value.slice(1).replace(/\./g, '');
87
+ } else {
88
+ const dotIndex = value.indexOf('.');
89
+ if (dotIndex > 0) {
90
+ const parts = value.split('.');
91
+ value = parts[0] + '.' + parts.slice(1).join('');
92
+ }
93
+ }
94
+
95
+ // 处理精度
96
+ if (value && value !== '-' && this.precision != null && this.precision >= 0) {
97
+ const dotIndex = value.indexOf('.');
98
+ if (this.precision === 0) {
99
+ // 精度为0时,移除小数点及之后的内容
100
+ value = dotIndex > 0 ? value.slice(0, dotIndex) : value;
101
+ } else if (dotIndex > 0) {
102
+ // 限制小数位数
103
+ const decimalPart = value.slice(dotIndex + 1);
104
+ if (decimalPart.length > this.precision) {
105
+ value = value.slice(0, dotIndex + 1 + this.precision);
106
+ }
107
+ }
108
+ }
109
+
110
+ // 实时校验最大值和最小值
111
+ if (value && value !== '-') {
112
+ const numValue = parseFloat(value);
113
+ if (!isNaN(numValue)) {
114
+ // 校验最大值
115
+ if (this.max != null && numValue > parseFloat(this.max)) {
116
+ value = String(this.max);
117
+ }
118
+ // 校验最小值
119
+ if (this.min != null && numValue < parseFloat(this.min)) {
120
+ value = String(this.min);
121
+ }
122
+ }
123
+ }
124
+
125
+ this.val = value;
126
+ },200),
127
+ /**
128
+ * change事件上报
129
+ * */
130
+ valChange() {
131
+ // 处理只输入负号的情况
132
+ if (this.val === '-') {
133
+ this.val = '';
134
+ }
135
+ // 最大值最小值校验已在valInput中实时处理,这里保留作为兜底
136
+ if (this.val) {
137
+ const numValue = parseFloat(this.val);
138
+ if (!isNaN(numValue)) {
139
+ if (this.max != null && numValue > parseFloat(this.max)) {
140
+ this.val = String(this.max);
141
+ }
142
+ if (this.min != null && numValue < parseFloat(this.min)) {
143
+ this.val = String(this.min);
144
+ }
145
+ }
146
+ }
147
+ },
148
+ /**
149
+ * blur事件上报
150
+ * */
151
+ inputBlur() {
152
+ this.$emit('blur', this.val);
153
+ }
154
+ },
155
+ }
156
+ </script>
157
+ <style lang="scss" scoped>
158
+ .bnInput-underline {
159
+ :deep(.el-input__wrapper) {
160
+ box-shadow: none;
161
+ border-bottom: 1px solid #dcdfe6;
162
+ border-radius: 0;
163
+ }
164
+ }
165
+ </style>
@@ -0,0 +1,502 @@
1
+ <!--
2
+ @Description: 侧边导航栏组件,纯自研实现,支持多级嵌套、矢量图标及 smart 智能路由模式
3
+ @VERSION: 1.0
4
+ -->
5
+ <template>
6
+ <div class="BnMenu" :class="{ 'BnMenu--collapsed': isCollapsed }">
7
+ <!-- 收起态:隐藏导航内容,仅保留边缘展开按钮 -->
8
+ <div
9
+ v-if="isCollapsed"
10
+ class="BnMenu-edge-open bnUIcon bnUIcon-enlarge"
11
+ title="展开"
12
+ @click="toggleCollapse"
13
+ ></div>
14
+ <!-- 展开态:完整导航栏 -->
15
+ <template v-else>
16
+ <!-- Logo / 标题区域,可通过 #menuLogo 插槽自定义 -->
17
+ <div class="BnMenu-logo">
18
+ <div class="BnMenu-logo-main">
19
+ <slot name="menuLogo" :collapsed="isCollapsed">
20
+ <div class="BnMenu-logo-title">{{ title }}</div>
21
+ </slot>
22
+ </div>
23
+ <!-- 收起按钮 -->
24
+ <span
25
+ class="BnMenu-logo-toggle bnUIcon bnUIcon-shrink"
26
+ title="收起"
27
+ @click="toggleCollapse"
28
+ ></span>
29
+ </div>
30
+ <!-- 可滚动菜单列表 -->
31
+ <div class="BnMenu-list">
32
+ <el-scrollbar height="100%">
33
+ <div class="BnMenu-body">
34
+ <menu-item
35
+ v-for="(menu, index) in menuList"
36
+ :key="getMenuIndex(menu) || index"
37
+ :menu="menu"
38
+ :level="1"
39
+ :theme-index="index"
40
+ />
41
+ </div>
42
+ </el-scrollbar>
43
+ </div>
44
+ </template>
45
+ </div>
46
+ </template>
47
+ <script>
48
+ import MenuItem from './MenuItem.vue'
49
+ import { isEmpty } from 'lodash'
50
+
51
+ /** 一级菜单主题色循环列表,选中高亮按此顺序依次取用,超出后从头循环 */
52
+ const THEME_COLORS = [
53
+ '#1890ff', // 蓝
54
+ '#4f46e5', // 靛紫
55
+ '#d97706', // 琥珀
56
+ '#059669', // 祖母绿
57
+ '#db2777', // 玫红
58
+ '#0891b2', // 青
59
+ '#7c3aed', // 紫
60
+ '#e11d48', // 玫瑰红
61
+ '#009966' // 玫瑰红
62
+ ]
63
+
64
+ export default {
65
+ name: "BnMenus",
66
+ components: {
67
+ MenuItem
68
+ },
69
+ /**
70
+ * 向子组件 MenuItem 注入当前实例,供其调用菜单状态与方法
71
+ */
72
+ provide() {
73
+ return {
74
+ bnMenus: this
75
+ }
76
+ },
77
+ props: {
78
+ // Logo 区域标题文字,使用 #menuLogo 插槽后失效
79
+ title: String,
80
+ // 智能模式:自动高亮路由、自动跳转、自动缓存、根路径自动重定向首菜单
81
+ smart: Boolean,
82
+ // 智能模式下的 localStorage 缓存主键,用于刷新后恢复上次路由
83
+ cacheKey: String,
84
+ // 导航菜单数据,通过 children 字段实现子菜单嵌套;每项可配置 icon、meta.title 等
85
+ menuList: {
86
+ default() {
87
+ return []
88
+ }
89
+ },
90
+ // 默认展开的子菜单 name 数组,如 ['company', 'actionEngine']
91
+ defaultOpens: Array,
92
+ // 当前选中项,支持 v-model:activeNav;可为字符串(activeKey 值)或对象(完整菜单项)
93
+ activeNav: {},
94
+ // 匹配选中项的字段名,默认按 name 匹配
95
+ activeKey: { default: 'name' },
96
+ // 是否收起导航栏,支持 v-model:collapsed;收起后宽度为 0,展开按钮绝对定位悬浮在边缘
97
+ collapsed: { type: Boolean, default: undefined },
98
+ // 禁止所有子菜单展开/收起,默认全部展开且不可折叠
99
+ disableFold: { type: Boolean, default: false }
100
+ },
101
+ emits: ['update:activeNav', 'update:collapsed', 'collapse-change', 'change'],
102
+ data() {
103
+ return {
104
+ // 未受控时的内部收起状态
105
+ innerCollapsed: false,
106
+ // 当前已展开的子菜单 name 列表
107
+ openedKeys: []
108
+ }
109
+ },
110
+ watch: {
111
+ /**
112
+ * 监听 defaultOpens,合并到 openedKeys 作为初始展开项
113
+ */
114
+ defaultOpens: {
115
+ immediate: true,
116
+ handler(opens) {
117
+ if (opens?.length) {
118
+ this.openedKeys = [...new Set([...this.openedKeys, ...opens])]
119
+ }
120
+ }
121
+ },
122
+ /**
123
+ * 选中项变化时,自动展开其所有父级子菜单,确保高亮项可见
124
+ */
125
+ activeKeys: {
126
+ immediate: true,
127
+ handler(keys) {
128
+ keys.forEach(key => {
129
+ const path = this.findMenuPath(this.menuList, key)
130
+ if (path?.length > 1) {
131
+ path.slice(0, -1).forEach(name => this.openSubMenu(name))
132
+ }
133
+ })
134
+ }
135
+ }
136
+ },
137
+ methods: {
138
+ /**
139
+ * 切换导航栏收起 / 展开
140
+ */
141
+ toggleCollapse() {
142
+ const next = !this.isCollapsed
143
+ if (this.collapsed !== undefined) {
144
+ this.$emit('update:collapsed', next)
145
+ } else {
146
+ this.innerCollapsed = next
147
+ }
148
+ this.$emit('collapse-change', next)
149
+ },
150
+ /**
151
+ * 根据一级菜单索引获取主题色
152
+ * @param {Number} index 一级菜单在 menuList 中的索引
153
+ * @returns {String} 十六进制颜色值
154
+ */
155
+ getThemeColor(index) {
156
+ return THEME_COLORS[index % THEME_COLORS.length]
157
+ },
158
+ /**
159
+ * 根据一级菜单索引获取选中背景色(主题色 20% 透明度)
160
+ * @param {Number} index 一级菜单在 menuList 中的索引
161
+ * @returns {String} rgba 颜色值
162
+ */
163
+ getThemeBg(index) {
164
+ const hex = this.getThemeColor(index)
165
+ const r = parseInt(hex.slice(1, 3), 16)
166
+ const g = parseInt(hex.slice(3, 5), 16)
167
+ const b = parseInt(hex.slice(5, 7), 16)
168
+ return `rgba(${r}, ${g}, ${b}, 0.1)`
169
+ },
170
+ /**
171
+ * 生成菜单项唯一标识,用于高亮匹配与 v-for key
172
+ * @param {Object} menu 菜单项对象
173
+ * @returns {String} name 与 id 拼接后的唯一标识
174
+ */
175
+ getMenuIndex(menu) {
176
+ return `${menu.name}${menu.id || ''}`
177
+ },
178
+ /**
179
+ * 按菜单声明的字段子集匹配路由 params / query(值统一转字符串比较)
180
+ * @param {Object} expected 菜单项上的 params 或 query
181
+ * @param {Object} actual 当前路由上的 params 或 query
182
+ * @returns {Boolean}
183
+ */
184
+ matchRouteRecord(expected, actual = {}) {
185
+ return Object.keys(expected).every(key => String(actual[key]) === String(expected[key]))
186
+ },
187
+ /**
188
+ * smart 模式下判断菜单项是否与当前路由匹配(支持同 name 多项靠 query / id 区分)
189
+ * @param {Object} menu 菜单项对象
190
+ * @param {Object} route 当前路由
191
+ * @returns {Boolean}
192
+ */
193
+ isRouteMatch(menu, route) {
194
+ if (menu.name !== route.name) return false
195
+ const routeParams = route.params || {}
196
+ const routeQuery = route.query || {}
197
+ const hasMenuParams = !isEmpty(menu.params)
198
+ const hasMenuQuery = !isEmpty(menu.query)
199
+ const hasMenuId = menu.id != null && menu.id !== ''
200
+ const hasRouteParams = !isEmpty(routeParams)
201
+ const hasRouteQuery = !isEmpty(routeQuery)
202
+
203
+ if (hasMenuParams) {
204
+ if (!this.matchRouteRecord(menu.params, routeParams)) return false
205
+ } else if (hasRouteParams) {
206
+ return false
207
+ }
208
+
209
+ if (hasMenuQuery) {
210
+ if (!this.matchRouteRecord(menu.query, routeQuery)) return false
211
+ } else if (hasMenuId) {
212
+ const routeId = routeQuery.id ?? routeParams.id
213
+ if (routeId == null) return !hasRouteQuery
214
+ return String(routeId) === String(menu.id)
215
+ } else if (hasRouteQuery || hasRouteParams) {
216
+ return false
217
+ }
218
+ return true
219
+ },
220
+ /**
221
+ * 菜单项是否具备路由区分标识(同 name 多项时不应仅靠 name 高亮)
222
+ * @param {Object} menu 菜单项对象
223
+ * @returns {Boolean}
224
+ */
225
+ hasRouteIdentity(menu) {
226
+ return menu.id != null && menu.id !== '' || !isEmpty(menu.query) || !isEmpty(menu.params)
227
+ },
228
+ /**
229
+ * 判断菜单项是否处于选中高亮状态
230
+ * @param {Object} menu 菜单项对象
231
+ * @returns {Boolean}
232
+ */
233
+ isActive(menu) {
234
+ const index = this.getMenuIndex(menu)
235
+ if (this.activeKeys.includes(index)) return true
236
+ const key = menu[this.activeKey]
237
+ // 看板等同 name 多项场景:禁止仅用 name 匹配,避免全部高亮
238
+ return !!(key && !this.hasRouteIdentity(menu) && this.activeKeys.includes(key))
239
+ },
240
+ /**
241
+ * 判断菜单项是否禁止展开/收起
242
+ * 全局 disableFold 为 true 时全部禁止;单项可通过 menu.disableFold 或 menu.meta.disableFold 单独禁止
243
+ * @param {Object} menu 菜单项对象
244
+ * @returns {Boolean}
245
+ */
246
+ isDisableFold(menu) {
247
+ if (this.disableFold) return true
248
+ return !!(menu.disableFold || menu.meta?.disableFold)
249
+ },
250
+ /**
251
+ * 判断子菜单是否已展开
252
+ * @param {Object} menu 菜单项对象
253
+ * @returns {Boolean}
254
+ */
255
+ isOpen(menu) {
256
+ if (this.isDisableFold(menu)) return true
257
+ return this.openedKeys.includes(menu.name)
258
+ },
259
+ /**
260
+ * 切换子菜单展开 / 收起状态
261
+ * @param {Object} menu 菜单项对象
262
+ */
263
+ toggleSubMenu(menu) {
264
+ if (this.isDisableFold(menu)) return
265
+ const name = menu.name
266
+ if (this.openedKeys.includes(name)) {
267
+ this.openedKeys = this.openedKeys.filter(k => k !== name)
268
+ } else {
269
+ this.openedKeys = [...this.openedKeys, name]
270
+ }
271
+ },
272
+ /**
273
+ * 展开指定子菜单(若已展开则忽略)
274
+ * @param {String} name 子菜单 name
275
+ */
276
+ openSubMenu(name) {
277
+ if (!this.openedKeys.includes(name)) {
278
+ this.openedKeys = [...this.openedKeys, name]
279
+ }
280
+ },
281
+ /**
282
+ * 选中导航项
283
+ * smart 模式:自动路由跳转并可选缓存到 localStorage
284
+ * 普通模式:更新 v-model:activeNav 并触发 change 事件
285
+ * @param {Object} item 被点击的菜单项完整对象
286
+ */
287
+ selectMenu(item) {
288
+ const navVal = typeof this.activeNav === 'string'
289
+ ? item[this.activeKey]
290
+ : item
291
+ if (this.smart) {
292
+ const routeData = { name: item.name, params: item.params, query: item.query }
293
+ if (this.cacheKey) {
294
+ localStorage.setItem(this.cacheKey, JSON.stringify(routeData))
295
+ }
296
+ this.$router.push(routeData)
297
+ } else {
298
+ this.$emit('update:activeNav', navVal)
299
+ }
300
+ // 等待父级 v-model:activeNav 完成同步后再触发 change
301
+ this.$nextTick(() => {
302
+ this.$emit('change', item, navVal)
303
+ })
304
+ },
305
+ /**
306
+ * 深度优先获取第一个可跳转的叶子菜单项(无 children 且含 name)
307
+ * @param {Array} list 菜单树
308
+ * @returns {Object|null}
309
+ */
310
+ getFirstMenu(list) {
311
+ const queue = [...list]
312
+ while (queue.length) {
313
+ const item = queue.shift()
314
+ if (!item.children || !item.children.length) {
315
+ if (item.name) return item
316
+ } else {
317
+ queue.unshift(...item.children)
318
+ }
319
+ }
320
+ return null
321
+ },
322
+ /**
323
+ * 迭代平铺树形菜单为一维数组(避免递归栈溢出)
324
+ * @param {Array} list 菜单树
325
+ * @returns {Array} 不含 children 的菜单项数组
326
+ */
327
+ flatTree(list) {
328
+ const result = []
329
+ const stack = [...list]
330
+ while (stack.length) {
331
+ const { children, ...rest } = stack.pop()
332
+ result.push(rest)
333
+ if (children?.length) {
334
+ stack.push(...children)
335
+ }
336
+ }
337
+ return result
338
+ },
339
+ /**
340
+ * 在菜单树中查找目标项的 name 路径(从根到目标)
341
+ * @param {Array} list 菜单树
342
+ * @param {String} targetKey 目标标识(getMenuIndex 或 activeKey 值)
343
+ * @param {Array} path 当前递归路径
344
+ * @returns {Array|null} name 路径数组,未找到返回 null
345
+ */
346
+ findMenuPath(list, targetKey, path = []) {
347
+ for (const menu of list) {
348
+ const index = this.getMenuIndex(menu)
349
+ const key = menu[this.activeKey]
350
+ const currentPath = [...path, menu.name]
351
+ if (index === targetKey || key === targetKey) return currentPath
352
+ if (menu.children?.length) {
353
+ const found = this.findMenuPath(menu.children, targetKey, currentPath)
354
+ if (found) return found
355
+ }
356
+ }
357
+ return null
358
+ }
359
+ },
360
+ mounted() {
361
+ if (!this.smart) return
362
+ // 尝试从 localStorage 恢复上次路由
363
+ if (this.cacheKey) {
364
+ try {
365
+ const cached = localStorage.getItem(this.cacheKey)
366
+ if (cached) {
367
+ this.$router.push(JSON.parse(cached))
368
+ return
369
+ }
370
+ } catch (e) {
371
+ // 缓存数据损坏时清除,避免持续报错
372
+ localStorage.removeItem(this.cacheKey)
373
+ }
374
+ }
375
+ // 无缓存且当前在根路径时,自动跳转到第一个可用菜单
376
+ if (this.$route?.path === '/') {
377
+ const first = this.getFirstMenu(this.menuList)
378
+ if (first) {
379
+ this.$router.push({ name: first.name, params: first.params, query: first.query })
380
+ }
381
+ }
382
+ },
383
+ computed: {
384
+ /** 导航栏是否处于收起状态(支持受控与非受控) */
385
+ isCollapsed() {
386
+ return this.collapsed !== undefined ? this.collapsed : this.innerCollapsed
387
+ },
388
+ /**
389
+ * 当前所有选中高亮项的标识列表(统一为字符串数组)
390
+ * smart 模式:根据 $route 自动匹配
391
+ * 普通模式:由 activeNav 转换而来
392
+ */
393
+ activeKeys() {
394
+ if (this.smart && this.$route?.name) {
395
+ const matched = this.menuMap.find(menu => this.isRouteMatch(menu, this.$route))
396
+ return matched ? [this.getMenuIndex(matched)] : []
397
+ }
398
+ if (typeof this.activeNav === 'string') {
399
+ return this.activeNav ? [this.activeNav] : []
400
+ }
401
+ if (this.activeNav && typeof this.activeNav === 'object') {
402
+ return [this.getMenuIndex(this.activeNav)]
403
+ }
404
+ return []
405
+ },
406
+ /** 拍平后的菜单列表,用于 smart 模式路由匹配 */
407
+ menuMap() {
408
+ return this.flatTree(this.menuList)
409
+ }
410
+ }
411
+ }
412
+ </script>
413
+
414
+ <style scoped lang="scss">
415
+ @import "../font/iconfont.css";
416
+
417
+ .BnMenu {
418
+ display: flex;
419
+ flex-direction: column;
420
+ width: 100%;
421
+ min-width: 200px;
422
+ height: 100%;
423
+ position: relative;
424
+
425
+ &-logo {
426
+ position: relative;
427
+ flex-shrink: 0;
428
+
429
+ &-main {
430
+ min-height: 56px;
431
+ padding-right: 36px;
432
+ }
433
+
434
+ &-title {
435
+ height: 56px;
436
+ padding: 16px 20px;
437
+ font-size: 16px;
438
+ font-weight: 500;
439
+ }
440
+
441
+ &-toggle {
442
+ position: absolute;
443
+ top: 20px;
444
+ right: 10px;
445
+ z-index: 1;
446
+ font-size: 16px;
447
+ line-height: 1;
448
+ color: #86909c;
449
+ cursor: pointer;
450
+ transition: color 0.2s;
451
+
452
+ &:hover {
453
+ color: #1890ff;
454
+ }
455
+ }
456
+ }
457
+
458
+ // flex 子项占满剩余高度,height: 0 触发 flex 收缩以启用滚动
459
+ &-list {
460
+ flex: 1;
461
+ height: 0;
462
+ }
463
+
464
+ &-body {
465
+ border: none;
466
+ padding: 0;
467
+ }
468
+
469
+ // 收起态:不占宽度,展开按钮绝对定位悬浮在边缘
470
+ &--collapsed {
471
+ width: 0;
472
+ min-width: 0;
473
+ overflow: visible;
474
+ }
475
+
476
+ &-edge-open {
477
+ position: absolute;
478
+ top: 16px;
479
+ left: 0;
480
+ z-index: 10;
481
+ display: flex;
482
+ align-items: center;
483
+ justify-content: center;
484
+ width: 32px;
485
+ height: 32px;
486
+ font-size: 16px;
487
+ line-height: 1;
488
+ color: #86909c;
489
+ cursor: pointer;
490
+ border: 1px solid #e8eaed;
491
+ border-left: none;
492
+ border-radius: 0 6px 6px 0;
493
+ background: rgba(255,255,255,.5);
494
+ box-shadow: 2px 0 8px rgba(0, 0, 0, 0.06);
495
+ transition: color 0.2s;
496
+
497
+ &:hover {
498
+ color: #1890ff;
499
+ }
500
+ }
501
+ }
502
+ </style>