xt-element-ui 1.2.6 → 1.2.8

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": "1.2.6",
3
+ "version": "1.2.8",
4
4
  "description": "基于 Vue 2.7 + ElementUI 的企业级组件库,提供丰富的自定义组件和扩展组件",
5
5
  "main": "lib/index.common.js",
6
6
  "module": "lib/index.esm.js",
@@ -59,10 +59,10 @@
59
59
  "bugs": {
60
60
  "url": "https://github.com/JobWebNie/xt-element-ui/issues"
61
61
  },
62
- "homepage": "https://JobWebNie.github.io/xt-element-ui/",
63
- "repository": {
62
+ "homepage": "https://jobwebnie.github.io/xt-element-ui/",
63
+ "repository": {
64
64
  "type": "git",
65
- "url": "git+https://github.com/JobWebNie/xt-element-ui.git"
65
+ "url": "https://github.com/JobWebNie/xt-element-ui.git"
66
66
  },
67
67
  "engines": {
68
68
  "node": ">=12.0.0",
@@ -42,7 +42,7 @@ export default {
42
42
  color: "#ffffff"
43
43
  },
44
44
  itemStyle: {
45
- borderColor: variables.xtColorBorder
45
+ borderColor: "#dcdfe6"
46
46
  }
47
47
  },
48
48
  gauge: {
@@ -83,11 +83,12 @@ export default {
83
83
  }
84
84
  },
85
85
  tooltip: {
86
- backgroundColor: variables.xtColorBgOverlay,
87
- borderColor: variables.xtColorBorder,
86
+ backgroundColor: "#333333",
88
87
  borderWidth: 1,
88
+ borderColor: "#dcdfe6",
89
89
  textStyle: {
90
90
  color: "#fff"
91
91
  }
92
- }
92
+ },
93
+ trigger: "axis"
93
94
  };
