liuyun 1ee3ed28a5 feat: search field width + collapse actions into dropdown
- ty-form 字段支持 width(行内默认 200px、弹窗默认 100%)
- ty-search-form 展开为多行高级搜索
- ty-crud 操作列数据驱动(actions 内置/自定义),超过 collapseCount 折叠为「更多」下拉

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 09:34:39 +08:00

144 lines
4.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>