- ty-form 字段支持 width(行内默认 200px、弹窗默认 100%) - ty-search-form 展开为多行高级搜索 - ty-crud 操作列数据驱动(actions 内置/自定义),超过 collapseCount 折叠为「更多」下拉 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
144 lines
4.2 KiB
Vue
144 lines
4.2 KiB
Vue
<template>
|
||
<el-form ref="formRef" :inline="inline" :model="modelValue" :rules="rules" :label-width="inline ? 'auto' : '90px'">
|
||
<el-form-item
|
||
v-for="(prop, key) in visibleProperties"
|
||
:key="key"
|
||
:label="prop.title ?? key"
|
||
:prop="key"
|
||
>
|
||
<el-select
|
||
v-if="prop.enum"
|
||
v-model="modelValue[key]"
|
||
:placeholder="prop.placeholder ?? '请选择'"
|
||
:clearable="!isRequired(key)"
|
||
:disabled="isReadonly(key, prop)"
|
||
:style="widgetStyle(prop)"
|
||
>
|
||
<el-option
|
||
v-for="(v, i) in prop.enum"
|
||
:key="String(v)"
|
||
:label="prop.enumNames?.[i] ?? String(v)"
|
||
:value="v"
|
||
/>
|
||
</el-select>
|
||
|
||
<el-switch v-else-if="prop.type === 'boolean'" v-model="modelValue[key]" :disabled="isReadonly(key, prop)" />
|
||
|
||
<el-input-number
|
||
v-else-if="prop.type === 'integer' || prop.type === 'number'"
|
||
v-model="modelValue[key]"
|
||
:min="prop.minimum"
|
||
:max="prop.maximum"
|
||
:disabled="isReadonly(key, prop)"
|
||
:style="widgetStyle(prop)"
|
||
/>
|
||
|
||
<el-input
|
||
v-else-if="prop.format === 'password'"
|
||
v-model="modelValue[key]"
|
||
type="password"
|
||
show-password
|
||
:placeholder="prop.placeholder"
|
||
:disabled="isReadonly(key, prop)"
|
||
:style="widgetStyle(prop)"
|
||
/>
|
||
|
||
<el-input
|
||
v-else-if="prop.format === 'textarea'"
|
||
v-model="modelValue[key]"
|
||
type="textarea"
|
||
:placeholder="prop.placeholder"
|
||
:disabled="isReadonly(key, prop)"
|
||
:style="widgetStyle(prop)"
|
||
/>
|
||
|
||
<el-input
|
||
v-else
|
||
v-model="modelValue[key]"
|
||
:placeholder="prop.placeholder"
|
||
:disabled="isReadonly(key, prop)"
|
||
:style="widgetStyle(prop)"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<slot />
|
||
</el-form>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, ref } from 'vue'
|
||
import type { FormInstance, FormRules } from 'element-plus'
|
||
import type { JsonSchema, JsonSchemaProperty } from '@/types/crud'
|
||
|
||
defineOptions({ name: 'TyForm' })
|
||
|
||
const props = withDefaults(
|
||
defineProps<{
|
||
/** 表单 JSON Schema。 */
|
||
schema: JsonSchema
|
||
/** 表单数据(就地绑定,会直接修改传入对象)。 */
|
||
modelValue: Record<string, any>
|
||
/** 是否行内布局(用于搜索)。 */
|
||
inline?: boolean
|
||
/** 表单模式(create/edit),用于 showOn 字段过滤。 */
|
||
mode?: 'create' | 'edit'
|
||
/** 强制只读的字段名(如编辑模式下的 username)。 */
|
||
readonlyKeys?: string[]
|
||
}>(),
|
||
{
|
||
inline: false,
|
||
mode: undefined,
|
||
readonlyKeys: () => [],
|
||
},
|
||
)
|
||
|
||
const formRef = ref<FormInstance>()
|
||
|
||
/** 按 mode 过滤 showOn 字段。 */
|
||
const visibleProperties = computed(() => {
|
||
const result: Record<string, JsonSchemaProperty> = {}
|
||
for (const [key, prop] of Object.entries(props.schema.properties)) {
|
||
if (!props.mode || !prop.showOn || prop.showOn.includes(props.mode)) {
|
||
result[key] = prop
|
||
}
|
||
}
|
||
return result
|
||
})
|
||
|
||
const rules = computed<FormRules>(() => {
|
||
const result: FormRules = {}
|
||
for (const [key, prop] of Object.entries(props.schema.properties)) {
|
||
const itemRules: Record<string, unknown>[] = []
|
||
if (props.schema.required?.includes(key)) {
|
||
itemRules.push({ required: true, message: `${prop.title ?? key}不能为空`, trigger: 'blur' })
|
||
}
|
||
if (prop.minLength) {
|
||
itemRules.push({ min: prop.minLength, message: `长度不能少于 ${prop.minLength} 位`, trigger: 'blur' })
|
||
}
|
||
if (prop.format === 'email') {
|
||
itemRules.push({ type: 'email', message: '邮箱格式不正确', trigger: 'blur' })
|
||
}
|
||
if (itemRules.length) result[key] = itemRules
|
||
}
|
||
return result
|
||
})
|
||
|
||
function isRequired(key: string): boolean {
|
||
return props.schema.required?.includes(key) ?? false
|
||
}
|
||
|
||
function isReadonly(key: string, prop: { readOnly?: boolean }): boolean {
|
||
return prop.readOnly === true || props.readonlyKeys.includes(key)
|
||
}
|
||
|
||
function widgetStyle(prop: JsonSchemaProperty): Record<string, string> {
|
||
return { width: prop.width ?? (props.inline ? '200px' : '100%') }
|
||
}
|
||
|
||
defineExpose({
|
||
validate: () => formRef.value?.validate(),
|
||
resetFields: () => formRef.value?.resetFields(),
|
||
clearValidate: () => formRef.value?.clearValidate(),
|
||
})
|
||
</script>
|