ec-admin/src/components/ty-form.vue
liuyun b5d95c95d8 feat: dictionary & organization management pages
- 字典管理:左右分栏主从布局,20 个字典类型,el-scrollbar 滚动
- 组织管理:树形表格 + 上级组织 tree-select,级联删除下级
- ty-table 新增树形支持(row-key/tree-props/default-expand-all)
- ty-form 新增 tree-select 控件(format/treeData/treeProps)
- JsonSchemaProperty 扩展 tree-select 相关字段

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

185 lines
5.3 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-tree-select
v-else-if="prop.format === 'tree-select'"
v-model="modelValue[key]"
:data="prop.treeData ?? []"
:node-key="prop.treeProps?.value ?? 'value'"
:props="{
label: prop.treeProps?.label ?? 'label',
children: prop.treeProps?.children ?? 'children',
}"
check-strictly
:placeholder="prop.placeholder ?? '请选择'"
:clearable="!isRequired(key)"
:disabled="isReadonly(key, prop)"
:style="widgetStyle(prop)"
/>
<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)"
@keyup.enter="onEnter"
/>
</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'
/** 输入框回车触发 submit 事件。 */
enterToSubmit?: boolean
/** 强制只读的字段名(如编辑模式下的 username)。 */
readonlyKeys?: string[]
}>(),
{
inline: false,
mode: undefined,
enterToSubmit: false,
readonlyKeys: () => [],
},
)
const emit = defineEmits<{
(e: 'submit'): void
}>()
const formRef = ref<FormInstance>()
function onEnter(): void {
if (props.enterToSubmit) emit('submit')
}
/** 按 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%') }
}
async function submit(): Promise<Record<string, any>> {
await formRef.value?.validate()
return { ...props.modelValue }
}
defineExpose({
submit,
validate: () => formRef.value?.validate(),
resetFields: () => formRef.value?.resetFields(),
clearValidate: () => formRef.value?.clearValidate(),
})
</script>
<style scoped lang="scss">
/* inline 模式下让 el-form 成为 flex 容器,使「查询/重置」按钮可通过 ml-auto 贴右 */
.el-form--inline {
@apply flex flex-wrap items-center;
}
</style>