vue2server7 7.0.7 → 7.0.9

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,318 @@
1
+ <template>
2
+ <div class="number-range" :class="{ 'is-disabled': disabled }">
3
+ <el-input
4
+ v-model="minDisplay"
5
+ class="number-range__input"
6
+ :placeholder="minPlaceholder"
7
+ :disabled="disabled"
8
+ clearable
9
+ @input="onMinInput"
10
+ @focus="onMinFocus"
11
+ @blur="onMinBlur"
12
+ @clear="onMinClear"
13
+ />
14
+ <span class="number-range__separator">{{ separator }}</span>
15
+ <el-input
16
+ v-model="maxDisplay"
17
+ class="number-range__input"
18
+ :placeholder="maxPlaceholder"
19
+ :disabled="disabled"
20
+ clearable
21
+ @input="onMaxInput"
22
+ @focus="onMaxFocus"
23
+ @blur="onMaxBlur"
24
+ @clear="onMaxClear"
25
+ />
26
+ </div>
27
+ </template>
28
+
29
+ <script lang="ts">
30
+ import type { FormItemRule } from 'element-plus'
31
+
32
+ type RangeValue = [number | null, number | null]
33
+ type ValidatorCb = (error?: Error) => void
34
+
35
+ /**
36
+ * NumberRange 数字区间输入组件
37
+ *
38
+ * 两种绑定模式:
39
+ * 数组模式 v-model="form.amountRange"
40
+ * 双字段模式 v-model:start="form.ageMin" v-model:end="form.ageMax"
41
+ *
42
+ * 配套校验规则(配合 el-form 使用):
43
+ * import { rangeRule, rangeRequired } from '@/components/NumberRange.vue'
44
+ *
45
+ * 数组模式:
46
+ * rangeRequired('请输入金额范围')
47
+ * rangeRule('结束金额不能小于起始金额')
48
+ *
49
+ * 双字段模式:
50
+ * rangeRequired('请输入年龄范围', 'ageMin')
51
+ * rangeRule('结束年龄不能小于起始年龄', 'ageMin')
52
+ */
53
+
54
+ /** 区间必填校验规则 */
55
+ export function rangeRequired(
56
+ message = '请输入完整区间',
57
+ startField?: string,
58
+ trigger = 'change'
59
+ ): FormItemRule {
60
+ return {
61
+ trigger,
62
+ validator: startField
63
+ ? (_r: unknown, _v: unknown, cb: ValidatorCb, source: Record<string, unknown>) =>
64
+ cb(source[startField] == null || source[startField] === '' ? new Error(message) : undefined)
65
+ : (_r: unknown, v: unknown, cb: ValidatorCb) => {
66
+ const arr = v as RangeValue | undefined
67
+ cb(!Array.isArray(arr) || arr[0] == null || arr[1] == null ? new Error(message) : undefined)
68
+ }
69
+ }
70
+ }
71
+
72
+ /** 区间大小校验规则(结束值 >= 起始值) */
73
+ export function rangeRule(
74
+ message = '结束值不能小于起始值',
75
+ startField?: string,
76
+ trigger = 'change'
77
+ ): FormItemRule {
78
+ return {
79
+ trigger,
80
+ validator: startField
81
+ ? (_r: unknown, v: unknown, cb: ValidatorCb, source: Record<string, unknown>) =>
82
+ cb(source[startField] != null && v != null && (v as number) < (source[startField] as number) ? new Error(message) : undefined)
83
+ : (_r: unknown, v: unknown, cb: ValidatorCb) => {
84
+ const arr = v as RangeValue | undefined
85
+ cb(arr?.[0] != null && arr?.[1] != null && arr[1] < arr[0] ? new Error(message) : undefined)
86
+ }
87
+ }
88
+ }
89
+ </script>
90
+
91
+ <script setup lang="ts">
92
+ import { ref, watch, computed } from 'vue'
93
+
94
+ type Nullable<T> = T | null | undefined
95
+
96
+ interface Props {
97
+ /** 数组模式绑定值 [min, max],与 start/end 二选一 */
98
+ modelValue?: RangeValue
99
+ /** 双字段模式 - 起始值 */
100
+ start?: number | null
101
+ /** 双字段模式 - 结束值 */
102
+ end?: number | null
103
+ minPlaceholder?: string
104
+ maxPlaceholder?: string
105
+ /** 两个输入框之间的分隔文字 */
106
+ separator?: string
107
+ /** 允许输入的最小边界 */
108
+ min?: number
109
+ /** 允许输入的最大边界 */
110
+ max?: number
111
+ step?: number
112
+ /** 小数位数限制,如 2 表示最多两位小数 */
113
+ precision?: number
114
+ disabled?: boolean
115
+ }
116
+
117
+ const props = withDefaults(defineProps<Props>(), {
118
+ modelValue: undefined,
119
+ start: undefined,
120
+ end: undefined,
121
+ minPlaceholder: '最小值',
122
+ maxPlaceholder: '最大值',
123
+ separator: '到',
124
+ min: -Infinity,
125
+ max: Infinity,
126
+ step: 1,
127
+ precision: undefined,
128
+ disabled: false
129
+ })
130
+
131
+ const emit = defineEmits<{
132
+ 'update:modelValue': [value: RangeValue]
133
+ 'update:start': [value: number | null]
134
+ 'update:end': [value: number | null]
135
+ change: [value: RangeValue]
136
+ }>()
137
+
138
+ /** 根据是否传入 modelValue 自动判断绑定模式 */
139
+ const isArrayMode = computed(() => props.modelValue !== undefined)
140
+
141
+ function getInitMin(): Nullable<number> {
142
+ return isArrayMode.value ? props.modelValue?.[0] : props.start
143
+ }
144
+ function getInitMax(): Nullable<number> {
145
+ return isArrayMode.value ? props.modelValue?.[1] : props.end
146
+ }
147
+
148
+ function toDisplay(val: Nullable<number>): string {
149
+ if (val === null || val === undefined) return ''
150
+ return String(val)
151
+ }
152
+
153
+ const minDisplay = ref(toDisplay(getInitMin()))
154
+ const maxDisplay = ref(toDisplay(getInitMax()))
155
+ const minFocused = ref(false)
156
+ const maxFocused = ref(false)
157
+
158
+ /**
159
+ * 输入过滤:只允许数字、小数点、负号;
160
+ * 小数点仅保留第一个,小数位数受 precision 约束
161
+ */
162
+ function sanitize(raw: string | null | undefined): string {
163
+ if (raw === '' || raw === null || raw === undefined) return ''
164
+ let s = raw.replace(/[^0-9.\-]/g, '')
165
+
166
+ const negative = s.startsWith('-')
167
+ s = s.replace(/-/g, '')
168
+ if (negative) s = '-' + s
169
+
170
+ const dotIdx = s.indexOf('.')
171
+ if (dotIdx !== -1) {
172
+ const intPart = s.slice(0, dotIdx)
173
+ let decPart = s.slice(dotIdx + 1).replace(/\./g, '')
174
+ if (props.precision !== undefined) {
175
+ decPart = decPart.slice(0, props.precision)
176
+ }
177
+ s = intPart + '.' + decPart
178
+ }
179
+ return s
180
+ }
181
+
182
+ function parseNum(val: string | null | undefined): number | null {
183
+ if (val === '' || val === null || val === undefined) return null
184
+ const n = Number(val)
185
+ return Number.isNaN(n) ? null : n
186
+ }
187
+
188
+ /** 按 precision 格式化数值 */
189
+ function formatNum(val: number | null | undefined): number | null {
190
+ if (val === null || val === undefined) return null
191
+ if (props.precision !== undefined) {
192
+ return Number(Number(val).toFixed(props.precision))
193
+ }
194
+ return Number(val)
195
+ }
196
+
197
+ const rangeInvalid = computed(() => {
198
+ const minNum = parseNum(minDisplay.value)
199
+ const maxNum = parseNum(maxDisplay.value)
200
+ return minNum !== null && maxNum !== null && maxNum < minNum
201
+ })
202
+
203
+ const isValid = computed(() => !rangeInvalid.value)
204
+
205
+ /** 根据绑定模式 emit 对应事件 */
206
+ function emitValue(): void {
207
+ const minVal = formatNum(parseNum(minDisplay.value))
208
+ const maxVal = formatNum(parseNum(maxDisplay.value))
209
+ if (isArrayMode.value) {
210
+ emit('update:modelValue', [minVal, maxVal])
211
+ } else {
212
+ emit('update:start', minVal)
213
+ emit('update:end', maxVal)
214
+ }
215
+ emit('change', [minVal, maxVal])
216
+ }
217
+
218
+ function onMinInput(val: string | number): void {
219
+ minDisplay.value = sanitize(String(val))
220
+ }
221
+
222
+ function onMaxInput(val: string | number): void {
223
+ maxDisplay.value = sanitize(String(val))
224
+ }
225
+
226
+ function onMinClear(): void {
227
+ minDisplay.value = ''
228
+ emitValue()
229
+ }
230
+
231
+ function onMaxClear(): void {
232
+ maxDisplay.value = ''
233
+ emitValue()
234
+ }
235
+
236
+ function onMinFocus(): void {
237
+ minFocused.value = true
238
+ }
239
+
240
+ function onMaxFocus(): void {
241
+ maxFocused.value = true
242
+ }
243
+
244
+ /** 失焦时格式化数值、钳位到 [min, max] 边界,并触发 emit */
245
+ function onMinBlur(): void {
246
+ minFocused.value = false
247
+ let v = parseNum(minDisplay.value)
248
+ if (v !== null) {
249
+ if (v < props.min) v = props.min
250
+ if (v > props.max) v = props.max
251
+ v = formatNum(v)!
252
+ minDisplay.value = String(v)
253
+ } else {
254
+ minDisplay.value = ''
255
+ }
256
+ emitValue()
257
+ }
258
+
259
+ function onMaxBlur(): void {
260
+ maxFocused.value = false
261
+ let v = parseNum(maxDisplay.value)
262
+ if (v !== null) {
263
+ if (v > props.max) v = props.max
264
+ if (v < props.min) v = props.min
265
+ v = formatNum(v)!
266
+ maxDisplay.value = String(v)
267
+ } else {
268
+ maxDisplay.value = ''
269
+ }
270
+ emitValue()
271
+ }
272
+
273
+ function validate(): boolean {
274
+ return isValid.value
275
+ }
276
+
277
+ defineExpose({ validate, isValid })
278
+
279
+ /** 外部 v-model 变化时同步内部显示值;重置为空时清除焦点状态 */
280
+ watch(
281
+ () => isArrayMode.value ? props.modelValue : [props.start, props.end],
282
+ (val) => {
283
+ const newMin = toDisplay(val?.[0] ?? null)
284
+ const newMax = toDisplay(val?.[1] ?? null)
285
+ if (newMin !== minDisplay.value) minDisplay.value = newMin
286
+ if (newMax !== maxDisplay.value) maxDisplay.value = newMax
287
+ if ((val?.[0] == null) && (val?.[1] == null)) {
288
+ minFocused.value = false
289
+ maxFocused.value = false
290
+ }
291
+ },
292
+ { deep: true }
293
+ )
294
+ </script>
295
+
296
+ <style scoped>
297
+ .number-range {
298
+ display: inline-flex;
299
+ align-items: center;
300
+ gap: 8px;
301
+ flex-wrap: wrap;
302
+ position: relative;
303
+ }
304
+
305
+ .number-range__input {
306
+ width: 140px;
307
+ }
308
+
309
+ .number-range__separator {
310
+ color: var(--el-text-color-primary);
311
+ font-size: 14px;
312
+ flex-shrink: 0;
313
+ }
314
+
315
+ .number-range.is-disabled .number-range__separator {
316
+ color: var(--el-text-color-placeholder);
317
+ }
318
+ </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.9",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "dev": "nodemon --watch src --ext ts --exec \"ts-node src/app.ts\"",
package/test/docs.zip DELETED
Binary file
Binary file