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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue_zhongyou",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "keywords": [],
@@ -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
+