vue_zhongyou 1.0.8 → 1.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.
package/package.json
CHANGED
|
@@ -0,0 +1,399 @@
|
|
|
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
|
+
:placeholder="field.placeholder || `请输入${field.label}`"
|
|
12
|
+
:rules="field.rules"
|
|
13
|
+
:maxlength="field.maxlength"
|
|
14
|
+
:rows="field.rows || (field.type === 'textarea' ? 3 : undefined)"
|
|
15
|
+
:autosize="field.type === 'textarea'"
|
|
16
|
+
:disabled="field.disabled"
|
|
17
|
+
clearable
|
|
18
|
+
@update:model-value="(val) => updateFieldValue(field.field, val)"
|
|
19
|
+
/>
|
|
20
|
+
|
|
21
|
+
<!-- 单选 -->
|
|
22
|
+
<van-field v-else-if="field.type === 'radio'" name="radio" :label="field.label">
|
|
23
|
+
<template #input>
|
|
24
|
+
<van-radio-group
|
|
25
|
+
v-model="formData[field.field]"
|
|
26
|
+
direction="horizontal"
|
|
27
|
+
@change="(val) => updateFieldValue(field.field, val)"
|
|
28
|
+
>
|
|
29
|
+
<van-radio
|
|
30
|
+
v-for="option in field.options"
|
|
31
|
+
:key="option.value"
|
|
32
|
+
:name="option.value"
|
|
33
|
+
shape="dot"
|
|
34
|
+
>
|
|
35
|
+
{{ option.label }}
|
|
36
|
+
</van-radio>
|
|
37
|
+
</van-radio-group>
|
|
38
|
+
</template>
|
|
39
|
+
</van-field>
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
<!-- 多选 -->
|
|
43
|
+
<van-field v-else-if="field.type === 'checkbox'" name="checkboxGroup" :label="field.label">
|
|
44
|
+
<template #input>
|
|
45
|
+
<van-checkbox-group
|
|
46
|
+
v-model="formData[field.field]"
|
|
47
|
+
direction="horizontal"
|
|
48
|
+
@change="(val) => updateFieldValue(field.field, val)"
|
|
49
|
+
>
|
|
50
|
+
<van-checkbox
|
|
51
|
+
v-for="option in field.options"
|
|
52
|
+
:key="option.value"
|
|
53
|
+
:name="option.value"
|
|
54
|
+
shape="square"
|
|
55
|
+
>
|
|
56
|
+
{{ option.label }}
|
|
57
|
+
</van-checkbox>
|
|
58
|
+
</van-checkbox-group>
|
|
59
|
+
</template>
|
|
60
|
+
</van-field>
|
|
61
|
+
|
|
62
|
+
<!-- 选择器 -->
|
|
63
|
+
<van-field
|
|
64
|
+
v-else-if="field.type === 'select'"
|
|
65
|
+
is-link
|
|
66
|
+
readonly
|
|
67
|
+
:label="field.label"
|
|
68
|
+
:placeholder="field.placeholder || `请选择${field.label}`"
|
|
69
|
+
:model-value="getSelectLabel(field)"
|
|
70
|
+
@click="openSelect(field)"
|
|
71
|
+
/>
|
|
72
|
+
|
|
73
|
+
<!-- 时间范围 -->
|
|
74
|
+
<van-field
|
|
75
|
+
v-else-if="field.type === 'dateRange'"
|
|
76
|
+
is-link
|
|
77
|
+
readonly
|
|
78
|
+
:label="field.label"
|
|
79
|
+
:placeholder="field.placeholder || '请选择时间范围'"
|
|
80
|
+
:model-value="getDateRangeText(field)"
|
|
81
|
+
@click="openDateRange(field)"
|
|
82
|
+
/>
|
|
83
|
+
|
|
84
|
+
<!-- 地址选择 -->
|
|
85
|
+
<van-field
|
|
86
|
+
v-else-if="field.type === 'address'"
|
|
87
|
+
is-link
|
|
88
|
+
readonly
|
|
89
|
+
:label="field.label"
|
|
90
|
+
:placeholder="field.placeholder || '请选择地址'"
|
|
91
|
+
:model-value="getAddressText(field)"
|
|
92
|
+
@click="openAddressPicker(field)"
|
|
93
|
+
/>
|
|
94
|
+
|
|
95
|
+
<!-- 自定义内容 -->
|
|
96
|
+
<slot v-else :field="field" :value="formData[field.field]" />
|
|
97
|
+
</template>
|
|
98
|
+
</van-cell-group>
|
|
99
|
+
|
|
100
|
+
<slot name="actions">
|
|
101
|
+
<div class="form-actions">
|
|
102
|
+
<van-button size="small" @click.prevent="handleReset">
|
|
103
|
+
{{ resetButtonLabel }}
|
|
104
|
+
</van-button>
|
|
105
|
+
<van-button
|
|
106
|
+
type="primary"
|
|
107
|
+
size="small"
|
|
108
|
+
native-type="submit"
|
|
109
|
+
:loading="submitting"
|
|
110
|
+
>
|
|
111
|
+
{{ submitButtonLabel }}
|
|
112
|
+
</van-button>
|
|
113
|
+
</div>
|
|
114
|
+
</slot>
|
|
115
|
+
</van-form>
|
|
116
|
+
|
|
117
|
+
<!-- 下拉选择 -->
|
|
118
|
+
<van-popup v-model:show="selectPopup.visible" position="bottom" round>
|
|
119
|
+
<van-picker
|
|
120
|
+
show-toolbar
|
|
121
|
+
:columns="selectPopup.options"
|
|
122
|
+
@confirm="onSelectConfirm"
|
|
123
|
+
@cancel="closeSelect"
|
|
124
|
+
/>
|
|
125
|
+
</van-popup>
|
|
126
|
+
|
|
127
|
+
<!-- 时间范围 -->
|
|
128
|
+
<van-calendar
|
|
129
|
+
v-model:show="dateRangePopup.visible"
|
|
130
|
+
type="range"
|
|
131
|
+
color="#1989fa"
|
|
132
|
+
:min-date="dateRangePopup.minDate"
|
|
133
|
+
:max-date="dateRangePopup.maxDate"
|
|
134
|
+
@confirm="onDateRangeConfirm"
|
|
135
|
+
@cancel="closeDateRange"
|
|
136
|
+
/>
|
|
137
|
+
|
|
138
|
+
<!-- 地址 -->
|
|
139
|
+
<van-popup v-model:show="addressPopup.visible" position="bottom" round>
|
|
140
|
+
<van-area
|
|
141
|
+
:area-list="areaList"
|
|
142
|
+
:columns-placeholder="addressPopup.columnsPlaceholder || ['请选择', '请选择', '请选择']"
|
|
143
|
+
@confirm="onAddressConfirm"
|
|
144
|
+
@cancel="closeAddressPicker"
|
|
145
|
+
/>
|
|
146
|
+
</van-popup>
|
|
147
|
+
</template>
|
|
148
|
+
|
|
149
|
+
<script setup>
|
|
150
|
+
import { computed, reactive, ref, watch } from 'vue'
|
|
151
|
+
import { areaList } from '@vant/area-data'
|
|
152
|
+
const props = defineProps({
|
|
153
|
+
schema: {
|
|
154
|
+
type: Array,
|
|
155
|
+
default: () => []
|
|
156
|
+
},
|
|
157
|
+
modelValue: {
|
|
158
|
+
type: Object,
|
|
159
|
+
default: () => ({})
|
|
160
|
+
},
|
|
161
|
+
submitButtonLabel: {
|
|
162
|
+
type: String,
|
|
163
|
+
default: '提交'
|
|
164
|
+
},
|
|
165
|
+
resetButtonLabel: {
|
|
166
|
+
type: String,
|
|
167
|
+
default: '重置'
|
|
168
|
+
},
|
|
169
|
+
submitting: {
|
|
170
|
+
type: Boolean,
|
|
171
|
+
default: false
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const emit = defineEmits(['update:modelValue', 'change', 'submit', 'reset'])
|
|
176
|
+
|
|
177
|
+
const formData = reactive({})
|
|
178
|
+
|
|
179
|
+
const selectPopup = ref({
|
|
180
|
+
visible: false,
|
|
181
|
+
field: null,
|
|
182
|
+
options: []
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
const dateRangePopup = ref({
|
|
186
|
+
visible: false,
|
|
187
|
+
field: null,
|
|
188
|
+
minDate: null,
|
|
189
|
+
maxDate: null
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
const addressPopup = ref({
|
|
193
|
+
visible: false,
|
|
194
|
+
field: null,
|
|
195
|
+
areaList: null,
|
|
196
|
+
columnsPlaceholder: null
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
const normalizedSchema = computed(() =>
|
|
200
|
+
props.schema.map((item) => ({
|
|
201
|
+
...item,
|
|
202
|
+
field: item.field || item.name
|
|
203
|
+
}))
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
const getDefaultValue = (type) => {
|
|
207
|
+
if (type === 'checkbox') return []
|
|
208
|
+
if (type === 'dateRange') return ['', '']
|
|
209
|
+
if (type === 'address') return { province: '', city: '', county: '', code: '' }
|
|
210
|
+
return ''
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const snapshot = () => JSON.parse(JSON.stringify(formData))
|
|
214
|
+
|
|
215
|
+
const initializeFormData = () => {
|
|
216
|
+
normalizedSchema.value.forEach((field) => {
|
|
217
|
+
const key = field.field
|
|
218
|
+
const incoming = props.modelValue[key]
|
|
219
|
+
formData[key] = incoming !== undefined ? incoming : field.default ?? getDefaultValue(field.type)
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
watch(
|
|
224
|
+
() => props.schema,
|
|
225
|
+
() => {
|
|
226
|
+
initializeFormData()
|
|
227
|
+
},
|
|
228
|
+
{ immediate: true, deep: true }
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
watch(
|
|
232
|
+
() => props.modelValue,
|
|
233
|
+
(val) => {
|
|
234
|
+
if (!val) return
|
|
235
|
+
Object.keys(val).forEach((key) => {
|
|
236
|
+
formData[key] = val[key]
|
|
237
|
+
})
|
|
238
|
+
},
|
|
239
|
+
{ deep: true }
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
const updateFieldValue = (field, value) => {
|
|
243
|
+
formData[field] = value
|
|
244
|
+
emit('update:modelValue', snapshot())
|
|
245
|
+
emit('change', { field, value, values: snapshot() })
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const handleSubmit = () => {
|
|
249
|
+
emit('submit', snapshot())
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const handleReset = () => {
|
|
253
|
+
initializeFormData()
|
|
254
|
+
emit('update:modelValue', snapshot())
|
|
255
|
+
emit('reset', snapshot())
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Select
|
|
259
|
+
const openSelect = (field) => {
|
|
260
|
+
selectPopup.value = {
|
|
261
|
+
visible: true,
|
|
262
|
+
field,
|
|
263
|
+
options: (field.options || []).map((opt) => ({
|
|
264
|
+
text: opt.label,
|
|
265
|
+
value: opt.value
|
|
266
|
+
}))
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const closeSelect = () => {
|
|
271
|
+
selectPopup.value.visible = false
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const onSelectConfirm = ({ selectedOptions }) => {
|
|
275
|
+
const option = selectedOptions?.[0]
|
|
276
|
+
if (option && selectPopup.value.field) {
|
|
277
|
+
updateFieldValue(selectPopup.value.field.field, option.value)
|
|
278
|
+
}
|
|
279
|
+
closeSelect()
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const getSelectLabel = (field) => {
|
|
283
|
+
const value = formData[field.field]
|
|
284
|
+
const option = (field.options || []).find((opt) => opt.value === value)
|
|
285
|
+
return option ? option.label : ''
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Date range
|
|
289
|
+
const openDateRange = (field) => {
|
|
290
|
+
dateRangePopup.value.visible = true
|
|
291
|
+
dateRangePopup.value.field = field
|
|
292
|
+
// 计算前后五年的日期范围
|
|
293
|
+
const currentDate = new Date()
|
|
294
|
+
const fiveYearsAgo = new Date(currentDate.getFullYear() - 5, currentDate.getMonth(), currentDate.getDate())
|
|
295
|
+
const fiveYearsLater = new Date(currentDate.getFullYear() + 5, currentDate.getMonth(), currentDate.getDate())
|
|
296
|
+
|
|
297
|
+
// 如果没有设置minDate和maxDate,则使用前后五年的范围
|
|
298
|
+
dateRangePopup.value.minDate = field.minDate || fiveYearsAgo
|
|
299
|
+
dateRangePopup.value.maxDate = field.maxDate || fiveYearsLater
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const closeDateRange = () => {
|
|
303
|
+
dateRangePopup.value.visible = false
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const formatDate = (date) => {
|
|
307
|
+
const y = date.getFullYear()
|
|
308
|
+
const m = `${date.getMonth() + 1}`.padStart(2, '0')
|
|
309
|
+
const d = `${date.getDate()}`.padStart(2, '0')
|
|
310
|
+
return `${y}-${m}-${d}`
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const onDateRangeConfirm = (values) => {
|
|
314
|
+
if (!values || values.length !== 2 || !dateRangePopup.value.field) {
|
|
315
|
+
closeDateRange()
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
const [start, end] = values
|
|
319
|
+
updateFieldValue(dateRangePopup.value.field.field, [formatDate(start), formatDate(end)])
|
|
320
|
+
closeDateRange()
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const getDateRangeText = (field) => {
|
|
324
|
+
const value = formData[field.field]
|
|
325
|
+
if (Array.isArray(value) && value[0] && value[1]) {
|
|
326
|
+
return `${value[0]} ~ ${value[1]}`
|
|
327
|
+
}
|
|
328
|
+
return ''
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Address
|
|
332
|
+
const openAddressPicker = (field) => {
|
|
333
|
+
addressPopup.value = {
|
|
334
|
+
visible: true,
|
|
335
|
+
field,
|
|
336
|
+
areaList: areaList,
|
|
337
|
+
columnsPlaceholder: field.columnsPlaceholder
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const closeAddressPicker = () => {
|
|
342
|
+
addressPopup.value.visible = false
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const onAddressConfirm = ({ selectedOptions }) => {
|
|
346
|
+
if (!selectedOptions || !addressPopup.value.field) {
|
|
347
|
+
closeAddressPicker()
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
console.log(selectedOptions);
|
|
351
|
+
|
|
352
|
+
const [province, city, county] = selectedOptions
|
|
353
|
+
updateFieldValue(addressPopup.value.field.field, {
|
|
354
|
+
province: province?.text || '',
|
|
355
|
+
city: city?.text || '',
|
|
356
|
+
county: county?.text || '',
|
|
357
|
+
code: county?.value || city?.value || province?.value || ''
|
|
358
|
+
})
|
|
359
|
+
closeAddressPicker()
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const getAddressText = (field) => {
|
|
363
|
+
const value = formData[field.field]
|
|
364
|
+
if (!value) return ''
|
|
365
|
+
const parts = [value.province, value.city, value.county].filter(Boolean)
|
|
366
|
+
return parts.join(' ')
|
|
367
|
+
}
|
|
368
|
+
</script>
|
|
369
|
+
|
|
370
|
+
<style scoped lang="scss">
|
|
371
|
+
.dynamic-mobile-form {
|
|
372
|
+
.field-wrapper {
|
|
373
|
+
padding: 12px 16px;
|
|
374
|
+
border-bottom: 1px solid #f7f8fa;
|
|
375
|
+
&:last-of-type {
|
|
376
|
+
border-bottom: none;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.field-label {
|
|
380
|
+
font-size: 14px;
|
|
381
|
+
color: #666;
|
|
382
|
+
margin-bottom: 8px;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.form-actions {
|
|
387
|
+
display: flex;
|
|
388
|
+
flex-direction: row-reverse;
|
|
389
|
+
|
|
390
|
+
padding: 16px;
|
|
391
|
+
gap: 12px;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
:deep(.van-cell) {
|
|
396
|
+
padding: 8px;
|
|
397
|
+
}
|
|
398
|
+
</style>
|
|
399
|
+
|