vue2server7 7.0.7 → 7.0.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.
@@ -0,0 +1,354 @@
1
+ <!--
2
+ NumberRange 数字区间输入组件
3
+ 用于表单中输入数值范围(如金额区间、年龄区间等)
4
+
5
+ ═══════════════════════════════════════════════════
6
+ 两种绑定模式
7
+ ═══════════════════════════════════════════════════
8
+
9
+ 1. 数组模式 —— v-model 绑定 [min, max] 数组
10
+ <NumberRange v-model="form.amountRange" />
11
+
12
+ 2. 双字段模式 —— v-model:start / v-model:end 分别绑定
13
+ <NumberRange v-model:start="form.amountMin" v-model:end="form.amountMax" />
14
+
15
+ ═══════════════════════════════════════════════════
16
+ Props
17
+ ═══════════════════════════════════════════════════
18
+
19
+ | Prop | 类型 | 默认值 | 说明 |
20
+ |────────────────|─────────|─────────────|───────────────────────────────|
21
+ | modelValue | Array | undefined | 数组模式绑定值 [min, max] |
22
+ | start | Number | undefined | 双字段模式起始值 |
23
+ | end | Number | undefined | 双字段模式结束值 |
24
+ | minPlaceholder | String | '最小值' | 起始输入框占位文本 |
25
+ | maxPlaceholder | String | '最大值' | 结束输入框占位文本 |
26
+ | separator | String | '到' | 两个输入框之间的分隔文字 |
27
+ | min | Number | -Infinity | 允许输入的最小边界 |
28
+ | max | Number | Infinity | 允许输入的最大边界 |
29
+ | precision | Number | undefined | 小数位数限制(不传则不限制) |
30
+ | disabled | Boolean | false | 是否禁用 |
31
+
32
+ ═══════════════════════════════════════════════════
33
+ Events
34
+ ═══════════════════════════════════════════════════
35
+
36
+ | Event | 参数 | 说明 |
37
+ |────────────────────|─────────────────|───────────────────────────────|
38
+ | update:modelValue | [min, max] | 数组模式值变更 |
39
+ | update:start | number | null | 双字段模式起始值变更 |
40
+ | update:end | number | null | 双字段模式结束值变更 |
41
+ | change | [min, max] | 值变更(两种模式都触发) |
42
+
43
+ ═══════════════════════════════════════════════════
44
+ Expose(通过 ref 调用)
45
+ ═══════════════════════════════════════════════════
46
+
47
+ | 属性/方法 | 类型 | 说明 |
48
+ |───────────|───────────────────|───────────────────────|
49
+ | validate | () => boolean | 手动触发校验 |
50
+ | isValid | ComputedRef<bool> | 当前区间是否合法 |
51
+
52
+ ═══════════════════════════════════════════════════
53
+ 配套校验规则(配合 el-form 使用)
54
+ ═══════════════════════════════════════════════════
55
+
56
+ import { rangeRule, rangeRequired } from '@/components/NumberRange.vue'
57
+
58
+ - rangeRequired(message?, startField?, trigger?)
59
+ 区间必填校验
60
+ 数组模式: rangeRequired('请输入金额范围')
61
+ 双字段模式: rangeRequired('请输入年龄范围', 'ageMin')
62
+
63
+ - rangeRule(message?, startField?, trigger?)
64
+ 区间大小校验(结束值 >= 起始值)
65
+ 数组模式: rangeRule('结束金额不能小于起始金额')
66
+ 双字段模式: rangeRule('结束年龄不能小于起始年龄', 'ageMin')
67
+
68
+ ═══════════════════════════════════════════════════
69
+ 完整示例
70
+ ═══════════════════════════════════════════════════
71
+
72
+ <!-- 数组模式 -->
73
+ <el-form-item prop="amountRange" label="金额范围">
74
+ <NumberRange v-model="form.amountRange" :precision="2"
75
+ min-placeholder="最小金额" max-placeholder="最大金额" />
76
+ </el-form-item>
77
+ rules: { amountRange: [rangeRule('结束金额不能小于起始金额')] }
78
+
79
+ <!-- 双字段模式 -->
80
+ <el-form-item prop="ageMax" label="年龄范围" required>
81
+ <NumberRange v-model:start="form.ageMin" v-model:end="form.ageMax"
82
+ :precision="0" min-placeholder="最小年龄" max-placeholder="最大年龄" />
83
+ </el-form-item>
84
+ rules: { ageMax: [rangeRequired('请输入年龄范围', 'ageMin'),
85
+ rangeRule('结束年龄不能小于起始年龄', 'ageMin')] }
86
+ -->
87
+
88
+ <template>
89
+ <div class="number-range" :class="{ 'is-disabled': disabled }">
90
+ <el-input
91
+ v-model="minDisplay"
92
+ class="number-range__input"
93
+ :placeholder="minPlaceholder"
94
+ :disabled="disabled"
95
+ clearable
96
+ @input="onMinInput"
97
+ @focus="onMinFocus"
98
+ @blur="onMinBlur"
99
+ @clear="onMinClear"
100
+ />
101
+ <span class="number-range__separator">{{ separator }}</span>
102
+ <el-input
103
+ v-model="maxDisplay"
104
+ class="number-range__input"
105
+ :placeholder="maxPlaceholder"
106
+ :disabled="disabled"
107
+ clearable
108
+ @input="onMaxInput"
109
+ @focus="onMaxFocus"
110
+ @blur="onMaxBlur"
111
+ @clear="onMaxClear"
112
+ />
113
+ </div>
114
+ </template>
115
+
116
+ <script>
117
+ /**
118
+ * 区间必填校验规则,用于 el-form rules
119
+ * @param {string} message - 校验失败提示文案
120
+ * @param {string} startField - 双字段模式下起始字段名;不传则为数组模式
121
+ * @param {string} trigger - 触发方式,默认 'change'
122
+ */
123
+ export function rangeRequired(message = '请输入完整区间', startField, trigger = 'change') {
124
+ return {
125
+ trigger,
126
+ validator: startField
127
+ ? (_r, _v, cb, source) => cb(source[startField] == null || source[startField] === '' ? new Error(message) : undefined)
128
+ : (_r, v, cb) => cb(!Array.isArray(v) || v[0] == null || v[1] == null ? new Error(message) : undefined)
129
+ }
130
+ }
131
+
132
+ /**
133
+ * 区间大小校验规则(结束值 >= 起始值),用于 el-form rules
134
+ * @param {string} message - 校验失败提示文案
135
+ * @param {string} startField - 双字段模式下起始字段名;不传则为数组模式
136
+ * @param {string} trigger - 触发方式,默认 'change'
137
+ */
138
+ export function rangeRule(message = '结束值不能小于起始值', startField, trigger = 'change') {
139
+ return {
140
+ trigger,
141
+ validator: startField
142
+ ? (_r, v, cb, source) => cb(source[startField] != null && v != null && v < source[startField] ? new Error(message) : undefined)
143
+ : (_r, v, cb) => cb(v?.[0] != null && v?.[1] != null && v[1] < v[0] ? new Error(message) : undefined)
144
+ }
145
+ }
146
+ </script>
147
+
148
+ <script setup>
149
+ import { ref, watch, computed } from 'vue'
150
+
151
+ const props = defineProps({
152
+ /** 数组模式绑定值 [min, max],与 start/end 二选一 */
153
+ modelValue: { type: Array, default: undefined },
154
+ /** 双字段模式 - 起始值 */
155
+ start: { type: Number, default: undefined },
156
+ /** 双字段模式 - 结束值 */
157
+ end: { type: Number, default: undefined },
158
+ minPlaceholder: { type: String, default: '最小值' },
159
+ maxPlaceholder: { type: String, default: '最大值' },
160
+ separator: { type: String, default: '到' },
161
+ /** 允许输入的最小边界 */
162
+ min: { type: Number, default: -Infinity },
163
+ /** 允许输入的最大边界 */
164
+ max: { type: Number, default: Infinity },
165
+ step: { type: Number, default: 1 },
166
+ /** 小数位数限制,如 2 表示最多两位小数 */
167
+ precision: { type: Number, default: undefined },
168
+ disabled: { type: Boolean, default: false }
169
+ })
170
+
171
+ const emit = defineEmits(['update:modelValue', 'update:start', 'update:end', 'change'])
172
+
173
+ /** 根据是否传入 modelValue 自动判断绑定模式 */
174
+ const isArrayMode = computed(() => props.modelValue !== undefined)
175
+
176
+ function getInitMin() {
177
+ return isArrayMode.value ? props.modelValue?.[0] : props.start
178
+ }
179
+ function getInitMax() {
180
+ return isArrayMode.value ? props.modelValue?.[1] : props.end
181
+ }
182
+
183
+ function toDisplay(val) {
184
+ if (val === null || val === undefined) return ''
185
+ return String(val)
186
+ }
187
+
188
+ const minDisplay = ref(toDisplay(getInitMin()))
189
+ const maxDisplay = ref(toDisplay(getInitMax()))
190
+ const minFocused = ref(false)
191
+ const maxFocused = ref(false)
192
+
193
+ /**
194
+ * 输入过滤:只允许数字、小数点、负号;
195
+ * 限制小数点只出现一次,小数位数受 precision 约束
196
+ */
197
+ function sanitize(raw) {
198
+ if (raw === '' || raw === null || raw === undefined) return ''
199
+ let s = raw.replace(/[^0-9.\-]/g, '')
200
+
201
+ const negative = s.startsWith('-')
202
+ s = s.replace(/-/g, '')
203
+ if (negative) s = '-' + s
204
+
205
+ const dotIdx = s.indexOf('.')
206
+ if (dotIdx !== -1) {
207
+ const intPart = s.slice(0, dotIdx)
208
+ let decPart = s.slice(dotIdx + 1).replace(/\./g, '')
209
+ if (props.precision !== undefined) {
210
+ decPart = decPart.slice(0, props.precision)
211
+ }
212
+ s = intPart + '.' + decPart
213
+ }
214
+ return s
215
+ }
216
+
217
+ function parseNum(val) {
218
+ if (val === '' || val === null || val === undefined) return null
219
+ const n = Number(val)
220
+ return Number.isNaN(n) ? null : n
221
+ }
222
+
223
+ /** 按 precision 格式化数值 */
224
+ function formatNum(val) {
225
+ if (val === null || val === undefined) return null
226
+ if (props.precision !== undefined) {
227
+ return Number(Number(val).toFixed(props.precision))
228
+ }
229
+ return Number(val)
230
+ }
231
+
232
+ const rangeInvalid = computed(() => {
233
+ const minNum = parseNum(minDisplay.value)
234
+ const maxNum = parseNum(maxDisplay.value)
235
+ return minNum !== null && maxNum !== null && maxNum < minNum
236
+ })
237
+
238
+ const isValid = computed(() => !rangeInvalid.value)
239
+
240
+ /** 根据绑定模式 emit 对应事件 */
241
+ function emitValue() {
242
+ const minVal = formatNum(parseNum(minDisplay.value))
243
+ const maxVal = formatNum(parseNum(maxDisplay.value))
244
+ if (isArrayMode.value) {
245
+ emit('update:modelValue', [minVal, maxVal])
246
+ } else {
247
+ emit('update:start', minVal)
248
+ emit('update:end', maxVal)
249
+ }
250
+ emit('change', [minVal, maxVal])
251
+ }
252
+
253
+ function onMinInput(val) {
254
+ minDisplay.value = sanitize(val)
255
+ }
256
+
257
+ function onMaxInput(val) {
258
+ maxDisplay.value = sanitize(val)
259
+ }
260
+
261
+ function onMinClear() {
262
+ minDisplay.value = ''
263
+ emitValue()
264
+ }
265
+
266
+ function onMaxClear() {
267
+ maxDisplay.value = ''
268
+ emitValue()
269
+ }
270
+
271
+ function onMinFocus() {
272
+ minFocused.value = true
273
+ }
274
+
275
+ function onMaxFocus() {
276
+ maxFocused.value = true
277
+ }
278
+
279
+ /** 失焦时格式化数值、钳位到 [min, max] 边界,并触发 emit */
280
+ function onMinBlur() {
281
+ minFocused.value = false
282
+ let v = parseNum(minDisplay.value)
283
+ if (v !== null) {
284
+ if (v < props.min) v = props.min
285
+ if (v > props.max) v = props.max
286
+ v = formatNum(v)
287
+ minDisplay.value = String(v)
288
+ } else {
289
+ minDisplay.value = ''
290
+ }
291
+ emitValue()
292
+ }
293
+
294
+ function onMaxBlur() {
295
+ maxFocused.value = false
296
+ let v = parseNum(maxDisplay.value)
297
+ if (v !== null) {
298
+ if (v > props.max) v = props.max
299
+ if (v < props.min) v = props.min
300
+ v = formatNum(v)
301
+ maxDisplay.value = String(v)
302
+ } else {
303
+ maxDisplay.value = ''
304
+ }
305
+ emitValue()
306
+ }
307
+
308
+ function validate() {
309
+ return isValid.value
310
+ }
311
+
312
+ defineExpose({ validate, isValid })
313
+
314
+ /** 外部 v-model 变化时同步内部显示值;重置为空时清除焦点状态 */
315
+ watch(
316
+ () => isArrayMode.value ? props.modelValue : [props.start, props.end],
317
+ (val) => {
318
+ const newMin = toDisplay(val?.[0])
319
+ const newMax = toDisplay(val?.[1])
320
+ if (newMin !== minDisplay.value) minDisplay.value = newMin
321
+ if (newMax !== maxDisplay.value) maxDisplay.value = newMax
322
+ if ((val?.[0] === null || val?.[0] === undefined) &&
323
+ (val?.[1] === null || val?.[1] === undefined)) {
324
+ minFocused.value = false
325
+ maxFocused.value = false
326
+ }
327
+ },
328
+ { deep: true }
329
+ )
330
+ </script>
331
+
332
+ <style scoped>
333
+ .number-range {
334
+ display: inline-flex;
335
+ align-items: center;
336
+ gap: 8px;
337
+ flex-wrap: wrap;
338
+ position: relative;
339
+ }
340
+
341
+ .number-range__input {
342
+ width: 140px;
343
+ }
344
+
345
+ .number-range__separator {
346
+ color: var(--el-text-color-primary);
347
+ font-size: 14px;
348
+ flex-shrink: 0;
349
+ }
350
+
351
+ .number-range.is-disabled .number-range__separator {
352
+ color: var(--el-text-color-placeholder);
353
+ }
354
+ </style>
@@ -1,21 +1,33 @@
1
1
  <template>
