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.
@@ -1,14 +1,14 @@
1
1
  <template>
2
- <div class="number-range" :class="{ 'is-disabled': disabled, 'is-error': showInternalError }">
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': showInternalError }"
6
+ :class="{ 'is-error': showError }"
7
7
  :placeholder="minPlaceholder"
8
8
  :disabled="disabled"
9
9
  clearable
10
10
  @input="onMinInput"
11
- @focus="onMinFocus"
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': showInternalError }"
19
+ :class="{ 'is-error': showError }"
20
20
  :placeholder="maxPlaceholder"
21
21
  :disabled="disabled"
22
22
  clearable
23
23
  @input="onMaxInput"
24
- @focus="onMaxFocus"
24
+ @focus="onFocus"
25
25
  @blur="onMaxBlur"
26
26
  @clear="onMaxClear"
27
27
  />
28
- <span v-if="showInternalError" class="number-range__error">结束值不能小于起始值</span>
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 type { FormItemRule } from 'element-plus'
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
- /** 将字符串/数字统一转为 number,空值返回 null */
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
- export interface NumberRangeProps {
46
- /** 数组模式绑定值 [min, max],与 start/end 二选一;值可以是 string 或 number */
37
+ const props = withDefaults(defineProps<{
38
+ /** 数组模式绑定值 [min, max] */
47
39
  modelValue?: RangeValue
48
- /** 双字段模式 - 起始值(支持 string 类型数字) */
49
- start?: number | string | null
50
- /** 双字段模式 - 结束值(支持 string 类型数字) */
51
- end?: number | string | null
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
- change: [value: RangeValue]
68
+ /** 校验状态变化:true 表示通过,false 表示不通过 */
69
+ 'validate': [valid: boolean]
150
70
  }>()
151
71
 
152
- /** 检测是否处于 el-form-item 内,有则交给 form 校验,无则组件内部校验 */
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
- function getInitMin(): number | null {
160
- return toNum(isArrayMode.value ? props.modelValue?.[0] : props.start)
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
- const minDisplay = ref(toDisplay(getInitMin()))
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
- * 小数点仅保留第一个,小数位数受 precision 约束
179
- */
180
- function sanitize(raw: string | null | undefined): string {
181
- if (raw === '' || raw === null || raw === undefined) return ''
182
- let s = raw.replace(/[^0-9.\-]/g, '')
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 parseNum(val: string | null | undefined): number | null {
201
- if (val === '' || val === null || val === undefined) return null
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(Number(val).toFixed(props.precision))
123
+ return Number(val.toFixed(props.precision))
211
124
  }
212
- return Number(val)
125
+ return val
213
126
  }
214
127
 
215
- const rangeInvalid = computed(() => {
216
- const minNum = parseNum(minDisplay.value)
217
- const maxNum = parseNum(maxDisplay.value)
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
- emit('change', [minVal, maxVal])
137
+ // 发送校验状态
138
+ emit('validate', !rangeInvalid.value)
240
139
  }
241
140
 
242
- function onMinInput(val: string | number): void {
141
+ function onMinInput(val: string | number) {
243
142
  minDisplay.value = sanitize(String(val))
244
143
  }
245
144
 
246
- function onMaxInput(val: string | number): void {
145
+ function onMaxInput(val: string | number) {
247
146
  maxDisplay.value = sanitize(String(val))
248
147
  }
249
148
 
250
- function onMinClear(): void {
149
+ function onMinClear() {
251
150
  minDisplay.value = ''
252
151
  emitValue()
253
152
  }
254
153
 
255
- function onMaxClear(): void {
154
+ function onMaxClear() {
256
155
  maxDisplay.value = ''
257
156
  emitValue()
258
157
  }
259
158
 
260
- function onMinFocus(): void {
261
- minFocused.value = true
262
- }
263
-
264
- function onMaxFocus(): void {
265
- maxFocused.value = true
159
+ function onFocus() {
160
+ // 获取焦点时清除错误提示
161
+ hasBlurred.value = false
266
162
  }
267
163
 
268
- /** 失焦时格式化数值、钳位到 [min, max] 边界,并触发 emit */
269
- function onMinBlur(): void {
164
+ function onMinBlur() {
270
165
  minFocused.value = false
271
- let v = parseNum(minDisplay.value)
166
+ // 格式化
167
+ let v = toNum(minDisplay.value)
272
168
  if (v !== null) {
273
- if (v < props.min) v = props.min
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
- if (!minFocused.value && !maxFocused.value) blurred.value = true
172
+ // 两个都失焦后标记
173
+ if (!minFocused.value && !maxFocused.value) {
174
+ hasBlurred.value = true
175
+ }
282
176
  }
283
177
 
284
- function onMaxBlur(): void {
178
+ function onMaxBlur() {
285
179
  maxFocused.value = false
286
- let v = parseNum(maxDisplay.value)
180
+ let v = toNum(maxDisplay.value)
287
181
  if (v !== null) {
288
- if (v > props.max) v = props.max
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) blurred.value = true
185
+ if (!minFocused.value && !maxFocused.value) {
186
+ hasBlurred.value = true
187
+ }
297
188
  }
298
189
 
190
+ // 手动校验方法
299
191
  function validate(): boolean {
300
- blurred.value = true
301
- return isValid.value
192
+ hasBlurred.value = true
193
+ return !rangeInvalid.value
302
194
  }
303
195
 
304
- defineExpose({ validate, isValid })
196
+ // 重置方法
197
+ function reset() {
198
+ minDisplay.value = ''
199
+ maxDisplay.value = ''
200
+ hasBlurred.value = false
201
+ }
305
202
 
306
- /** 外部 v-model 变化时同步内部显示值;重置为空时清除焦点状态 */
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 = toDisplay(toNum(val?.[0]))
311
- const newMax = toDisplay(toNum(val?.[1]))
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: 2px;
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 prop="amountRange" label="金额范围">
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 prop="ageMax" label="年龄范围" required>
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
- </template>
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, { rangeRule, rangeRequired } from '../components/NumberRange.vue'
90
+ import NumberRange from '../components/NumberRange.vue'
88
91
 
89
- export default {
90
- name: 'TablePage',
91
- data() {
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
- },
108
- columns: [
109
- { key: 'organizationName', prop: 'organizationName', label: '所属机构', minWidth: 160 },
110
- { key: 'meetSubj', prop: 'meetSubj', label: '会议主题', minWidth: 240 },
111
- { key: 'meetNo', prop: 'meetNo', label: '会议编号', minWidth: 180 },
112
- { key: 'displayName', prop: 'displayName', label: '创建人', width: 120 },
113
- { key: 'meetTypeCd', prop: 'meetTypeCd', label: '类型', width: 120, align: 'center', type: 'type' },
114
- { key: 'createTm', prop: 'createTm', label: '创建日期', width: 160, align: 'center', type: 'date', sortable: 'custom' },
115
- { key: 'updateTm', prop: 'updateTm', label: '最近编辑时间', width: 190, align: 'center', type: 'datetime', sortable: 'custom' },
116
- { key: 'action', prop: '', label: '操作', width: 140, align: 'center', fixed: 'right', type: 'action' }
117
- ],
118
- visibleKeys: [],
119
- rows: [],
120
- total: 0,
121
- page: 1,
122
- pageSize: 10,
123
- sortField: 'createTm',
124
- sortOrder: 'descending',
125
- tableKey: 0
126
- }
127
- },
128
- components: {
129
- ColumnSettings,
130
- NumberRange
131
- },
132
- created() {
133
- this.visibleKeys = this.columns.map(c => c.key)
134
- this.fetch()
135
- },
136
- methods: {
137
- async fetch() {
138
- const { keyword, type, amountRange, ageMin, ageMax } = this.searchForm
139
- const params = {
140
- page: this.page,
141
- pageSize: this.pageSize,
142
- sortField: this.sortField,
143
- sortOrder: this.sortOrder
144
- }
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
151
- const res = await get('/meeting/list', params)
152
- this.rows = Array.isArray(res.list) ? res.list : []
153
- this.total = Number.isFinite(res.total) ? res.total : this.rows.length
154
- },
155
- onSearch() {
156
- this.$refs.searchFormRef.validate((valid) => {
157
- if (!valid) return
158
- this.page = 1
159
- this.fetch()
160
- })
161
- },
162
- onReset() {
163
- this.$refs.searchFormRef.resetFields()
164
- this.page = 1
165
- this.fetch()
166
- },
167
- onPageChange(p) {
168
- this.page = p
169
- this.fetch()
170
- },
171
- onSortChange({ prop, order }) {
172
- this.sortField = prop || ''
173
- this.sortOrder = order || ''
174
- this.page = 1
175
- this.fetch()
176
- },
177
- // 通过改变 key 强制表格重渲染,确保动态列显示/隐藏立即生效
178
- bumpTableKey() {
179
- this.tableKey += 1
180
- },
181
- onView(row) {
182
- const lines = [
183
- `会议主题:${row.meetSubj || ''}`,
184
- `内容:${row.plainContent || ''}`,
185
- `所属机构:${row.organizationName || ''}`,
186
- `会议类型:${this.formatType(row.meetTypeCd)}`,
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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue2server7",
3
- "version": "7.0.19",
3
+ "version": "7.0.20",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "dev": "nodemon --watch src --ext ts --exec \"ts-node src/app.ts\"",