@@ -102,6 +102,7 @@ export default {
102
102
  tooltip: {
103
103
  borderWidth: 1,
104
104
  backgroundColor: "#ffffff",
105
+ borderColor: "#dcdfe6",
105
106
  textStyle: {
106
107
  color: "#333333"
107
108
  },
@@ -0,0 +1,2 @@
1
+ import ExDatePicker from './index.vue'
2
+ export default ExDatePicker
@@ -20,7 +20,7 @@ const typeFormatEnum = {
20
20
  import FlexBox from '../xt-flex-box/index.vue'
21
21
  import Quarter from "./quarter.vue";
22
22
  export default {
23
- name: "XtDatePicker",
23
+ name: "ExDatePicker",
24
24
  components: {
25
25
  FlexBox,
26
26
  Quarter
@@ -21,6 +21,12 @@
21
21
  // Text 组件样式
22
22
  @import './xt-text/style/index.scss';
23
23
 
24
+ // StepPrice 组件样式
25
+ @import './xt-step-price/style/index.scss';
26
+
27
+ // Time 组件样式
28
+ @import './xt-time/style/index.scss';
29
+
24
30
  // ExButton 组件样式
25
31
  @import './ex-button/style/index.scss';
26
32
 
@@ -10,7 +10,9 @@
10
10
  </div>
11
11
  </div>
12
12
  <div style="flex: 1; height: 100%;">
13
- <xt-text size="extra-large" :type="type"><i class="el-icon-user"></i></xt-text>
13
+ <slot name="icon">
14
+ <xt-text size="extra-large" :type="type"><i class="el-icon-user"></i></xt-text>
15
+ </slot>
14
16
  </div>
15
17
  </xt-flex-box>
16
18
  </ex-card>
@@ -0,0 +1,2 @@
1
+ import XtStepPrice from './index.vue'
2
+ export default XtStepPrice
@@ -0,0 +1,270 @@
1
+ <template>
2
+ <div class="xt-step-price" :class="{ 'is-disabled': disabled }">
3
+ <div v-if="title || $slots.header" class="xt-step-price__header">
4
+ <xt-text v-if="title" bold size="medium">{{ title }}</xt-text>
5
+ <slot name="header" />
6
+ <el-button
7
+ v-if="!disabled && !isLimitReached"
8
+ type="primary"
9
+ size="small"
10
+ icon="el-icon-plus"
11
+ plain
12
+ @click="onAdd"
13
+ >新增阶梯</el-button>
14
+ <xt-text v-if="isLimitReached" size="small" type-color="info">已达上限({{ localItems.length }}/{{ limit }})</xt-text>
15
+ </div>
16
+
17
+ <div class="xt-step-price__list">
18
+ <XtStepPriceItem
19
+ v-for="(item, idx) in localItems"
20
+ :key="idx"
21
+ :value="item"
22
+ :index="idx"
23
+ :is-first="idx === 0"
24
+ :is-last="idx === localItems.length - 1"
25
+ :items-length="localItems.length"
26
+ :removable="idx !== 0"
27
+ :min-locked="idx !== 0 ? true : false"
28
+ :unit="unit"
29
+ :precision="precision"
30
+ :left-bracket="leftBracket"
31
+ :right-bracket="rightBracket"
32
+ :field-keys="fieldKeys"
33
+ :disabled="disabled"
34
+ @input="(val) => onItemInput(val, idx)"
35
+ @max-change="onMaxChange"
36
+ @min-change="onMinChange"
37
+ @delete="onDelete"
38
+ />
39
+ </div>
40
+
41
+ <div v-if="localItems.length === 0" class="xt-step-price__empty">
42
+ <span>暂无数据,点击右上角「新增阶梯」开始配置</span>
43
+ </div>
44
+
45
+ <div v-if="tip || $slots.tip" class="xt-step-price__tip">
46
+ <slot name="tip">
47
+ <xt-text size="small" type-color="warning">{{ tip }}</xt-text>
48
+ </slot>
49
+ </div>
50
+ </div>
51
+ </template>
52
+
53
+ <script>
54
+ import XtStepPriceItem from '../xt-step-price-item/index.vue'
55
+
56
+ export default {
57
+ name: 'XtStepPrice',
58
+
59
+ components: { XtStepPriceItem },
60
+
61
+ computed: {
62
+ keyMin() { return (this.fieldKeys && this.fieldKeys.min) || 'min' },
63
+ keyMax() { return (this.fieldKeys && this.fieldKeys.max) || 'max' },
64
+ keyPrice() { return (this.fieldKeys && this.fieldKeys.price) || 'price' },
65
+ isLimitReached() {
66
+ const lim = Number(this.limit)
67
+ return lim > 0 && this.localItems.length >= lim
68
+ }
69
+ },
70
+
71
+ props: {
72
+ value: {
73
+ type: Array,
74
+ default: () => []
75
+ },
76
+ title: { type: String, default: '' },
77
+ unit: { type: String, default: '元' },
78
+ precision: { type: Number, default: 2 },
79
+ // 左括号:默认 '[',传空字符串则不显示
80
+ leftBracket: { type: String, default: '[' },
81
+ // 右括号:默认 null,走内置逻辑(只有1条为 ']',多条为 ')');传具体值则强制使用
82
+ rightBracket: { type: String, default: null },
83
+ // 字段名映射:{ min, max, price },允许传入的 value 使用自定义字段名
84
+ fieldKeys: {
85
+ type: Object,
86
+ default: () => ({ min: 'min', max: 'max', price: 'price' })
87
+ },
88
+ // 阶梯数量上限;<= 0 表示不限制
89
+ limit: { type: Number, default: 0 },
90
+ disabled: { type: Boolean, default: false },
91
+ tip: {
92
+ type: String,
93
+ default: '区间左闭右闭 [min, max],最后一级为 [min, +∞),保证连续且不重叠。'
94
+ },
95
+ // 默认初始值:当传入空数组时,是否自动生成首条默认阶梯 [0, +∞)
96
+ defaultFirst: { type: Boolean, default: true }
97
+ },
98
+
99
+ data() {
100
+ return {
101
+ localItems: this.normalize(this.value)
102
+ }
103
+ },
104
+
105
+ watch: {
106
+ value: {
107
+ deep: true,
108
+ immediate: true,
109
+ handler(val) {
110
+ this.localItems = this.normalize(val)
111
+ }
112
+ }
113
+ },
114
+
115
+ methods: {
116
+ // 统一数字转换:空值/非法值一律转为 fallback(默认 0)
117
+ safeNumber(v, fallback = 0) {
118
+ if (v === null || v === undefined || v === '' || v === Infinity || v === -Infinity) return fallback
119
+ const n = Number(v)
120
+ return isNaN(n) ? fallback : n
121
+ },
122
+
123
+ cloneItems(items) {
124
+ if (!Array.isArray(items)) return []
125
+ return items.map((it) => ({
126
+ [this.keyMin]: this.safeNumber(it && it[this.keyMin], 0),
127
+ [this.keyMax]: (it && it[this.keyMax] == null) || (it && it[this.keyMax] === '') ? null : this.safeNumber(it[this.keyMax], null),
128
+ [this.keyPrice]: this.safeNumber(it && it[this.keyPrice], 0)
129
+ }))
130
+ },
131
+
132
+ normalize(items) {
133
+ const list = this.cloneItems(items)
134
+ if (list.length === 0 && this.defaultFirst) {
135
+ list.push({ [this.keyMin]: 0, [this.keyMax]: null, [this.keyPrice]: 0 })
136
+ }
137
+ this.ensureContinuity(list)
138
+ return list
139
+ },
140
+
141
+ // 保证 items 连续且不重叠:items[i].max === items[i+1].min,首条 min === 0,末条 max === null
142
+ ensureContinuity(list) {
143
+ if (!Array.isArray(list) || list.length === 0) return
144
+ list[0][this.keyMin] = 0
145
+
146
+ for (let i = 0; i < list.length; i++) {
147
+ const cur = list[i]
148
+ const next = list[i + 1]
149
+ const curMin = this.safeNumber(cur[this.keyMin], 0)
150
+ if (next) {
151
+ let curMax = this.safeNumber(cur[this.keyMax], curMin + 1)
152
+ if (curMax <= curMin) curMax = curMin + 1
153
+ cur[this.keyMax] = curMax
154
+ next[this.keyMin] = curMax
155
+ } else {
156
+ cur[this.keyMax] = null
157
+ }
158
+ cur[this.keyPrice] = this.safeNumber(cur[this.keyPrice], 0)
159
+ }
160
+ },
161
+
162
+ emit() {
163
+ this.ensureContinuity(this.localItems)
164
+ const cloned = this.cloneItems(this.localItems)
165
+ this.$emit('input', cloned)
166
+ this.$emit('change', cloned)
167
+ },
168
+
169
+ onItemInput(val, idx) {
170
+ const cur = this.localItems[idx]
171
+ if (!cur) return
172
+ if (val && val[this.keyPrice] !== undefined) cur[this.keyPrice] = this.safeNumber(val[this.keyPrice], 0)
173
+ },
174
+
175
+ onMaxChange(val, idx) {
176
+ const cur = this.localItems[idx]
177
+ if (!cur) return
178
+ const n = this.safeNumber(val, this.safeNumber(cur[this.keyMin], 0) + 1)
179
+ cur[this.keyMax] = n
180
+ const next = this.localItems[idx + 1]
181
+ if (next) next[this.keyMin] = n
182
+ this.emit()
183
+ },
184
+
185
+ onMinChange(val, idx) {
186
+ const cur = this.localItems[idx]
187
+ if (!cur) return
188
+ if (idx === 0) {
189
+ cur[this.keyMin] = 0
190
+ } else {
191
+ const n = this.safeNumber(val, this.safeNumber(cur[this.keyMin], 0))
192
+ cur[this.keyMin] = n
193
+ const prev = this.localItems[idx - 1]
194
+ if (prev) prev[this.keyMax] = n
195
+ }
196
+ this.emit()
197
+ },
198
+
199
+ onAdd() {
200
+ const list = this.localItems
201
+ const lim = Number(this.limit)
202
+ if (lim > 0 && list.length >= lim) return
203
+
204
+ // 场景 1:空数组 —— 直接 push 一条 [0, +∞)
205
+ if (list.length === 0) {
206
+ list.push({
207
+ [this.keyMin]: 0,
208
+ [this.keyMax]: null,
209
+ [this.keyPrice]: 10
210
+ })
211
+ this.emit()
212
+ return
213
+ }
214
+
215
+ // 场景 2:已有数据 —— 在末条前插入新条
216
+ const last = list[list.length - 1]
217
+ const newMin = this.safeNumber(last[this.keyMin], 0)
218
+ const newMax = newMin + 1
219
+
220
+ // 新条 price:优先继承「倒数第二条」的 price,其次用末条 price(>0 时),否则默认 10
221
+ const prev = list[list.length - 2]
222
+ let newPrice = 10
223
+ if (prev) {
224
+ newPrice = this.safeNumber(prev[this.keyPrice], 10)
225
+ } else {
226
+ const lastPrice = this.safeNumber(last[this.keyPrice], 0)
227
+ newPrice = lastPrice > 0 ? lastPrice : 10
228
+ }
229
+
230
+ // 关键修复:构造全新数组,避免 splice 导致 Vue 组件复用 & 响应式不同步
231
+ // 新条 = { newMin, newMax, newPrice }
232
+ // 新末条 = { min: newMax, max: null, price: newPrice }
233
+ const newArr = [
234
+ ...list.slice(0, -1),
235
+ {
236
+ [this.keyMin]: newMin,
237
+ [this.keyMax]: newMax,
238
+ [this.keyPrice]: newPrice
239
+ },
240
+ {
241
+ [this.keyMin]: newMax,
242
+ [this.keyMax]: null,
243
+ [this.keyPrice]: newPrice
244
+ }
245
+ ]
246
+
247
+ this.localItems = newArr
248
+ this.emit()
249
+ },
250
+
251
+ onDelete(idx) {
252
+ const list = this.localItems
253
+ if (list.length <= 1) return // 至少保留一条
254
+ if (idx === 0) return // 首条不允许删除,保证区间起点始终为 0
255
+ const cur = list[idx]
256
+ const prev = list[idx - 1]
257
+ const next = list[idx + 1]
258
+ if (prev && next) {
259
+ prev[this.keyMax] = cur[this.keyMax] == null ? null : Number(cur[this.keyMax])
260
+ } else if (prev && !next) {
261
+ prev[this.keyMax] = null
262
+ } else if (!prev && next) {
263
+ next[this.keyMin] = 0
264
+ }
265
+ list.splice(idx, 1)
266
+ this.emit()
267
+ }
268
+ }
269
+ }
270
+ </script>
@@ -0,0 +1,115 @@
1
+ .xt-step-price {
2
+ padding: 12px 16px;
3
+ background-color: #ffffff;
4
+ border: 1px solid #ebeef5;
5
+ border-radius: 4px;
6
+
7
+ &__header {
8
+ display: flex;
9
+ align-items: center;
10
+ justify-content: space-between;
11
+ margin-bottom: 12px;
12
+ }
13
+
14
+ &__list {
15
+ display: flex;
16
+ flex-direction: column;
17
+ gap: 8px;
18
+ }
19
+
20
+ &__empty {
21
+ padding: 24px;
22
+ text-align: center;
23
+ color: #909399;
24
+ font-size: 13px;
25
+ }
26
+
27
+ &__tip {
28
+ margin-top: 12px;
29
+ color: #909399;
30
+ font-size: 12px;
31
+ line-height: 1.6;
32
+ }
33
+
34
+ &.is-disabled {
35
+ background-color: #fafafa;
36
+ opacity: 0.85;
37
+ pointer-events: none;
38
+ }
39
+ }
40
+
41
+ .xt-step-price-item {
42
+ display: flex;
43
+ align-items: center;
44
+ padding: 8px 12px;
45
+ background-color: #f5f7fa;
46
+ border-radius: 4px;
47
+ transition: background-color 0.2s ease;
48
+
49
+ &:hover {
50
+ background-color: #ecf0f4;
51
+ }
52
+
53
+ &__range {
54
+ display: inline-flex;
55
+ align-items: center;
56
+ flex: 0 0 auto;
57
+ color: #606266;
58
+ font-size: 13px;
59
+ }
60
+
61
+ &__bracket {
62
+ margin: 0 4px;
63
+ color: #909399;
64
+ font-weight: 600;
65
+ }
66
+
67
+ &__comma {
68
+ margin: 0 6px;
69
+ color: #909399;
70
+ }
71
+
72
+ &__infinity {
73
+ display: inline-block;
74
+ min-width: 60px;
75
+ text-align: center;
76
+ color: #c0c4cc;
77
+ font-size: 13px;
78
+ }
79
+
80
+ &__arrow {
81
+ margin: 0 12px;
82
+ color: #409eff;
83
+ font-weight: 600;
84
+ }
85
+
86
+ &__price {
87
+ display: inline-flex;
88
+ align-items: center;
89
+ flex: 1;
90
+ }
91
+
92
+ &__unit {
93
+ margin-left: 8px;
94
+ color: #606266;
95
+ font-size: 13px;
96
+ }
97
+
98
+ &__input {
99
+ width: 80px;
100
+ text-align: center;
101
+
102
+ &--price {
103
+ width: 140px;
104
+ }
105
+ }
106
+
107
+ &__delete {
108
+ margin-left: 12px;
109
+ color: #f56c6c !important;
110
+ }
111
+
112
+ &__delete:hover {
113
+ color: #f78989 !important;
114
+ }
115
+ }
@@ -0,0 +1,2 @@
1
+ import XtStepPriceItem from './index.vue'
2
+ export default XtStepPriceItem
@@ -0,0 +1,183 @@
1
+ <template>
2
+ <div class="xt-step-price-item">
3
+ <div class="xt-step-price-item__range">
4
+ <span class="xt-step-price-item__bracket">{{ finalLeftBracket }}</span>
5
+ <el-input
6
+ v-model.number="minInput"
7
+ :disabled="disabled || minLocked"
8
+ size="small"
9
+ class="xt-step-price-item__input"
10
+ @blur="onMinBlur"
11
+ />
12
+ <span class="xt-step-price-item__comma">,</span>
13
+ <el-input
14
+ v-if="!isLast"
15
+ v-model.number="maxInput"
16
+ :disabled="disabled"
17
+ size="small"
18
+ class="xt-step-price-item__input"
19
+ @blur="onMaxBlur"
20
+ />
21
+ <span v-else class="xt-step-price-item__infinity">+∞</span>
22
+ <span class="xt-step-price-item__bracket">{{ finalRightBracket }}</span>
23
+ </div>
24
+
25
+ <span class="xt-step-price-item__arrow">→</span>
26
+
27
+ <div class="xt-step-price-item__price">
28
+ <el-input
29
+ v-model.number="priceInput"
30
+ :disabled="disabled"
31
+ size="small"
32
+ placeholder="价格"
33
+ class="xt-step-price-item__input xt-step-price-item__input--price"
34
+ @blur="onPriceBlur"
35
+ />
36
+ <span class="xt-step-price-item__unit">{{ unit }}</span>
37
+ </div>
38
+
39
+ <el-button
40
+ v-if="!disabled && removable && itemsLength > 1"
41
+ type="text"
42
+ icon="el-icon-delete"
43
+ class="xt-step-price-item__delete"
44
+ @click="onDelete"
45
+ />
46
+ </div>
47
+ </template>
48
+
49
+ <script>
50
+ export default {
51
+ name: 'XtStepPriceItem',
52
+
53
+ props: {
54
+ value: {
55
+ type: Object,
56
+ required: true,
57
+ default: () => ({ min: 0, max: null, price: 0 })
58
+ },
59
+ index: { type: Number, default: 0 },
60
+ isFirst: { type: Boolean, default: false },
61
+ isLast: { type: Boolean, default: false },
62
+ itemsLength: { type: Number, default: 1 },
63
+ removable: { type: Boolean, default: true },
64
+ disabled: { type: Boolean, default: false },
65
+ minLocked: { type: Boolean, default: false },
66
+ unit: { type: String, default: '元' },
67
+ precision: { type: Number, default: 2 },
68
+ // 左括号:默认 '[',传空则不显示
69
+ leftBracket: { type: String, default: '[' },
70
+ // 右括号:默认 null,走内置规则(只有1条为 ']',多条为 ')');传值则强制使用
71
+ rightBracket: { type: String, default: null },
72
+ // 字段名映射:{ min, max, price },允许传入的 value 使用自定义字段名
73
+ fieldKeys: {
74
+ type: Object,
75
+ default: () => ({ min: 'min', max: 'max', price: 'price' })
76
+ }
77
+ },
78
+
79
+ computed: {
80
+ keyMin() { return (this.fieldKeys && this.fieldKeys.min) || 'min' },
81
+ keyMax() { return (this.fieldKeys && this.fieldKeys.max) || 'max' },
82
+ keyPrice() { return (this.fieldKeys && this.fieldKeys.price) || 'price' },
83
+ finalRightBracket() {
84
+ if (this.rightBracket !== null && this.rightBracket !== undefined && this.rightBracket !== '') return this.rightBracket
85
+ return this.itemsLength === 1 ? ']' : ')'
86
+ },
87
+ finalLeftBracket() {
88
+ return (this.leftBracket === null || this.leftBracket === undefined) ? '[' : this.leftBracket
89
+ }
90
+ },
91
+
92
+ data() {
93
+ const { minVal, maxVal, priceVal } = this.getPriceItem(this.value)
94
+ return {
95
+ minInput: minVal,
96
+ maxInput: maxVal,
97
+ priceInput: priceVal
98
+ }
99
+ },
100
+
101
+ watch: {
102
+ value: {
103
+ deep: true,
104
+ immediate: true,
105
+ handler(val) {
106
+ const { minVal, maxVal, priceVal } = this.getPriceItem(val)
107
+ this.minInput = minVal
108
+ this.maxInput = maxVal
109
+ this.priceInput = priceVal
110
+ }
111
+ },
112
+ isLast(val) {
113
+ if (val) {
114
+ this.maxInput = null
115
+ } else {
116
+ const minVal = this.safeNumber(this.minInput, 0)
117
+ const v = this.value
118
+ this.maxInput = this.safeNumber(v && v[this.keyMax], minVal + 1)
119
+ }
120
+ }
121
+ },
122
+
123
+ methods: {
124
+ getPriceItem(value) {
125
+ const v = value
126
+ const minVal = this.safeNumber(v && v[this.keyMin], 0)
127
+ const maxVal = this.isLast ? null : this.safeNumber(v && v[this.keyMax], minVal + 1)
128
+ const priceVal = this.safeNumber(v && v[this.keyPrice], 0)
129
+ return {
130
+ minVal,
131
+ maxVal,
132
+ priceVal
133
+ }
134
+ },
135
+ // 统一兜底:非数字输入一律转为 fallback(默认 0)
136
+ safeNumber(v, fallback = 0) {
137
+ if (v === null || v === undefined || v === '' || v === Infinity || v === -Infinity) return fallback
138
+ const n = Number(v)
139
+ return isNaN(n) ? fallback : n
140
+ },
141
+
142
+ emitChange(partial) {
143
+ const next = {
144
+ [this.keyMin]: this.minInput,
145
+ [this.keyMax]: this.isLast ? null : this.maxInput,
146
+ [this.keyPrice]: this.priceInput,
147
+ ...partial
148
+ }
149
+ this.$emit('input', next)
150
+ this.$emit('change', next, this.index)
151
+ },
152
+
153
+ onMinBlur() {
154
+ const v = this.safeNumber(this.minInput, 0)
155
+ this.minInput = v
156
+ this.$emit('min-change', v, this.index)
157
+ this.emitChange({ [this.keyMin]: v })
158
+ },
159
+
160
+ onMaxBlur() {
161
+ if (this.isLast) return
162
+ const minVal = this.safeNumber(this.minInput, 0)
163
+ let v = this.safeNumber(this.maxInput, minVal + 1)
164
+ if (v <= minVal) v = minVal + 1
165
+ this.maxInput = v
166
+ this.$emit('max-change', v, this.index)
167
+ this.emitChange({ [this.keyMax]: v })
168
+ },
169
+
170
+ onPriceBlur() {
171
+ let v = this.safeNumber(this.priceInput, 0)
172
+ if (v < 0) v = 0
173
+ v = Number(v.toFixed(this.precision))
174
+ this.priceInput = v
175
+ this.emitChange({ [this.keyPrice]: v })
176
+ },
177
+
178
+ onDelete() {
179
+ this.$emit('delete', this.index)
180
+ }
181
+ }
182
+ }
183
+ </script>
@@ -0,0 +1,2 @@
1
+ import XtTime from './index.vue'
2
+ export default XtTime