form-create-wot 0.1.0

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.
@@ -0,0 +1,62 @@
1
+ <template>
2
+ <wd-select-picker
3
+ v-model="innerValue"
4
+ :columns="columns"
5
+ :label="label"
6
+ :placeholder="placeholder"
7
+ :disabled="disabled"
8
+ :clearable="clearable"
9
+ @confirm="onConfirm"
10
+ />
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ import { ref, watch, computed } from 'vue'
15
+ import type { OptionItem } from '../types'
16
+
17
+ const props = withDefaults(defineProps<{
18
+ modelValue?: any
19
+ options?: OptionItem[]
20
+ placeholder?: string
21
+ disabled?: boolean
22
+ label?: string
23
+ clearable?: boolean
24
+ mode?: string // antdv: 'multiple' | 'tags'
25
+ }>(), {
26
+ modelValue: undefined,
27
+ options: () => [],
28
+ placeholder: '请选择',
29
+ disabled: false,
30
+ label: '',
31
+ clearable: false,
32
+ })
33
+
34
+ const emit = defineEmits<{
35
+ (e: 'update:modelValue', value: any): void
36
+ (e: 'change', value: any): void
37
+ }>()
38
+
39
+ const innerValue = ref<any>(props.modelValue)
40
+
41
+ watch(() => props.modelValue, (val) => {
42
+ innerValue.value = val
43
+ })
44
+
45
+ watch(innerValue, (val) => {
46
+ emit('update:modelValue', val)
47
+ emit('change', val)
48
+ })
49
+
50
+ /** 将 OptionItem[] 转换为 wd-select-picker columns 格式 */
51
+ const columns = computed(() => {
52
+ return (props.options || []).map(opt => ({
53
+ value: opt.value,
54
+ label: opt.label,
55
+ disabled: opt.disabled,
56
+ }))
57
+ })
58
+
59
+ const onConfirm = ({ value }: any) => {
60
+ innerValue.value = value
61
+ }
62
+ </script>
@@ -0,0 +1,408 @@
1
+ <template>
2
+ <view class="fc-form">
3
+ <!-- 表单项列表 -->
4
+ <view class="fc-form__body">
5
+ <fc-form-item
6
+ v-for="rule in parsedRules"
7
+ :key="rule._id"
8
+ :ref="(el: any) => setItemRef(rule.field, el)"
9
+ :rule="rule"
10
+ :modelValue="getFieldValue(rule.field)"
11
+ :globalDisabled="formDisabled"
12
+ @update:modelValue="(val: any) => setFieldValue(rule.field, val)"
13
+ @validate="onFieldValidate"
14
+ />
15
+ </view>
16
+
17
+ <!-- 按钮区域 -->
18
+ <view class="fc-form__actions" v-if="showSubmitBtn || showResetBtn">
19
+ <wd-button
20
+ v-if="showResetBtn"
21
+ class="fc-form__btn fc-form__btn--reset"
22
+ plain
23
+ @click="handleReset"
24
+ >
25
+ {{ resetBtnText }}
26
+ </wd-button>
27
+ <wd-button
28
+ v-if="showSubmitBtn"
29
+ class="fc-form__btn fc-form__btn--submit"
30
+ type="primary"
31
+ :disabled="submitDisabled"
32
+ @click="handleSubmit"
33
+ >
34
+ {{ submitBtnText }}
35
+ </wd-button>
36
+ </view>
37
+ </view>
38
+ </template>
39
+
40
+ <script setup lang="ts">
41
+ import { ref, reactive, computed, watch, onMounted, nextTick } from 'vue'
42
+ import type { FormRule, FormOption, FormApi } from '../types'
43
+ import { parseRules, flattenFieldRules, normalizeRule } from '../core/parser'
44
+ import { validateValue } from '../core/validator'
45
+ import FcFormItem from './FcFormItem.vue'
46
+
47
+ const props = withDefaults(defineProps<{
48
+ /** JSON 规则数组 */
49
+ rule: FormRule[]
50
+ /** 表单配置 */
51
+ option?: FormOption
52
+ /** 外部绑定的 api 对象 */
53
+ api?: FormApi | null
54
+ }>(), {
55
+ rule: () => [],
56
+ option: () => ({}),
57
+ api: null,
58
+ })
59
+
60
+ const emit = defineEmits<{
61
+ (e: 'submit', formData: Record<string, any>): void
62
+ (e: 'update:api', api: FormApi): void
63
+ (e: 'change', field: string, value: any): void
64
+ (e: 'mounted', api: FormApi): void
65
+ }>()
66
+
67
+ // ===== 规则解析 =====
68
+ const parsedRules = ref<FormRule[]>([])
69
+ const formData = reactive<Record<string, any>>({})
70
+ const errors = reactive<Record<string, string>>({})
71
+ const itemRefs = new Map<string | undefined, any>()
72
+
73
+ watch(() => props.rule, (newRules) => {
74
+ initForm(newRules)
75
+ }, { immediate: true, deep: true })
76
+
77
+ function initForm(rules: FormRule[]) {
78
+ parsedRules.value = parseRules(rules)
79
+ // 初始化 formData
80
+ const fields = flattenFieldRules(parsedRules.value)
81
+ for (const field of fields) {
82
+ if (field.field && !(field.field in formData)) {
83
+ formData[field.field] = field.value ?? getDefaultValue(field.type)
84
+ }
85
+ }
86
+ }
87
+
88
+ function getDefaultValue(type: string): any {
89
+ switch (type) {
90
+ case 'checkbox':
91
+ case 'a-checkbox-group':
92
+ case 'checkboxGroup':
93
+ return []
94
+ case 'switch':
95
+ case 'a-switch':
96
+ return false
97
+ case 'rate':
98
+ case 'a-rate':
99
+ return 0
100
+ case 'slider':
101
+ case 'a-slider':
102
+ return 0
103
+ case 'InputNumber':
104
+ case 'inputNumber':
105
+ case 'a-input-number':
106
+ return 0
107
+ default:
108
+ return undefined
109
+ }
110
+ }
111
+
112
+ // ===== Refs 管理 =====
113
+ function setItemRef(field: string | undefined, el: any) {
114
+ if (field) {
115
+ itemRefs.set(field, el)
116
+ }
117
+ }
118
+
119
+ // ===== 按钮配置 =====
120
+ const showSubmitBtn = computed(() => props.option?.submitBtn?.show !== false)
121
+ const showResetBtn = computed(() => props.option?.resetBtn?.show === true)
122
+ const submitBtnText = computed(() => props.option?.submitBtn?.text || '提交')
123
+ const resetBtnText = computed(() => props.option?.resetBtn?.text || '重置')
124
+ const submitDisabled = computed(() => props.option?.submitBtn?.disabled || false)
125
+ const formDisabled = computed(() => props.option?.form?.disabled || false)
126
+
127
+ // ===== 值操作 =====
128
+ function getFieldValue(field?: string): any {
129
+ if (!field) return undefined
130
+ return formData[field]
131
+ }
132
+
133
+ function setFieldValue(field: string | undefined, value: any) {
134
+ if (!field) return
135
+ formData[field] = value
136
+ emit('change', field, value)
137
+ // 联动处理
138
+ processControl(field, value)
139
+ }
140
+
141
+ // ===== 联动控制 =====
142
+ function processControl(field: string, value: any) {
143
+ const rule = parsedRules.value.find(r => r.field === field)
144
+ if (!rule?.control || rule.control.length === 0) return
145
+
146
+ for (const ctrl of rule.control) {
147
+ let match = false
148
+
149
+ if (ctrl.condition) {
150
+ // 条件表达式(简化版:仅支持相等判断)
151
+ match = value === ctrl.value
152
+ } else if (ctrl.value !== undefined) {
153
+ match = Array.isArray(ctrl.value) ? ctrl.value.includes(value) : value === ctrl.value
154
+ } else if (ctrl.handle) {
155
+ // 暂不支持函数处理
156
+ match = !!value
157
+ }
158
+
159
+ if (match && ctrl.rule) {
160
+ // 显示联动的子规则
161
+ for (const subRule of ctrl.rule) {
162
+ const existing = parsedRules.value.find(r => r.field === subRule.field)
163
+ if (existing) {
164
+ existing.hidden = false
165
+ existing.display = true
166
+ }
167
+ }
168
+ } else if (ctrl.rule) {
169
+ // 隐藏联动的子规则
170
+ for (const subRule of ctrl.rule) {
171
+ const existing = parsedRules.value.find(r => r.field === subRule.field)
172
+ if (existing) {
173
+ existing.hidden = true
174
+ }
175
+ }
176
+ }
177
+ }
178
+ }
179
+
180
+ // ===== 校验 =====
181
+ function onFieldValidate(field: string, error: string | null) {
182
+ if (error) {
183
+ errors[field] = error
184
+ } else {
185
+ delete errors[field]
186
+ }
187
+ }
188
+
189
+ async function validateAll(): Promise<Record<string, any>> {
190
+ const allErrors: Record<string, string> = {}
191
+ const fields = flattenFieldRules(parsedRules.value)
192
+
193
+ for (const rule of fields) {
194
+ if (!rule.field || rule.hidden || rule.display === false) continue
195
+ if (!rule.validate || rule.validate.length === 0) continue
196
+
197
+ const error = await validateValue(formData[rule.field], rule.validate, rule.title)
198
+ if (error) {
199
+ allErrors[rule.field] = error
200
+ // 同步到 ref 的错误状态
201
+ const itemRef = itemRefs.get(rule.field)
202
+ if (itemRef?.validate) {
203
+ await itemRef.validate()
204
+ }
205
+ }
206
+ }
207
+
208
+ if (Object.keys(allErrors).length > 0) {
209
+ Object.assign(errors, allErrors)
210
+ return Promise.reject(allErrors)
211
+ }
212
+ return { ...formData }
213
+ }
214
+
215
+ // ===== 提交 / 重置 =====
216
+ async function handleSubmit() {
217
+ try {
218
+ const data = await validateAll()
219
+ emit('submit', data)
220
+ if (props.option?.onSubmit) {
221
+ props.option.onSubmit(data, fApi)
222
+ }
223
+ } catch (errs) {
224
+ console.warn('[form-create-wot] 表单校验失败:', errs)
225
+ uni.showToast({ title: '请检查表单填写', icon: 'none' })
226
+ }
227
+ }
228
+
229
+ function handleReset(fields?: string | string[]) {
230
+ const targetFields = fields
231
+ ? (typeof fields === 'string' ? [fields] : fields)
232
+ : Object.keys(formData)
233
+
234
+ for (const field of targetFields) {
235
+ const rule = parsedRules.value.find(r => r.field === field)
236
+ formData[field] = rule?.value ?? getDefaultValue(rule?.type || '')
237
+ const itemRef = itemRefs.get(field)
238
+ if (itemRef?.clearError) {
239
+ itemRef.clearError()
240
+ }
241
+ }
242
+ // 清除错误
243
+ for (const field of targetFields) {
244
+ delete errors[field]
245
+ }
246
+ }
247
+
248
+ // ===== fApi 构建 =====
249
+ const fApi: FormApi = {
250
+ formData: () => ({ ...formData }),
251
+ getValue: (field: string) => formData[field],
252
+ setValue: (field: string, value: any) => {
253
+ formData[field] = value
254
+ const itemRef = itemRefs.get(field)
255
+ if (itemRef?.setValue) {
256
+ itemRef.setValue(value)
257
+ }
258
+ },
259
+ setValues: (values: Record<string, any>) => {
260
+ for (const [field, value] of Object.entries(values)) {
261
+ fApi.setValue(field, value)
262
+ }
263
+ },
264
+ validate: validateAll,
265
+ validateField: async (field: string) => {
266
+ const itemRef = itemRefs.get(field)
267
+ if (itemRef?.validate) {
268
+ return itemRef.validate()
269
+ }
270
+ return null
271
+ },
272
+ clearValidateState: (fields?: string | string[]) => {
273
+ const targetFields = fields
274
+ ? (typeof fields === 'string' ? [fields] : fields)
275
+ : Object.keys(errors)
276
+ for (const field of targetFields) {
277
+ delete errors[field]
278
+ const itemRef = itemRefs.get(field)
279
+ if (itemRef?.clearError) itemRef.clearError()
280
+ }
281
+ },
282
+ submit: async (onSubmit?) => {
283
+ const data = await validateAll()
284
+ if (onSubmit) onSubmit(data)
285
+ else emit('submit', data)
286
+ },
287
+ resetFields: handleReset,
288
+ hidden: (hidden: boolean, fields?: string | string[]) => {
289
+ const targets = fields
290
+ ? (typeof fields === 'string' ? [fields] : fields)
291
+ : parsedRules.value.filter(r => r.field).map(r => r.field!)
292
+ for (const field of targets) {
293
+ const rule = parsedRules.value.find(r => r.field === field)
294
+ if (rule) rule.hidden = hidden
295
+ }
296
+ },
297
+ display: (display: boolean, fields?: string | string[]) => {
298
+ const targets = fields
299
+ ? (typeof fields === 'string' ? [fields] : fields)
300
+ : parsedRules.value.filter(r => r.field).map(r => r.field!)
301
+ for (const field of targets) {
302
+ const rule = parsedRules.value.find(r => r.field === field)
303
+ if (rule) rule.display = display
304
+ }
305
+ },
306
+ disabled: (disabled: boolean, fields?: string | string[]) => {
307
+ const targets = fields
308
+ ? (typeof fields === 'string' ? [fields] : fields)
309
+ : parsedRules.value.filter(r => r.field).map(r => r.field!)
310
+ for (const field of targets) {
311
+ const rule = parsedRules.value.find(r => r.field === field)
312
+ if (rule) {
313
+ if (!rule.props) rule.props = {}
314
+ rule.props.disabled = disabled
315
+ }
316
+ }
317
+ },
318
+ getRule: () => parsedRules.value,
319
+ updateRule: (field: string, partial: Partial<FormRule>) => {
320
+ const rule = parsedRules.value.find(r => r.field === field)
321
+ if (rule) {
322
+ Object.assign(rule, partial)
323
+ }
324
+ },
325
+ updateRules: (updates: Record<string, Partial<FormRule>>) => {
326
+ for (const [field, partial] of Object.entries(updates)) {
327
+ fApi.updateRule(field, partial)
328
+ }
329
+ },
330
+ getEl: (field: string) => itemRefs.get(field),
331
+ append: (rule: FormRule, after?: string) => {
332
+ const normalized = normalizeRule(rule)
333
+ if (after) {
334
+ const idx = parsedRules.value.findIndex(r => r.field === after)
335
+ if (idx >= 0) {
336
+ parsedRules.value.splice(idx + 1, 0, normalized)
337
+ return
338
+ }
339
+ }
340
+ parsedRules.value.push(normalized)
341
+ },
342
+ prepend: (rule: FormRule, before?: string) => {
343
+ const normalized = normalizeRule(rule)
344
+ if (before) {
345
+ const idx = parsedRules.value.findIndex(r => r.field === before)
346
+ if (idx >= 0) {
347
+ parsedRules.value.splice(idx, 0, normalized)
348
+ return
349
+ }
350
+ }
351
+ parsedRules.value.unshift(normalized)
352
+ },
353
+ removeField: (field: string) => {
354
+ const idx = parsedRules.value.findIndex(r => r.field === field)
355
+ if (idx >= 0) {
356
+ parsedRules.value.splice(idx, 1)
357
+ delete formData[field]
358
+ delete errors[field]
359
+ }
360
+ },
361
+ refresh: () => {
362
+ initForm(props.rule)
363
+ },
364
+ on: () => {
365
+ // 事件监听简化版(可后续扩展)
366
+ },
367
+ }
368
+
369
+ // 挂载 API
370
+ onMounted(() => {
371
+ emit('update:api', fApi)
372
+ emit('mounted', fApi)
373
+ })
374
+ </script>
375
+
376
+ <style lang="scss">
377
+ .fc-form {
378
+ width: 100%;
379
+ min-height: 100rpx;
380
+
381
+ &__body {
382
+ background: #fff;
383
+ border-radius: 16rpx;
384
+ overflow: hidden;
385
+ }
386
+
387
+ &__actions {
388
+ display: flex;
389
+ justify-content: center;
390
+ gap: 24rpx;
391
+ padding: 32rpx;
392
+ margin-top: 24rpx;
393
+ }
394
+
395
+ &__btn {
396
+ flex: 1;
397
+ max-width: 320rpx;
398
+
399
+ &--submit {
400
+ // 主色按钮
401
+ }
402
+
403
+ &--reset {
404
+ // 次要按钮
405
+ }
406
+ }
407
+ }
408
+ </style>