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.
- package/README.md +182 -0
- package/package.json +110 -0
- package/src/fc/adapter/config.ts +245 -0
- package/src/fc/adapter/fc-checkbox.vue +44 -0
- package/src/fc/adapter/fc-date-picker.vue +55 -0
- package/src/fc/adapter/fc-radio.vue +44 -0
- package/src/fc/adapter/fc-select.vue +62 -0
- package/src/fc/components/FcForm.vue +408 -0
- package/src/fc/components/FcFormItem.vue +267 -0
- package/src/fc/core/parser.ts +132 -0
- package/src/fc/core/validator.ts +122 -0
- package/src/fc/index.ts +34 -0
- package/src/fc/types/api.ts +49 -0
- package/src/fc/types/index.ts +2 -0
- package/src/fc/types/rule.ts +151 -0
|
@@ -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>
|