快速了解
JNPF原有表单与控件基本上均使用Ant Design for Vue的开源组件,对antd组件进行了二次封装。
其中,每个JNPF组件都有一个核心对象贯穿整个表单的生命周期(编辑-填写-结果),该对象包含了该组件的类型(jnpfKey
)、标签名(label
)、值(defaultValue
)、可见性(visibility
)、图标(tagIcon
)、校验规则(regList
)、校验触发方式(trigger
)、额外类名(className
)、额外样式(style
)、额外脚本事件(on: { change: ..., blur: ..., ... }
)、额外参数(如required
、placeholder
、disabled
等)以及组件自有的额外属性(如多选框组的options
列表等)。
JNPF会对组件的核心对象进行parse等操作,得到该表单的drawingList
,配合外层的antd的Form组件,最终添加到DOM树并渲染。
因此,所有操作的核心都是围绕着组件的核心对象进行的:根据核心对象反显组件、通过各种方式修改核心对象以修改组件、拓展核心对象的属性以拓展组件等。
此外,每个组件Comp
都有一个相对应的右边栏组件RComp
用来配置组件和设置组件特有的额外属性等。
关键理解
核心对象(componentMap.ts
)
简介
在该文件中定制组件的核心对象,且添加到该文件中的每一个对象都会作为【左面板】中的一个可添加组件。
实际添加的组件类型根据对象中的jnpfKey
属性来区分、索引。
组件的核心值(保存用户的填写内容)为defaultValue
属性,不同组件可以有不同的类型。
路径
src/components/FormGenerator/src/helper/componentMap.ts
内容
如多选框组件:
// 基础控件 【左面板】 (只有新增的控件会应用新配置)
export const inputComponents: GenItem[] = [
{
__config__: {
jnpfKey: 'checkbox', // 区别于其他组件的key
label: '多选框组', // 也被作为校验非法时的提示名 "请至少选择一个${label}"
tipLabel: '',
labelWidth: undefined,
showLabel: true,
tag: 'JnpfCheckbox',
tagIcon: 'icon-ym icon-ym-generator-checkbox',
className: [],
defaultValue: [], // 核心值
required: false, // 控制是否必填
layout: 'colFormItem',
span: 24,
dragDisabled: false,
visibility: ['pc', 'app'],
tableName: '',
noShow: false, // 控制是否隐藏
// 在regExp中用 \S 会有问题,会被转换为 S ,如 /^(?!\[\])\S*$/ 就会被转换为 /^(?!\[\])S*$/ 从而表现异常
// 所以要用 \\ 来代替 \
// regList: [{ pattern: `/^(?!\[\])\\S*$/`}], // 对value的额外校验规则
trigger: ['change', 'blur'], // 校验的触发方式
dataType: 'static',
dictionaryType: '',
propsUrl: '',
propsName: '',
templateJson: [],
hasInput: false, // 自定义额外属性
inputLabel: '其他', // 自定义额外属性
},
on: {
change: '({ data, formData, setFormData, setShowOrHide, setRequired, setDisabled, request }) => {\n // 在此编写代码\n \n}',
},
style: { width: '100%' },
options: [
{
fullName: '选项一',
id: '1',
},
{
fullName: '选项二',
id: '2',
},
],
props: {
label: 'fullName',
value: 'id',
},
direction: 'horizontal',
disabled: false, // 控制是否被禁用
},
// 其他组件...
];
组件示例
Input.vue - JNPF表单组件
- 路径为
src/components/Jnpf/Input/src/Input.vue
- 【反显】可以通过
props.value
获取核心对象中的defaultValue
(当前组件的核心值) - 【反显】可以通过
props.XXX
获取直接在核心对象中自定义的属性XXX
- 【更新】组件可通过
emit
发出update:value
和change
事件信号触发父级表单的状态更新与校验
<template>
<component :is="Comp" :class="prefixCls" v-bind="getBindValue" v-model:value="innerValue" @change="onChange">
<template #[item]="data" v-for="item in Object.keys($slots)"><slot :name="item" v-bind="data || {}"></slot></template>
<template #prefix v-if="prefixIcon">
<i :class="prefixIcon"></i>
</template>
<template #suffix v-if="suffixIcon">
<i :class="suffixIcon"></i>
</template>
</component>
</template>
<script lang="ts" setup>
import { Input } from 'ant-design-vue';
import { computed, ref, unref, watch } from 'vue';
import { inputProps } from './props';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { useDesign } from '/@/hooks/web/useDesign';
import { useDebounceFn } from '@vueuse/core';
const InputPassword = Input.Password;
defineOptions({ name: 'JnpfInput', inheritAttrs: false });
const props = defineProps(inputProps);
const emit = defineEmits(['update:value', 'change']);
const attrs = useAttrs({ excludeDefaultKeys: false });
const innerValue = ref('');
const Comp = props.showPassword ? InputPassword : Input;
const { prefixCls } = useDesign('input');
const debounceOnChange = useDebounceFn(value => {
emit('change', value);
}, 200);
const getBindValue = computed(() => ({ ...unref(attrs) }));
watch(
() => props.value,
val => {
setValue(val);
},
{ immediate: true },
);
function setValue(value) {
innerValue.value = value;
}
function onChange(e) {
emit('update:value', e.target.value);
debounceOnChange(e.target.value);
}
</script>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-input';
.@{prefix-cls} {
:deep(.ant-input-prefix),
:deep(.ant-input-suffix) {
i {
line-height: 20px;
color: @text-color-help-dark;
}
}
}
</style>
RInput.vue - 右边栏组件
- 路径为
src/components/FormGenerator/src/rightComponents/RInput.vue
- 是
Input.vue
对应的组件的右边栏组件,可以在此提供一些控件用于编辑组件的属性等 activeData
是该组件对应的Input.vue
表单组件的响应式的核心对象,可通过activeData.XXX
访问或修改核心对象的属性
<template>
<a-form-item label="默认值">
<a-input v-model:value="activeData.__config__.defaultValue" placeholder="请输入默认值" />
</a-form-item>
<a-form-item label="前缀" v-show="showType === 'pc'">
<a-input v-model:value="activeData.addonBefore" placeholder="请输入前缀" />
</a-form-item>
<a-form-item label="后缀" v-show="showType === 'pc'">
<a-input v-model:value="activeData.addonAfter" placeholder="请输入后缀" />
</a-form-item>
<a-form-item label="前图标" v-show="showType === 'pc'">
<jnpf-icon-picker v-model:value="activeData.prefixIcon" placeholder="请选择前图标" />
</a-form-item>
<a-form-item label="后图标" v-show="showType === 'pc'">
<jnpf-icon-picker v-model:value="activeData.suffixIcon" placeholder="请选择后图标" />
</a-form-item>
<a-form-item label="最多输入">
<a-input-number v-model:value="activeData.maxlength" placeholder="请输入字符长度" :min="0" addonAfter="个字符" />
</a-form-item>
<a-form-item label="能否清空">
<a-switch v-model:checked="activeData.clearable" />
</a-form-item>
<a-form-item label="是否密码">
<a-switch v-model:checked="activeData.showPassword" @change="activeData.__config__.renderKey = +new Date()" />
</a-form-item>
<a-form-item label="是否只读" v-show="showType === 'pc'">
<a-switch v-model:checked="activeData.readonly" />
</a-form-item>
<a-form-item label="是否禁用">
<a-switch v-model:checked="activeData.disabled" />
</a-form-item>
<a-form-item label="是否隐藏">
<a-switch v-model:checked="activeData.__config__.noShow" />
</a-form-item>
<a-divider>校验规则</a-divider>
<a-form-item label="是否必填">
<a-switch v-model:checked="activeData.__config__.required" />
</a-form-item>
<a-form-item>
<template #label>是否唯一<BasicHelp text="输入值唯一性校验" /></template>
<a-switch v-model:checked="activeData.__config__.unique" />
</a-form-item>
<div v-for="(item, index) in activeData.__config__.regList" :key="index" class="reg-item">
<span class="close-btn" @click="activeData.__config__.regList.splice(index, 1)">
<i class="icon-ym icon-ym-nav-close" />
</span>
<a-form-item label="表达式">
<a-input v-model:value="item.pattern" placeholder="请输入正则" @change="onPatternChange" />
</a-form-item>
<a-form-item label="错误提示" style="margin-bottom: 0">
<a-input v-model:value="item.message" placeholder="请输入错误提示" />
</a-form-item>
</div>
<div class="mt-10px">
<a-dropdown>
<template #overlay>
<a-menu>
<a-menu-item v-for="(item, i) in ruleList" :key="i" @click="addReg(item)">{{ item.label }}</a-menu-item>
</a-menu>
</template>
<a-button type="primary">添加常用校验<DownOutlined /></a-button>
</a-dropdown>
<a-button type="primary" style="margin-left: 10px" @click="addReg()">自定义规则</a-button>
</div>
</template>
<script lang="ts" setup>
import { inject, computed } from 'vue';
import { DownOutlined } from '@ant-design/icons-vue';
import { useMessage } from '/@/hooks/web/useMessage';
const ruleList = [
{
pattern: '/^\\d+$/',
message: '请输入正确的数字',
label: '数字',
},
{
pattern: '/^([1-9][\\d]*|0)(\\.[\\d]+)?$/',
message: '请输入正确的金额',
label: '金额',
},
{
pattern: '/^0\\d{2,3}-?\\d{7,8}$/',
message: '请输入正确的电话号码',
label: '电话',
},
{
pattern: '/^1[3456789]\\d{9}$/',
message: '请输入正确的手机号码',
label: '手机',
},
{
pattern: '/^1[3456789]\\d{9}$|^0\\d{2,3}-?\\d{7,8}$/',
message: '请输入正确的联系方式',
label: '电话/手机',
},
{
pattern: '/^[a-z0-9]+([._\\-]*[a-z0-9])*@([a-z0-9]+[-a-z0-9]*[a-z0-9]+.){1,63}[a-z0-9]+$/',
message: '请输入正确的邮箱',
label: '邮箱',
},
{
pattern: '/^[1-9]\\d{5}(18|19|([23]\\d))\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$/',
message: '请输入正确的身份证号码',
label: '身份证',
},
];
defineOptions({ inheritAttrs: false });
const props = defineProps(['activeData']);
const { createMessage } = useMessage();
const getShowType: (() => string | undefined) | undefined = inject('getShowType');
const showType = computed(() => (getShowType as () => string | undefined)());
function addReg(row?) {
const item = {
pattern: row?.pattern ? row?.pattern : '',
message: row?.message ? row?.message : '',
};
props.activeData.__config__.regList.push(item);
}
function onPatternChange(e) {
try {
const val = e.target.value;
const isRegExp = Object.prototype.toString.call(eval(val)) === '[object RegExp]';
if (!isRegExp) createMessage.error('请输入正确的正则表达式');
} catch {
createMessage.error('请输入正确的正则表达式');
}
}
</script>
表单校验
方式一:默认校验
默认的“必选”校验。
方式二:正则表达式校验
通过自定义正则表达式(核心对象的regList
属性(是正则表达式字符串,类型为string
))对组件的核心值defaultValue
属性进行合法性校验。
PS:JNPF原有的组件均为通过正则表达式的方式进行校验,尚未有较复杂的自定义校验。
方式三:自定义校验
按照antd Form
组件的文档中的方式进行自定义校验。[>自定义表单校验 - 官方示例<]
【思路】
为了进行自定义校验,我们需要在<a-form />
组件上添加一个rules
属性,其中包含若干个规则,每个规则都有一个包含validator
属性的对象,该属性是自定义校验函数,该函数可接收value
参数以进行自定义校验。我们可以在自定义组件的核心对象中添加一个自定义校验规则的属性,然后在<a-form />
和<a-form-item></a-form-item>
组件处获取所有核心对象并动态引入所有规则,然后就可以在自定义组件中进行自定义校验。
这部分的实现可看src/components/FormGenerator/src/components/Parser.vue
中的buildRules
函数对state.formRules
的处理。
【操作】
我们可以把核心对象中的__config__.regList
数组中的元素当做一个校验规则对象来写,比如:
regList: [{
// 需要注意的是 这里不能直接写函数,要写字符串,否则不会被解析
// (因为核心对象会先被转换为json字符串,属性值不能是函数)
validator: `\
(function(){
return function (_rule, value) {
if (value.length === 0) {
return Promise.reject('请至少选择一个选项!');
} else {
return Promise.resolve();
}
}
})()`,
}],
在这里添加validator
校验函数即可实现自定义校验,其他可选配置请看antd官方文档。
PS: 自定义校验函数理论上不应与已有的required
校验重复,不然会有两条警告提示信息。
【优化】
为了便于自定义校验函数的编写,我扩展了右边栏组件,现在可以在表单编辑页面像编辑“脚本事件”那样在Web代码编辑器中编辑自定义校验函数了。我们可以在项目代码中编写该类组件的默认自定义校验函数(如【操作】中所述方法),然后在表单编辑页面,在默认内容的基础上,再对创建的每一个组件实例二次定制自定义校验函数。
手动触发校验的方法
import {Form} from 'ant-design-vue';
const {id, onFieldChange, onFieldBlur} = Form.useInjectFormItemContext();
在自定义组件中可以手动调用onFieldChange
和onFieldBlur
方法来触发校验。
给自定义校验函数脚本添加上下文作用域
在src/components/FormGenerator/src/components/Parser.vue
的buildRules
函数中可扩充自定义校验函数脚本的上下文作用域(在item.validator && (item.validator = eval(item.validator));
之前)。
给事件脚本添加可用参数和上下文作用域
在src/components/FormGenerator/src/components/Parser.vue
的getParameter
函数返回值中中可添加事件脚本的可用参数;
在src/utils/jnpf.ts
的getScriptFunc
函数中可扩充事件脚本的上下文作用域(在func = eval(str);
之前)。
扩充脚本编辑器左边栏功能区
在src/components/FormGenerator/src/components/FormScript.vue
中进行扩展。
表单组件间的传值与控制显隐
可通过编辑表单组件时在右边栏的“脚本事件”处编辑脚本来实现,脚本可获取所有组件的核心对象并更改。
data--当前组件的选中数据,formData--表单数据,setFormData--设置表单某个组件数据(prop,value)
setShowOrHide--设置显示或隐藏(prop,value),setRequired--设置必填项(prop,value)
setDisabled--设置禁用项(prop,value),request--异步请求(url,method,data)
附录:添加自定义组件
1. 创建组件
在src/components/Jnpf/TestText/src/TestText.vue
路径下完善新增组件。
需要注意的是要手动watch一下props.value
来更新internalValue
的值,否则在右边栏修改defaultValue
无效。
自定义组件示例:
<!-- TestText.vue -->
<template>
<div class="unique-input-wrapper">
<label :for="id">{{ label }}</label>
<input :id="id" v-model="internalValue" @input="emitValue" :placeholder="props.placeholder"/>
</div>
</template>
<script setup>
import {ref, defineProps, defineEmits, watch} from 'vue';
const emit = defineEmits();
const props = defineProps({
label: {
type: String,
default: 'Unique Input'
},
placeholder: {
type: String,
default: 'Unique Input'
},
value: {
type: String,
default: ''
},
id: {
type: String,
default: 'unique-input'
}
});
const internalValue = ref(props.value);
const emitValue = () => {
emit('update:value', internalValue.value);
};
// 要watch一下props.value 来改变internalValue
watch(
() => props.value,
val => {
setValue(val);
},
{ immediate: true },
)
function setValue(value) {
internalValue.value = value;
}
</script>
<style scoped>
.unique-input-wrapper {
display: flex;
flex-direction: column;
margin-bottom: 20px;
}
label {
font-size: 16px;
margin-bottom: 8px;
}
input {
padding: 10px;
border: none;
border-radius: 12px;
background: linear-gradient(45deg, #ff9a9e, #fad0c4, #f6a4d5);
color: white;
font-size: 16px;
}
input:focus {
outline: none;
box-shadow: 0 0 5px rgba(255, 255, 255, 0.5);
}
input::placeholder {
font-weight: bold;
opacity: 0.5;
color: white;
}
</style>
2. 导出组件
新建src/components/Jnpf/TestText/index.ts
文件并export该组件。
// index.ts
import { withInstall } from '/@/utils';
import TestText from './src/TestText.vue';
export const JnpfTestText = withInstall(TestText);
在src/components/Jnpf/index.ts
中(补充)export该组件。
// index.ts
export {
// ...
JnpfTestText,
}
3. 映射组件
在src/components/Form/src/componentMap.ts
中(补充)import该组件。
这里设置的'TestText'
就是用来索引该组件的jnpfKey
。
// componentMap.ts
import {
// ...
JnpfTestText,
} from '@/components/Jnpf';
componentMap.set('TestText', JnpfTestText);
4. 类型定义
在src/components/Form/src/types/index.ts
中(补充)ComponentType
类型。
这里也是添加组件的jnpfKey
。
// index.ts
export type ComponentType =
// ...
| 'TestText';
5. 配置组件
在src/components/FormGenerator/src/helper/componentMap.ts
中补充左边栏面板的添加组件按钮(和其他参数、属性的配置)。
需要注意这里的jnpfKey
属性很重要,用来区分不同的表单组件。
// componentMap.ts
export const inputComponents: ... = [
{
__config__: {
jnpfKey: 'TestText',
label: '新增组件',
tipLabel: '',
labelWidth: undefined,
showLabel: true,
tag: 'JnpfTestText',
tagIcon: 'icon-ym icon-ym-generator-input',
className: [],
defaultValue: '',
required: false,
layout: 'colFormItem',
span: 24,
dragDisabled: false,
visibility: ['pc', 'app'],
tableName: '',
noShow: false,
regList: [],
trigger: 'blur',
},
on: {
change: '({ data, formData, setFormData, setShowOrHide, setRequired, setDisabled, request }) => {\n // 在此编写代码\n \n}',
blur: '({ data, formData, setFormData, setShowOrHide, setRequired, setDisabled, request }) => {\n // 在此编写代码\n \n}',
},
placeholder: '请输入内容',
style: { width: '100%' },
clearable: true,
label: '这里是label区',
readonly: false,
disabled: false,
},
// ...
];
6. 添加到有效字段列表
在src/views/common/formShortLink/form/index.vue
中将该组件的jnpfKey
添加进有效字段列表。
这里加的是组件的jnpfKey
。
// index.vue
const validFieldsList = [
// ...
'TestText',
];
同上,在src/views/common/formShortLink/list/index.vue
中将该组件的jnpfKey
添加进有效字段列表。
7. 自定义组件右边栏
在src/components/FormGenerator/src/rightComponents/RTestText.vue
中创建该组件的右边栏组件。
<template>
<a-form-item label="默认值">
<a-input v-model:value="activeData.__config__.defaultValue" placeholder="请输入默认值" />
</a-form-item>
<a-form-item label="Label">
<a-input v-model:value="activeData.label" placeholder="请输入Label" />
</a-form-item>
</template>
<script lang="ts" setup>
import { inject, computed } from 'vue';
import { useMessage } from '/@/hooks/web/useMessage';
defineOptions({ inheritAttrs: false });
const props = defineProps(['activeData']);
const { createMessage } = useMessage();
const getShowType: (() => string | undefined) | undefined = inject('getShowType');
const showType = computed(() => (getShowType as () => string | undefined)());
</script>
需要注意的是,组件要有一个activeData
属性参数,其值就是这样一个对象(在src/components/FormGenerator/src/helper/componentMap.ts
中自定义的):
{
__config__: {
jnpfKey: 'TestText',
label: '新增组件',
tipLabel: '',
labelWidth: undefined,
showLabel: true,
tag: 'JnpfTestText',
tagIcon: 'icon-ym icon-ym-generator-input',
className: [],
defaultValue: '',
required: false,
layout: 'colFormItem',
span: 24,
dragDisabled: false,
visibility: ['pc', 'app'],
tableName: '',
noShow: false,
regList: [],
trigger: 'blur',
},
on: {
change: '({ data, formData, setFormData, setShowOrHide, setRequired, setDisabled, request }) => {\n // 在此编写代码\n \n}',
blur: '({ data, formData, setFormData, setShowOrHide, setRequired, setDisabled, request }) => {\n // 在此编写代码\n \n}',
},
placeholder: '请输入内容',
style: { width: '100%' },
clearable: true,
label: '这里是label区',
readonly: false,
disabled: false,
},
8. 导出右边栏组件
在src/components/FormGenerator/src/rightComponents/index.ts
中export该右边栏组件
// ...
export { default as RTestText } from './RTestText.vue';
9. 自定义通用右边栏(可选)
如有需要,可在src/components/FormGenerator/src/RightPanel.vue
中定制右边栏面板。
到此就完成了添加自定义组件。
附录2:相关文件速查
JNPF组件
- 路径为
src/components/Jnpf/...
- 可在此添加新的自定义组件
- 需在
src/components/Jnpf/index.ts
中export
自定义新增组件才能使用
JNPF组件 - 右边栏组件
- 路径为
src/components/FormGenerator/src/rightComponents
- 可在此添加新组件的右边栏组件
- 需在
src/components/FormGenerator/src/rightComponents/index.ts
中export
自定义新增组件的右边栏组件才能使用
RightPanel.vue - 通用右边栏组件
- 路径为
src/components/FormGenerator/src/RightPanel.vue
- 可在此设置通用的右边栏内容
- 有筛选规则,在
src/components/FormGenerator/src/helper/rightPanel.ts
中
Parser.vue - 获取作答界面的表单组件
- 路径为
src/components/FormGenerator/src/components/Parser.vue
- 涉及
submit()
、handleSubmit()
、beforeSubmit()
、handleReset()
、buildRules()
、buildListeners()
、a-form
、Item
、a-form-item
、formElRef
等 - 添加校验规则(
buildRules(componentList)
) - 响应
render.ts
中的buildVModel中emit('update:value', val)
- 渲染整个表单(
renderForm()
) - 在作答界面被调用(与
DraggableItem.vue
的区别之一)
FormGenerator.vue - 表单编辑界面
- 路径为
src/components/FormGenerator/src/FormGenerator.vue
- 表单编辑界面及相关操作
DraggableItem.vue - 表单编辑界面的表单组件
-
路径为
src/components/FormGenerator/src/DraggableItem.vue
-
涉及
layouts
、Item
、colFormItem
、rowFormItem
、a-col
、a-form-item
、render
、onUpdate:value
、draggable
等 -
在表单编辑界面被调用(与
Parser.vue
的区别之一)
render.ts
- 路径为
src/components/FormGenerator/src/helper/render.ts
- 涉及
buildDataObject
、componentMap.get()
、emitEvents
、realDataObject
、jnpfKey
等 - 将json表单配置转化为vue render可以识别的数据对象(
dataObject
) - 根据
jnpfKey
获取到相应的组件
formShortLink/form - 表单作答界面
- 路径为
src/views/common/formShortLink/form/index.vue
- 涉及
handleReset()
、handleSubmit()
、validFieldsList
、Parser
等 - 表单作答界面及相关操作
- 需将新增组件的
jnpfKey
添加到validFieldsList
中才能在作答界面显示出来
formShortLink/list - 作答详情界面
- 路径为
src/views/common/formShortLink/list/index.vue
- 表单作答详情界面及相关操作
- 需将新增组件的
jnpfKey
添加到validFieldsList
中才能在详情界面显示出来
.env.development - 配置文件
# 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH = /
# 本地开发代理,可以解决跨域及多地址代理
# 如果接口地址匹配到,则会转发到http://localhost:30000,防止本地出现跨域问题
# 可以有多个,注意多个不能换行,否则代理将会失效
VITE_PROXY = [["/dev","http://192.168.2.213"]]
# 是否删除Console.log
VITE_DROP_CONSOLE = false
# 接口地址
# 如果没有跨域问题,直接在这里配置即可
VITE_GLOB_API_URL=/dev
# WebSocket基础地址
VITE_GLOB_WEBSOCKET_URL='ws://192.168.2.213'
# 接口地址前缀,有些系统所有接口地址都有前缀,可以在这里统一加,方便切换
VITE_GLOB_API_URL_PREFIX=
useWebSocket.ts - URL需更新
- 路径为
src/hooks/web/useWebSocket.ts
url
需要修改,如下
const url = isDevMode()
? globSetting.webSocketUrl + '/websocket/'
: globSetting.webSocketUrl
? globSetting.webSocketUrl + '/websocket/'
: window.location.origin + '/websocket/';
Comments NOTHING