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,267 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<view v-if="visible" class="fc-form-item" :class="{ 'fc-form-item--error': errorMsg }">
|
|
3
|
+
<!-- 标题/标签行 -->
|
|
4
|
+
<view class="fc-form-item__label" v-if="rule.title">
|
|
5
|
+
<text class="fc-form-item__required" v-if="isRequired">*</text>
|
|
6
|
+
<text>{{ rule.title }}</text>
|
|
7
|
+
<view class="fc-form-item__info" v-if="rule.info">
|
|
8
|
+
<text class="fc-form-item__info-text">{{ infoText }}</text>
|
|
9
|
+
</view>
|
|
10
|
+
</view>
|
|
11
|
+
|
|
12
|
+
<!-- 组件区域 -->
|
|
13
|
+
<view class="fc-form-item__body">
|
|
14
|
+
<!-- input -->
|
|
15
|
+
<wd-input
|
|
16
|
+
v-if="componentName === 'wd-input'"
|
|
17
|
+
v-model="fieldValue"
|
|
18
|
+
v-bind="componentProps"
|
|
19
|
+
:disabled="isDisabled"
|
|
20
|
+
@blur="onBlur"
|
|
21
|
+
@input="onInput"
|
|
22
|
+
/>
|
|
23
|
+
|
|
24
|
+
<!-- textarea -->
|
|
25
|
+
<wd-textarea
|
|
26
|
+
v-else-if="componentName === 'wd-textarea'"
|
|
27
|
+
v-model="fieldValue"
|
|
28
|
+
v-bind="componentProps"
|
|
29
|
+
:disabled="isDisabled"
|
|
30
|
+
@blur="onBlur"
|
|
31
|
+
@input="onInput"
|
|
32
|
+
/>
|
|
33
|
+
|
|
34
|
+
<!-- input-number -->
|
|
35
|
+
<wd-input-number
|
|
36
|
+
v-else-if="componentName === 'wd-input-number'"
|
|
37
|
+
v-model="fieldValue"
|
|
38
|
+
v-bind="componentProps"
|
|
39
|
+
:disabled="isDisabled"
|
|
40
|
+
@change="onChange"
|
|
41
|
+
/>
|
|
42
|
+
|
|
43
|
+
<!-- select (custom adapter) -->
|
|
44
|
+
<fc-select
|
|
45
|
+
v-else-if="componentName === 'fc-select'"
|
|
46
|
+
v-model="fieldValue"
|
|
47
|
+
:options="rule.options"
|
|
48
|
+
v-bind="componentProps"
|
|
49
|
+
:disabled="isDisabled"
|
|
50
|
+
@change="onChange"
|
|
51
|
+
/>
|
|
52
|
+
|
|
53
|
+
<!-- radio (custom adapter) -->
|
|
54
|
+
<fc-radio
|
|
55
|
+
v-else-if="componentName === 'fc-radio'"
|
|
56
|
+
v-model="fieldValue"
|
|
57
|
+
:options="rule.options"
|
|
58
|
+
v-bind="componentProps"
|
|
59
|
+
:disabled="isDisabled"
|
|
60
|
+
@change="onChange"
|
|
61
|
+
/>
|
|
62
|
+
|
|
63
|
+
<!-- checkbox (custom adapter) -->
|
|
64
|
+
<fc-checkbox
|
|
65
|
+
v-else-if="componentName === 'fc-checkbox'"
|
|
66
|
+
v-model="fieldValue"
|
|
67
|
+
:options="rule.options"
|
|
68
|
+
v-bind="componentProps"
|
|
69
|
+
:disabled="isDisabled"
|
|
70
|
+
@change="onChange"
|
|
71
|
+
/>
|
|
72
|
+
|
|
73
|
+
<!-- switch -->
|
|
74
|
+
<wd-switch
|
|
75
|
+
v-else-if="componentName === 'wd-switch'"
|
|
76
|
+
v-model="fieldValue"
|
|
77
|
+
v-bind="componentProps"
|
|
78
|
+
:disabled="isDisabled"
|
|
79
|
+
@change="onChange"
|
|
80
|
+
/>
|
|
81
|
+
|
|
82
|
+
<!-- date-picker (custom adapter) -->
|
|
83
|
+
<fc-date-picker
|
|
84
|
+
v-else-if="componentName === 'fc-date-picker'"
|
|
85
|
+
v-model="fieldValue"
|
|
86
|
+
v-bind="componentProps"
|
|
87
|
+
:disabled="isDisabled"
|
|
88
|
+
@change="onChange"
|
|
89
|
+
/>
|
|
90
|
+
|
|
91
|
+
<!-- rate -->
|
|
92
|
+
<wd-rate
|
|
93
|
+
v-else-if="componentName === 'wd-rate'"
|
|
94
|
+
v-model="fieldValue"
|
|
95
|
+
v-bind="componentProps"
|
|
96
|
+
:readonly="isDisabled"
|
|
97
|
+
@change="onChange"
|
|
98
|
+
/>
|
|
99
|
+
|
|
100
|
+
<!-- slider -->
|
|
101
|
+
<wd-slider
|
|
102
|
+
v-else-if="componentName === 'wd-slider'"
|
|
103
|
+
v-model="fieldValue"
|
|
104
|
+
v-bind="componentProps"
|
|
105
|
+
:disabled="isDisabled"
|
|
106
|
+
@change="onChange"
|
|
107
|
+
/>
|
|
108
|
+
|
|
109
|
+
<!-- 未知类型 fallback -->
|
|
110
|
+
<view v-else class="fc-form-item__unknown">
|
|
111
|
+
<text>不支持的组件类型: {{ rule.type }}</text>
|
|
112
|
+
</view>
|
|
113
|
+
</view>
|
|
114
|
+
|
|
115
|
+
<!-- 错误信息 -->
|
|
116
|
+
<view class="fc-form-item__error" v-if="errorMsg">
|
|
117
|
+
<text class="fc-form-item__error-text">{{ errorMsg }}</text>
|
|
118
|
+
</view>
|
|
119
|
+
</view>
|
|
120
|
+
</template>
|
|
121
|
+
|
|
122
|
+
<script setup lang="ts">
|
|
123
|
+
import { ref, computed, watch } from 'vue'
|
|
124
|
+
import type { FormRule } from '../types'
|
|
125
|
+
import { getComponentConfig, convertProps } from '../adapter/config'
|
|
126
|
+
import { validateValue } from '../core/validator'
|
|
127
|
+
|
|
128
|
+
const props = defineProps<{
|
|
129
|
+
rule: FormRule
|
|
130
|
+
modelValue?: any
|
|
131
|
+
globalDisabled?: boolean
|
|
132
|
+
}>()
|
|
133
|
+
|
|
134
|
+
const emit = defineEmits<{
|
|
135
|
+
(e: 'update:modelValue', value: any): void
|
|
136
|
+
(e: 'validate', field: string, error: string | null): void
|
|
137
|
+
}>()
|
|
138
|
+
|
|
139
|
+
const errorMsg = ref<string | null>(null)
|
|
140
|
+
|
|
141
|
+
// 计算组件名称和 props
|
|
142
|
+
const config = computed(() => getComponentConfig(props.rule.type))
|
|
143
|
+
const componentName = computed(() => config.value?.component || 'unknown')
|
|
144
|
+
const componentProps = computed(() => {
|
|
145
|
+
if (!config.value) return {}
|
|
146
|
+
return convertProps(props.rule.props || {}, config.value)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// 可见性
|
|
150
|
+
const visible = computed(() => {
|
|
151
|
+
return props.rule.display !== false && props.rule.hidden !== true
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// 是否禁用
|
|
155
|
+
const isDisabled = computed(() => {
|
|
156
|
+
return props.globalDisabled || props.rule.props?.disabled || false
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
// 是否必填
|
|
160
|
+
const isRequired = computed(() => {
|
|
161
|
+
return props.rule.required || props.rule.validate?.some(v => v.required) || false
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
// 提示信息
|
|
165
|
+
const infoText = computed(() => {
|
|
166
|
+
if (!props.rule.info) return ''
|
|
167
|
+
if (typeof props.rule.info === 'string') return props.rule.info
|
|
168
|
+
return props.rule.info.content || ''
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
// 表单值
|
|
172
|
+
const fieldValue = ref<any>(props.modelValue)
|
|
173
|
+
|
|
174
|
+
watch(() => props.modelValue, (val) => {
|
|
175
|
+
fieldValue.value = val
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
watch(fieldValue, (val) => {
|
|
179
|
+
emit('update:modelValue', val)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// 事件处理
|
|
183
|
+
const onBlur = () => {
|
|
184
|
+
doValidate()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const onInput = () => {
|
|
188
|
+
if (errorMsg.value) {
|
|
189
|
+
errorMsg.value = null
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const onChange = () => {
|
|
194
|
+
doValidate()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 校验
|
|
198
|
+
const doValidate = async (): Promise<string | null> => {
|
|
199
|
+
if (!props.rule.validate || props.rule.validate.length === 0) {
|
|
200
|
+
errorMsg.value = null
|
|
201
|
+
emit('validate', props.rule.field || '', null)
|
|
202
|
+
return null
|
|
203
|
+
}
|
|
204
|
+
const error = await validateValue(fieldValue.value, props.rule.validate, props.rule.title)
|
|
205
|
+
errorMsg.value = error
|
|
206
|
+
emit('validate', props.rule.field || '', error)
|
|
207
|
+
return error
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 暴露方法
|
|
211
|
+
defineExpose({
|
|
212
|
+
validate: doValidate,
|
|
213
|
+
clearError: () => { errorMsg.value = null },
|
|
214
|
+
setValue: (val: any) => { fieldValue.value = val },
|
|
215
|
+
getValue: () => fieldValue.value,
|
|
216
|
+
field: props.rule.field,
|
|
217
|
+
})
|
|
218
|
+
</script>
|
|
219
|
+
|
|
220
|
+
<style lang="scss">
|
|
221
|
+
.fc-form-item {
|
|
222
|
+
padding: 24rpx 32rpx;
|
|
223
|
+
background: #fff;
|
|
224
|
+
|
|
225
|
+
&__label {
|
|
226
|
+
display: flex;
|
|
227
|
+
align-items: center;
|
|
228
|
+
margin-bottom: 16rpx;
|
|
229
|
+
font-size: 28rpx;
|
|
230
|
+
color: #333;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
&__required {
|
|
234
|
+
color: #f56c6c;
|
|
235
|
+
margin-right: 4rpx;
|
|
236
|
+
font-size: 28rpx;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
&__info {
|
|
240
|
+
margin-left: 8rpx;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
&__info-text {
|
|
244
|
+
font-size: 24rpx;
|
|
245
|
+
color: #999;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
&__body {
|
|
249
|
+
width: 100%;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
&__error {
|
|
253
|
+
margin-top: 8rpx;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
&__error-text {
|
|
257
|
+
font-size: 24rpx;
|
|
258
|
+
color: #f56c6c;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
&__unknown {
|
|
262
|
+
padding: 16rpx;
|
|
263
|
+
background: #fff3cd;
|
|
264
|
+
border-radius: 8rpx;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
</style>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Rule 解析器
|
|
3
|
+
* 将 form-create 的 JSON Rule 解析并标准化为内部格式
|
|
4
|
+
*/
|
|
5
|
+
import type { FormRule, OptionItem } from '../types'
|
|
6
|
+
|
|
7
|
+
let idCounter = 0
|
|
8
|
+
|
|
9
|
+
/** 生成唯一 ID */
|
|
10
|
+
function genId(): string {
|
|
11
|
+
return `fc_${++idCounter}_${Date.now()}`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 标准化单条规则
|
|
16
|
+
*/
|
|
17
|
+
export function normalizeRule(rule: FormRule): FormRule {
|
|
18
|
+
const normalized: FormRule = {
|
|
19
|
+
...rule,
|
|
20
|
+
_id: rule._id || genId(),
|
|
21
|
+
hidden: rule.hidden ?? false,
|
|
22
|
+
display: rule.display ?? true,
|
|
23
|
+
props: { ...rule.props },
|
|
24
|
+
validate: rule.validate ? [...rule.validate] : []
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 处理 required 快捷设置 → 自动插入 validate 规则
|
|
28
|
+
if (normalized.required && !normalized.validate?.some(v => v.required)) {
|
|
29
|
+
normalized.validate = [
|
|
30
|
+
{ required: true, message: `${normalized.title || normalized.field || '该字段'}不能为空` },
|
|
31
|
+
...(normalized.validate || [])
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 标准化 options 格式
|
|
36
|
+
if (normalized.options) {
|
|
37
|
+
normalized.options = normalizeOptions(normalized.options)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 处理 props 中的 options (antdv 的 select 等可能把 options 放在 props 中)
|
|
41
|
+
if (normalized.props?.options && !normalized.options) {
|
|
42
|
+
normalized.options = normalizeOptions(normalized.props.options)
|
|
43
|
+
delete normalized.props.options
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 标准化子规则
|
|
47
|
+
if (Array.isArray(normalized.children)) {
|
|
48
|
+
normalized.children = normalized.children.map(child =>
|
|
49
|
+
typeof child === 'string' ? child : normalizeRule(child)
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 标准化 control
|
|
54
|
+
if (normalized.control) {
|
|
55
|
+
normalized.control = normalized.control.map(ctrl => ({
|
|
56
|
+
...ctrl,
|
|
57
|
+
rule: ctrl.rule ? ctrl.rule.map(normalizeRule) : undefined
|
|
58
|
+
}))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return normalized
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 标准化 options
|
|
66
|
+
* 兼容多种格式:
|
|
67
|
+
* - [{label, value}] 标准格式
|
|
68
|
+
* - [{name, id}] 芋道后端常见格式
|
|
69
|
+
* - ['val1', 'val2'] 简写格式
|
|
70
|
+
*/
|
|
71
|
+
export function normalizeOptions(options: any[]): OptionItem[] {
|
|
72
|
+
if (!Array.isArray(options)) return []
|
|
73
|
+
return options.map(opt => {
|
|
74
|
+
if (typeof opt === 'string' || typeof opt === 'number') {
|
|
75
|
+
return { label: String(opt), value: opt }
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
label: opt.label ?? opt.name ?? opt.title ?? String(opt.value),
|
|
79
|
+
value: opt.value ?? opt.id ?? opt.key,
|
|
80
|
+
disabled: opt.disabled ?? false,
|
|
81
|
+
children: opt.children ? normalizeOptions(opt.children) : undefined
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 解析规则列表
|
|
88
|
+
*/
|
|
89
|
+
export function parseRules(rules: FormRule[]): FormRule[] {
|
|
90
|
+
idCounter = 0
|
|
91
|
+
return rules.map(normalizeRule)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 从芋道后端返回的 JSON 字符串解析
|
|
96
|
+
*/
|
|
97
|
+
export function parseJsonString(json: string): FormRule[] {
|
|
98
|
+
try {
|
|
99
|
+
const parsed = JSON.parse(json)
|
|
100
|
+
if (Array.isArray(parsed)) {
|
|
101
|
+
return parseRules(parsed)
|
|
102
|
+
}
|
|
103
|
+
return []
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.error('[form-create-wot] JSON 解析失败:', e)
|
|
106
|
+
return []
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 获取所有包含 field 的规则(扁平化)
|
|
112
|
+
*/
|
|
113
|
+
export function flattenFieldRules(rules: FormRule[]): FormRule[] {
|
|
114
|
+
const result: FormRule[] = []
|
|
115
|
+
const walk = (list: FormRule[]) => {
|
|
116
|
+
for (const rule of list) {
|
|
117
|
+
if (rule.field) {
|
|
118
|
+
result.push(rule)
|
|
119
|
+
}
|
|
120
|
+
if (Array.isArray(rule.children)) {
|
|
121
|
+
walk(rule.children as FormRule[])
|
|
122
|
+
}
|
|
123
|
+
if (rule.control) {
|
|
124
|
+
for (const ctrl of rule.control) {
|
|
125
|
+
if (ctrl.rule) walk(ctrl.rule)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
walk(rules)
|
|
131
|
+
return result
|
|
132
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 表单校验引擎
|
|
3
|
+
* 兼容 antdv 的校验规则格式
|
|
4
|
+
*/
|
|
5
|
+
import type { ValidateRule } from '../types'
|
|
6
|
+
|
|
7
|
+
/** 预注册的自定义校验函数 */
|
|
8
|
+
const validatorRegistry = new Map<string, (value: any) => Promise<boolean | string>>()
|
|
9
|
+
|
|
10
|
+
/** 注册自定义校验函数 */
|
|
11
|
+
export function registerValidator(name: string, fn: (value: any) => Promise<boolean | string>) {
|
|
12
|
+
validatorRegistry.set(name, fn)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** 校验单个值 */
|
|
16
|
+
export async function validateValue(
|
|
17
|
+
value: any,
|
|
18
|
+
rules: ValidateRule[],
|
|
19
|
+
fieldTitle?: string
|
|
20
|
+
): Promise<string | null> {
|
|
21
|
+
for (const rule of rules) {
|
|
22
|
+
const errMsg = rule.message || `${fieldTitle || '该字段'}校验失败`
|
|
23
|
+
|
|
24
|
+
// required
|
|
25
|
+
if (rule.required) {
|
|
26
|
+
if (value === undefined || value === null || value === '') {
|
|
27
|
+
return rule.message || `${fieldTitle || '该字段'}不能为空`
|
|
28
|
+
}
|
|
29
|
+
if (Array.isArray(value) && value.length === 0) {
|
|
30
|
+
return rule.message || `${fieldTitle || '该字段'}不能为空`
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 如果值为空且非必填,跳过后续校验
|
|
35
|
+
if (value === undefined || value === null || value === '') {
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// type
|
|
40
|
+
if (rule.type) {
|
|
41
|
+
const valid = checkType(value, rule.type)
|
|
42
|
+
if (!valid) return errMsg
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// pattern
|
|
46
|
+
if (rule.pattern) {
|
|
47
|
+
const regex = typeof rule.pattern === 'string' ? new RegExp(rule.pattern) : rule.pattern
|
|
48
|
+
if (!regex.test(String(value))) {
|
|
49
|
+
return errMsg
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// min / max (字符串长度或数字大小)
|
|
54
|
+
if (rule.min !== undefined) {
|
|
55
|
+
if (typeof value === 'string' && value.length < rule.min) return errMsg
|
|
56
|
+
if (typeof value === 'number' && value < rule.min) return errMsg
|
|
57
|
+
if (Array.isArray(value) && value.length < rule.min) return errMsg
|
|
58
|
+
}
|
|
59
|
+
if (rule.max !== undefined) {
|
|
60
|
+
if (typeof value === 'string' && value.length > rule.max) return errMsg
|
|
61
|
+
if (typeof value === 'number' && value > rule.max) return errMsg
|
|
62
|
+
if (Array.isArray(value) && value.length > rule.max) return errMsg
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// len
|
|
66
|
+
if (rule.len !== undefined) {
|
|
67
|
+
if (typeof value === 'string' && value.length !== rule.len) return errMsg
|
|
68
|
+
if (Array.isArray(value) && value.length !== rule.len) return errMsg
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 自定义校验函数
|
|
72
|
+
if (rule.validatorName) {
|
|
73
|
+
const fn = validatorRegistry.get(rule.validatorName)
|
|
74
|
+
if (fn) {
|
|
75
|
+
const result = await fn(value)
|
|
76
|
+
if (result !== true) {
|
|
77
|
+
return typeof result === 'string' ? result : errMsg
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 类型检查 */
|
|
87
|
+
function checkType(value: any, type: string): boolean {
|
|
88
|
+
switch (type) {
|
|
89
|
+
case 'string':
|
|
90
|
+
return typeof value === 'string'
|
|
91
|
+
case 'number':
|
|
92
|
+
return typeof value === 'number'
|
|
93
|
+
case 'boolean':
|
|
94
|
+
return typeof value === 'boolean'
|
|
95
|
+
case 'array':
|
|
96
|
+
return Array.isArray(value)
|
|
97
|
+
case 'object':
|
|
98
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
99
|
+
case 'email':
|
|
100
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value))
|
|
101
|
+
case 'url':
|
|
102
|
+
return /^https?:\/\/.+/.test(String(value))
|
|
103
|
+
default:
|
|
104
|
+
return true
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 批量校验多个字段 */
|
|
109
|
+
export async function validateFields(
|
|
110
|
+
formData: Record<string, any>,
|
|
111
|
+
fieldRules: Array<{ field: string; title?: string; validate: ValidateRule[] }>
|
|
112
|
+
): Promise<Record<string, string>> {
|
|
113
|
+
const errors: Record<string, string> = {}
|
|
114
|
+
for (const { field, title, validate } of fieldRules) {
|
|
115
|
+
if (!validate || validate.length === 0) continue
|
|
116
|
+
const error = await validateValue(formData[field], validate, title)
|
|
117
|
+
if (error) {
|
|
118
|
+
errors[field] = error
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return errors
|
|
122
|
+
}
|
package/src/fc/index.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* form-create-wot 插件入口
|
|
3
|
+
* UniApp 移动端 form-create JSON 渲染引擎 (wot-design-uni)
|
|
4
|
+
*/
|
|
5
|
+
import type { App } from 'vue'
|
|
6
|
+
import FcForm from './components/FcForm.vue'
|
|
7
|
+
import FcFormItem from './components/FcFormItem.vue'
|
|
8
|
+
import FcSelect from './adapter/fc-select.vue'
|
|
9
|
+
import FcRadio from './adapter/fc-radio.vue'
|
|
10
|
+
import FcCheckbox from './adapter/fc-checkbox.vue'
|
|
11
|
+
import FcDatePicker from './adapter/fc-date-picker.vue'
|
|
12
|
+
|
|
13
|
+
// Core
|
|
14
|
+
export { parseRules, parseJsonString, flattenFieldRules, normalizeRule } from './core/parser'
|
|
15
|
+
export { validateValue, validateFields, registerValidator } from './core/validator'
|
|
16
|
+
export { getComponentConfig, registerComponent, convertProps } from './adapter/config'
|
|
17
|
+
|
|
18
|
+
// Types
|
|
19
|
+
export type { FormRule, FormOption, FormApi, ValidateRule, ControlRule, OptionItem } from './types'
|
|
20
|
+
|
|
21
|
+
// Components
|
|
22
|
+
export { FcForm, FcFormItem, FcSelect, FcRadio, FcCheckbox, FcDatePicker }
|
|
23
|
+
|
|
24
|
+
/** Vue 插件安装函数 */
|
|
25
|
+
export function install(app: App) {
|
|
26
|
+
app.component('fc-form', FcForm)
|
|
27
|
+
app.component('fc-form-item', FcFormItem)
|
|
28
|
+
app.component('fc-select', FcSelect)
|
|
29
|
+
app.component('fc-radio', FcRadio)
|
|
30
|
+
app.component('fc-checkbox', FcCheckbox)
|
|
31
|
+
app.component('fc-date-picker', FcDatePicker)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default { install }
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fApi 表单操作接口定义
|
|
3
|
+
*/
|
|
4
|
+
import type { FormRule, ValidateRule } from './rule'
|
|
5
|
+
|
|
6
|
+
export interface FormApi {
|
|
7
|
+
/** 获取所有表单数据 */
|
|
8
|
+
formData(): Record<string, any>
|
|
9
|
+
/** 获取指定字段的值 */
|
|
10
|
+
getValue(field: string): any
|
|
11
|
+
/** 设置指定字段的值 */
|
|
12
|
+
setValue(field: string, value: any): void
|
|
13
|
+
/** 批量设置字段值 */
|
|
14
|
+
setValues(values: Record<string, any>): void
|
|
15
|
+
/** 触发表单校验 */
|
|
16
|
+
validate(): Promise<Record<string, any>>
|
|
17
|
+
/** 校验指定字段 */
|
|
18
|
+
validateField(field: string): Promise<any>
|
|
19
|
+
/** 清除校验状态 */
|
|
20
|
+
clearValidateState(fields?: string | string[]): void
|
|
21
|
+
/** 提交表单 */
|
|
22
|
+
submit(onSubmit?: (formData: Record<string, any>) => void): Promise<void>
|
|
23
|
+
/** 重置表单 */
|
|
24
|
+
resetFields(fields?: string | string[]): void
|
|
25
|
+
/** 隐藏指定字段 */
|
|
26
|
+
hidden(hidden: boolean, fields?: string | string[]): void
|
|
27
|
+
/** 显示/隐藏指定字段 */
|
|
28
|
+
display(display: boolean, fields?: string | string[]): void
|
|
29
|
+
/** 禁用指定字段 */
|
|
30
|
+
disabled(disabled: boolean, fields?: string | string[]): void
|
|
31
|
+
/** 获取所有规则 */
|
|
32
|
+
getRule(): FormRule[]
|
|
33
|
+
/** 更新某个字段的规则 */
|
|
34
|
+
updateRule(field: string, rule: Partial<FormRule>): void
|
|
35
|
+
/** 更新多个字段的规则 */
|
|
36
|
+
updateRules(rules: Record<string, Partial<FormRule>>): void
|
|
37
|
+
/** 获取组件 ref */
|
|
38
|
+
getEl(field: string): any
|
|
39
|
+
/** 动态追加规则 */
|
|
40
|
+
append(rule: FormRule, after?: string): void
|
|
41
|
+
/** 动态前插规则 */
|
|
42
|
+
prepend(rule: FormRule, before?: string): void
|
|
43
|
+
/** 删除字段 */
|
|
44
|
+
removeField(field: string): void
|
|
45
|
+
/** 刷新表单 */
|
|
46
|
+
refresh(): void
|
|
47
|
+
/** 监听字段值变化 */
|
|
48
|
+
on(event: string, callback: (...args: any[]) => void): void
|
|
49
|
+
}
|