xt-element-ui 2.0.1 → 2.0.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xt-element-ui",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "基于 Vue 2.7 + ElementUI 的企业级组件库,提供丰富的自定义组件和扩展组件",
5
5
  "main": "lib/index.common.js",
6
6
  "module": "lib/index.esm.js",
@@ -36,7 +36,8 @@
36
36
  "echarts": "^6.1.0",
37
37
  "element-ui": "^2.15.14",
38
38
  "sass": "^1.32.13",
39
- "vue-server-renderer": "^2.7.14"
39
+ "vue-server-renderer": "^2.7.14",
40
+ "xt-element-ui": "^2.0.2"
40
41
  },
41
42
  "keywords": [
42
43
  "vue",
@@ -40,4 +40,7 @@
40
40
  @import './xt-tabs/style/index.scss';
41
41
 
42
42
  // Badge 组件样式
43
- @import './xt-badge/style/index.scss';
43
+ @import './xt-badge/style/index.scss';
44
+
45
+ // ScrollArrow 组件样式
46
+ @import './xt-scroll-arrow/style/index.scss';
@@ -7,17 +7,17 @@
7
7
  ]"
8
8
  >
9
9
  <el-input
10
- :value="value"
10
+ :value="displayValue"
11
11
  :placeholder="placeholder"
12
- :type="type"
12
+ :type="isNumberType ? 'text' : type"
13
13
  :size="size"
14
14
  :disabled="disabled"
15
15
  :readonly="readonly"
16
16
  :style="inputStyle"
17
- @input="$emit('input', $event)"
18
- @change="$emit('change', $event)"
19
- @focus="$emit('focus', $event)"
20
- @blur="$emit('blur', $event)"
17
+ @input="handleInput"
18
+ @change="handleChange"
19
+ @focus="handleFocus"
20
+ @blur="handleBlur"
21
21
  />
22
22
  </div>
23
23
  </template>
@@ -52,7 +52,22 @@ export default {
52
52
  default: ''
53
53
  }
54
54
  },
