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,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
+ }
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export type { FormRule, FormOption, ValidateRule, ControlRule, OptionItem } from './rule'
2
+ export type { FormApi } from './api'