web-component-gallery 2.0.21 → 2.0.23

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.
@@ -11,16 +11,16 @@
11
11
  <Select
12
12
  show-search
13
13
  :style="{ width: width || '100%' }"
14
- :mode="mode"
14
+ :mode="mode"
15
15
  :open="isSelectOpen"
16
16
  :placeholder="placeholder"
17
- :filterOption="false"
17
+ :filterOption="listPageHandler ? false : filterOption"
18
18
  :getPopupContainer="tirggerNode => tirggerNode.parentNode"
19
19
  @select="handleSelect"
20
20
  @search="handleSearch"
21
21
  @popupScroll="handleScroll"
22
22
  v-bind="attrs"
23
- v-model="childSelectedValue"
23
+ v-model="innerValue"
24
24
  >
25
25
  <!-- v-on="$listeners" -->
26
26
  <template v-for="(index, name) in $slots" v-slot:[name]>
@@ -28,16 +28,16 @@
28
28
  </template>
29
29
  <template v-for="(index, name) in $scopedSlots" v-slot:[name]="data">
30
30
  <slot :name="name" v-bind="data"></slot>
31
- </template>
31
+ </template>
32
32
  <div slot="dropdownRender" slot-scope="menu">
33
- <Checkbox v-if="mode" :checked="selectChecked" @change="handleSelectAll">全选</Checkbox>
33
+ <Checkbox v-if="mode" :checked="checkedAll" @change="handleSelectAll">全选</Checkbox>
34
34
  <v-nodes :vnodes="menu" />
35
35
  <div v-if="isLoadingMore" class="loading-more">
36
36
  <Icon type="loading" />
37
37
  加载中...
38
38
  </div>
39
39
  </div>
40
- <SelectOption v-for="(item, index) in options" :key="index" :value="item[valueKey]">
40
+ <SelectOption v-for="(item, index) in innerOptions" :key="index" :value="item[valueKey]">
41
41
  {{ customLabel ? customLabelHandler(item) : item[labelKey] }}
42
42
  </SelectOption>
43
43
  </Select>
@@ -61,53 +61,72 @@ export default {
61
61
  Checkbox
62
62
  },
63
63
  props: {
64
+ // 基础配置
64
65
  value: {
65
66
  type: [String, Number, Array, Boolean, Object],
66
67
  default: undefined
67
68
  },
68
- // 是否多选
69
69
  mode: String,
70
- // 是否支持输入保留字符
71
70
  isInput: {
72
71
  type: Boolean,
73
72
  default: false
74
73
  },
75
- // 提示文字
76
74
  placeholder: {
77
75
  type: String,
78
76
  default: '请选择'
79
77
  },
80
78
  width: String,
81
- // 根据valueKey来进行自定义label
82
- customLabel: String,
79
+
80
+ // 数据映射
83
81
  valueKey: {
84
82
  type: String,
85
83
  default: 'value'
86
84
  },
87
85
  labelKey: {
88
86
  type: String,
89
- default: 'label'
87
+ default: 'label'
90
88
  },
89
+ customLabel: String,
90
+
91
91
  // 数据源
92
92
  options: {
93
93
  type: Array,
94
- default: () => ([])
94
+ default: () => []
95
+ },
96
+ listPageHandler: Function,
97
+
98
+ // 分页配置
99
+ pSize: {
100
+ type: Number,
101
+ default: 50
95
102
  },
96
- // 分页页数
97
- pSize: Number,
98
- // 分页总数,如要实现完整分页功能要传递接口总数
99
- pTotal: Number
103
+ searchKey: {
104
+ type: String,
105
+ default: 'keyword'
106
+ }
100
107
  },
108
+
101
109
  data() {
102
110
  return {
111
+ // 分页状态
112
+ pagination: {
113
+ current: 1,
114
+ size: 10,
115
+ total: 0
116
+ },
117
+
118
+ // 当前数据
119
+ innerOptions: [],
120
+
121
+ // UI状态
103
122
  isSelectOpen: false,
104
- currentPage: 1,
105
123
  isLoadingMore: false,
106
- searchKeyword: ''
124
+ searchValue: ''
107
125
  }
108
126
  },