55
+ data() {
56
+ return {
57
+ currentStr: '',
58
+ isFocused: false
59
+ }
60
+ },
55
61
  computed: {
62
+ isNumberType() {
63
+ return this.type === 'number'
64
+ },
65
+ displayValue() {
66
+ if (this.isNumberType) {
67
+ return this.currentStr
68
+ }
69
+ return this.value
70
+ },
56
71
  inputStyle() {
57
72
  if (this.color) {
58
73
  return {
@@ -61,6 +76,71 @@ export default {
61
76
  }
62
77
  return {}
63
78
  }
79
+ },
80
+ watch: {
81
+ value: {
82
+ immediate: true,
83
+ handler(val) {
84
+ if (this.isNumberType && !this.isFocused) {
85
+ this.currentStr = val === null || val === undefined || val === '' ? '' : String(val)
86
+ }
87
+ }
88
+ }
89
+ },
90
+ methods: {
91
+ isValidNumber(str) {
92
+ return /^[+-]?\d*\.?\d*$/.test(str)
93
+ },
94
+ handleInput(val) {
95
+ if (this.isNumberType) {
96
+ if (this.isValidNumber(val)) {
97
+ this.currentStr = val
98
+ const num = this.parseToNumber(val)
99
+ if (num !== undefined) {
100
+ this.$emit('input', num)
101
+ }
102
+ }
103
+ } else {
104
+ this.$emit('input', val)
105
+ }
106
+ },
107
+ handleChange(val) {
108
+ if (this.isNumberType) {
109
+ const num = this.parseToNumber(val)
110
+ this.$emit('change', num)
111
+ } else {
112
+ this.$emit('change', val)
113
+ }
114
+ },
115
+ handleFocus(e) {
116
+ this.isFocused = true
117
+ this.$emit('focus', e)
118
+ },
119
+ handleBlur(e) {
120
+ this.isFocused = false
121
+ if (this.isNumberType) {
122
+ const num = this.parseToNumber(this.currentStr)
123
+ this.$emit('blur', num, e)
124
+ if (num !== undefined) {
125
+ this.currentStr = String(num)
126
+ this.$emit('input', num)
127
+ } else {
128
+ if (this.currentStr !== '') {
129
+ this.currentStr = ''
130
+ this.$emit('input', undefined)
131
+ }
132
+ }
133
+ } else {
134
+ this.$emit('blur', e)
135
+ }
136
+ },
137
+ parseToNumber(str) {
138
+ if (!str || str === '+' || str === '-' || str === '.' || str === '+.' || str === '-.' || str === '-.') {
139
+ return undefined
140
+ }
141
+ const num = parseFloat(str)
142
+ return isNaN(num) ? undefined : num
143
+ }
64
144
  }
65
145
  }
66
146
  </script>
@@ -0,0 +1,8 @@
1
+ import XtScrollArrow from './index.vue'
2
+
3
+ XtScrollArrow.install = function (Vue) {
4
+ Vue.component(XtScrollArrow.name, XtScrollArrow)
5
+ }
6
+
7
+ export default XtScrollArrow
8
+ export { XtScrollArrow }
@@ -0,0 +1,190 @@
1
+ <template>
2
+ <div class="xt-scroll-arrow" :class="[`xt-scroll-arrow--${direction}`]" :style="containerStyle">
3
+ <div
4
+ v-if="showLeftArrow"
5
+ class="xt-scroll-arrow__btn xt-scroll-arrow__btn--left"
6
+ @click="scrollLeft"
7
+ >
8
+ <i class="el-icon-arrow-left"></i>
9
+ </div>
10
+
11
+ <div ref="scrollContainer" class="xt-scroll-arrow__content" @scroll="handleScroll">
12
+ <slot></slot>
13
+ </div>
14
+
15
+ <div
16
+ v-if="showRightArrow"
17
+ class="xt-scroll-arrow__btn xt-scroll-arrow__btn--right"
18
+ @click="scrollRight"
19
+ >
20
+ <i class="el-icon-arrow-right"></i>
21
+ </div>
22
+
23
+ <div
24
+ v-if="showTopArrow && direction === 'vertical'"
25
+ class="xt-scroll-arrow__btn xt-scroll-arrow__btn--top"
26
+ @click="scrollTop"
27
+ >
28
+ <i class="el-icon-arrow-up"></i>
29
+ </div>
30
+
31
+ <div
32
+ v-if="showBottomArrow && direction === 'vertical'"
33
+ class="xt-scroll-arrow__btn xt-scroll-arrow__btn--bottom"
34
+ @click="scrollBottom"
35
+ >
36
+ <i class="el-icon-arrow-down"></i>
37
+ </div>
38
+ </div>
39
+ </template>
40
+
41
+ <script>
42
+ export default {
43
+ name: 'XtScrollArrow',
44
+ props: {
45
+ direction: {
46
+ type: String,
47
+ default: 'horizontal',
48
+ validator: (val) => ['horizontal', 'vertical'].includes(val)
49
+ },
50
+ scrollStep: {
51
+ type: Number,
52
+ default: 100
53
+ },
54
+ autoHide: {
55
+ type: Boolean,
56
+ default: true
57
+ },
58
+ height: {
59
+ type: [String, Number],
60
+ default: ''
61
+ },
62
+ width: {
63
+ type: [String, Number],
64
+ default: ''
65
+ }
66
+ },
67
+ data() {
68
+ return {
69
+ showLeftArrow: false,
70
+ showRightArrow: false,
71
+ showTopArrow: false,
72
+ showBottomArrow: false
73
+ }
74
+ },
75
+ computed: {
76
+ containerStyle() {
77
+ const style = {}
78
+ if (this.height) {
79
+ style.height = typeof this.height === 'number' ? `${this.height}px` : this.height
80
+ }
81
+ if (this.width) {
82
+ style.width = typeof this.width === 'number' ? `${this.width}px` : this.width
83
+ }
84
+ return style
85
+ }
86
+ },
87
+ watch: {
88
+ direction() {
89
+ this.$nextTick(() => {
90
+ this.checkScroll()
91
+ })
92
+ }
93
+ },
94
+ mounted() {
95
+ this.$nextTick(() => {
96
+ this.checkScroll()
97
+ })
98
+ this.addResizeObserver()
99
+ },
100
+ beforeDestroy() {
101
+ this.removeResizeObserver()
102
+ },
103
+ methods: {
104
+ addResizeObserver() {
105
+ if (typeof ResizeObserver !== 'undefined') {
106
+ this.resizeObserver = new ResizeObserver(() => {
107
+ this.$nextTick(() => {
108
+ this.checkScroll()
109
+ })
110
+ })
111
+ const container = this.$refs.scrollContainer
112
+ if (container) {
113
+ this.resizeObserver.observe(container)
114
+ }
115
+ }
116
+ },
117
+ removeResizeObserver() {
118
+ if (this.resizeObserver) {
119
+ this.resizeObserver.disconnect()
120
+ }
121
+ },
122
+ checkScroll() {
123
+ const container = this.$refs.scrollContainer
124
+ if (!container) return
125
+
126
+ if (this.direction === 'horizontal') {
127
+ const scrollLeft = container.scrollLeft
128
+ const scrollWidth = container.scrollWidth
129
+ const clientWidth = container.clientWidth
130
+ const maxScroll = scrollWidth - clientWidth
131
+
132
+ this.showTopArrow = false
133
+ this.showBottomArrow = false
134
+
135
+ if (this.autoHide) {
136
+ this.showLeftArrow = scrollLeft > 0
137
+ this.showRightArrow = maxScroll > 0 && scrollLeft < maxScroll
138
+ } else {
139
+ this.showLeftArrow = maxScroll > 0
140
+ this.showRightArrow = maxScroll > 0
141
+ }
142
+ } else {
143
+ const scrollTop = container.scrollTop
144
+ const scrollHeight = container.scrollHeight
145
+ const clientHeight = container.clientHeight
146
+ const maxScroll = scrollHeight - clientHeight
147
+
148
+ this.showLeftArrow = false
149
+ this.showRightArrow = false
150
+
151
+ if (this.autoHide) {
152
+ this.showTopArrow = scrollTop > 0
153
+ this.showBottomArrow = maxScroll > 0 && scrollTop < maxScroll
154
+ } else {
155
+ this.showTopArrow = maxScroll > 0
156
+ this.showBottomArrow = maxScroll > 0
157
+ }
158
+ }
159
+ },
160
+ handleScroll() {
161
+ this.checkScroll()
162
+ this.$emit('scroll', this.$refs.scrollContainer)
163
+ },
164
+ scrollLeft() {
165
+ const container = this.$refs.scrollContainer
166
+ if (container) {
167
+ container.scrollBy({ left: -this.scrollStep, behavior: 'smooth' })
168
+ }
169
+ },
170
+ scrollRight() {
171
+ const container = this.$refs.scrollContainer
172
+ if (container) {
173
+ container.scrollBy({ left: this.scrollStep, behavior: 'smooth' })
174
+ }
175
+ },
176
+ scrollTop() {
177
+ const container = this.$refs.scrollContainer
178
+ if (container) {
179
+ container.scrollBy({ top: -this.scrollStep, behavior: 'smooth' })
180
+ }
181
+ },
182
+ scrollBottom() {
183
+ const container = this.$refs.scrollContainer
184
+ if (container) {
185
+ container.scrollBy({ top: this.scrollStep, behavior: 'smooth' })
186
+ }
187
+ }
188
+ }
189
+ }
190
+ </script>
@@ -0,0 +1,89 @@
1
+ @import '../../../styles/variables.scss';
2
+
3
+ .xt-scroll-arrow {
4
+ position: relative;
5
+ display: flex;
6
+ align-items: center;
7
+ overflow: hidden;
8
+
9
+ &--horizontal {
10
+ flex-direction: row;
11
+ }
12
+
13
+ &--vertical {
14
+ flex-direction: column;
15
+ }
16
+
17
+ &__content {
18
+ flex: 1;
19
+ overflow: auto;
20
+ scrollbar-width: none;
21
+ -ms-overflow-style: none;
22
+
23
+ &::-webkit-scrollbar {
24
+ display: none;
25
+ }
26
+ }
27
+
28
+ &__btn {
29
+ position: absolute;
30
+ z-index: 10;
31
+ width: 32px;
32
+ height: 32px;
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: center;
36
+ background: rgba(255, 255, 255, 0.9);
37
+ border-radius: 50%;
38
+ cursor: pointer;
39
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
40
+ transition: all 0.3s ease;
41
+ border: none;
42
+ padding: 0;
43
+
44
+ &:hover {
45
+ background: rgba(255, 255, 255, 1);
46
+ transform: scale(1.1);
47
+ }
48
+
49
+ &:active {
50
+ transform: scale(0.95);
51
+ }
52
+
53
+ &--left {
54
+ left: 8px;
55
+ }
56
+
57
+ &--right {
58
+ right: 8px;
59
+ }
60
+
61
+ &--top {
62
+ top: 8px;
63
+ left: 50%;
64
+ transform: translateX(-50%);
65
+
66
+ &:hover {
67
+ transform: translateX(-50%) scale(1.1);
68
+ }
69
+
70
+ &:active {
71
+ transform: translateX(-50%) scale(0.95);
72
+ }
73
+ }
74
+
75
+ &--bottom {
76
+ bottom: 8px;
77
+ left: 50%;
78
+ transform: translateX(-50%);
79
+
80
+ &:hover {
81
+ transform: translateX(-50%) scale(1.1);
82
+ }
83
+
84
+ &:active {
85
+ transform: translateX(-50%) scale(0.95);
86
+ }
87
+ }
88
+ }
89
+ }
@@ -10,7 +10,7 @@
10
10
  icon="el-icon-plus"
11
11
  plain
12
12
  @click="onAdd"
13
- >新增档位</xt-button>
13
+ >新增{{stepName}}</xt-button>
14
14
  <xt-text v-if="isLimitReached" size="small" type-color="info">已达上限({{ localItems.length }}/{{ limit }})</xt-text>
15
15
  </div>
16
16
 
@@ -27,6 +27,7 @@
27
27
  :min-locked="idx !== 0 ? true : false"
28
28
  :unit="unit"
29
29
  :precision="precision"
30
+ :step-name="stepName"
30
31
  :step="step"
31
32
  :left-bracket="leftBracket"
32
33
  :right-bracket="rightBracket"
@@ -36,11 +37,12 @@
36
37
  @max-change="onMaxChange"
37
38
  @min-change="onMinChange"
38
39
  @delete="onDelete"
40
+ @blur="onFieldBlur"
39
41
  />
40
42
  </div>
41
43
 
42
44
  <div v-if="localItems.length === 0" class="xt-step-price__empty">
43
- <span>暂无数据,点击右上角「新增档位」开始配置</span>
45
+ <span>暂无数据,点击右上角「新增{{stepName}}」开始配置</span>
44
46
  </div>
45
47
 
46
48
  <div v-if="tip || $slots.tip" class="xt-step-price__tip">
@@ -59,6 +61,12 @@ export default {
59
61
 
60
62
  components: { XtStepPriceItem },
61
63
 
64
+ inject: {
65
+ elFormItem: {
66
+ default: ''
67
+ }
68
+ },
69
+
62
70
  computed: {
63
71
  keyMin() { return (this.fieldKeys && this.fieldKeys.min) || 'min' },
64
72
  keyMax() { return (this.fieldKeys && this.fieldKeys.max) || 'max' },
@@ -76,6 +84,7 @@ export default {
76
84
  },
77
85
  title: { type: String, default: '' },
78
86
  unit: { type: String, default: '元' },
87
+ stepName: { type: String, default: '阶梯' },
79
88
  precision: { type: Number, default: 2 },
80
89
  // 左括号:默认 '[',传空字符串则不显示
81
90
  leftBracket: { type: String, default: '[' },
@@ -125,11 +134,14 @@ export default {
125
134
 
126
135
  cloneItems(items) {
127
136
  if (!Array.isArray(items)) return []
128
- return items.map((it) => ({
129
- [this.keyMin]: this.safeNumber(it && it[this.keyMin], 0),
130
- [this.keyMax]: (it && it[this.keyMax] == null) || (it && it[this.keyMax] === '') ? null : this.safeNumber(it[this.keyMax], null),
131
- [this.keyPrice]: this.safeNumber(it && it[this.keyPrice], 0)
132
- }))
137
+ return items.map((it) => {
138
+ const price = (it && it[this.keyPrice])
139
+ return {
140
+ [this.keyMin]: this.safeNumber(it && it[this.keyMin], 0),
141
+ [this.keyMax]: (it && it[this.keyMax] == null) || (it && it[this.keyMax] === '') ? null : this.safeNumber(it[this.keyMax], null),
142
+ [this.keyPrice]: (price === null || price === undefined || price === '') ? '' : this.safeNumber(price, 0)
143
+ }
144
+ })
133
145
  },
134
146
 
135
147
  normalize(items) {
@@ -160,7 +172,8 @@ export default {
160
172
  } else {
161
173
  cur[this.keyMax] = null
162
174
  }
163
- cur[this.keyPrice] = this.safeNumber(cur[this.keyPrice], 0)
175
+ const price = cur[this.keyPrice]
176
+ cur[this.keyPrice] = (price === null || price === undefined || price === '') ? '' : this.safeNumber(price, 0)
164
177
  }
165
178
  },
166
179
 
@@ -169,15 +182,118 @@ export default {
169
182
  const cloned = this.cloneItems(this.localItems)
170
183
  this.$emit('input', cloned)
171
184
  this.$emit('change', cloned)
185
+ this.dispatchFormEvent('el.form.change', cloned)
186
+ },
187
+
188
+ dispatchFormEvent(eventName, value) {
189
+ if (this.elFormItem) {
190
+ this.elFormItem.$emit(eventName, value)
191
+ }
192
+ },
193
+
194
+ onFieldBlur() {
195
+ this.dispatchFormEvent('el.form.blur', this.localItems)
196
+ },
197
+
198
+ validate(callback) {
199
+ const list = this.localItems
200
+ if (!Array.isArray(list) || list.length === 0) {
201
+ callback && callback(false, [{ field: 'stepPrice', message: `请配置${this.stepName}`, type: 'error' }])
202
+ return false
203
+ }
204
+
205
+ const errors = []
206
+ for (let i = 0; i < list.length; i++) {
207
+ const item = list[i]
208
+ const rawPrice = item[this.keyPrice]
209
+ const rawMin = item[this.keyMin]
210
+ const rawMax = item[this.keyMax]
211
+ const price = this.safeNumber(rawPrice, 0)
212
+ const min = this.safeNumber(rawMin, 0)
213
+
214
+ if (rawPrice === null || rawPrice === undefined || rawPrice === '') {
215
+ errors.push({
216
+ field: `stepPrice[${i}].price`,
217
+ message: `第${i + 1}${this.stepName}价格不能为空`,
218
+ type: 'error'
219
+ })
220
+ } else if (price < 0) {
221
+ errors.push({
222
+ field: `stepPrice[${i}].price`,
223
+ message: `第${i + 1}${this.stepName}价格不能为负数`,
224
+ type: 'error'
225
+ })
226
+ }
227
+
228
+ if (rawMin === null || rawMin === undefined || rawMin === '') {
229
+ errors.push({
230
+ field: `stepPrice[${i}].min`,
231
+ message: `第${i + 1}${this.stepName}下限不能为空`,
232
+ type: 'error'
233
+ })
234
+ } else if (min < 0) {
235
+ errors.push({
236
+ field: `stepPrice[${i}].min`,
237
+ message: `第${i + 1}${this.stepName}下限不能为负数`,
238
+ type: 'error'
239
+ })
240
+ }
241
+
242
+ if (!this.isLast(i)) {
243
+ if (rawMax === null || rawMax === undefined || rawMax === '') {
244
+ errors.push({
245
+ field: `stepPrice[${i}].max`,
246
+ message: `第${i + 1}${this.stepName}上限不能为空`,
247
+ type: 'error'
248
+ })
249
+ } else if (this.safeNumber(rawMax, 0) <= min) {
250
+ errors.push({
251
+ field: `stepPrice[${i}].max`,
252
+ message: `第${i + 1}${this.stepName}上限必须大于下限`,
253
+ type: 'error'
254
+ })
255
+ }
256
+ }
257
+
258
+ if (i > 0) {
259
+ const prev = list[i - 1]
260
+ const prevMax = prev[this.keyMax]
261
+ if (prevMax !== null && this.safeNumber(prevMax, 0) !== min) {
262
+ errors.push({
263
+ field: `stepPrice[${i}].min`,
264
+ message: `第${i + 1}${this.stepName}下限必须等于上一${this.stepName}上限`,
265
+ type: 'error'
266
+ })
267
+ }
268
+ }
269
+ }
270
+
271
+ if (errors.length > 0) {
272
+ callback && callback(false, errors)
273
+ return false
274
+ }
275
+
276
+ callback && callback(true)
277
+ return true
278
+ },
279
+
280
+ isLast(idx) {
281
+ return idx === this.localItems.length - 1
172
282
  },
173
283
 
174
284
  onItemInput(val, idx) {
175
285
  const cur = this.localItems[idx]
176
286
  if (!cur) return
177
287
  if (val && val[this.keyPrice] !== undefined) {
178
- let p = this.safeNumber(val[this.keyPrice], 0)
179
- p = Number(p.toFixed(this.precision))
180
- cur[this.keyPrice] = p
288
+ const p = val[this.keyPrice]
289
+ if (p === null || p === undefined || p === '') {
290
+ cur[this.keyPrice] = ''
291
+ } else {
292
+ let num = this.safeNumber(p, 0)
293
+ num = Number(num.toFixed(this.precision))
294
+ cur[this.keyPrice] = num
295
+ }
296
+ this.emit()
181
297
  }
182
298
  },
183
299
 
@@ -232,15 +348,8 @@ export default {
232
348
  const newMin = this.safeNumber(last[this.keyMin], 0)
233
349
  const newMax = newMin + stepVal
234
350
 
235
- // 新条 price:优先继承「倒数第二条」的 price,其次用末条 price(>0 时),否则默认 10
236
- const prev = list[list.length - 2]
237
- let newPrice = 10
238
- if (prev) {
239
- newPrice = this.safeNumber(prev[this.keyPrice], 10)
240
- } else {
241
- const lastPrice = this.safeNumber(last[this.keyPrice], 0)
242
- newPrice = lastPrice > 0 ? lastPrice : 10
243
- }
351
+ // 新条 price:使用默认值,由用户自行填写
352
+ let newPrice = 0
244
353
 
245
354
  const newArr = [
246
355
  ...list.slice(0, -1),