2
2
  <section class="page table-page">
3
3
  <h1 class="title">表格页面</h1>
4
- <div class="toolbar">
5
- <el-input v-model="keyword" placeholder="按主题搜索" clearable style="width: 240px" />
6
- <el-select v-model="type" placeholder="类型" clearable style="width: 160px; margin-left: 8px">
7
- <el-option label="培训" value="TRAINING" />
8
- <el-option label="晨夕会" value="MORNING" />
9
- </el-select>
10
- <el-button type="primary" @click="onSearch" style="margin-left: 8px">查询</el-button>
11
- <el-button @click="onReset" style="margin-left: 8px">重置</el-button>
12
- <el-popover placement="bottom" trigger="click" width="260">
13
- <template #reference>
14
- <el-button style="margin-left: 8px">列设置</el-button>
15
- </template>
16
- <ColumnSettings v-model="visibleKeys" :columns="columns" />
17
- </el-popover>
18
- </div>
4
+ <el-form ref="searchFormRef" :model="searchForm" :rules="searchRules" inline class="toolbar">
5
+ <el-form-item prop="keyword">
6
+ <el-input v-model="searchForm.keyword" placeholder="按主题搜索" clearable style="width: 240px" />
7
+ </el-form-item>
8
+ <el-form-item prop="type" label="类型">
9
+ <el-select v-model="searchForm.type" placeholder="类型" clearable style="width: 160px">
10
+ <el-option label="培训" value="TRAINING" />
11
+ <el-option label="晨夕会" value="MORNING" />
12
+ </el-select>
13
+ </el-form-item>
14
+ <el-form-item prop="amountRange" label="金额范围">
15
+ <NumberRange v-model="searchForm.amountRange" min-placeholder="最小金额" max-placeholder="最大金额" :precision="2" />
16
+ </el-form-item>
17
+ <el-form-item prop="ageMax" label="年龄范围" required>
18
+ <NumberRange v-model:start="searchForm.ageMin" v-model:end="searchForm.ageMax" min-placeholder="最小年龄" max-placeholder="最大年龄" :precision="0" />
19
+ </el-form-item>
20
+ <el-form-item>
21
+ <el-button type="primary" @click="onSearch">查询</el-button>
22
+ <el-button @click="onReset">重置</el-button>
23
+ <el-popover placement="bottom" trigger="click" width="260">
24
+ <template #reference>
25
+ <el-button>列设置</el-button>
26
+ </template>
27
+ <ColumnSettings v-model="visibleKeys" :columns="columns" />
28
+ </el-popover>
29
+ </el-form-item>
30
+ </el-form>
19
31
 