127
+
109
128
  computed: {
110
- childSelectedValue: {
129
+ innerValue: {
111
130
  get() {
112
131
  return this.value
113
132
  },
@@ -115,7 +134,7 @@ export default {
115
134
  this.$emit('input', val)
116
135
  }
117
136
  },
118
- attrs() {
137
+ attrs() {
119
138
  return {
120
139
  allowClear: true,
121
140
  showSearch: true,
@@ -123,95 +142,142 @@ export default {
123
142
  }
124
143
  },
125
144
  hasMore() {
126
- return this.options.length < this.pTotal
145
+ return this.innerOptions.length < this.pagination.total
127
146
  },
128
- selectChecked: {
129
- get() {
130
- return this.childSelectedValue?.length === this.options?.length
147
+ checkedAll() {
148
+ return this.innerValue?.length === this.innerOptions?.length
149
+ }
150
+ },
151
+
152
+ watch: {
153
+ options: {
154
+ handler(newVal) {
155
+ this.innerOptions = [...newVal]
131
156
  },
132
- set(val) {
133
- this.$emit('input', val)
134
- }
157
+ immediate: true
135
158
  }
136
159
  },
160
+
137
161
  created() {
138
- this.debouncedSearch = debounce(this.handleSearch, 800)
139
- this.debouncedLoadMore = debounce(this.loadMoreData, 300)
162
+ this.debouncedSearch = debounce(this.handleSearch, 800, { leading: true, trailing: false })
163
+ this.debouncedLoadMore = debounce(this.loadMoreData, 300, { leading: false, trailing: true })
140
164
  },
165
+
141
166
  mounted() {
142
- document.addEventListener('click', this.bodyCloseMenus)
167
+ this.pagination.size = this.pSize
168
+ this.fetchData()
169
+ document.addEventListener('click', this.handleDocumentClick)
143
170
  },
171
+
144
172
  beforeDestroy() {
145
- document.removeEventListener('click', this.bodyCloseMenus)
173
+ document.removeEventListener('click', this.handleDocumentClick)
174
+ // 清理debounce定时器
175
+ this.debouncedSearch.cancel()
176
+ this.debouncedLoadMore.cancel()
146
177
  },
178
+
147
179
  methods: {
180
+ async fetchData() {
181
+ if (!this.listPageHandler) return
182
+
183
+ try {
184
+ this.isLoadingMore = true
185
+ const { total, records } = await this.listPageHandler({
186
+ ...this.pagination,
187
+ [this.searchKey]: this.searchValue
188
+ })
189
+
190
+ this.pagination.current === 1 ?
191
+ this.innerOptions = [...records] :
192
+ this.innerOptions = [...this.innerOptions, ...records]
193
+
194
+ this.pagination.total = total
195
+ } catch (error) {
196
+ this.pagination.current--
197
+ console.error('分页加载失败:', error)
198
+ } finally {
199
+ this.isLoadingMore = false
200
+ }
201
+ },
202
+
148
203
  // 下拉滚动加载更多
149
204
  handleScroll(e) {
150
205
  const { scrollTop, scrollHeight, clientHeight } = e.target
151
206
  const reachBottom = scrollTop + clientHeight >= scrollHeight - 10
152
207
  if (reachBottom && !this.isLoadingMore && this.hasMore) this.debouncedLoadMore()
153
208
  },
154
- // 搜索事件
209
+
210
+ // 搜索事件处理
155
211
  handleSearch(value) {
156
- this.isInput && (this.childSelectedValue = value)
157
- this.searchKeyword = value
158
- this.currentPage = 1
159
-
160
- // 根据是否有分页决定emit参数格式
161
- this.$emit('search', this.pTotal ? {
162
- keyword: value,
163
- current: this.currentPage,
164
- size: this.pSize
165
- } : value)
166
- },
167
- // 点击选择器外内容下拉回收
168
- bodyCloseMenus(e) {
212
+ this.searchValue = value
213
+ this.pagination.current = 1
214
+
215
+ this.fetchData()
216
+ },
217
+
218
+ // 本地搜索
219
+ filterOption(input, option) {
220
+ this.isInput && (this.innerValue = input)
221
+ return option.componentOptions.children[0].text
222
+ .toLowerCase()
223
+ .indexOf(input.toLowerCase()) >= 0
224
+ },
225
+
226
+ // 点击选择器外内容关闭下拉
227
+ handleDocumentClick(e) {
169
228
  if (this.$refs.main && !this.$refs.main.contains(e.target)) this.isSelectOpen = false
170
229
  },
171
- // 自定义label显示
230
+
231
+ // 安全的自定义label处理
172
232
  customLabelHandler(item) {
233
+ if (!this.customLabel) return item[this.labelKey]
234
+
173
235
  try {
174
- // 使用函数式替代eval
175
- const func = new Function('item', `return ${this.customLabel}`)
176
- return func(item)
236
+ // 使用安全的模板字符串解析
237
+ const template = this.customLabel
238
+ .replace(/{{([^{}]+)}}/g, (match, key) => {
239
+ const path = key.trim().split('.')
240
+ let value = item
241
+ for (const p of path) {
242
+ if (value == null) return ''
243
+ value = value[p]
244
+ }
245
+ return value == null ? '' : value
246
+ })
247
+ return template
177
248
  } catch (e) {
178
249
  console.error('自定义label解析错误:', e)
179
250
  return item[this.labelKey]
180
251
  }
181
252
  },
182
- // 选中全部
183
- handleSelectAll(event) {
184
- const isChecked = event.target.checked
185
- const selectOptions = isChecked ? this.options.map(item => item[this.valueKey]) : []
186
- this.$emit('input', selectOptions)
253
+
254
+ // 选中全部选项
255
+ handleSelectAll({target}) {
256
+ const selectOptions = target.checked
257
+ ? this.innerOptions.map(item => item[this.valueKey])
258
+ : []
259
+
187
260
  this.isSelectOpen = false
261
+ this.$emit('input', selectOptions)
188
262
  },
189
- // 选中事件
263
+
264
+ // 选中单个选项
190
265
  handleSelect(value, option) {
191
266
  if (!value) return
267
+
192
268
  this.isSelectOpen = false
193
- const selectedOption = this.options.find(item => item[this.valueKey] === value)
269
+ const selectedOption = this.innerOptions.find(
270
+ item => item[this.valueKey] === value
271
+ )
194
272
  selectedOption && this.$emit('select', value, selectedOption)
195
273
  },
196
- // 加载更多
197
- loadMoreData() {
274
+
275
+ // 加载更多数据
276
+ async loadMoreData() {
198
277
  if (this.isLoadingMore || !this.hasMore) return
199
278
 
200
- this.isLoadingMore = true
201
- this.currentPage++
202
-
203
- try {
204
- this.$emit('load-more', {
205
- keyword: this.searchKeyword,
206
- current: this.currentPage,
207
- size: this.pSize
208
- })
209
- } catch (error) {
210
- this.currentPage--
211
- console.error('加载更多失败:', error)
212
- } finally {
213
- this.isLoadingMore = false
214
- }
279
+ this.pagination.current++
280
+ await this.fetchData()
215
281
  }
216
282
  }
217
283
  }
@@ -59,5 +59,6 @@
59
59
  }
60
60
  &-body {
61
61
  flex: 1;
62
+ min-height: 0;
62
63
  }
63
64
  }
@@ -1,12 +1,13 @@
1
1
  import { FormModel } from 'ant-design-vue'
2
2
  import { getFormWidth, setFormItemRule, setFormItem } from './utils/render'
3
+ import _ from 'lodash'
3
4
 
4
5
  export default {
5
6
  name: 'WModel',
6
7
  model: {
7
8
  prop: 'value',
8
9
  event: 'Change:Model'
9
- },
10
+ },
10
11
  data() {
11
12
  return {
12
13
  formRules: {}
@@ -16,7 +17,7 @@ export default {
16
17
  value: {
17
18
  type: Object,
18
19
  default: () => ({})
19
- },
20
+ },
20
21
  /* Form布局方式 */
21
22
  layout: {
22
23
  type: String,
@@ -39,7 +40,7 @@ export default {
39
40
  /** 校验单独赋值延迟的字段 */
40
41
  for (const k in this.value) {
41
42
  Object.hasOwnProperty.call(this.formRules, k) && this.$refs.FormModel?.validateField(k)
42
- }
43
+ }
43
44
  return this.value
44
45
  },
45
46
  set(value) {
@@ -47,15 +48,13 @@ export default {
47
48
  }
48
49
  },
49
50
  formAttrs() {
50
- let attr = {
51
- labelCol: { span: 4 },
52
- wrapperCol: { span: 20 },
53
- ...this.$attrs
54
- }
55
- this.layout === 'vertical' && (
56
- delete attr.labelCol,
57
- delete attr.wrapperCol
58
- )
51
+ const attr =
52
+ this.layout === 'vertical' ?
53
+ this.$attrs :
54
+ {
55
+ ...getDefaultFormAttrs(),
56
+ ...this.$attrs
57
+ }
59
58
  return attr
60
59
  },
61
60
  filterSetting() {
@@ -76,58 +75,77 @@ export default {
76
75
  this.$emit('update:refForm', this.$refs.FormModel)
77
76
  },
78
77
  methods: {
79
- /** 包含单表单项,多表单项,动态增减表单项合并处理 */
80
- setModelRender(props) {
78
+ getDefaultFormAttrs() {
79
+ return {
80
+ labelCol: { span: 4 },
81
+ wrapperCol: { span: 20 }
82
+ }
83
+ },
84
+
85
+ renderSingleFormItem(h, props) {
86
+ return setFormItem.call(this, h, this.form, props)
87
+ },
88
+
89
+ renderDynamicFormItems(h, dynamicModel, props) {
90
+ // multipleConfig为配置的动态项数据
91
+ return dynamicModel.map((modelItem, key) => {
92
+ // 为动态项时重新定义绑定key、prop等来进行检验
93
+ props.multipleConfig.map((configItem) => {
94
+ const childAttrs = {
95
+ key,
96
+ prop: `${props.model}.${key}.${configItem.model}`,
97
+ style: getFormWidth.call(this, configItem, this.layoutSize ?? props.layoutSize),
98
+ rules: setFormItemRule.call(this, configItem, props),
99
+ parentModel: props.model
100
+ }
101
+ return setFormItem.call(this, h, modelItem, configItem, childAttrs)
102
+ })
103
+ })
104
+ },
105
+
106
+ setModelRender(h, props) {
81
107
  const dynamicModel = this.form[props.model]
82
108
  return (
83
- /**
84
- * multiple为动态多项
85
- * multipleConfig为配置的动态项数据 */
86
- <div class={props.multiple && ['MultipleForm']} {...{ attrs: { style: getFormWidth.call(this, props, this.layoutSize, false) } }}>
87
- {/* 使用场景:添加分层提示信息及其他隔层操作 */}
109
+ <div
110
+ class={props.multiple && ['MultipleForm']}
111
+ {...{ attrs: { style: getFormWidth.call(this, props, this.layoutSize, props.multiple ? 0 : 24) } }}
112
+ >
113
+ {/* 添加分层提示信息及其他隔层操作 */}
88
114
  {this.$scopedSlots[`${props.model}Tips`] && this.$scopedSlots[`${props.model}Tips`](props)}
89
115
  {
90
- /** 处理动态增减表单 */
91
- dynamicModel instanceof Array && props.multiple ?
92
- dynamicModel.map( (modelItem, index) =>
93
- props.multipleConfig.map( (configItem, key) => {
94
- /** 为动态项时重新定义绑定key、prop等来进行检验 */
95
- /** 目前还有一个动态项layoutSize排版问题 */
96
- const childAttrs = {
97
- key: index,
98
- prop: `${props.model}.${index}.${configItem.model}`,
99
- style: getFormWidth.call(this, configItem, this.layoutSize ?? props.layoutSize),
100
- rules: setFormItemRule.call(this, configItem, props),
101
- parentModel: props.model
102
- }
103
- return setFormItem.call(this, h, modelItem, configItem, childAttrs)
104
- } )
105
- ) :
106
- setFormItem.call(this, h, this.form, props)
116
+ // multiple为动态多项(使用场景:处理动态增减表单
117
+ dynamicModel instanceof Array && props.multiple
118
+ ? this.renderDynamicFormItems(h, dynamicModel, props)
119
+ : this.renderSingleFormItem(h, props)
107
120
  }
108
121
  {/* 使用场景:如另起一行添加其他额外信息及操作 */}
109
122
  {this.$scopedSlots[`${props.model}Operate`] && this.$scopedSlots[`${props.model}Operate`](props)}
110
123
  </div>
111
124
  )
112
125
  },
113
- /** 提交测验 */
126
+
127
+ getFormWidth: _.memoize(function(props, layoutSize, multiple) {
128
+ return getFormWidth.call(this, props, layoutSize, multiple ? 24 : 0)
129
+ }),
130
+
114
131
  formSubmit() {
115
132
  return new Promise((resolve, reject) => {
116
133
  this.$refs.FormModel.validate()
117
- .then(resolve)
118
- .catch(err =>
119
- this.$nextTick(() => {
120
- const errorDiv = document.getElementsByClassName('has-error')
121
- errorDiv[0].scrollIntoView({
122
- behavior: "smooth",
123
- block: "center"
124
- })
125
- })
126
- )
134
+ .then(resolve)
135
+ .catch(err => {
136
+ console.error('表单验证失败:', err)
137
+ this.$nextTick(() => {
138
+ const errorDiv = document.getElementsByClassName('has-error')
139
+ errorDiv[0].scrollIntoView({
140
+ behavior: "smooth",
141
+ block: "center"
142
+ })
143
+ })
144
+ })
127
145
  })
128
146
  }
129
147
  },
130
- render() {
148
+ render(h) {
131
149
  const {layout, formAttrs, setModelRender} = this
132
150
 
133
151
  return (
@@ -143,9 +161,9 @@ export default {
143
161
  >
144
162
  {
145
163
  this.$slots.default ??
146
- this.filterSetting.map(props => setModelRender(props))
164
+ this.filterSetting.map(props => setModelRender(h, props))
147
165
  }
148
166
  </FormModel>
149
167
  )
150
168
  }
151
- }
169
+ }
@@ -3,81 +3,104 @@ import RenderComp from '../../form-comp/RenderComp.vue'
3
3
 
4
4
  const FormModelItem = FormModel.Item
5
5
 
6
- /** 根据layoutSize来进行距离及大小切分 */
7
- export function getFormWidth(child, layoutSize, gap = true) {
8
- if (this.layout === 'vertical')
9
- return `flex: 0 1 calc((${(100 / layoutSize) * (child.size ?? 1)}% - 24px)); ${gap && 'margin-right:24px;'}`
10
- return `flex: 0 1 ${(100 / layoutSize) * (child.size ?? 1)}%;`
11
- }
6
+ /**
7
+ * 根据布局大小计算表单宽度
8
+ * @param {Object} child - 表单项配置
9
+ * @param {number} layoutSize - 一行显示的表单项数量
10
+ * @param {number} [gap=24] - 表单项间距
11
+ * @returns {string} - 计算后的CSS样式
12
+ */
13
+ export function getFormWidth(child, layoutSize, gap = 24) {
14
+ const width = (100 / layoutSize) * (child.size ?? 1)
15
+ if (this.layout === 'vertical') return `flex: 0 1 calc(${width}% - ${gap}px); margin-right: ${gap}px;`
16
+ return `flex: 0 1 ${width}%;`
17
+ }
12
18
 
13
- /** 动态设置单个表单项规则 (同时解决处理动态增减表单项操作 */
19
+ /**
20
+ * 动态设置表单项验证规则
21
+ * @param {Object} node - 当前表单项配置
22
+ * @param {Object} [nodeParent={}] - 父表单项配置
23
+ * @returns {Array} - 验证规则数组
24
+ */
14
25
  export function setFormItemRule(node, nodeParent = {}) {
26
+ // 确定是否必填
15
27
  const required = (node.required ?? nodeParent.required ?? this.formAttrs.required) ?? true
16
-
17
- /** node.placeholder与组件内的提示文字位置不对等 */
18
- const message =
19
- node.attrs?.placeholder ||
28
+
29
+ // 生成提示信息
30
+ const message = node.attrs?.placeholder ||
20
31
  (/(select|picker|radio|upload)/.test((node.is ?? 'Input').toLowerCase()) ? '请选择' : '请输入') +
21
- (node.label || '')
32
+ (node.label || '')
22
33
 
23
- const rules = [
34
+ // 基础规则
35
+ const baseRules = [
24
36
  {
25
37
  required,
26
38
  message,
27
- trigger: ['change', 'blur']
39
+ trigger: ['change', 'blur']
40
+ }
41
+ ]
42
+
43
+ // 合并自定义规则
44
+ const customRules = (node.rules || []).filter(rule => {
45
+ if (rule.pattern) {
46
+ rule.pattern = new RegExp(rule.pattern)
28
47
  }
29
- ].concat(
30
- (node.rules || []).filter(i => {
31
- i.pattern && (i.pattern = new RegExp(i.pattern))
32
- return i
33
- }))
48
+ return rule
49
+ })
34
50
 
35
- return rules
51
+ return [...baseRules, ...customRules]
36
52
  }
37
53
 
38
- /** 动态渲染表单项 */
54
+ /**
55
+ * 动态渲染表单项
56
+ * @param {Function} h - Vue的createElement函数
57
+ * @param {Object} vModel - 表单数据对象
58
+ * @param {Object} child - 表单项配置
59
+ * @param {Object} [childAttrs] - 表单项属性
60
+ * @returns {VNode} - 渲染结果
61
+ */
39
62
  export function setFormItem(h, vModel, child, childAttrs) {
40
-
41
63
  const {layout, layoutSize, $scopedSlots} = this
42
64
 
43
- let formItemAttrs =
44
- childAttrs ??
45
- {
46
- key: child.model,
47
- prop: child.model,
48
- colon: !(layout == 'inline')
49
- }
65
+ // 设置表单项属性
66
+ const formItemAttrs = childAttrs ?? {
67
+ key: child.model,
68
+ prop: child.model,
69
+ colon: !(layout === 'inline')
70
+ }
71
+
72
+ // 生成插槽名称
73
+ const slotsName = (childAttrs?.parentModel ?? '') + child.model
50
74
 
51
- const slotsName =
52
- (childAttrs?.parentModel ?? '') + child.model
75
+ // 渲染标签
76
+ const renderLabel = customLabel =>
77
+ customLabel ? customLabel(vModel) : <span>{child.label}</span>
53
78
 
54
- const {label, customLabel} = child
79
+ // 渲染表单项内容
80
+ const renderContent = () => {
81
+ if ($scopedSlots[child.model]) return $scopedSlots[child.model](child)
82
+ return (
83
+ <RenderComp
84
+ v-model={vModel[child.model]}
85
+ component={child.is}
86
+ {...{ on: child.event, attrs: child.attrs }}
87
+ />
88
+ )
89
+ }
55
90
 
56
91
  return (
57
92
  <FormModelItem
58
93
  class={[
59
- { FormLineVertical: child.size === layoutSize && layout === 'vertical' },
60
- { FormLine: child.size === layoutSize }
94
+ { 'FormLineVertical': child.size === layoutSize && layout === 'vertical' },
95
+ { 'FormLine': child.size === layoutSize }
61
96
  ]}
62
97
  {...{ attrs: formItemAttrs }}
63
98
  >
64
99
  <template slot="label">
65
- {
66
- customLabel ?
67
- customLabel(vModel) :
68
- <span>{label}</span>
69
- }
100
+ {renderLabel(child.customLabel)}
70
101
  </template>
71
102
  {/* 当前表单项可slot进行单独开发配置 (目前仅支持非动态增减情况) */}
72
- {$scopedSlots[child.model] ? (
73
- $scopedSlots[child.model](child)
74
- ) : (
75
- <RenderComp
76
- v-model={vModel[child.model]}
77
- component={child.is}
78
- {...{ on: child.event, attrs: child.attrs }}
79
- />
80
- )}
103
+ {renderContent()}
81
104
  {/* 如若再某个model后需添加按钮等场景(例如地图的选址按钮 */}
82
105
  {$scopedSlots[`${slotsName}Handle`] && $scopedSlots[`${slotsName}Handle`](formItemAttrs)}
83
106
  </FormModelItem>