feat(form): 扩展表单组件功能支持数字输入和文件上传

- 新增 IsNotBlank 和 IsBlank 工具函数用于字符串空值判断
- 添加数字输入框组件支持,包括最小值、最大值、步长、精度配置
- 集成文件拖拽上传功能,支持本地模式上传
- 优化表单验证规则,支持不同字段类型的精确验证
- 更新下拉选择框支持多选和过滤功能
- 修改表单容器样式增加内边距提升用户体验
- 修复字段禁用状态绑定错误问题
- 扩展表单类型定义文档,新增 number 和 upload 类型说明
This commit is contained in:
2026-03-04 19:26:43 +08:00
parent b096d933a6
commit afbdbbdea8
9 changed files with 218 additions and 68 deletions
+18 -15
View File
@@ -254,19 +254,22 @@ export function FormatRowEnvStr(row, column, cellValue, index) {
return ''
}
export function IsEmpty(obj) {
if (obj === undefined) {
// console.error('is undefined')
return true
}
if (obj === '') {
// console.error('is ""')
return true
}
if (obj === 'undefined') {
// console.error('is "undefined"')
return true
}
// console.error('is null')
return obj === null
/**
* 判断字符串是否不是空白字符串
* 功能等同于 Java Hutool 的 StrUtil.isNotBlank
* @param {string} str - 待检查的字符串
* @returns {boolean} - 如果字符串不是空白则返回 true,否则返回 false
*/
export function IsNotBlank(str) {
return str != null && String(str).trim().length > 0
}
/**
* 判断字符串是否是空白字符串
* 功能等同于 Java Hutool 的 StrUtil.isBlank
* @param {string} str - 待检查的字符串
* @returns {boolean} - 如果字符串是空白则返回 true,否则返回 false
*/
export function IsBlank(str) {
return str == null || String(str).trim().length === 0
}
@@ -110,7 +110,9 @@ export default {
.container-form {
overflow-y: scroll; /* 启用滚动 */
scrollbar-width: none; /* Firefox */
padding-right: 20px;
padding-top: 10px;
padding-left: 15px;
padding-right: 15px;
-ms-overflow-style: none; /* Internet Explorer 10+ */
max-height: calc(100vh - 200px); /* 调整高度以适应页头和底部按钮 */
}
@@ -14,28 +14,42 @@
:modal="false"
>
<div class="container-form">
<el-form ref="form" :model="model" :rule="dynamicRules" :inline="false" size="small" label-position="left" label-width="100px">
<el-form ref="form" :model="model" :rules="dynamicRules" :inline="false" size="small" label-position="left" label-width="100px">
<el-form-item v-for="item in schema" :key="item.name" :prop="item.name" :label="item.title" :required="item.required === true">
<!-- 输入框 -->
<el-input
v-if="item.type === 'input'"
v-model="model[item.name]"
:placeholder="`请输入${item.title}`"
:disabled="model.disabled === true"
:disabled="schema.disabled === true"
clearable
style="width: 100%"
/>
<!-- 输入框(仅数字) -->
<el-input-number
v-if="item.type === 'number'"
v-model="model[item.name]"
:placeholder="`请输入${item.title}`"
:disabled="schema.disabled === true"
style="width: 100%"
:min="item.nMin ? item.nMin : -100000000"
:max="item.nMax ? item.nMax : 100000000"
:step="item.nStep ? item.nStep : 10"
:precision="item.nPrecision ? item.nPrecision : 0"
/>
<!-- 选择框 -->
<el-select
v-if="item.type === 'select'"
v-model="model[item.name]"
clearable
:placeholder="`请选择${item.title}`"
:disabled="model.disabled === true"
:disabled="schema.disabled === true"
style="width: 100%"
:multiple="schema.multiple ? schema.multiple : false"
filterable
>
<el-option
v-for="option in (item.dataSource || [])"
v-for="option in (item.sOpts || [])"
:key="option.value"
:label="option.label"
:value="option.value"
@@ -48,15 +62,15 @@
type="daterange"
align="right"
unlink-panels
:disabled="model.disabled === true"
:disabled="schema.disabled === true"
clearable
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd"
:picker-options="pickerOptions"
style="width: 100%"
:picker-options="pickerOptions"
/>
<!-- 日期时间选择 -->
<el-date-picker
@@ -65,15 +79,21 @@
type="datetimerange"
align="right"
clearable
:disabled="model.disabled === true"
:disabled="schema.disabled === true"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="yyyy-MM-dd HH:mm:ss"
value-format="yyyy-MM-dd HH:mm:ss"
:default-time="['00:00:00', '23:59:59']"
:picker-options="pickerOptions"
style="width: 100%"
:picker-options="pickerOptions"
/>
<!-- 文件上传 -->
<cute-file-drag-upload
v-if="item.type === 'upload'"
v-model="model[item.name]"
mode="local"
/>
</el-form-item>
</el-form>
@@ -88,9 +108,11 @@
</template>
<script>
import { IsNotBlank } from '@/utils/KitUtil'
import CuteFileDragUpload from '@/views/components/business/CuteFileDragUpload.vue'
export default {
name: 'CuteFormDialogPro',
components: { CuteFileDragUpload },
props: {
/**
* 弹窗标题
@@ -142,7 +164,6 @@ export default {
data() {
return {
visible: false,
rules: {},
pickerOptions: {
shortcuts: [{
text: '最近一周',
@@ -176,18 +197,44 @@ export default {
dynamicRules() {
const schema = this.schema
const formRules = {}
let trigger = 'blur'
let tip = '请输入'
for (const element of schema) {
const rule1 = {}
// 布尔值,表示该字段是否必填
rule1.required = element.required === true
// 字符串,触发验证的时机,如 blur(失去焦点时),change(数据改变时),input(输入时)等
rule1.trigger = (element.type === 'input' || element.type === 'number') ? 'blur' : 'change'
// 字符串,验证失败时显示的错误信息
rule1.message = (element.type === 'input' || element.type === 'number') ? '请输入' + element.title : '请选择' + element.title
// 验证的类型,如 string, number, date, array, object 等。其中array适用于checkbox,date类型会触发日期值对象校验
let ruleType = null
if (element.type === 'input') {
trigger = 'blur'
} else {
trigger = 'change'
tip = '请选择'
ruleType = 'string'
} else if (element.type === 'number') {
ruleType = 'number'
} else if (element.type === 'select') {
ruleType = null
} else if (element.type === 'date') {
ruleType = null
} else if (element.type === 'datetime') {
ruleType = null
}
formRules[element.name] = [
{ required: element.required === true, message: tip + element.title, trigger: trigger }
]
if (ruleType != null) {
rule1.type = ruleType
}
// 当类型为number时的限制
if (element.type === 'number') {
rule1.min = element.nMin ? element.nMin : -100000000
rule1.max = element.nMax ? element.nMax : 100000000
}
// 当存在正则表达式时,用于匹配输入值
if (IsNotBlank(element.pattern)) {
rule1.pattern = element.pattern
}
// 当存在自定义验证器时,自定义验证函数。该函数接收两个参数:rule 和 value,返回一个 Promise 或一个布尔值
if (element.validator) {
rule1.validator = element.validator
}
formRules[element.name] = [rule1]
}
return formRules
}
@@ -216,7 +263,7 @@ export default {
const schema = this.schema
const model = this.model
for (const element of schema) {
if (element.type === 'input' || element.type === 'select') {
if (element.type === 'input' || element.type === 'number' || element.type === 'select' || element.type === 'upload') {
model[element.name] = null
continue
}
@@ -238,7 +285,9 @@ export default {
.container-form {
overflow-y: scroll; /* 启用滚动 */
scrollbar-width: none; /* Firefox */
padding-right: 20px;
padding-top: 10px;
padding-left: 15px;
padding-right: 15px;
-ms-overflow-style: none; /* Internet Explorer 10+ */
max-height: calc(100vh - 200px); /* 调整高度以适应页头和底部按钮 */
}
@@ -110,7 +110,9 @@ export default {
overflow-y: scroll; /* 启用滚动 */
scrollbar-width: none; /* Firefox */
height: calc(100% - 60px); /* 预留出底部按钮的空间 */
padding-right: 20px;
padding-top: 10px;
padding-left: 15px;
padding-right: 15px;
-ms-overflow-style: none; /* Internet Explorer 10+ */
}
@@ -18,25 +18,39 @@
:modal="false"
>
<div class="container-form">
<el-form ref="form" :model="model" :rule="dynamicRules" :inline="false" size="small" label-position="left" label-width="100px">
<el-form ref="form" :model="model" :rules="dynamicRules" :inline="false" size="small" label-position="left" label-width="100px">
<el-form-item v-for="item in schema" :key="item.name" :prop="item.name" :label="item.title" :required="item.required === true">
<!-- 输入框 -->
<el-input
v-if="item.type === 'input'"
v-model="model[item.name]"
:placeholder="`请输入${item.title}`"
:disabled="model.disabled === true"
:disabled="schema.disabled === true"
clearable
style="width: 100%"
/>
<!-- 输入框(仅数字) -->
<el-input-number
v-if="item.type === 'number'"
v-model="model[item.name]"
:placeholder="`请输入${item.title}`"
:disabled="schema.disabled === true"
style="width: 100%"
:min="item.nMin ? item.nMin : -100000000"
:max="item.nMax ? item.nMax : 100000000"
:step="item.nStep ? item.nStep : 10"
:precision="item.nPrecision ? item.nPrecision : 0"
/>
<!-- 选择框 -->
<el-select
v-if="item.type === 'select'"
v-model="model[item.name]"
clearable
:placeholder="`请选择${item.title}`"
:disabled="model.disabled === true"
:disabled="schema.disabled === true"
style="width: 100%"
:multiple="schema.multiple ? schema.multiple : false"
filterable
>
<el-option
v-for="option in (item.dataSource || [])"
@@ -52,15 +66,15 @@
type="daterange"
align="right"
unlink-panels
:disabled="model.disabled === true"
:disabled="schema.disabled === true"
clearable
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd"
:picker-options="pickerOptions"
style="width: 100%"
:picker-options="pickerOptions"
/>
<!-- 日期时间选择 -->
<el-date-picker
@@ -69,15 +83,21 @@
type="datetimerange"
align="right"
clearable
:disabled="model.disabled === true"
:disabled="schema.disabled === true"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="yyyy-MM-dd HH:mm:ss"
value-format="yyyy-MM-dd HH:mm:ss"
:default-time="['00:00:00', '23:59:59']"
:picker-options="pickerOptions"
style="width: 100%"
:picker-options="pickerOptions"
/>
<!-- 文件上传 -->
<cute-file-drag-upload
v-if="item.type === 'upload'"
v-model="model[item.name]"
mode="local"
/>
</el-form-item>
</el-form>
@@ -92,9 +112,11 @@
</template>
<script>
import { IsNotBlank } from '@/utils/KitUtil'
import CuteFileDragUpload from '@/views/components/business/CuteFileDragUpload.vue'
export default {
name: 'CuteFormDrawerPro',
components: { CuteFileDragUpload },
props: {
/**
* 弹窗标题
@@ -138,7 +160,6 @@ export default {
data() {
return {
visible: false,
rules: {},
pickerOptions: {
shortcuts: [{
text: '最近一周',
@@ -172,18 +193,44 @@ export default {
dynamicRules() {
const schema = this.schema
const formRules = {}
let trigger = 'blur'
let tip = '请输入'
for (const element of schema) {
const rule1 = {}
// 布尔值,表示该字段是否必填
rule1.required = element.required === true
// 字符串,触发验证的时机,如 blur(失去焦点时),change(数据改变时),input(输入时)等
rule1.trigger = (element.type === 'input' || element.type === 'number') ? 'blur' : 'change'
// 字符串,验证失败时显示的错误信息
rule1.message = (element.type === 'input' || element.type === 'number') ? '请输入' + element.title : '请选择' + element.title
// 验证的类型,如 string, number, date, array, object 等。其中array适用于checkbox,date类型会触发日期值对象校验
let ruleType = null
if (element.type === 'input') {
trigger = 'blur'
} else {
trigger = 'change'
tip = '请选择'
ruleType = 'string'
} else if (element.type === 'number') {
ruleType = 'number'
} else if (element.type === 'select') {
ruleType = null
} else if (element.type === 'date') {
ruleType = null
} else if (element.type === 'datetime') {
ruleType = null
}
formRules[element.name] = [
{ required: element.required === true, message: tip + element.title, trigger: trigger }
]
if (ruleType != null) {
rule1.type = ruleType
}
// 当类型为number时的限制
if (element.type === 'number') {
rule1.min = element.nMin ? element.nMin : -100000000
rule1.max = element.nMax ? element.nMax : 100000000
}
// 当存在正则表达式时,用于匹配输入值
if (IsNotBlank(element.pattern)) {
rule1.pattern = element.pattern
}
// 当存在自定义验证器时,自定义验证函数。该函数接收两个参数:rule 和 value,返回一个 Promise 或一个布尔值
if (element.validator) {
rule1.validator = element.validator
}
formRules[element.name] = [rule1]
}
return formRules
}
@@ -212,7 +259,7 @@ export default {
const schema = this.schema
const model = this.model
for (const element of schema) {
if (element.type === 'input' || element.type === 'select') {
if (element.type === 'input' || element.type === 'number' || element.type === 'select' || element.type === 'upload') {
model[element.name] = null
continue
}
@@ -234,7 +281,9 @@ export default {
.container-form {
overflow-y: scroll; /* 启用滚动 */
scrollbar-width: none; /* Firefox */
padding-right: 20px;
padding-top: 10px;
padding-left: 15px;
padding-right: 15px;
-ms-overflow-style: none; /* Internet Explorer 10+ */
max-height: calc(100vh - 200px); /* 调整高度以适应页头和底部按钮 */
}
@@ -28,6 +28,8 @@
v-model="model[item.name]"
clearable
:placeholder="`请选择${item.title}`"
style="width: 100%"
filterable
>
<el-option
v-for="option in (item.dataSource || [])"
@@ -68,7 +70,7 @@
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onFormSubmit('searchForm')">查询</el-button>
<el-button v-prevent-re-click type="primary" @click="onFormSubmit('searchForm')">查询</el-button>
<el-button @click="onFormReset('searchForm')">重置</el-button>
</el-form-item>
<!-- 通过 <template v-slot:operation> 插槽后置操作按钮 -->
@@ -28,7 +28,7 @@ import { getToken } from '@/utils/auth'
import KitMessage from '@/utils/elementui/KitMessage'
export default {
name: 'CuteFileUpload',
name: 'CuteFileDragUpload',
props: {
value: {
type: String,
@@ -52,9 +52,11 @@ export default {
model: {},
schema: [
{ name: 'name', title: '姓名', type: 'input', required: true },
{ name: 'sex', title: '性别', type: 'select', required: true, dataSource: [{ label: '男', value: 'nn' }, { label: '女', value: 'mm' }] },
{ name: 'sex', title: '性别', type: 'select', required: true, sOpts: [{ label: '男', value: 'nn' }, { label: '女', value: 'mm' }] },
{ name: 'createDate', title: '创建日期', type: 'date', required: false },
{ name: 'createTime', title: '创建时间', type: 'datetime', required: false }
{ name: 'createTime', title: '创建时间', type: 'datetime', required: false },
{ name: 'price', title: '价格', type: 'number', required: true, nMin: 0, nMax: 100, nStep: 10.5, nPrecision: 1 },
{ name: 'extraFile', title: '附件', type: 'upload', required: true }
],
apiData: [
{ name: 'title', remark: '弹窗的标题', type: 'string', defaultValue: '默认标题', required: '否' },
@@ -70,9 +72,29 @@ export default {
schemaData: [
{ name: 'name', remark: '表单项名称', type: 'string', defaultValue: '-', required: '是' },
{ name: 'title', remark: '表单项标题', type: 'string', defaultValue: '-', required: '是' },
{ name: 'type', remark: '表单项类型。input 输入框 | select 单选框 | date 日期选择框 | datetime 日期时间选择框', type: 'string', defaultValue: '-', required: '是' },
{
name: 'type',
remark: '表单项类型。input 输入框 | number 输入框(仅数字) | select 单选框 | date 日期选择框 | datetime 日期时间选择框 | upload 文件上传',
type: 'string',
defaultValue: '-',
required: '是'
},
{ name: 'required', remark: '是否必填', type: 'boolean', defaultValue: 'false', required: '否' },
{ name: 'dataSource', remark: '数据源,当type=select时必填', type: 'array', defaultValue: '[]', required: '否' }
{ name: 'disabled', remark: '是否禁用', type: 'boolean', defaultValue: 'false', required: '否' },
{ name: 'pattern', remark: '正则表达式,用于匹配输入值', type: 'string', defaultValue: '-', required: '否' },
{
name: 'validator',
remark: '自定义验证函数',
type: '(rule, value, callback) => { // 验证失败 callback(new Error(\'请输入密码\')); // 验证通过,无错误信息返回 callback() }',
defaultValue: '-',
required: '否'
},
{ name: 'sOpts', remark: '数据源,当type=select时必填', type: 'array', defaultValue: '[]', required: '否' },
{ name: 'sMultiple', remark: '是否多选,当type=select时有效', type: 'boolean', defaultValue: 'false', required: '否' },
{ name: 'nMin', remark: '最小值,当type=number时有效', type: 'number', defaultValue: '-100000000', required: '否' },
{ name: 'nMax', remark: '最大值,当type=number时有效', type: 'number', defaultValue: '100000000', required: '否' },
{ name: 'nStep', remark: '步长,当type=number时有效', type: 'number', defaultValue: '10', required: '否' },
{ name: 'nPrecision', remark: '精度,当type=number时有效', type: 'number', defaultValue: '0', required: '否' }
],
methodData: [
{ name: 'show', remark: '显示对话框', inputArgs: '-', outArgs: '-' },
@@ -52,9 +52,10 @@ export default {
model: {},
schema: [
{ name: 'name', title: '姓名', type: 'input', required: true },
{ name: 'sex', title: '性别', type: 'select', required: true, dataSource: [{ label: '男', value: 'nn' }, { label: '女', value: 'mm' }] },
{ name: 'sex', title: '性别', type: 'select', required: true, sOpts: [{ label: '男', value: 'nn' }, { label: '女', value: 'mm' }] },
{ name: 'createDate', title: '创建日期', type: 'date', required: false },
{ name: 'createTime', title: '创建时间', type: 'datetime', required: false }
{ name: 'createTime', title: '创建时间', type: 'datetime', required: false },
{ name: 'price', title: '价格', type: 'number', required: true, nMin: 0, nMax: 100, nStep: 10.5, nPrecision: 1 }
],
apiData: [
{ name: 'title', remark: '弹窗的标题', type: 'string', defaultValue: '默认标题', required: '否' },
@@ -69,9 +70,29 @@ export default {
schemaData: [
{ name: 'name', remark: '表单项名称', type: 'string', defaultValue: '-', required: '是' },
{ name: 'title', remark: '表单项标题', type: 'string', defaultValue: '-', required: '是' },
{ name: 'type', remark: '表单项类型。input 输入框 | select 单选框 | date 日期选择框 | datetime 日期时间选择框', type: 'string', defaultValue: '-', required: '是' },
{
name: 'type',
remark: '表单项类型。input 输入框 | number 输入框(仅数字) | select 单选框 | date 日期选择框 | datetime 日期时间选择框 | upload 文件上传',
type: 'string',
defaultValue: '-',
required: '是'
},
{ name: 'required', remark: '是否必填', type: 'boolean', defaultValue: 'false', required: '否' },
{ name: 'dataSource', remark: '数据源,当type=select时必填', type: 'array', defaultValue: '[]', required: '否' }
{ name: 'disabled', remark: '是否禁用', type: 'boolean', defaultValue: 'false', required: '否' },
{ name: 'pattern', remark: '正则表达式,用于匹配输入值', type: 'string', defaultValue: '-', required: '否' },
{
name: 'validator',
remark: '自定义验证函数',
type: '(rule, value, callback) => { // 验证失败 callback(new Error(\'请输入密码\')); // 验证通过,无错误信息返回 callback() }',
defaultValue: '-',
required: '否'
},
{ name: 'sOpts', remark: '数据源,当type=select时必填', type: 'array', defaultValue: '[]', required: '否' },
{ name: 'sMultiple', remark: '是否多选,当type=select时有效', type: 'boolean', defaultValue: 'false', required: '否' },
{ name: 'nMin', remark: '最小值,当type=number时有效', type: 'number', defaultValue: '-100000000', required: '否' },
{ name: 'nMax', remark: '最大值,当type=number时有效', type: 'number', defaultValue: '100000000', required: '否' },
{ name: 'nStep', remark: '步长,当type=number时有效', type: 'number', defaultValue: '10', required: '否' },
{ name: 'nPrecision', remark: '精度,当type=number时有效', type: 'number', defaultValue: '0', required: '否' }
],
methodData: [
{ name: 'show', remark: '显示对话框', inputArgs: '-', outArgs: '-' },