vue_zhongyou 1.0.8 → 1.0.10

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": "vue_zhongyou",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "keywords": [],
@@ -0,0 +1,637 @@
1
+ <template>
2
+ <van-form class="dynamic-mobile-form" @submit="handleSubmit" >
3
+ <van-cell-group inset style="margin: 0px;">
4
+ <template v-for="field in normalizedSchema" :key="field.field">
5
+ <!-- 输入框 -->
6
+ <van-field
7
+ v-if="field.type === 'input' || field.type === 'textarea'"
8
+ v-model="formData[field.field]"
9
+ :type="field.type === 'textarea' ? 'textarea' : 'text'"
10
+ :label="field.label"
11
+ :label-width="60"
12
+ :placeholder="field.placeholder || `请输入${field.label}`"
13
+ :rules="field.rules || []"
14
+ :maxlength="field.maxlength"
15
+ :rows="field.rows || (field.type === 'textarea' ? 3 : undefined)"
16
+ :autosize="field.type === 'textarea'"
17
+ :disabled="field.disabled"
18
+ clearable
19
+ @update:model-value="(val) => updateFieldValue(field.field, val)"
20
+ />
21
+
22
+ <!-- 单选 -->
23
+ <van-field v-else-if="field.type === 'radio'" name="radio" :label="field.label" :rules="field.rules || []">
24
+ <template #input>
25
+ <van-radio-group
26
+ v-model="formData[field.field]"
27
+ direction="horizontal"
28
+ @change="(val) => updateFieldValue(field.field, val)"
29
+ >
30
+ <van-radio
31
+ v-for="option in field.options"
32
+ :key="option.value"
33
+ :name="option.value"
34
+ shape="dot"
35
+ >
36
+ {{ option.label }}
37
+ </van-radio>
38
+ </van-radio-group>
39
+ </template>
40
+ </van-field>
41
+
42
+
43
+ <!-- 多选 -->
44
+ <van-field v-else-if="field.type === 'checkbox'" name="checkboxGroup" :label="field.label" :rules="field.rules || []">
45
+ <template #input>
46
+ <van-checkbox-group
47
+ v-model="formData[field.field]"
48
+ direction="horizontal"
49
+ @change="(val) => updateFieldValue(field.field, val)"
50
+ >
51
+ <van-checkbox
52
+ v-for="option in field.options"
53
+ :key="option.value"
54
+ :name="option.value"
55
+ shape="square"
56
+ >
57
+ {{ option.label }}
58
+ </van-checkbox>
59
+ </van-checkbox-group>
60
+ </template>
61
+ </van-field>
62
+
63
+ <!-- 选择器 -->
64
+ <van-field
65
+ v-else-if="field.type === 'select'"
66
+ is-link
67
+ readonly
68
+ :label="field.label"
69
+ :placeholder="field.placeholder || `请选择${field.label}`"
70
+ :model-value="getSelectLabel(field)"
71
+ :rules="field.rules || []"
72
+ @click="openSelect(field)"
73
+ />
74
+
75
+ <!-- 时间范围 -->
76
+ <van-field
77
+ v-else-if="field.type === 'dateRange'"
78
+ is-link
79
+ readonly
80
+ :label="field.label"
81
+ :placeholder="field.placeholder || '请选择时间范围'"
82
+ :rules="field.rules || []"
83
+ :model-value="getDateRangeText(field)"
84
+ @click="openDateRange(field)"
85
+ />
86
+
87
+ <!-- 日期时间范围 -->
88
+ <van-field
89
+ v-else-if="field.type === 'datetimeRange'"
90
+ is-link
91
+ readonly
92
+ :label="field.label"
93
+ :label-width="60"
94
+ :placeholder="field.placeholder || '请选择日期时间范围'"
95
+ :model-value="getDateTimeRangeText(field)"
96
+ :rules="field.rules || []"
97
+ @click="openDateTimeRange(field)"
98
+ />
99
+
100
+ <!-- 地址选择 -->
101
+ <van-field
102
+ v-else-if="field.type === 'address'"
103
+ is-link
104
+ readonly
105
+ :label="field.label"
106
+ :placeholder="field.placeholder || '请选择地址'"
107
+ :rules="field.rules || []"
108
+ :model-value="getAddressText(field)"
109
+ @click="openAddressPicker(field)"
110
+ />
111
+
112
+
113
+ <!-- 自定义内容 -->
114
+ <slot v-else :field="field" :value="formData[field.field]" />
115
+ </template>
116
+ </van-cell-group>
117
+
118
+ <slot name="actions">
119
+ <div class="form-actions">
120
+ <van-button size="small" @click.prevent="handleReset">
121
+ {{ resetButtonLabel }}
122
+ </van-button>
123
+ <van-button
124
+ type="primary"
125
+ size="small"
126
+ native-type="submit"
127
+ :loading="submitting"
128
+ >
129
+ {{ submitButtonLabel }}
130
+ </van-button>
131
+ </div>
132
+ </slot>
133
+ </van-form>
134
+
135
+ <!-- 下拉选择 -->
136
+ <van-popup v-model:show="selectPopup.visible" position="bottom" round>
137
+ <van-picker
138
+ show-toolbar
139
+ :columns="selectPopup.options"
140
+ @confirm="onSelectConfirm"
141
+ @cancel="closeSelect"
142
+ />
143
+
144
+ </van-popup>
145
+
146
+ <!-- 时间范围 -->
147
+ <van-calendar
148
+ v-model:show="dateRangePopup.visible"
149
+ type="range"
150
+ color="#1989fa"
151
+ :min-date="dateRangePopup.minDate"
152
+ :max-date="dateRangePopup.maxDate"
153
+ @confirm="onDateRangeConfirm"
154
+ @cancel="closeDateRange"
155
+ />
156
+
157
+ <!-- 日期时间范围选择器 -->
158
+ <van-popup v-model:show="dateTimeRangePopup.visible" position="bottom" round>
159
+ <div class="datetime-range-popup">
160
+ <div class="popup-header">
161
+ <span class="cancel-btn" @click="closeDateTimeRange">取消</span>
162
+ <span class="confirm-btn" @click="confirmDateTimeRange">确定</span>
163
+ </div>
164
+ <div class="time_title">开始时间</div>
165
+ <nut-date-picker
166
+ v-model="dateTimeRangePopup.startTime"
167
+ type="datetime"
168
+ :show-toolbar="false"
169
+ :filter="startTimeFilter"
170
+ @change="(e)=>onDateTimeChange(e,'startTime')"
171
+ />
172
+ <div class="time_title">结束时间</div>
173
+ <nut-date-picker
174
+ v-model="dateTimeRangePopup.endTime"
175
+ type="datetime"
176
+ :show-toolbar="false"
177
+ :filter="endTimeFilter"
178
+ @change="(e)=>onDateTimeChange(e,'endTime')"
179
+ />
180
+ </div>
181
+ </van-popup>
182
+
183
+
184
+ <!-- 地址 -->
185
+ <van-popup v-model:show="addressPopup.visible" position="bottom" round>
186
+ <van-area
187
+ :area-list="areaList"
188
+ :columns-placeholder="addressPopup.columnsPlaceholder || ['请选择', '请选择', '请选择']"
189
+ @confirm="onAddressConfirm"
190
+ @cancel="closeAddressPicker"
191
+ />
192
+ </van-popup>
193
+ </template>
194
+
195
+ <script setup>
196
+ import { computed, reactive, ref, watch } from 'vue'
197
+ import { areaList } from '@vant/area-data'
198
+ import { showNotify } from 'vant';
199
+
200
+ const props = defineProps({
201
+ schema: {
202
+ type: Array,
203
+ default: () => []
204
+ },
205
+ modelValue: {
206
+ type: Object,
207
+ default: () => ({})
208
+ },
209
+ submitButtonLabel: {
210
+ type: String,
211
+ default: '提交'
212
+ },
213
+ resetButtonLabel: {
214
+ type: String,
215
+ default: '重置'
216
+ },
217
+ submitting: {
218
+ type: Boolean,
219
+ default: false
220
+ }
221
+ })
222
+
223
+ const emit = defineEmits(['update:modelValue', 'change', 'submit', 'reset'])
224
+
225
+ const formData = reactive({})
226
+
227
+ const selectPopup = ref({
228
+ visible: false,
229
+ field: null,
230
+ options: []
231
+ })
232
+
233
+ const dateRangePopup = ref({
234
+ visible: false,
235
+ field: null,
236
+ minDate: null,
237
+ maxDate: null
238
+ })
239
+ // 日期时间范围弹窗状态
240
+ const dateTimeRangePopup = ref({
241
+ visible: false, // 弹窗显示状态
242
+ field: null, // 当前操作的字段
243
+ startTime: new Date(), // 开始时间
244
+ endTime: new Date(), // 结束时间
245
+ currentDate: new Date(), // 当前选中日期
246
+ currentHour: '8' // 当前选择的小时值,默认为'12'
247
+ })
248
+
249
+
250
+ const addressPopup = ref({
251
+ visible: false,
252
+ field: null,
253
+ areaList: null,
254
+ columnsPlaceholder: null
255
+ })
256
+
257
+
258
+
259
+
260
+ const normalizedSchema = computed(() =>
261
+ props.schema.map((item) => ({
262
+ ...item,
263
+ field: item.field || item.name
264
+ }))
265
+ )
266
+
267
+ const getDefaultValue = (type) => {
268
+ if (type === 'checkbox') return []
269
+ if (type === 'dateRange') return ['', '']
270
+ if (type === 'datetimeRange') return ['', '']
271
+ if (type === 'address') return { province: '', city: '', county: '', code: '' }
272
+ return ''
273
+ }
274
+
275
+ const snapshot = () => JSON.parse(JSON.stringify(formData))
276
+
277
+ const initializeFormData = () => {
278
+ normalizedSchema.value.forEach((field) => {
279
+ const key = field.field
280
+ const incoming = props.modelValue[key]
281
+ formData[key] = incoming !== undefined ? incoming : field.default ?? getDefaultValue(field.type)
282
+ })
283
+ }
284
+
285
+ watch(
286
+ () => props.schema,
287
+ () => {
288
+ initializeFormData()
289
+ },
290
+ { immediate: true, deep: true }
291
+ )
292
+
293
+ watch(
294
+ () => props.modelValue,
295
+ (val) => {
296
+ if (!val) return
297
+ Object.keys(val).forEach((key) => {
298
+ formData[key] = val[key]
299
+ })
300
+ },
301
+ { deep: true }
302
+ )
303
+
304
+ const updateFieldValue = (field, value) => {
305
+ formData[field] = value
306
+ emit('update:modelValue', snapshot())
307
+ emit('change', { field, value, values: snapshot() })
308
+ }
309
+
310
+ const handleSubmit = () => {
311
+ emit('submit', snapshot())
312
+ }
313
+
314
+ const handleReset = () => {
315
+ initializeFormData()
316
+ emit('update:modelValue', snapshot())
317
+ emit('reset', snapshot())
318
+ }
319
+
320
+ // Select
321
+ const openSelect = (field) => {
322
+ selectPopup.value = {
323
+ visible: true,
324
+ field,
325
+ options: (field.options || []).map((opt) => ({
326
+ text: opt.label,
327
+ value: opt.value
328
+ }))
329
+ }
330
+ }
331
+
332
+ const closeSelect = () => {
333
+ selectPopup.value.visible = false
334
+ }
335
+
336
+ const onSelectConfirm = ({ selectedOptions }) => {
337
+ const option = selectedOptions?.[0]
338
+ if (option && selectPopup.value.field) {
339
+ updateFieldValue(selectPopup.value.field.field, option.value)
340
+ }
341
+ closeSelect()
342
+ }
343
+
344
+ const getSelectLabel = (field) => {
345
+ const value = formData[field.field]
346
+ const option = (field.options || []).find((opt) => opt.value === value)
347
+ return option ? option.label : ''
348
+ }
349
+
350
+ // Date range
351
+ const openDateRange = (field) => {
352
+ dateRangePopup.value.visible = true
353
+ dateRangePopup.value.field = field
354
+ // 计算前后五年的日期范围
355
+ const currentDate = new Date()
356
+ const fiveYearsAgo = new Date(currentDate.getFullYear() - 5, currentDate.getMonth(), currentDate.getDate())
357
+ const fiveYearsLater = new Date(currentDate.getFullYear() + 5, currentDate.getMonth(), currentDate.getDate())
358
+
359
+ // 如果没有设置minDate和maxDate,则使用前后五年的范围
360
+ dateRangePopup.value.minDate = field.minDate || fiveYearsAgo
361
+ dateRangePopup.value.maxDate = field.maxDate || fiveYearsLater
362
+ }
363
+
364
+ const closeDateRange = () => {
365
+ dateRangePopup.value.visible = false
366
+ }
367
+
368
+ // 打开日期时间范围选择
369
+ const openDateTimeRange = (field) => {
370
+ dateTimeRangePopup.value.visible = true
371
+ dateTimeRangePopup.value.field = field
372
+
373
+ // 设置默认时间:开始时间为当天8:30,结束时间为当天17:30
374
+ const today = new Date()
375
+ const defaultStartTime = new Date(today)
376
+ defaultStartTime.setHours(8, 30, 0, 0)
377
+ const defaultEndTime = new Date(today)
378
+ defaultEndTime.setHours(17, 30, 0, 0)
379
+
380
+ // 将field的startTime和endTime转换为日期时间格式
381
+ dateTimeRangePopup.value.startTime = typeof field.startTime === 'string' ? reverseFormatDateTime(field.startTime) : defaultStartTime
382
+ dateTimeRangePopup.value.endTime = typeof field.endTime === 'string' ? reverseFormatDateTime(field.endTime) : defaultEndTime
383
+ }
384
+
385
+ const closeDateTimeRange = () => {
386
+ dateTimeRangePopup.value.visible = false
387
+ }
388
+
389
+ // 日期时间选择器改变事件处理
390
+ const onDateTimeChange = (params,type) => {
391
+
392
+ // 更新当前选择的小时值
393
+ if (params && params.selectedValue && params.columnIndex === 3) {
394
+ // 更新小时值
395
+ // 根据小时值设置默认分钟值
396
+ if (['08', '17'].includes(params.selectedValue[3])) {
397
+ // 小时为08或17时,分钟设为30
398
+ if(type === 'startTime'){
399
+ dateTimeRangePopup.value.startTime.setMinutes(30)
400
+ }else{
401
+ dateTimeRangePopup.value.endTime.setMinutes(30)
402
+ }
403
+ } else if (['12', '13'].includes(params.selectedValue[3])) {
404
+ // 小时为12或13时,分钟设为00
405
+ if(type === 'startTime'){
406
+ dateTimeRangePopup.value.startTime.setMinutes(0)
407
+ }else{
408
+ dateTimeRangePopup.value.endTime.setMinutes(0)
409
+ }
410
+ }
411
+ if(type === 'startTime'){
412
+ dateTimeRangePopup.value.currentStartHour = params.selectedValue[3];
413
+ }else{
414
+ dateTimeRangePopup.value.currentEndHour = params.selectedValue[3];
415
+ }
416
+
417
+ }
418
+ }
419
+
420
+ // 确认日期时间范围选择
421
+ const confirmDateTimeRange = () => {
422
+ if(!dateTimeRangePopup.value.startTime || !dateTimeRangePopup.value.endTime){
423
+ return
424
+ }
425
+ if(dateTimeRangePopup.value.startTime.getTime()>=dateTimeRangePopup.value.endTime.getTime()){
426
+ showNotify({ type: 'warning', message: '结束时间必须晚于开始时间' });
427
+ return
428
+ }
429
+ console.log(dateTimeRangePopup.value.startTime.getTime()<dateTimeRangePopup.value.endTime.getTime())
430
+ // 格式化日期时间范围
431
+ const startDateStr = formatDateTime(dateTimeRangePopup.value.startTime)
432
+ const endDateStr = formatDateTime(dateTimeRangePopup.value.endTime)
433
+ console.log('开始时间:', startDateStr);
434
+ console.log('结束时间:', endDateStr);
435
+ // 更新表单数据
436
+ if (dateTimeRangePopup.value.field) {
437
+ updateFieldValue(dateTimeRangePopup.value.field.field, [startDateStr, endDateStr])
438
+ }
439
+ // 关闭弹窗
440
+ closeDateTimeRange()
441
+ }
442
+
443
+ // 日期时间选择器筛选函数
444
+ const startTimeFilter = (type, options) => {
445
+ if (type === 'hour') {
446
+ return options.filter(option => ['08', '12', '13', '17'].includes(option.value))
447
+ } else if (type === 'minute') {
448
+ // 当小时位是'08'或者'12'时,分钟只保留'00'
449
+ if (['12','13'].includes(dateTimeRangePopup.value.currentStartHour)) {
450
+ return options.filter(option => ['00'].includes(option.value))
451
+ }else{
452
+ return options.filter(option => ['30'].includes(option.value))
453
+ }
454
+ }
455
+ return options
456
+ }
457
+ const endTimeFilter = (type, options) => {
458
+ if (type === 'hour') {
459
+ return options.filter(option => ['08', '12', '13', '17'].includes(option.value))
460
+ } else if (type === 'minute') {
461
+ // 当小时位是'08'或者'12'时,分钟只保留'00'
462
+ if (['12','13'].includes(dateTimeRangePopup.value.currentEndHour)) {
463
+ return options.filter(option => ['00'].includes(option.value))
464
+ }else{
465
+ return options.filter(option => ['30'].includes(option.value))
466
+ }
467
+ }
468
+ return options
469
+ }
470
+ // 格式化日期时间为字符串
471
+ var formatDateTime = (date) => {
472
+ const y = date.getFullYear()
473
+ const m = `${date.getMonth() + 1}`.padStart(2, '0')
474
+ const d = `${date.getDate()}`.padStart(2, '0')
475
+ const h = `${date.getHours()}`.padStart(2, '0')
476
+ const min = `${date.getMinutes()}`.padStart(2, '0')
477
+ return `${y}-${m}-${d} ${h}:${min}`
478
+ }
479
+ // 解析字符串为日期时间对象
480
+ var reverseFormatDateTime = (dateStr) => {
481
+ const [datePart, timePart] = dateStr.split(' ')
482
+ const [y, m, d] = datePart.split('-')
483
+ const [h, min] = timePart.split(':')
484
+ return new Date(`${y}-${m}-${d}T${h}:${min}`)
485
+ }
486
+
487
+ const formatDate = (date) => {
488
+ const y = date.getFullYear()
489
+ const m = `${date.getMonth() + 1}`.padStart(2, '0')
490
+ const d = `${date.getDate()}`.padStart(2, '0')
491
+ return `${y}-${m}-${d}`
492
+ }
493
+
494
+ const onDateRangeConfirm = (values) => {
495
+ if (!values || values.length !== 2 || !dateRangePopup.value.field) {
496
+ closeDateRange()
497
+ return
498
+ }
499
+ const [start, end] = values
500
+ updateFieldValue(dateRangePopup.value.field.field, [formatDate(start), formatDate(end)])
501
+ closeDateRange()
502
+ }
503
+
504
+ const getDateRangeText = (field) => {
505
+ const value = formData[field.field]
506
+ if (Array.isArray(value) && value[0] && value[1]) {
507
+ return `${value[0]} ~ ${value[1]}`
508
+ }
509
+ return ''
510
+ }
511
+
512
+ const getDateTimeRangeText = (field) => {
513
+ const value = formData[field.field]
514
+ if (Array.isArray(value) && value[0] && value[1]) {
515
+ // 只显示日期部分用于预览
516
+ const startDate = value[0]
517
+ const endDate = value[1]
518
+ return `${startDate} ~ ${endDate}`
519
+ }
520
+ return ''
521
+ }
522
+
523
+ // Address
524
+ const openAddressPicker = (field) => {
525
+ addressPopup.value = {
526
+ visible: true,
527
+ field,
528
+ areaList: areaList,
529
+ columnsPlaceholder: field.columnsPlaceholder
530
+ }
531
+ }
532
+
533
+ const closeAddressPicker = () => {
534
+ addressPopup.value.visible = false
535
+ }
536
+
537
+ const onAddressConfirm = ({ selectedOptions }) => {
538
+ if (!selectedOptions || !addressPopup.value.field) {
539
+ closeAddressPicker()
540
+ return
541
+ }
542
+ console.log(selectedOptions);
543
+
544
+ const [province, city, county] = selectedOptions
545
+ updateFieldValue(addressPopup.value.field.field, {
546
+ province: province?.text || '',
547
+ city: city?.text || '',
548
+ county: county?.text || '',
549
+ code: county?.value || city?.value || province?.value || ''
550
+ })
551
+ closeAddressPicker()
552
+ }
553
+
554
+ const getAddressText = (field) => {
555
+ const value = formData[field.field]
556
+ if (!value) return ''
557
+ const parts = [value.province, value.city, value.county].filter(Boolean)
558
+ return parts.join('')
559
+ }
560
+
561
+
562
+
563
+ </script>
564
+
565
+ <style scoped lang="scss">
566
+ .dynamic-mobile-form {
567
+ .field-wrapper {
568
+ padding: 12px 16px;
569
+ border-bottom: 1px solid #f7f8fa;
570
+ &:last-of-type {
571
+ border-bottom: none;
572
+ }
573
+
574
+ .field-label {
575
+ font-size: 14px;
576
+ color: #666;
577
+ margin-bottom: 8px;
578
+ }
579
+ }
580
+
581
+ .form-actions {
582
+ display: flex;
583
+ flex-direction: row-reverse;
584
+
585
+ padding: 16px;
586
+ gap: 12px;
587
+ }
588
+ }
589
+
590
+ .datetime-range-popup {
591
+ .popup-header {
592
+ display: flex;
593
+ justify-content: space-between;
594
+ align-items: center;
595
+ padding: 16px;
596
+ border-bottom: 1px solid #f0f0f0;
597
+
598
+ .cancel-btn, .confirm-btn {
599
+ font-size: 16px;
600
+ padding: 4px 8px;
601
+ cursor: pointer;
602
+ }
603
+
604
+ .cancel-btn {
605
+ color: #666;
606
+ }
607
+
608
+ .confirm-btn {
609
+ color: #1989fa;
610
+ }
611
+
612
+ .title {
613
+ font-size: 16px;
614
+ font-weight: 500;
615
+ }
616
+ }
617
+
618
+ .datetime-content {
619
+ padding: 20px 16px;
620
+
621
+ :deep(.van-picker-column__item--selected) {
622
+ font-weight: 500;
623
+ }
624
+ }
625
+
626
+ .time_title {
627
+ font-size: 14px;
628
+ color: #666;
629
+ text-align: center;
630
+ }
631
+ }
632
+
633
+ :deep(.van-cell) {
634
+ padding: 8px;
635
+ }
636
+ </style>
637
+