20
32
  <el-table
21
33
  :key="tableKey"
@@ -72,11 +84,27 @@
72
84
  <script>
73
85
  import { get } from '../utils/request'
74
86
  import ColumnSettings from '../components/ColumnSettings.vue'
87
+ import NumberRange, { rangeRule, rangeRequired } from '../components/NumberRange.vue'
75
88
 
76
89
  export default {
77
90
  name: 'TablePage',
78
91
  data() {
79
92
  return {
93
+ searchForm: {
94
+ keyword: '',
95
+ type: '',
96
+ amountRange: [null, null],
97
+ ageMin: null,
98
+ ageMax: null
99
+ },
100
+ searchRules: {
101
+ type: [{ required: true, message: '请选择类型', trigger: 'change' }],
102
+ amountRange: [rangeRule('结束金额不能小于起始金额')],
103
+ ageMax: [
104
+ rangeRequired('请输入年龄范围', 'ageMin'),
105
+ rangeRule('结束年龄不能小于起始年龄', 'ageMin')
106
+ ]
107
+ },
80
108
  columns: [
81
109
  { key: 'organizationName', prop: 'organizationName', label: '所属机构', minWidth: 160 },
82
110
  { key: 'meetSubj', prop: 'meetSubj', label: '会议主题', minWidth: 240 },
@@ -94,13 +122,12 @@ export default {
94
122
  pageSize: 10,
95
123
  sortField: 'createTm',
96
124
  sortOrder: 'descending',
97
- keyword: '',
98
- type: '',
99
125
  tableKey: 0
100
126
  }
101
127
  },
102
128
  components: {
103
- ColumnSettings
129
+ ColumnSettings,
130
+ NumberRange
104
131
  },
105
132
  created() {
106
133
  this.visibleKeys = this.columns.map(c => c.key)
@@ -108,25 +135,32 @@ export default {
108
135
  },
109
136
  methods: {
110
137
  async fetch() {
138
+ const { keyword, type, amountRange, ageMin, ageMax } = this.searchForm
111
139
  const params = {
112
140
  page: this.page,
113
141
  pageSize: this.pageSize,
114
142
  sortField: this.sortField,
115
143
  sortOrder: this.sortOrder
116
144
  }
117
- if (this.keyword) params.meetSubj = this.keyword
118
- if (this.type) params.meetTypeCd = this.type
145
+ if (keyword) params.meetSubj = keyword
146
+ if (type) params.meetTypeCd = type
147
+ if (amountRange[0] !== null) params.amountMin = amountRange[0]
148
+ if (amountRange[1] !== null) params.amountMax = amountRange[1]
149
+ if (ageMin !== null) params.ageMin = ageMin
150
+ if (ageMax !== null) params.ageMax = ageMax
119
151
  const res = await get('/meeting/list', params)
120
152
  this.rows = Array.isArray(res.list) ? res.list : []
121
153
  this.total = Number.isFinite(res.total) ? res.total : this.rows.length
122
154
  },
123
155
  onSearch() {
124
- this.page = 1
125
- this.fetch()
156
+ this.$refs.searchFormRef.validate((valid) => {
157
+ if (!valid) return
158
+ this.page = 1
159
+ this.fetch()
160
+ })
126
161
  },
127
162
  onReset() {
128
- this.keyword = ''
129
- this.type = ''
163
+ this.$refs.searchFormRef.resetFields()
130
164
  this.page = 1
131
165
  this.fetch()
132
166
  },
@@ -221,8 +255,6 @@ export default {
221
255
  }
222
256
  .toolbar {
223
257
  margin-bottom: 12px;
224
- display: flex;
225
- align-items: center;
226
258
  }
227
259
  .pagination {
228
260
  margin-top: 12px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue2server7",
3
- "version": "7.0.7",
3
+ "version": "7.0.8",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "dev": "nodemon --watch src --ext ts --exec \"ts-node src/app.ts\"",