vue2server7 7.0.19 → 7.0.20
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/frontEnd/src/components/NumberRange.vue +95 -202
- package/frontEnd/src/pages/TablePage.vue +228 -162
- package/package.json +1 -1
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="number-range" :class="{ 'is-disabled': disabled
|
|
2
|
+
<div class="number-range" :class="{ 'is-disabled': disabled }">
|
|
3
3
|
<el-input
|
|
4
4
|
v-model="minDisplay"
|
|
5
5
|
class="number-range__input"
|
|
6
|
-
:class="{ 'is-error':
|
|
6
|
+
:class="{ 'is-error': showError }"
|
|
7
7
|
:placeholder="minPlaceholder"
|
|
8
8
|
:disabled="disabled"
|
|
9
9
|
clearable
|
|
10
10
|
@input="onMinInput"
|
|
11
|
-
@focus="
|
|
11
|
+
@focus="onFocus"
|
|
12
12
|
@blur="onMinBlur"
|
|
13
13
|
@clear="onMinClear"
|
|
14
14
|
/>
|
|
@@ -16,175 +16,95 @@
|
|
|
16
16
|
<el-input
|
|
17
17
|
v-model="maxDisplay"
|
|
18
18
|
class="number-range__input"
|
|
19
|
-
:class="{ 'is-error':
|
|
19
|
+
:class="{ 'is-error': showError }"
|
|
20
20
|
:placeholder="maxPlaceholder"
|
|
21
21
|
:disabled="disabled"
|
|
22
22
|
clearable
|
|
23
23
|
@input="onMaxInput"
|
|
24
|
-
@focus="
|
|
24
|
+
@focus="onFocus"
|
|
25
25
|
@blur="onMaxBlur"
|
|
26
26
|
@clear="onMaxClear"
|
|
27
27
|
/>
|
|
28
|
-
<span v-if="
|
|
28
|
+
<span v-if="showError" class="number-range__error">{{ errorMessage }}</span>
|
|
29
29
|
</div>
|
|
30
30
|
</template>
|
|
31
31
|
|
|
32
|
-
<script lang="ts">
|
|
33
|
-
import
|
|
34
|
-
|
|
35
|
-
export type RangeValue = [number | string | null, number | string | null]
|
|
36
|
-
type ValidatorCb = (error?: Error) => void
|
|
32
|
+
<script setup lang="ts">
|
|
33
|
+
import { ref, watch, computed } from 'vue'
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
function toNum(val: unknown): number | null {
|
|
40
|
-
if (val === null || val === undefined || val === '') return null
|
|
41
|
-
const n = Number(val)
|
|
42
|
-
return Number.isNaN(n) ? null : n
|
|
43
|
-
}
|
|
35
|
+
export type RangeValue = [number | null, number | null]
|
|
44
36
|
|
|
45
|
-
|
|
46
|
-
/** 数组模式绑定值 [min, max]
|
|
37
|
+
const props = withDefaults(defineProps<{
|
|
38
|
+
/** 数组模式绑定值 [min, max] */
|
|
47
39
|
modelValue?: RangeValue
|
|
48
|
-
/** 双字段模式 -
|
|
49
|
-
start?: number |
|
|
50
|
-
/** 双字段模式 -
|
|
51
|
-
end?: number |
|
|
40
|
+
/** 双字段模式 - 起始值 */
|
|
41
|
+
start?: number | null
|
|
42
|
+
/** 双字段模式 - 结束值 */
|
|
43
|
+
end?: number | null
|
|
52
44
|
minPlaceholder?: string
|
|
53
45
|
maxPlaceholder?: string
|
|
54
|
-
/** 两个输入框之间的分隔文字 */
|
|
55
46
|
separator?: string
|
|
56
|
-
/**
|
|
57
|
-
min?: number
|
|
58
|
-
/** 允许输入的最大边界 */
|
|
59
|
-
max?: number
|
|
60
|
-
step?: number
|
|
61
|
-
/** 小数位数限制,如 2 表示最多两位小数 */
|
|
47
|
+
/** 小数位数限制 */
|
|
62
48
|
precision?: number
|
|
63
49
|
disabled?: boolean
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
* NumberRange 数字区间输入组件
|
|
68
|
-
*
|
|
69
|
-
* 两种绑定模式:
|
|
70
|
-
* 数组模式 v-model="form.amountRange"
|
|
71
|
-
* 双字段模式 v-model:start="form.ageMin" v-model:end="form.ageMax"
|
|
72
|
-
*
|
|
73
|
-
* 配套校验规则(配合 el-form 使用):
|
|
74
|
-
* import { rangeRule, rangeRequired } from '@/components/NumberRange.vue'
|
|
75
|
-
*
|
|
76
|
-
* 数组模式:
|
|
77
|
-
* rangeRequired('请输入金额范围')
|
|
78
|
-
* rangeRule('结束金额不能小于起始金额')
|
|
79
|
-
*
|
|
80
|
-
* 双字段模式:
|
|
81
|
-
* rangeRequired('请输入年龄范围', 'ageMin')
|
|
82
|
-
* rangeRule('结束年龄不能小于起始年龄', 'ageMin')
|
|
83
|
-
*/
|
|
84
|
-
|
|
85
|
-
/** 区间必填校验规则 */
|
|
86
|
-
export function rangeRequired(
|
|
87
|
-
message = '请输入完整区间',
|
|
88
|
-
startField?: string,
|
|
89
|
-
trigger = 'change'
|
|
90
|
-
): FormItemRule {
|
|
91
|
-
return {
|
|
92
|
-
trigger,
|
|
93
|
-
validator: startField
|
|
94
|
-
? (_r: unknown, _v: unknown, cb: ValidatorCb, source: Record<string, unknown>) =>
|
|
95
|
-
cb(toNum(source[startField]) == null ? new Error(message) : undefined)
|
|
96
|
-
: (_r: unknown, v: unknown, cb: ValidatorCb) => {
|
|
97
|
-
const arr = v as RangeValue | undefined
|
|
98
|
-
cb(!Array.isArray(arr) || toNum(arr[0]) == null || toNum(arr[1]) == null ? new Error(message) : undefined)
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** 区间大小校验规则(结束值 >= 起始值) */
|
|
104
|
-
export function rangeRule(
|
|
105
|
-
message = '结束值不能小于起始值',
|
|
106
|
-
startField?: string,
|
|
107
|
-
trigger = 'change'
|
|
108
|
-
): FormItemRule {
|
|
109
|
-
return {
|
|
110
|
-
trigger,
|
|
111
|
-
validator: startField
|
|
112
|
-
? (_r: unknown, v: unknown, cb: ValidatorCb, source: Record<string, unknown>) => {
|
|
113
|
-
const startNum = toNum(source[startField])
|
|
114
|
-
const endNum = toNum(v)
|
|
115
|
-
cb(startNum != null && endNum != null && endNum < startNum ? new Error(message) : undefined)
|
|
116
|
-
}
|
|
117
|
-
: (_r: unknown, v: unknown, cb: ValidatorCb) => {
|
|
118
|
-
const arr = v as RangeValue | undefined
|
|
119
|
-
const minNum = toNum(arr?.[0])
|
|
120
|
-
const maxNum = toNum(arr?.[1])
|
|
121
|
-
cb(minNum != null && maxNum != null && maxNum < minNum ? new Error(message) : undefined)
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
</script>
|
|
126
|
-
|
|
127
|
-
<script setup lang="ts">
|
|
128
|
-
import { ref, watch, computed, inject } from 'vue'
|
|
129
|
-
import { formItemContextKey } from 'element-plus'
|
|
130
|
-
|
|
131
|
-
const props = withDefaults(defineProps<NumberRangeProps>(), {
|
|
50
|
+
/** 自定义错误提示 */
|
|
51
|
+
errorMessage?: string
|
|
52
|
+
}>(), {
|
|
132
53
|
modelValue: undefined,
|
|
133
54
|
start: undefined,
|
|
134
55
|
end: undefined,
|
|
135
56
|
minPlaceholder: '最小值',
|
|
136
57
|
maxPlaceholder: '最大值',
|
|
137
58
|
separator: '到',
|
|
138
|
-
min: -Infinity,
|
|
139
|
-
max: Infinity,
|
|
140
|
-
step: 1,
|
|
141
59
|
precision: undefined,
|
|
142
|
-
disabled: false
|
|
60
|
+
disabled: false,
|
|
61
|
+
errorMessage: '结束值不能小于起始值'
|
|
143
62
|
})
|
|
144
63
|
|
|
145
64
|
const emit = defineEmits<{
|
|
146
65
|
'update:modelValue': [value: RangeValue]
|
|
147
66
|
'update:start': [value: number | null]
|
|
148
67
|
'update:end': [value: number | null]
|
|
149
|
-
|
|
68
|
+
/** 校验状态变化:true 表示通过,false 表示不通过 */
|
|
69
|
+
'validate': [valid: boolean]
|
|
150
70
|
}>()
|
|
151
71
|
|
|
152
|
-
|
|
153
|
-
const formItemContext = inject(formItemContextKey, undefined)
|
|
154
|
-
const useInternalValidation = computed(() => !formItemContext)
|
|
155
|
-
|
|
156
|
-
/** 根据是否传入 modelValue 自动判断绑定模式 */
|
|
72
|
+
// 判断绑定模式
|
|
157
73
|
const isArrayMode = computed(() => props.modelValue !== undefined)
|
|
158
74
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
function getInitMax(): number | null {
|
|
163
|
-
return toNum(isArrayMode.value ? props.modelValue?.[1] : props.end)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function toDisplay(val: number | null): string {
|
|
167
|
-
if (val === null) return ''
|
|
168
|
-
return String(val)
|
|
169
|
-
}
|
|
75
|
+
// 显示值
|
|
76
|
+
const minDisplay = ref('')
|
|
77
|
+
const maxDisplay = ref('')
|
|
170
78
|
|
|
171
|
-
|
|
172
|
-
const maxDisplay = ref(toDisplay(getInitMax()))
|
|
79
|
+
// 焦点状态
|
|
173
80
|
const minFocused = ref(false)
|
|
174
81
|
const maxFocused = ref(false)
|
|
82
|
+
const hasBlurred = ref(false) // 是否已失焦过
|
|
175
83
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
84
|
+
// 校验状态
|
|
85
|
+
const rangeInvalid = computed(() => {
|
|
86
|
+
const min = toNum(minDisplay.value)
|
|
87
|
+
const max = toNum(maxDisplay.value)
|
|
88
|
+
return min !== null && max !== null && max < min
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// 显示错误:两个输入框都失焦后且校验不通过
|
|
92
|
+
const showError = computed(() => hasBlurred.value && !minFocused.value && !maxFocused.value && rangeInvalid.value)
|
|
93
|
+
|
|
94
|
+
// 转换为数字
|
|
95
|
+
function toNum(val: unknown): number | null {
|
|
96
|
+
if (val === null || val === undefined || val === '') return null
|
|
97
|
+
const n = Number(val)
|
|
98
|
+
return Number.isNaN(n) ? null : n
|
|
99
|
+
}
|
|
183
100
|
|
|
101
|
+
// 输入过滤
|
|
102
|
+
function sanitize(raw: string): string {
|
|
103
|
+
if (!raw) return ''
|
|
104
|
+
let s = raw.replace(/[^0-9.\-]/g, '')
|
|
184
105
|
const negative = s.startsWith('-')
|
|
185
106
|
s = s.replace(/-/g, '')
|
|
186
107
|
if (negative) s = '-' + s
|
|
187
|
-
|
|
188
108
|
const dotIdx = s.indexOf('.')
|
|
189
109
|
if (dotIdx !== -1) {
|
|
190
110
|
const intPart = s.slice(0, dotIdx)
|
|
@@ -197,127 +117,101 @@ function sanitize(raw: string | null | undefined): string {
|
|
|
197
117
|
return s
|
|
198
118
|
}
|
|
199
119
|
|
|
200
|
-
function
|
|
201
|
-
if (val ===
|
|
202
|
-
const n = Number(val)
|
|
203
|
-
return Number.isNaN(n) ? null : n
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/** 按 precision 格式化数值 */
|
|
207
|
-
function formatNum(val: number | null | undefined): number | null {
|
|
208
|
-
if (val === null || val === undefined) return null
|
|
120
|
+
function formatNum(val: number | null): number | null {
|
|
121
|
+
if (val === null) return null
|
|
209
122
|
if (props.precision !== undefined) {
|
|
210
|
-
return Number(
|
|
123
|
+
return Number(val.toFixed(props.precision))
|
|
211
124
|
}
|
|
212
|
-
return
|
|
125
|
+
return val
|
|
213
126
|
}
|
|
214
127
|
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
return minNum !== null && maxNum !== null && maxNum < minNum
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
const isValid = computed(() => !rangeInvalid.value)
|
|
222
|
-
|
|
223
|
-
/** 内部校验:仅在无 el-form-item 且两个输入框都失焦后展示 */
|
|
224
|
-
const blurred = ref(false)
|
|
225
|
-
const showInternalError = computed(() =>
|
|
226
|
-
useInternalValidation.value && blurred.value && rangeInvalid.value
|
|
227
|
-
)
|
|
228
|
-
|
|
229
|
-
/** 根据绑定模式 emit 对应事件 */
|
|
230
|
-
function emitValue(): void {
|
|
231
|
-
const minVal = formatNum(parseNum(minDisplay.value))
|
|
232
|
-
const maxVal = formatNum(parseNum(maxDisplay.value))
|
|
128
|
+
function emitValue() {
|
|
129
|
+
const minVal = formatNum(toNum(minDisplay.value))
|
|
130
|
+
const maxVal = formatNum(toNum(maxDisplay.value))
|
|
233
131
|
if (isArrayMode.value) {
|
|
234
132
|
emit('update:modelValue', [minVal, maxVal])
|
|
235
133
|
} else {
|
|
236
134
|
emit('update:start', minVal)
|
|
237
135
|
emit('update:end', maxVal)
|
|
238
136
|
}
|
|
239
|
-
|
|
137
|
+
// 发送校验状态
|
|
138
|
+
emit('validate', !rangeInvalid.value)
|
|
240
139
|
}
|
|
241
140
|
|
|
242
|
-
function onMinInput(val: string | number)
|
|
141
|
+
function onMinInput(val: string | number) {
|
|
243
142
|
minDisplay.value = sanitize(String(val))
|
|
244
143
|
}
|
|
245
144
|
|
|
246
|
-
function onMaxInput(val: string | number)
|
|
145
|
+
function onMaxInput(val: string | number) {
|
|
247
146
|
maxDisplay.value = sanitize(String(val))
|
|
248
147
|
}
|
|
249
148
|
|
|
250
|
-
function onMinClear()
|
|
149
|
+
function onMinClear() {
|
|
251
150
|
minDisplay.value = ''
|
|
252
151
|
emitValue()
|
|
253
152
|
}
|
|
254
153
|
|
|
255
|
-
function onMaxClear()
|
|
154
|
+
function onMaxClear() {
|
|
256
155
|
maxDisplay.value = ''
|
|
257
156
|
emitValue()
|
|
258
157
|
}
|
|
259
158
|
|
|
260
|
-
function
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
function onMaxFocus(): void {
|
|
265
|
-
maxFocused.value = true
|
|
159
|
+
function onFocus() {
|
|
160
|
+
// 获取焦点时清除错误提示
|
|
161
|
+
hasBlurred.value = false
|
|
266
162
|
}
|
|
267
163
|
|
|
268
|
-
|
|
269
|
-
function onMinBlur(): void {
|
|
164
|
+
function onMinBlur() {
|
|
270
165
|
minFocused.value = false
|
|
271
|
-
|
|
166
|
+
// 格式化
|
|
167
|
+
let v = toNum(minDisplay.value)
|
|
272
168
|
if (v !== null) {
|
|
273
|
-
|
|
274
|
-
if (v > props.max) v = props.max
|
|
275
|
-
v = formatNum(v)!
|
|
276
|
-
minDisplay.value = String(v)
|
|
277
|
-
} else {
|
|
278
|
-
minDisplay.value = ''
|
|
169
|
+
minDisplay.value = String(formatNum(v))
|
|
279
170
|
}
|
|
280
171
|
emitValue()
|
|
281
|
-
|
|
172
|
+
// 两个都失焦后标记
|
|
173
|
+
if (!minFocused.value && !maxFocused.value) {
|
|
174
|
+
hasBlurred.value = true
|
|
175
|
+
}
|
|
282
176
|
}
|
|
283
177
|
|
|
284
|
-
function onMaxBlur()
|
|
178
|
+
function onMaxBlur() {
|
|
285
179
|
maxFocused.value = false
|
|
286
|
-
let v =
|
|
180
|
+
let v = toNum(maxDisplay.value)
|
|
287
181
|
if (v !== null) {
|
|
288
|
-
|
|
289
|
-
if (v < props.min) v = props.min
|
|
290
|
-
v = formatNum(v)!
|
|
291
|
-
maxDisplay.value = String(v)
|
|
292
|
-
} else {
|
|
293
|
-
maxDisplay.value = ''
|
|
182
|
+
maxDisplay.value = String(formatNum(v))
|
|
294
183
|
}
|
|
295
184
|
emitValue()
|
|
296
|
-
if (!minFocused.value && !maxFocused.value)
|
|
185
|
+
if (!minFocused.value && !maxFocused.value) {
|
|
186
|
+
hasBlurred.value = true
|
|
187
|
+
}
|
|
297
188
|
}
|
|
298
189
|
|
|
190
|
+
// 手动校验方法
|
|
299
191
|
function validate(): boolean {
|
|
300
|
-
|
|
301
|
-
return
|
|
192
|
+
hasBlurred.value = true
|
|
193
|
+
return !rangeInvalid.value
|
|
302
194
|
}
|
|
303
195
|
|
|
304
|
-
|
|
196
|
+
// 重置方法
|
|
197
|
+
function reset() {
|
|
198
|
+
minDisplay.value = ''
|
|
199
|
+
maxDisplay.value = ''
|
|
200
|
+
hasBlurred.value = false
|
|
201
|
+
}
|
|
305
202
|
|
|
306
|
-
|
|
203
|
+
defineExpose({ validate, reset })
|
|
204
|
+
|
|
205
|
+
// 同步外部值
|
|
307
206
|
watch(
|
|
308
207
|
() => isArrayMode.value ? props.modelValue : [props.start, props.end],
|
|
309
208
|
(val) => {
|
|
310
|
-
const newMin =
|
|
311
|
-
const newMax =
|
|
209
|
+
const newMin = val?.[0] != null ? String(val[0]) : ''
|
|
210
|
+
const newMax = val?.[1] != null ? String(val[1]) : ''
|
|
312
211
|
if (newMin !== minDisplay.value) minDisplay.value = newMin
|
|
313
212
|
if (newMax !== maxDisplay.value) maxDisplay.value = newMax
|
|
314
|
-
if (toNum(val?.[0]) == null && toNum(val?.[1]) == null) {
|
|
315
|
-
minFocused.value = false
|
|
316
|
-
maxFocused.value = false
|
|
317
|
-
blurred.value = false
|
|
318
|
-
}
|
|
319
213
|
},
|
|
320
|
-
{ deep: true }
|
|
214
|
+
{ immediate: true, deep: true }
|
|
321
215
|
)
|
|
322
216
|
</script>
|
|
323
217
|
|
|
@@ -326,7 +220,6 @@ watch(
|
|
|
326
220
|
display: flex;
|
|
327
221
|
align-items: center;
|
|
328
222
|
gap: 8px;
|
|
329
|
-
width: 100%;
|
|
330
223
|
flex-wrap: wrap;
|
|
331
224
|
}
|
|
332
225
|
|
|
@@ -354,6 +247,6 @@ watch(
|
|
|
354
247
|
color: var(--el-color-danger);
|
|
355
248
|
font-size: 12px;
|
|
356
249
|
line-height: 1;
|
|
357
|
-
padding-top:
|
|
250
|
+
padding-top: 4px;
|
|
358
251
|
}
|
|
359
252
|
</style>
|
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
<el-option label="晨夕会" value="MORNING" />
|
|
12
12
|
</el-select>
|
|
13
13
|
</el-form-item>
|
|
14
|
-
<el-form-item
|
|
15
|
-
<NumberRange v-model="searchForm.amountRange" min-placeholder="最小金额" max-placeholder="最大金额" :precision="2" />
|
|
14
|
+
<el-form-item label="金额范围">
|
|
15
|
+
<NumberRange ref="amountRangeRef" v-model="searchForm.amountRange" min-placeholder="最小金额" max-placeholder="最大金额" :precision="2" error-message="结束金额不能小于起始金额" @validate="onAmountValidate" />
|
|
16
16
|
</el-form-item>
|
|
17
|
-
<el-form-item
|
|
18
|
-
<NumberRange v-model:start="searchForm.ageMin" v-model:end="searchForm.ageMax" min-placeholder="最小年龄" max-placeholder="最大年龄" :precision="0" />
|
|
17
|
+
<el-form-item label="年龄范围">
|
|
18
|
+
<NumberRange ref="ageRangeRef" v-model:start="searchForm.ageMin" v-model:end="searchForm.ageMax" min-placeholder="最小年龄" max-placeholder="最大年龄" :precision="0" error-message="结束年龄不能小于起始年龄" @validate="onAgeValidate" />
|
|
19
19
|
</el-form-item>
|
|
20
20
|
<el-form-item>
|
|
21
21
|
<el-button type="primary" @click="onSearch">查询</el-button>
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
<template v-else-if="col.type === 'action'">
|
|
66
66
|
<el-button type="text" size="small" @click="onView(row)">详情</el-button>
|
|
67
67
|
<el-button type="text" size="small" @click="onDelete(row)">删除</el-button>
|
|
68
|
-
|
|
68
|
+
x x </template>
|
|
69
69
|
<template v-else>
|
|
70
70
|
{{ row[col.prop] }}
|
|
71
71
|
</template>
|
|
@@ -81,168 +81,234 @@
|
|
|
81
81
|
</section>
|
|
82
82
|
</template>
|
|
83
83
|
|
|
84
|
-
<script>
|
|
84
|
+
<script setup lang="ts">
|
|
85
|
+
import { ref, reactive, onMounted, watch } from 'vue'
|
|
86
|
+
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
87
|
+
import type { FormInstance } from 'element-plus'
|
|
85
88
|
import { get } from '../utils/request'
|
|
86
89
|
import ColumnSettings from '../components/ColumnSettings.vue'
|
|
87
|
-
import NumberRange
|
|
90
|
+
import NumberRange from '../components/NumberRange.vue'
|
|
88
91
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
`创建人:${row.displayName || ''}`,
|
|
188
|
-
`创建日期:${this.formatDate(row.createTm)}`,
|
|
189
|
-
`最近编辑时间:${this.formatDateTime(row.updateTm)}`
|
|
190
|
-
]
|
|
191
|
-
this.$alert(lines.join('\n'), '会议详情', { confirmButtonText: '确定' })
|
|
192
|
-
},
|
|
193
|
-
onDelete(row) {
|
|
194
|
-
this.$confirm(`确定删除会议「${row.meetSubj || ''}」吗?`, '提示', {
|
|
195
|
-
type: 'warning',
|
|
196
|
-
confirmButtonText: '确定',
|
|
197
|
-
cancelButtonText: '取消'
|
|
198
|
-
})
|
|
199
|
-
.then(() => {
|
|
200
|
-
this.rows = this.rows.filter(item => item.id !== row.id)
|
|
201
|
-
this.$message({ type: 'success', message: '删除成功' })
|
|
202
|
-
})
|
|
203
|
-
.catch(() => {})
|
|
204
|
-
},
|
|
205
|
-
formatType(code) {
|
|
206
|
-
if (code === 'TRAINING') return '培训'
|
|
207
|
-
if (code === 'MORNING') return '晨夕会'
|
|
208
|
-
return code || ''
|
|
209
|
-
},
|
|
210
|
-
getTypeTag(code) {
|
|
211
|
-
if (code === 'TRAINING') return 'primary'
|
|
212
|
-
if (code === 'MORNING') return 'warning'
|
|
213
|
-
return 'info'
|
|
214
|
-
},
|
|
215
|
-
formatDate(value) {
|
|
216
|
-
if (!value) return ''
|
|
217
|
-
if (value.length === 10) return value
|
|
218
|
-
const d = new Date(value)
|
|
219
|
-
if (Number.isNaN(d.getTime())) return ''
|
|
220
|
-
const yyyy = d.getFullYear()
|
|
221
|
-
const mm = String(d.getMonth() + 1).padStart(2, '0')
|
|
222
|
-
const dd = String(d.getDate()).padStart(2, '0')
|
|
223
|
-
return `${yyyy}-${mm}-${dd}`
|
|
224
|
-
},
|
|
225
|
-
formatDateTime(value) {
|
|
226
|
-
if (!value) return ''
|
|
227
|
-
if (value.length >= 16 && value.includes(' ')) return value
|
|
228
|
-
const d = new Date(value)
|
|
229
|
-
if (Number.isNaN(d.getTime())) return ''
|
|
230
|
-
const yyyy = d.getFullYear()
|
|
231
|
-
const mm = String(d.getMonth() + 1).padStart(2, '0')
|
|
232
|
-
const dd = String(d.getDate()).padStart(2, '0')
|
|
233
|
-
const hh = String(d.getHours()).padStart(2, '0')
|
|
234
|
-
const mi = String(d.getMinutes()).padStart(2, '0')
|
|
235
|
-
const ss = String(d.getSeconds()).padStart(2, '0')
|
|
236
|
-
return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss}`
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
,
|
|
240
|
-
watch: {
|
|
241
|
-
visibleKeys() {
|
|
242
|
-
this.bumpTableKey()
|
|
243
|
-
}
|
|
92
|
+
// 类型定义
|
|
93
|
+
interface SearchForm {
|
|
94
|
+
keyword: string
|
|
95
|
+
type: string
|
|
96
|
+
amountRange: [number | null, number | null]
|
|
97
|
+
ageMin: number | null
|
|
98
|
+
ageMax: number | null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface ColumnConfig {
|
|
102
|
+
key: string
|
|
103
|
+
prop: string
|
|
104
|
+
label: string
|
|
105
|
+
width?: number
|
|
106
|
+
minWidth?: number
|
|
107
|
+
align?: 'left' | 'center' | 'right'
|
|
108
|
+
headerAlign?: 'left' | 'center' | 'right'
|
|
109
|
+
sortable?: boolean | 'custom'
|
|
110
|
+
fixed?: boolean | 'left' | 'right'
|
|
111
|
+
type?: string
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface MeetingRow {
|
|
115
|
+
id: string | number
|
|
116
|
+
organizationName?: string
|
|
117
|
+
meetSubj?: string
|
|
118
|
+
meetNo?: string
|
|
119
|
+
displayName?: string
|
|
120
|
+
meetTypeCd?: string
|
|
121
|
+
createTm?: string
|
|
122
|
+
updateTm?: string
|
|
123
|
+
plainContent?: string
|
|
124
|
+
[key: string]: any
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface SortChangeParams {
|
|
128
|
+
prop?: string
|
|
129
|
+
order?: 'ascending' | 'descending' | null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 组件引用
|
|
133
|
+
const searchFormRef = ref<FormInstance>()
|
|
134
|
+
const amountRangeRef = ref<InstanceType<typeof NumberRange>>()
|
|
135
|
+
const ageRangeRef = ref<InstanceType<typeof NumberRange>>()
|
|
136
|
+
|
|
137
|
+
// 校验状态
|
|
138
|
+
const amountValid = ref(true)
|
|
139
|
+
const ageValid = ref(true)
|
|
140
|
+
|
|
141
|
+
const onAmountValidate = (valid: boolean) => {
|
|
142
|
+
amountValid.value = valid
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const onAgeValidate = (valid: boolean) => {
|
|
146
|
+
ageValid.value = valid
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 响应式数据
|
|
150
|
+
const searchForm = reactive<SearchForm>({
|
|
151
|
+
keyword: '',
|
|
152
|
+
type: '',
|
|
153
|
+
amountRange: [null, null],
|
|
154
|
+
ageMin: null,
|
|
155
|
+
ageMax: null
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
const searchRules = {
|
|
159
|
+
type: [{ required: true, message: '请选择类型', trigger: 'change' }]
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const columns: ColumnConfig[] = [
|
|
163
|
+
{ key: 'organizationName', prop: 'organizationName', label: '所属机构', minWidth: 160 },
|
|
164
|
+
{ key: 'meetSubj', prop: 'meetSubj', label: '会议主题', minWidth: 240 },
|
|
165
|
+
{ key: 'meetNo', prop: 'meetNo', label: '会议编号', minWidth: 180 },
|
|
166
|
+
{ key: 'displayName', prop: 'displayName', label: '创建人', width: 120 },
|
|
167
|
+
{ key: 'meetTypeCd', prop: 'meetTypeCd', label: '类型', width: 120, align: 'center', type: 'type' },
|
|
168
|
+
{ key: 'createTm', prop: 'createTm', label: '创建日期', width: 160, align: 'center', type: 'date', sortable: 'custom' },
|
|
169
|
+
{ key: 'updateTm', prop: 'updateTm', label: '最近编辑时间', width: 190, align: 'center', type: 'datetime', sortable: 'custom' },
|
|
170
|
+
{ key: 'action', prop: '', label: '操作', width: 140, align: 'center', fixed: 'right', type: 'action' }
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
const visibleKeys = ref<string[]>([])
|
|
174
|
+
const rows = ref<MeetingRow[]>([])
|
|
175
|
+
const total = ref(0)
|
|
176
|
+
const page = ref(1)
|
|
177
|
+
const pageSize = ref(10)
|
|
178
|
+
const sortField = ref('createTm')
|
|
179
|
+
const sortOrder = ref<'ascending' | 'descending' | ''>('descending')
|
|
180
|
+
const tableKey = ref(0)
|
|
181
|
+
|
|
182
|
+
// 方法
|
|
183
|
+
const fetch = async () => {
|
|
184
|
+
const { keyword, type, amountRange, ageMin, ageMax } = searchForm
|
|
185
|
+
const params: Record<string, any> = {
|
|
186
|
+
page: page.value,
|
|
187
|
+
pageSize: pageSize.value,
|
|
188
|
+
sortField: sortField.value,
|
|
189
|
+
sortOrder: sortOrder.value
|
|
244
190
|
}
|
|
191
|
+
if (keyword) params.meetSubj = keyword
|
|
192
|
+
if (type) params.meetTypeCd = type
|
|
193
|
+
if (amountRange[0] !== null) params.amountMin = amountRange[0]
|
|
194
|
+
if (amountRange[1] !== null) params.amountMax = amountRange[1]
|
|
195
|
+
if (ageMin !== null) params.ageMin = ageMin
|
|
196
|
+
if (ageMax !== null) params.ageMax = ageMax
|
|
197
|
+
const res = await get('/meeting/list', params)
|
|
198
|
+
rows.value = Array.isArray(res.list) ? res.list : []
|
|
199
|
+
total.value = Number.isFinite(res.total) ? res.total : rows.value.length
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const onSearch = () => {
|
|
203
|
+
// 校验 NumberRange 组件
|
|
204
|
+
const isAmountValid = amountRangeRef.value?.validate() ?? true
|
|
205
|
+
const isAgeValid = ageRangeRef.value?.validate() ?? true
|
|
206
|
+
|
|
207
|
+
searchFormRef.value?.validate((valid: boolean) => {
|
|
208
|
+
if (!valid || !isAmountValid || !isAgeValid) return
|
|
209
|
+
page.value = 1
|
|
210
|
+
fetch()
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const onReset = () => {
|
|
215
|
+
searchFormRef.value?.resetFields()
|
|
216
|
+
amountRangeRef.value?.reset()
|
|
217
|
+
ageRangeRef.value?.reset()
|
|
218
|
+
page.value = 1
|
|
219
|
+
fetch()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const onPageChange = (p: number) => {
|
|
223
|
+
page.value = p
|
|
224
|
+
fetch()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const onSortChange = ({ prop, order }: SortChangeParams) => {
|
|
228
|
+
sortField.value = prop || ''
|
|
229
|
+
sortOrder.value = order || ''
|
|
230
|
+
page.value = 1
|
|
231
|
+
fetch()
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 通过改变 key 强制表格重渲染,确保动态列显示/隐藏立即生效
|
|
235
|
+
const bumpTableKey = () => {
|
|
236
|
+
tableKey.value += 1
|
|
245
237
|
}
|
|
238
|
+
|
|
239
|
+
const onView = (row: MeetingRow) => {
|
|
240
|
+
const lines = [
|
|
241
|
+
`会议主题:${row.meetSubj || ''}`,
|
|
242
|
+
`内容:${row.plainContent || ''}`,
|
|
243
|
+
`所属机构:${row.organizationName || ''}`,
|
|
244
|
+
`会议类型:${formatType(row.meetTypeCd)}`,
|
|
245
|
+
`创建人:${row.displayName || ''}`,
|
|
246
|
+
`创建日期:${formatDate(row.createTm)}`,
|
|
247
|
+
`最近编辑时间:${formatDateTime(row.updateTm)}`
|
|
248
|
+
]
|
|
249
|
+
ElMessageBox.alert(lines.join('\n'), '会议详情', { confirmButtonText: '确定' })
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const onDelete = (row: MeetingRow) => {
|
|
253
|
+
ElMessageBox.confirm(`确定删除会议「${row.meetSubj || ''}」吗?`, '提示', {
|
|
254
|
+
type: 'warning',
|
|
255
|
+
confirmButtonText: '确定',
|
|
256
|
+
cancelButtonText: '取消'
|
|
257
|
+
})
|
|
258
|
+
.then(() => {
|
|
259
|
+
rows.value = rows.value.filter(item => item.id !== row.id)
|
|
260
|
+
ElMessage({ type: 'success', message: '删除成功' })
|
|
261
|
+
})
|
|
262
|
+
.catch(() => {})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const formatType = (code?: string): string => {
|
|
266
|
+
if (code === 'TRAINING') return '培训'
|
|
267
|
+
if (code === 'MORNING') return '晨夕会'
|
|
268
|
+
return code || ''
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const getTypeTag = (code?: string): string => {
|
|
272
|
+
if (code === 'TRAINING') return 'primary'
|
|
273
|
+
if (code === 'MORNING') return 'warning'
|
|
274
|
+
return 'info'
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const formatDate = (value?: string): string => {
|
|
278
|
+
if (!value) return ''
|
|
279
|
+
if (value.length === 10) return value
|
|
280
|
+
const d = new Date(value)
|
|
281
|
+
if (Number.isNaN(d.getTime())) return ''
|
|
282
|
+
const yyyy = d.getFullYear()
|
|
283
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0')
|
|
284
|
+
const dd = String(d.getDate()).padStart(2, '0')
|
|
285
|
+
return `${yyyy}-${mm}-${dd}`
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const formatDateTime = (value?: string): string => {
|
|
289
|
+
if (!value) return ''
|
|
290
|
+
if (value.length >= 16 && value.includes(' ')) return value
|
|
291
|
+
const d = new Date(value)
|
|
292
|
+
if (Number.isNaN(d.getTime())) return ''
|
|
293
|
+
const yyyy = d.getFullYear()
|
|
294
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0')
|
|
295
|
+
const dd = String(d.getDate()).padStart(2, '0')
|
|
296
|
+
const hh = String(d.getHours()).padStart(2, '0')
|
|
297
|
+
const mi = String(d.getMinutes()).padStart(2, '0')
|
|
298
|
+
const ss = String(d.getSeconds()).padStart(2, '0')
|
|
299
|
+
return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss}`
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 监听器
|
|
303
|
+
watch(visibleKeys, () => {
|
|
304
|
+
bumpTableKey()
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
// 生命周期
|
|
308
|
+
onMounted(() => {
|
|
309
|
+
visibleKeys.value = columns.map(c => c.key)
|
|
310
|
+
fetch()
|
|
311
|
+
})
|
|
246
312
|
</script>
|
|
247
313
|
|
|
248
314
|
<style scoped>
|