初始代码

This commit is contained in:
wangmingwei
2026-04-21 17:48:26 +08:00
parent d3631949e9
commit 7f9e424a5c
1822 changed files with 288292 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
import BasicForm from './src/BasicForm.vue';
export * from './src/types/form';
export * from './src/types/formItem';
export { useComponentRegister } from './src/hooks/useComponentRegister';
export { useForm } from './src/hooks/useForm';
export { BasicForm };

View File

@@ -0,0 +1,358 @@
<template>
<Form v-bind="getBindValue" :class="getFormClass" ref="formElRef" :model="formModel" @keypress.enter="handleEnterPress" :name="getFormName">
<Row v-bind="getRow">
<slot name="formHeader"></slot>
<template v-for="schema in getSchema" :key="schema.field">
<FormItem
:isAdvanced="fieldsIsAdvancedMap[schema.field]"
:tableAction="tableAction"
:formActionType="formActionType"
:schema="schema"
:formProps="getProps"
:allDefaultValues="defaultValueRef"
:formModel="formModel"
:setFormModel="setFormModel">
<template #[item]="data" v-for="item in Object.keys($slots)">
<slot :name="item" v-bind="data || {}"></slot>
</template>
</FormItem>
</template>
<FormAction v-bind="getFormActionBindProps" @toggle-advanced="handleToggleAdvanced">
<template #[item]="data" v-for="item in ['resetBefore', 'submitBefore', 'advanceBefore', 'advanceAfter']">
<slot :name="item" v-bind="data || {}"></slot>
</template>
</FormAction>
<slot name="formFooter"></slot>
</Row>
</Form>
</template>
<script lang="ts">
import type { FormActionType, FormProps, FormSchema } from './types/form';
import type { AdvanceState } from './types/hooks';
import type { Ref } from 'vue';
import { defineComponent, reactive, ref, computed, unref, onMounted, watch, nextTick } from 'vue';
import { Form, Row } from 'ant-design-vue';
import FormItem from './components/FormItem.vue';
import FormAction from './components/FormAction.vue';
// import { cloneDeep } from 'lodash-es';
import { deepMerge } from '@/utils';
import { useFormValues } from './hooks/useFormValues';
import useAdvanced from './hooks/useAdvanced';
import { useFormEvents } from './hooks/useFormEvents';
import { createFormContext } from './hooks/useFormContext';
import { useAutoFocus } from './hooks/useAutoFocus';
import { useModalContext } from '@/components/Modal';
import { useDebounceFn } from '@vueuse/core';
import { basicProps } from './props';
import { useDesign } from '@/hooks/web/useDesign';
import { buildUUID } from '@/utils/uuid';
import { isFunction, isArray } from '@/utils/is';
export default defineComponent({
name: 'BasicForm',
components: { FormItem, Form, Row, FormAction },
props: basicProps,
emits: ['advanced-change', 'reset', 'submit', 'register', 'field-value-change'],
setup(props, { emit, attrs }) {
const formModel = reactive<Recordable>({});
const modalFn = useModalContext();
const advanceState = reactive<AdvanceState>({
// 默认是收起状态
isAdvanced: false,
hideAdvanceBtn: false,
isLoad: false,
actionSpan: 6,
});
const defaultValueRef = ref<Recordable>({});
const fullValueRef = ref<Recordable>({});
const isInitedDefaultRef = ref(false);
const propsRef = ref<Partial<FormProps>>({});
const schemaRef = ref<Nullable<FormSchema[]>>(null);
const formElRef = ref<Nullable<FormActionType>>(null);
const { prefixCls } = useDesign('basic-form');
// 每个表单生成不同name保证id不重复
const getFormName = computed((): string => {
return `form-${buildUUID()}`;
});
// Get the basic configuration of the form
const getProps = computed((): FormProps => {
const newProps: any = unref(propsRef);
return { ...props, ...newProps } as FormProps;
});
const getFormClass = computed(() => {
return [
prefixCls,
{
[`${prefixCls}--compact`]: unref(getProps).compact,
},
];
});
// Get uniform row style and Row configuration for the entire form
const getRow = computed((): Recordable => {
const { baseRowStyle = {}, rowProps } = unref(getProps);
return {
style: baseRowStyle,
gutter: 16,
...rowProps,
};
});
const getBindValue = computed(() => ({ ...attrs, ...props, ...unref(getProps) } as Recordable));
const getSchema = computed((): FormSchema[] => {
const schemas: FormSchema[] = unref(schemaRef) || (unref(getProps).schemas as any);
if (unref(getProps).showAdvancedButton) {
return schemas.filter(schema => schema.component !== 'Divider') as FormSchema[];
} else {
return schemas as FormSchema[];
}
});
const { handleToggleAdvanced, fieldsIsAdvancedMap } = useAdvanced({
advanceState,
emit,
getProps,
getSchema,
formModel,
defaultValueRef,
});
const { handleFormValues, initDefault } = useFormValues({
getProps,
defaultValueRef,
getSchema,
formModel,
});
useAutoFocus({
getSchema,
getProps,
isInitedDefault: isInitedDefaultRef,
formElRef: formElRef as Ref<FormActionType>,
});
const {
handleSubmit,
setFieldsValue,
clearValidate,
validate,
validateFields,
getFieldsValue,
updateSchema,
resetSchema,
appendSchemaByField,
removeSchemaByField,
resetFields,
scrollToField,
} = useFormEvents({
emit,
getProps,
formModel,
getSchema,
defaultValueRef,
fullValueRef,
formElRef: formElRef as Ref<FormActionType>,
schemaRef: schemaRef as Ref<FormSchema[]>,
handleFormValues,
isInitedDefaultRef,
});
createFormContext({
resetAction: resetFields,
submitAction: handleSubmit,
});
watch(
() => unref(getProps).model,
() => {
const { model } = unref(getProps);
if (!model) return;
setFieldsValue(model);
},
{
immediate: true,
},
);
watch(
() => unref(getProps).schemas,
schemas => {
resetSchema(schemas ?? []);
},
);
watch(
() => getSchema.value,
schema => {
nextTick(() => {
// Solve the problem of modal adaptive height calculation when the form is placed in the modal
modalFn?.redoModalHeight?.();
});
if (unref(isInitedDefaultRef)) {
return;
}
if (schema?.length) {
initDefault();
isInitedDefaultRef.value = true;
}
},
);
watch(
() => formModel,
useDebounceFn(() => {
unref(getProps).submitOnChange && handleSubmit();
}, 300),
{ deep: true },
);
async function setProps(formProps: Partial<FormProps>): Promise<void> {
propsRef.value = deepMerge(unref(propsRef) || {}, formProps);
}
function setFormModel(key: string, value: any, schema: FormSchema) {
formModel[key] = value;
const { validateTrigger } = unref(getBindValue);
if (isFunction(schema.dynamicRules) || isArray(schema.rules)) {
return;
}
if (!validateTrigger || validateTrigger === 'change') {
validateFields([key]).catch(_ => {});
}
emit('field-value-change', key, value);
}
function handleEnterPress(e: KeyboardEvent) {
const { autoSubmitOnEnter } = unref(getProps);
if (!autoSubmitOnEnter) return;
if (e.key === 'Enter' && e.target && e.target instanceof HTMLElement) {
const target: HTMLElement = e.target as HTMLElement;
if (target && target.tagName && target.tagName.toUpperCase() == 'INPUT') {
handleSubmit();
}
}
}
const formActionType: Partial<FormActionType> = {
getFieldsValue,
setFieldsValue,
resetFields,
updateSchema,
resetSchema,
setProps,
removeSchemaByField,
appendSchemaByField,
clearValidate,
validateFields,
validate,
submit: handleSubmit,
scrollToField: scrollToField,
};
onMounted(() => {
initDefault();
emit('register', formActionType);
});
return {
getBindValue,
handleToggleAdvanced,
handleEnterPress,
formModel,
defaultValueRef,
advanceState,
getRow,
getProps,
formElRef,
getSchema,
formActionType: formActionType as any,
setFormModel,
getFormClass,
getFormActionBindProps: computed((): Recordable => ({ ...getProps.value, ...advanceState })),
fieldsIsAdvancedMap,
...formActionType,
getFormName,
};
},
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-basic-form';
.@{prefix-cls} {
.ant-form-item {
// &-label label::after {
// margin: 0 6px 0 2px;
// }
&-with-help {
margin-bottom: 0;
.ant-form-item-explain {
font-size: 14px;
line-height: 20px;
min-height: 20px !important;
}
}
&:not(.ant-form-item-with-help) {
margin-bottom: 20px;
}
&.suffix-item {
.ant-form-item-children {
display: flex;
}
.ant-form-item-control {
margin-top: 4px;
}
.suffix {
display: inline-flex;
padding-left: 6px;
margin-top: 1px;
line-height: 1;
align-items: center;
}
}
}
.ant-form-item-explain {
height: 0;
}
.ant-form-item-extra {
font-size: 14px;
line-height: 20px;
min-height: 20px !important;
}
&--compact {
.ant-form-item {
margin-bottom: 10px !important;
}
}
&.search-form {
.ant-form-item {
display: flex;
.ant-form-item-row {
display: flex;
flex: 1;
}
.ant-form-item-label {
width: auto !important;
}
}
}
}
</style>

View File

@@ -0,0 +1,150 @@
import type { Component } from 'vue';
import type { ComponentType } from './types/index';
/**
* Component list, register here to setting it in the form
*/
import { StrengthMeter } from '@/components/StrengthMeter';
import { CountdownInput } from '@/components/CountDown';
// yunzhupaas 组件
import {
YunzhupaasAlert,
YunzhupaasAreaSelect,
YunzhupaasAutoComplete,
YunzhupaasButton,
YunzhupaasCron,
YunzhupaasCascader,
YunzhupaasColorPicker,
YunzhupaasCheckbox,
YunzhupaasCheckboxSingle,
YunzhupaasDatePicker,
YunzhupaasDateRange,
YunzhupaasTimePicker,
YunzhupaasTimeRange,
YunzhupaasMonthPicker,
YunzhupaasWeekPicker,
YunzhupaasDivider,
YunzhupaasEditor,
YunzhupaasGroupTitle,
YunzhupaasIconPicker,
YunzhupaasInput,
YunzhupaasInputPassword,
YunzhupaasInputGroup,
YunzhupaasInputSearch,
YunzhupaasTextarea,
YunzhupaasInputNumber,
YunzhupaasLink,
YunzhupaasOpenData,
YunzhupaasOrganizeSelect,
YunzhupaasDepSelect,
YunzhupaasPosSelect,
YunzhupaasGroupSelect,
YunzhupaasRoleSelect,
YunzhupaasUserSelect,
YunzhupaasUsersSelect,
YunzhupaasQrcode,
YunzhupaasBarcode,
YunzhupaasRadio,
YunzhupaasRate,
YunzhupaasSelect,
YunzhupaasSlider,
YunzhupaasSign,
YunzhupaasSignature,
YunzhupaasSwitch,
YunzhupaasText,
YunzhupaasTreeSelect,
YunzhupaasUploadFile,
YunzhupaasUploadImg,
YunzhupaasUploadImgSingle,
YunzhupaasRelationForm,
YunzhupaasRelationFormAttr,
YunzhupaasPopupSelect,
YunzhupaasPopupTableSelect,
YunzhupaasPopupAttr,
YunzhupaasNumberRange,
YunzhupaasCalculate,
YunzhupaasInputTable,
YunzhupaasLocation,
YunzhupaasIframe,
} from '@/components/Yunzhupaas';
const componentMap = new Map<ComponentType, Component>();
componentMap.set('StrengthMeter', StrengthMeter);
componentMap.set('InputCountDown', CountdownInput);
componentMap.set('InputGroup', YunzhupaasInputGroup);
componentMap.set('InputSearch', YunzhupaasInputSearch);
componentMap.set('MonthPicker', YunzhupaasMonthPicker);
componentMap.set('WeekPicker', YunzhupaasWeekPicker);
componentMap.set('Alert', YunzhupaasAlert);
componentMap.set('AreaSelect', YunzhupaasAreaSelect);
componentMap.set('AutoComplete', YunzhupaasAutoComplete);
componentMap.set('Button', YunzhupaasButton);
componentMap.set('Cron', YunzhupaasCron);
componentMap.set('Cascader', YunzhupaasCascader);
componentMap.set('ColorPicker', YunzhupaasColorPicker);
componentMap.set('Checkbox', YunzhupaasCheckbox);
componentMap.set('YunzhupaasCheckboxSingle', YunzhupaasCheckboxSingle);
componentMap.set('DatePicker', YunzhupaasDatePicker);
componentMap.set('DateRange', YunzhupaasDateRange);
componentMap.set('TimePicker', YunzhupaasTimePicker);
componentMap.set('TimeRange', YunzhupaasTimeRange);
componentMap.set('Divider', YunzhupaasDivider);
componentMap.set('Editor', YunzhupaasEditor);
componentMap.set('GroupTitle', YunzhupaasGroupTitle);
componentMap.set('Input', YunzhupaasInput);
componentMap.set('InputPassword', YunzhupaasInputPassword);
componentMap.set('Textarea', YunzhupaasTextarea);
componentMap.set('InputNumber', YunzhupaasInputNumber);
componentMap.set('IconPicker', YunzhupaasIconPicker);
componentMap.set('Link', YunzhupaasLink);
componentMap.set('OrganizeSelect', YunzhupaasOrganizeSelect);
componentMap.set('DepSelect', YunzhupaasDepSelect);
componentMap.set('PosSelect', YunzhupaasPosSelect);
componentMap.set('GroupSelect', YunzhupaasGroupSelect);
componentMap.set('RoleSelect', YunzhupaasRoleSelect);
componentMap.set('UserSelect', YunzhupaasUserSelect);
componentMap.set('UsersSelect', YunzhupaasUsersSelect);
componentMap.set('Qrcode', YunzhupaasQrcode);
componentMap.set('Barcode', YunzhupaasBarcode);
componentMap.set('Radio', YunzhupaasRadio);
componentMap.set('Rate', YunzhupaasRate);
componentMap.set('Select', YunzhupaasSelect);
componentMap.set('Slider', YunzhupaasSlider);
componentMap.set('Sign', YunzhupaasSign);
componentMap.set('Signature', YunzhupaasSignature);
componentMap.set('Switch', YunzhupaasSwitch);
componentMap.set('Text', YunzhupaasText);
componentMap.set('TreeSelect', YunzhupaasTreeSelect);
componentMap.set('UploadFile', YunzhupaasUploadFile);
componentMap.set('UploadImg', YunzhupaasUploadImg);
componentMap.set('UploadImgSingle', YunzhupaasUploadImgSingle);
componentMap.set('BillRule', YunzhupaasInput);
componentMap.set('ModifyUser', YunzhupaasInput);
componentMap.set('ModifyTime', YunzhupaasInput);
componentMap.set('CreateUser', YunzhupaasOpenData);
componentMap.set('CreateTime', YunzhupaasOpenData);
componentMap.set('CurrOrganize', YunzhupaasOpenData);
componentMap.set('CurrPosition', YunzhupaasOpenData);
componentMap.set('RelationForm', YunzhupaasRelationForm);
componentMap.set('RelationFormAttr', YunzhupaasRelationFormAttr);
componentMap.set('PopupSelect', YunzhupaasPopupSelect);
componentMap.set('PopupTableSelect', YunzhupaasPopupTableSelect);
componentMap.set('PopupAttr', YunzhupaasPopupAttr);
componentMap.set('NumberRange', YunzhupaasNumberRange);
componentMap.set('Calculate', YunzhupaasCalculate);
componentMap.set('InputTable', YunzhupaasInputTable);
componentMap.set('Location', YunzhupaasLocation);
componentMap.set('Iframe', YunzhupaasIframe);
export function add(compName: ComponentType, component: Component) {
componentMap.set(compName, component);
}
export function del(compName: ComponentType) {
componentMap.delete(compName);
}
export { componentMap };

View File

@@ -0,0 +1,114 @@
<template>
<a-col v-bind="actionColOpt" v-if="showActionButtonGroup">
<div class="w-full" :style="{ textAlign: actionColOpt.style.textAlign }">
<FormItem>
<slot name="submitBefore"></slot>
<Button type="primary" class="mr-2" v-bind="getSubmitBtnOptions" @click="submitAction" v-if="showSubmitButton">
{{ getSubmitBtnOptions.text }}
</Button>
<slot name="resetBefore"></slot>
<Button type="default" class="mr-2" v-bind="getResetBtnOptions" @click="resetAction" v-if="showResetButton">
{{ getResetBtnOptions.text }}
</Button>
<slot name="advanceBefore"></slot>
<Button type="link" size="small" @click="toggleAdvanced" v-if="showAdvancedButton && !hideAdvanceBtn">
{{ isAdvanced ? t('component.form.fold') : t('component.form.unfold') }}
<BasicArrow class="ml-1" :expand="!isAdvanced" up />
</Button>
<slot name="advanceAfter"></slot>
</FormItem>
</div>
</a-col>
</template>
<script lang="ts">
import type { ColEx } from '../types/index';
//import type { ButtonProps } from 'ant-design-vue/es/button/buttonTypes';
import { defineComponent, computed, PropType } from 'vue';
import { Form, Col } from 'ant-design-vue';
import { Button, ButtonProps } from '@/components/Button';
import { BasicArrow } from '@/components/Basic';
import { useFormContext } from '../hooks/useFormContext';
import { useI18n } from '@/hooks/web/useI18n';
import { propTypes } from '@/utils/propTypes';
type ButtonOptions = Partial<ButtonProps> & { text: string };
export default defineComponent({
name: 'BasicFormAction',
components: {
FormItem: Form.Item,
Button,
BasicArrow,
[Col.name]: Col,
},
props: {
showActionButtonGroup: propTypes.bool.def(true),
showResetButton: propTypes.bool.def(true),
showSubmitButton: propTypes.bool.def(true),
showAdvancedButton: propTypes.bool.def(true),
resetButtonOptions: {
type: Object as PropType<ButtonOptions>,
default: () => ({}),
},
submitButtonOptions: {
type: Object as PropType<ButtonOptions>,
default: () => ({}),
},
actionColOptions: {
type: Object as PropType<Partial<ColEx>>,
default: () => ({}),
},
actionSpan: propTypes.number.def(6),
isAdvanced: propTypes.bool,
hideAdvanceBtn: propTypes.bool,
},
emits: ['toggle-advanced'],
setup(props, { emit }) {
const { t } = useI18n();
const actionColOpt = computed(() => {
const { showAdvancedButton, actionSpan: span, actionColOptions } = props;
const actionSpan = 24 - span;
const advancedSpanObj = showAdvancedButton ? { span: actionSpan < 6 ? 24 : actionSpan } : {};
const actionColOpt: Partial<ColEx> = {
style: { textAlign: 'left' },
span: showAdvancedButton ? 6 : 4,
...advancedSpanObj,
...actionColOptions,
};
return actionColOpt;
});
const getResetBtnOptions = computed((): ButtonOptions => {
return Object.assign(
{
text: t('common.resetText'),
},
props.resetButtonOptions,
);
});
const getSubmitBtnOptions = computed(() => {
return Object.assign(
{
text: t('common.queryText'),
},
props.submitButtonOptions,
);
});
function toggleAdvanced() {
emit('toggle-advanced');
}
return {
t,
actionColOpt,
getResetBtnOptions,
getSubmitBtnOptions,
toggleAdvanced,
...useFormContext(),
};
},
});
</script>

View File

@@ -0,0 +1,383 @@
<script lang="tsx">
import type { PropType, Ref } from 'vue';
import { computed, defineComponent, toRefs, unref } from 'vue';
import type { FormActionType, FormProps, FormSchema } from '../types/form';
import type { Rule } from 'ant-design-vue/es/Form';
import type { TableActionType } from '@/components/Table';
import { Col, Form } from 'ant-design-vue';
import { YunzhupaasDivider } from '@/components/Yunzhupaas';
import { componentMap } from '../componentMap';
import { isBoolean, isFunction, isNull, isArray } from '@/utils/is';
import { getSlot } from '@/utils/helper/tsxHelper';
import { createPlaceholderMessage, setComponentRuleType, noFieldComponents, numberItemType, useInputComponents } from '../helper';
import { cloneDeep, upperFirst } from 'lodash-es';
import { useDebounceFn } from '@vueuse/core';
import { useItemLabelWidth } from '../hooks/useLabelWidth';
import { useI18n } from '@/hooks/web/useI18n';
import { usePermission } from '@/hooks/web/usePermission';
export default defineComponent({
name: 'BasicFormItem',
inheritAttrs: false,
props: {
schema: {
type: Object as PropType<FormSchema>,
default: () => ({}),
},
formProps: {
type: Object as PropType<FormProps>,
default: () => ({}),
},
allDefaultValues: {
type: Object as PropType<Recordable>,
default: () => ({}),
},
formModel: {
type: Object as PropType<Recordable>,
default: () => ({}),
},
setFormModel: {
type: Function as PropType<(key: string, value: any, schema: FormSchema) => void>,
default: null,
},
tableAction: {
type: Object as PropType<TableActionType>,
},
formActionType: {
type: Object as PropType<FormActionType>,
},
isAdvanced: {
type: Boolean,
},
},
setup(props, { slots }) {
const { t } = useI18n();
const { hasFormP } = usePermission();
const { schema, formProps } = toRefs(props) as {
schema: Ref<FormSchema>;
formProps: Ref<FormProps>;
};
const itemLabelWidthProp = useItemLabelWidth(schema, formProps);
const getValues = computed(() => {
const { allDefaultValues, formModel, schema } = props;
const { mergeDynamicData } = props.formProps;
return {
field: schema.field,
model: formModel,
values: {
...mergeDynamicData,
...allDefaultValues,
...formModel,
} as Recordable,
schema: schema,
};
});
const getComponentsProps = computed(() => {
const { schema, tableAction, formModel, formActionType } = props;
let { componentProps = {} } = schema;
if (isFunction(componentProps)) {
componentProps = componentProps({ schema, tableAction, formModel, formActionType }) ?? {};
}
return componentProps as Recordable;
});
const getDisable = computed(() => {
const { disabled: globDisabled } = props.formProps;
const { dynamicDisabled } = props.schema;
const { disabled: itemDisabled = false } = unref(getComponentsProps);
let disabled = !!globDisabled || itemDisabled;
if (isBoolean(dynamicDisabled)) {
disabled = dynamicDisabled;
}
if (isFunction(dynamicDisabled)) {
disabled = dynamicDisabled(unref(getValues));
}
return disabled;
});
function getShow(): { isShow: boolean; isIfShow: boolean } {
const { show, ifShow, auth = '' } = props.schema;
const { showAdvancedButton } = props.formProps;
const itemIsAdvanced = showAdvancedButton ? (isBoolean(props.isAdvanced) ? props.isAdvanced : true) : true;
let isShow = true;
let isIfShow = true;
if (isBoolean(show)) {
isShow = show;
}
if (isBoolean(ifShow)) {
isIfShow = ifShow;
}
if (isFunction(show)) {
isShow = show(unref(getValues));
}
if (isFunction(ifShow)) {
isIfShow = ifShow(unref(getValues));
}
isShow = isShow && itemIsAdvanced;
if (auth) isIfShow = !hasFormP(auth) ? hasFormP(auth) : isIfShow;
return { isShow, isIfShow };
}
function handleRules(): Rule[] {
const { rules: defRules = [], component, rulesMessageJoinLabel, label, dynamicRules, required } = props.schema;
if (isFunction(dynamicRules)) {
return dynamicRules(unref(getValues)) as Rule[];
}
let rules: Rule[] = cloneDeep(defRules) as Rule[];
const { rulesMessageJoinLabel: globalRulesMessageJoinLabel } = props.formProps;
const joinLabel = Reflect.has(props.schema, 'rulesMessageJoinLabel') ? rulesMessageJoinLabel : globalRulesMessageJoinLabel;
const defaultMsg = createPlaceholderMessage(component) + `${joinLabel ? label : ''}`;
function validator(rule: any, value: any) {
const msg = rule.message || defaultMsg;
if (value === undefined || isNull(value)) {
// 空值
return Promise.reject(msg);
} else if (Array.isArray(value) && value.length === 0) {
// 数组类型
return Promise.reject(msg);
} else if (typeof value === 'string' && value.trim() === '') {
// 空字符串
return Promise.reject(msg);
} else if (
typeof value === 'object' &&
Reflect.has(value, 'checked') &&
Reflect.has(value, 'halfChecked') &&
Array.isArray(value.checked) &&
Array.isArray(value.halfChecked) &&
value.checked.length === 0 &&
value.halfChecked.length === 0
) {
// 非关联选择的tree组件
return Promise.reject(msg);
}
return Promise.resolve();
}
const getRequired = isFunction(required) ? required(unref(getValues)) : required;
/*
* 1、若设置了required属性又没有其他的rules就创建一个验证规则
* 2、若设置了required属性又存在其他的rules则只rules中不存在required属性时才添加验证required的规则
* 也就是说rules中的required优先级大于required
*/
if (getRequired) {
if (!rules || rules.length === 0) {
rules = [{ required: getRequired, validator }];
} else {
const requiredIndex: number = rules.findIndex(rule => Reflect.has(rule, 'required'));
if (requiredIndex === -1) {
rules.push({ required: getRequired, validator });
}
}
}
const requiredRuleIndex: number = rules.findIndex(rule => Reflect.has(rule, 'required') && !Reflect.has(rule, 'validator'));
if (requiredRuleIndex !== -1) {
const rule = rules[requiredRuleIndex];
const { isShow } = getShow();
if (!isShow) {
rule.required = false;
}
if (component) {
if (!Reflect.has(rule, 'type')) {
rule.type = component === 'InputNumber' ? 'number' : 'string';
}
rule.message = rule.message || defaultMsg;
if (component.includes('Input') || component.includes('Textarea')) {
rule.whitespace = true;
}
const valueFormat = unref(getComponentsProps)?.valueFormat;
setComponentRuleType(rule, component, valueFormat);
}
}
// Maximum input length rule check
const characterInx = rules.findIndex(val => val.max);
if (characterInx !== -1 && !rules[characterInx].validator) {
rules[characterInx].message = rules[characterInx].message || t('component.form.maxTip', [rules[characterInx].max] as Recordable);
}
return rules;
}
function renderComponent() {
const { renderComponentContent, component, field, changeEvent = 'change', valueField } = props.schema;
if (useInputComponents.includes(component)) {
const Comp = componentMap.get(component) as ReturnType<typeof defineComponent>;
return <Comp readonly={true} placeholder="系统自动生成" />;
}
const eventKey = `on${upperFirst(changeEvent)}`;
const on = {
[eventKey]: (...args: Nullable<Recordable>[]) => {
const [e] = args;
if (propsData[eventKey]) {
propsData[eventKey](...args);
}
const target = e ? e.target : null;
const value = target ? target.value : e;
props.setFormModel(field, value, props.schema);
},
};
const Comp = componentMap.get(component) as ReturnType<typeof defineComponent>;
const { autoSetPlaceHolder, size } = props.formProps;
const propsData: Recordable = {
allowClear: true,
// getPopupContainer: (trigger: Element) => trigger.parentNode,
size,
...unref(getComponentsProps),
disabled: unref(getDisable),
};
const isCreatePlaceholder = !propsData.disabled && autoSetPlaceHolder;
// RangePicker place is an array
if (isCreatePlaceholder && component !== 'DateRange' && component !== 'TimeRange' && component) {
propsData.placeholder = unref(getComponentsProps)?.placeholder || createPlaceholderMessage(component);
}
propsData.codeField = field;
propsData.formValues = unref(getValues);
const getComponentValue = value => {
if (numberItemType.includes(component)) {
return value ?? 0;
} else {
return value;
}
};
const bindValue: Recordable = {
[valueField || 'value']: getComponentValue(props.formModel[field]),
};
// 列表搜索时部分输入框按回车触发搜索事件
const onPressEnter = () => {
props.formActionType?.submit();
};
const debouncePressEnter = useDebounceFn(onPressEnter, 200);
const keyupObj = component === 'Input' && unref(getComponentsProps)?.submitOnPressEnter ? { onPressEnter: debouncePressEnter } : {};
const compAttr: Recordable = {
...propsData,
...on,
...bindValue,
...keyupObj,
};
// 关闭输入框联想
if (component === 'Input') compAttr.autoComplete = 'off';
if (!renderComponentContent) {
return <Comp {...compAttr} />;
}
const compSlot = isFunction(renderComponentContent)
? { ...renderComponentContent(unref(getValues)) }
: {
default: () => renderComponentContent,
};
return <Comp {...compAttr}>{compSlot}</Comp>;
}
function renderLabelHelpMessage() {
const { label, helpMessage, helpComponentProps, subLabel, component } = props.schema;
if (noFieldComponents.includes(component) && !['Qrcode', 'Barcode'].includes(component)) {
return '';
}
const renderLabel = subLabel ? (
<span>
{label} <span class="text-secondary">{subLabel}</span>
</span>
) : (
label
);
const getHelpMessage = isFunction(helpMessage) ? helpMessage(unref(getValues)) : helpMessage;
if (!getHelpMessage || (Array.isArray(getHelpMessage) && getHelpMessage.length === 0)) {
return renderLabel;
}
return (
<span>
{renderLabel}
<BasicHelp placement="top" text={getHelpMessage} {...helpComponentProps} />
</span>
);
}
function renderItem() {
const { itemProps, slot, render, field, suffix, component, extra, className } = props.schema;
const { labelCol, wrapperCol } = unref(itemLabelWidthProp);
const { colon } = props.formProps;
if (component === 'Divider') {
return (
<Col span={24}>
<YunzhupaasDivider {...unref(getComponentsProps)} />
</Col>
);
} else {
const getContent = () => {
return slot ? getSlot(slots, slot, unref(getValues)) : render ? render(unref(getValues)) : renderComponent();
};
const showSuffix = !!suffix;
const getSuffix = isFunction(suffix) ? suffix(unref(getValues)) : suffix;
const name = noFieldComponents.includes(component) ? '' : field;
const itemClassName = isArray(className) ? [...className] : [className];
return (
<Form.Item
name={name}
colon={colon}
class={[...itemClassName, { 'suffix-item': showSuffix }]}
{...(itemProps as Recordable)}
label={renderLabelHelpMessage()}
extra={extra}
rules={handleRules()}
labelCol={labelCol}
wrapperCol={wrapperCol}>
<div style="display:flex">
<div style="flex:1;">{getContent()}</div>
{showSuffix && <span class="suffix">{getSuffix}</span>}
</div>
</Form.Item>
);
}
}
return () => {
const { colProps = {}, colSlot, renderColContent, component } = props.schema;
if (!componentMap.has(component)) {
return null;
}
const { baseColProps = {} } = props.formProps;
const realColProps = { ...baseColProps, ...colProps };
const { isIfShow, isShow } = getShow();
const values = unref(getValues);
const getContent = () => {
return colSlot ? getSlot(slots, colSlot, values) : renderColContent ? renderColContent(values) : renderItem();
};
return (
isIfShow && (
<Col {...realColProps} v-show={isShow}>
{getContent()}
</Col>
)
);
};
},
});
</script>

View File

@@ -0,0 +1,73 @@
import type { Rule } from 'ant-design-vue/es/Form';
import type { ComponentType } from './types/index';
import { useI18n } from '@/hooks/web/useI18n';
import { dateUtil, FormatDate } from '@/utils/dateUtil';
import { isNumber, isObject } from '@/utils/is';
const { t } = useI18n();
/**
* @description: 生成placeholder
*/
export function createPlaceholderMessage(component: ComponentType) {
if (component.includes('Input') || component.includes('Complete')) {
return t('common.inputText');
}
if (component.includes('Picker')) {
return t('common.chooseText');
}
if (
component.includes('Select') ||
component.includes('Cascader') ||
component.includes('Checkbox') ||
component.includes('Radio') ||
component.includes('Switch')
) {
// return `请选择${label}`;
return t('common.chooseText');
}
return '';
}
const DATE_TYPE = ['DatePicker', 'MonthPicker', 'WeekPicker'];
function genType() {
return [...DATE_TYPE, 'DateRange'];
}
export function setComponentRuleType(rule: Rule, component: ComponentType, valueFormat: string) {
if (['MonthPicker', 'WeekPicker'].includes(component)) {
rule.type = valueFormat ? 'string' : 'object';
} else if (['DateRange', 'TimeRange', 'Upload', 'CheckboxGroup'].includes(component)) {
rule.type = 'array';
} else if (['InputNumber', 'Switch', 'DatePicker'].includes(component)) {
rule.type = 'number';
}
}
export function processDateValue(attr: Recordable, component: string) {
const { valueFormat, value } = attr;
if (valueFormat) {
attr.value = isObject(value) ? dateUtil(value as FormatDate).format(valueFormat) : value;
} else if (DATE_TYPE.includes(component) && value) {
attr.value = dateUtil(attr.value);
}
}
export function handleInputNumberValue(component?: ComponentType, val?: any) {
if (!component) return val;
if (['Input', 'InputPassword', 'InputSearch', 'TextArea'].includes(component)) {
return val && isNumber(val) ? `${val}` : val;
}
return val;
}
/**
* 时间字段
*/
export const dateItemType = genType();
export const defaultValueComponents = ['Input', 'InputPassword', 'InputSearch', 'TextArea'];
export const noFieldComponents = ['Button', 'Divider', 'GroupTitle', 'Link', 'Text', 'Alert', 'Qrcode', 'Barcode'];
export const numberItemType = ['Slider', 'Switch'];
export const useInputComponents = ['BillRule', 'ModifyUser', 'ModifyTime'];

View File

@@ -0,0 +1,158 @@
import type { ColEx } from '../types';
import type { AdvanceState } from '../types/hooks';
import { ComputedRef, getCurrentInstance, Ref, shallowReactive } from 'vue';
import type { FormProps, FormSchema } from '../types/form';
import { computed, unref, watch } from 'vue';
import { isBoolean, isFunction, isNumber, isObject } from '@/utils/is';
import { useBreakpoint } from '@/hooks/event/useBreakpoint';
import { useDebounceFn } from '@vueuse/core';
const BASIC_COL_LEN = 24;
interface UseAdvancedContext {
advanceState: AdvanceState;
emit: EmitType;
getProps: ComputedRef<FormProps>;
getSchema: ComputedRef<FormSchema[]>;
formModel: Recordable;
defaultValueRef: Ref<Recordable>;
}
export default function ({ advanceState, emit, getProps, getSchema, formModel, defaultValueRef }: UseAdvancedContext) {
const vm = getCurrentInstance();
const fieldsIsAdvancedMap = shallowReactive({});
const { realWidthRef, screenEnum, screenRef } = useBreakpoint();
const getEmptySpan = computed((): number => {
if (!advanceState.isAdvanced) {
return 0;
}
// For some special cases, you need to manually specify additional blank lines
const emptySpan = unref(getProps).emptySpan || 0;
if (isNumber(emptySpan)) {
return emptySpan;
}
if (isObject(emptySpan)) {
const { span = 0 } = emptySpan;
const screen = unref(screenRef) as string;
const screenSpan = (emptySpan as any)[screen.toLowerCase()];
return screenSpan || span || 0;
}
return 0;
});
const debounceUpdateAdvanced = useDebounceFn(updateAdvanced, 0);
watch(
[() => unref(getSchema), () => advanceState.isAdvanced, () => unref(realWidthRef)],
() => {
const { showAdvancedButton } = unref(getProps);
if (showAdvancedButton) {
debounceUpdateAdvanced();
}
},
{ immediate: true },
);
function getAdvanced(itemCol: Partial<ColEx>, itemColSum = 0, isLastAction = false) {
const width = unref(realWidthRef);
const mdWidth =
parseInt(itemCol.md as string) || parseInt(itemCol.xs as string) || parseInt(itemCol.sm as string) || (itemCol.span as number) || BASIC_COL_LEN;
const lgWidth = parseInt(itemCol.lg as string) || mdWidth;
const xlWidth = parseInt(itemCol.xl as string) || lgWidth;
const xxlWidth = parseInt(itemCol.xxl as string) || xlWidth;
if (width <= screenEnum.LG) {
itemColSum += mdWidth;
} else if (width < screenEnum.XL) {
itemColSum += lgWidth;
} else if (width < screenEnum.XXL) {
itemColSum += xlWidth;
} else {
itemColSum += xxlWidth;
}
if (isLastAction) {
advanceState.hideAdvanceBtn = false;
if (itemColSum <= BASIC_COL_LEN * 2 - 6) {
// When less than or equal to 2 lines, the collapse and expand buttons are not displayed
advanceState.hideAdvanceBtn = true;
// 表单组件默认展开问题修复
// advanceState.isAdvanced = true;
} else if (itemColSum > BASIC_COL_LEN * 2 - 6 && itemColSum <= BASIC_COL_LEN * (unref(getProps).autoAdvancedLine || 30)) {
advanceState.hideAdvanceBtn = false;
// More than 3 lines collapsed by default
} else if (!advanceState.isLoad) {
advanceState.isLoad = true;
advanceState.isAdvanced = !advanceState.isAdvanced;
}
return { isAdvanced: advanceState.isAdvanced, itemColSum };
}
if (itemColSum > BASIC_COL_LEN * (unref(getProps).alwaysShowLines || 1) - 6) {
return { isAdvanced: advanceState.isAdvanced, itemColSum };
} else {
// The first line is always displayed
return { isAdvanced: true, itemColSum };
}
}
function updateAdvanced() {
let itemColSum = 0;
let realItemColSum = 0;
const { baseColProps = {} } = unref(getProps);
for (const schema of unref(getSchema)) {
const { show, colProps } = schema;
let isShow = true;
if (isBoolean(show)) {
isShow = show;
}
if (isFunction(show)) {
isShow = show({
schema: schema,
model: formModel,
field: schema.field,
values: {
...unref(defaultValueRef),
...formModel,
},
});
}
if (isShow && (colProps || baseColProps)) {
const { itemColSum: sum, isAdvanced } = getAdvanced({ ...baseColProps, ...colProps }, itemColSum);
itemColSum = sum || 0;
if (isAdvanced) {
realItemColSum = itemColSum;
}
fieldsIsAdvancedMap[schema.field] = isAdvanced;
}
}
// 确保页面发送更新(第一次不需要更新、第一次更新会报错)
try {
vm?.proxy?.$forceUpdate();
} catch (error) {}
advanceState.actionSpan = (realItemColSum % BASIC_COL_LEN) + unref(getEmptySpan);
getAdvanced(unref(getProps).actionColOptions || { span: BASIC_COL_LEN }, itemColSum, true);
emit('advanced-change');
}
function handleToggleAdvanced() {
advanceState.isAdvanced = !advanceState.isAdvanced;
}
return { handleToggleAdvanced, fieldsIsAdvancedMap };
}

View File

@@ -0,0 +1,40 @@
import type { ComputedRef, Ref } from 'vue';
import type { FormSchema, FormActionType, FormProps } from '../types/form';
import { unref, nextTick, watchEffect } from 'vue';
interface UseAutoFocusContext {
getSchema: ComputedRef<FormSchema[]>;
getProps: ComputedRef<FormProps>;
isInitedDefault: Ref<boolean>;
formElRef: Ref<FormActionType>;
}
export async function useAutoFocus({
getSchema,
getProps,
formElRef,
isInitedDefault,
}: UseAutoFocusContext) {
watchEffect(async () => {
if (unref(isInitedDefault) || !unref(getProps).autoFocusFirstItem) {
return;
}
await nextTick();
const schemas = unref(getSchema);
const formEl = unref(formElRef);
const el = (formEl as any)?.$el as HTMLElement;
if (!formEl || !el || !schemas || schemas.length === 0) {
return;
}
const firstItem = schemas[0];
// Only open when the first form item is input type
if (!firstItem.component.includes('Input')) {
return;
}
const inputEl = el.querySelector('.ant-row:first-child input') as Nullable<HTMLInputElement>;
if (!inputEl) return;
inputEl?.focus();
});
}

View File

@@ -0,0 +1,11 @@
import type { ComponentType } from '../types/index';
import { tryOnUnmounted } from '@vueuse/core';
import { add, del } from '../componentMap';
import type { Component } from 'vue';
export function useComponentRegister(compName: ComponentType, comp: Component) {
add(compName, comp);
tryOnUnmounted(() => {
del(compName);
});
}

View File

@@ -0,0 +1,116 @@
import type { FormProps, FormActionType, UseFormReturnType, FormSchema } from '../types/form';
import type { NamePath } from 'ant-design-vue/lib/form/interface';
import type { DynamicProps } from '#/utils';
import { ref, onUnmounted, unref, nextTick, watch } from 'vue';
import { isProdMode } from '@/utils/env';
import { error } from '@/utils/log';
import { getDynamicProps } from '@/utils';
export declare type ValidateFields = (nameList?: NamePath[]) => Promise<Recordable>;
type Props = Partial<DynamicProps<FormProps>>;
export function useForm(props?: Props): UseFormReturnType {
const formRef = ref<Nullable<FormActionType>>(null);
const loadedRef = ref<Nullable<boolean>>(false);
async function getForm() {
const form = unref(formRef);
if (!form) {
error('The form instance has not been obtained, please make sure that the form has been rendered when performing the form operation!');
}
await nextTick();
return form as FormActionType;
}
function register(instance: FormActionType) {
isProdMode() &&
onUnmounted(() => {
formRef.value = null;
loadedRef.value = null;
});
if (unref(loadedRef) && isProdMode() && instance === unref(formRef)) return;
formRef.value = instance;
loadedRef.value = true;
watch(
() => props,
() => {
props && instance.setProps(getDynamicProps(props));
},
{
immediate: true,
deep: true,
},
);
}
const methods: FormActionType = {
scrollToField: async (name: NamePath, options?: ScrollOptions | undefined) => {
const form = await getForm();
form.scrollToField(name, options);
},
setProps: async (formProps: Partial<FormProps>) => {
const form = await getForm();
form.setProps(formProps);
},
updateSchema: async (data: Partial<FormSchema> | Partial<FormSchema>[]) => {
const form = await getForm();
form.updateSchema(data);
},
resetSchema: async (data: Partial<FormSchema> | Partial<FormSchema>[]) => {
const form = await getForm();
form.resetSchema(data);
},
clearValidate: async (name?: string | string[]) => {
const form = await getForm();
form.clearValidate(name);
},
resetFields: async () => {
getForm().then(async form => {
await form.resetFields();
});
},
removeSchemaByField: async (field: string | string[]) => {
unref(formRef)?.removeSchemaByField(field);
},
// TODO promisify
getFieldsValue: <T>() => {
return unref(formRef)?.getFieldsValue() as T;
},
setFieldsValue: async <T extends Recordable<any>>(values: T) => {
const form = await getForm();
form.setFieldsValue<T>(values);
},
appendSchemaByField: async (schema: FormSchema | FormSchema[], prefixField: string | undefined, first: boolean) => {
const form = await getForm();
form.appendSchemaByField(schema, prefixField, first);
},
submit: async (): Promise<any> => {
const form = await getForm();
return form.submit();
},
validate: async (nameList?: NamePath[]): Promise<Recordable> => {
const form = await getForm();
return form.validate(nameList);
},
validateFields: async (nameList?: NamePath[]): Promise<Recordable> => {
const form = await getForm();
return form.validateFields(nameList);
},
};
return [register, methods];
}

View File

@@ -0,0 +1,17 @@
import type { InjectionKey } from 'vue';
import { createContext, useContext } from '@/hooks/core/useContext';
export interface FormContextProps {
resetAction: () => Promise<void>;
submitAction: () => Promise<void>;
}
const key: InjectionKey<FormContextProps> = Symbol();
export function createFormContext(context: FormContextProps) {
return createContext<FormContextProps>(context, key);
}
export function useFormContext() {
return useContext<FormContextProps>(key);
}

View File

@@ -0,0 +1,318 @@
import type { ComputedRef, Ref } from 'vue';
import type { FormProps, FormSchema, FormActionType } from '../types/form';
import type { NamePath } from 'ant-design-vue/lib/form/interface';
import { unref, toRaw, nextTick } from 'vue';
import { isArray, isFunction, isObject, isString, isDef, isNullOrUnDef, isEmpty } from '@/utils/is';
import { deepMerge } from '@/utils';
import { dateItemType, handleInputNumberValue, defaultValueComponents, noFieldComponents } from '../helper';
import { cloneDeep, uniqBy } from 'lodash-es';
import { error } from '@/utils/log';
interface UseFormActionContext {
emit: EmitType;
getProps: ComputedRef<FormProps>;
getSchema: ComputedRef<FormSchema[]>;
formModel: Recordable;
defaultValueRef: Ref<Recordable>;
fullValueRef: Ref<Recordable>;
formElRef: Ref<FormActionType>;
schemaRef: Ref<FormSchema[]>;
handleFormValues: Fn;
isInitedDefaultRef: Ref<boolean>;
}
export function useFormEvents({
emit,
getProps,
formModel,
getSchema,
defaultValueRef,
fullValueRef,
formElRef,
schemaRef,
handleFormValues,
isInitedDefaultRef,
}: UseFormActionContext) {
async function resetFields(): Promise<void> {
fullValueRef.value = {};
const { resetFunc, submitOnReset } = unref(getProps);
resetFunc && isFunction(resetFunc) && (await resetFunc());
const formEl = unref(formElRef);
if (!formEl) return;
Object.keys(formModel).forEach(key => {
const schema = unref(getSchema).find(item => item.field === key);
const isInput = schema?.component && defaultValueComponents.includes(schema.component);
const defaultValue = cloneDeep(defaultValueRef.value[key]);
formModel[key] = isInput ? defaultValue || '' : defaultValue;
});
nextTick(() => clearValidate());
emit('reset', toRaw(formModel));
submitOnReset && handleSubmit();
}
/**
* @description: Set form value
*/
async function setFieldsValue(values: Recordable): Promise<void> {
fullValueRef.value = { ...fullValueRef.value, ...values };
const fields = unref(getSchema)
.map(item => item.field)
.filter(Boolean);
// key 支持 a.b.c 的嵌套写法
const delimiter = '.';
const nestKeyArray = fields.filter(item => item.indexOf(delimiter) >= 0);
const validKeys: string[] = [];
Object.keys(values).forEach(key => {
const schema = unref(getSchema).find(item => item.field === key);
let value = values[key];
const hasKey = Reflect.has(values, key);
value = handleInputNumberValue(schema?.component, value);
// 0| '' is allow
if (hasKey && fields.includes(key)) {
formModel[key] = value;
validKeys.push(key);
} else {
nestKeyArray.forEach((nestKey: string) => {
try {
const value = nestKey.split('.').reduce((out, item) => out[item], values);
if (isDef(value)) {
formModel[nestKey] = value;
validKeys.push(nestKey);
}
} catch (e) {
// key not exist
if (isDef(defaultValueRef.value[nestKey])) {
formModel[nestKey] = cloneDeep(defaultValueRef.value[nestKey]);
}
}
});
}
});
validateFields(validKeys).catch(_ => {});
}
/**
* @description: Delete based on field name
*/
async function removeSchemaByField(fields: string | string[]): Promise<void> {
const schemaList: FormSchema[] = cloneDeep(unref(getSchema));
if (!fields) {
return;
}
let fieldList: string[] = isString(fields) ? [fields] : fields;
if (isString(fields)) {
fieldList = [fields];
}
for (const field of fieldList) {
_removeSchemaByField(field, schemaList);
}
schemaRef.value = schemaList;
}
/**
* @description: Delete based on field name
*/
function _removeSchemaByField(field: string, schemaList: FormSchema[]): void {
if (isString(field)) {
const index = schemaList.findIndex(schema => schema.field === field);
if (index !== -1) {
delete formModel[field];
schemaList.splice(index, 1);
}
}
}
/**
* @description: Insert after a certain field, if not insert the last
*/
async function appendSchemaByField(schema: FormSchema | FormSchema[], prefixField?: string, first = false) {
const schemaList: FormSchema[] = cloneDeep(unref(getSchema));
const addSchemaIds: string[] = Array.isArray(schema) ? schema.map(item => item.field) : [schema.field];
if (schemaList.find(item => addSchemaIds.includes(item.field))) {
error('There are schemas that have already been added');
return;
}
const index = schemaList.findIndex(schema => schema.field === prefixField);
const _schemaList = isObject(schema) ? [schema as FormSchema] : (schema as FormSchema[]);
if (!prefixField || index === -1 || first) {
first ? schemaList.unshift(..._schemaList) : schemaList.push(..._schemaList);
schemaRef.value = schemaList;
_setDefaultValue(schema);
return;
}
if (index !== -1) {
schemaList.splice(index + 1, 0, ..._schemaList);
}
_setDefaultValue(schema);
schemaRef.value = schemaList;
}
async function resetSchema(data: Partial<FormSchema> | Partial<FormSchema>[]) {
let updateData: Partial<FormSchema>[] = [];
if (isObject(data)) {
updateData.push(data as FormSchema);
}
if (isArray(data)) {
updateData = [...data];
}
const hasField = updateData.every(item => noFieldComponents.includes(item.component as string) || (Reflect.has(item, 'field') && item.field));
if (!hasField) {
error('All children of the form Schema array that need to be updated must contain the `field` field');
return;
}
schemaRef.value = updateData as FormSchema[];
}
async function updateSchema(data: Partial<FormSchema> | Partial<FormSchema>[]) {
let updateData: Partial<FormSchema>[] = [];
if (isObject(data)) {
updateData.push(data as FormSchema);
}
if (isArray(data)) {
updateData = [...data];
}
const hasField = updateData.every(item => noFieldComponents.includes(item.component as string) || (Reflect.has(item, 'field') && item.field));
if (!hasField) {
error('All children of the form Schema array that need to be updated must contain the `field` field');
return;
}
const schema: FormSchema[] = [];
unref(getSchema).forEach(val => {
let _val;
updateData.forEach(item => {
if (val.field === item.field) {
_val = item;
}
});
if (_val !== undefined && val.field === _val.field) {
const newSchema = deepMerge(val, _val);
if (Reflect.has(_val, 'componentProps') && Reflect.has(_val.componentProps, 'options')) {
newSchema.componentProps.options = _val.componentProps.options;
}
schema.push(newSchema as FormSchema);
} else {
schema.push(val);
}
});
_setDefaultValue(schema);
schemaRef.value = uniqBy(schema, 'field');
isInitedDefaultRef.value = false;
}
function _setDefaultValue(data: FormSchema | FormSchema[]) {
let schemas: FormSchema[] = [];
if (isObject(data)) {
schemas.push(data as FormSchema);
}
if (isArray(data)) {
schemas = [...data];
}
const obj: Recordable = {};
const currentFieldsValue = getFieldsValue();
schemas.forEach(item => {
if (
!noFieldComponents.includes(item.component) &&
Reflect.has(item, 'field') &&
item.field &&
!isNullOrUnDef(item.defaultValue) &&
(!(item.field in currentFieldsValue) || isNullOrUnDef(currentFieldsValue[item.field]) || isEmpty(currentFieldsValue[item.field]))
) {
obj[item.field] = item.defaultValue;
}
});
setFieldsValue(obj);
}
function getFieldsValue(): Recordable {
const formEl = unref(formElRef);
if (!formEl) return {};
return handleFormValues(toRaw(unref(formModel)));
}
/**
* @description: Is it time
*/
function itemIsDateType(key: string) {
return unref(getSchema).some(item => {
return item.field === key ? dateItemType.includes(item.component) : false;
});
}
async function validateFields(nameList?: NamePath[] | undefined) {
return unref(formElRef)?.validateFields(nameList);
}
async function validate(nameList?: NamePath[] | undefined) {
try {
const values = await unref(formElRef)?.validate(nameList);
return { ...fullValueRef.value, ...values };
} catch (error: any) {
if (!error.errorFields.length) {
return { ...fullValueRef.value, ...error.values };
} else {
return false;
}
}
}
async function clearValidate(name?: string | string[]) {
await unref(formElRef)?.clearValidate(name);
}
async function scrollToField(name: NamePath, options?: ScrollOptions | undefined) {
await unref(formElRef)?.scrollToField(name, options);
}
/**
* @description: Form submission
*/
async function handleSubmit(e?: Event): Promise<void> {
e && e.preventDefault();
const { submitFunc } = unref(getProps);
if (submitFunc && isFunction(submitFunc)) {
await submitFunc();
return;
}
const formEl = unref(formElRef);
if (!formEl) return;
try {
const values = await validate();
const res = handleFormValues(values);
emit('submit', res);
} catch (error: any) {
if (error?.outOfDate === false && error?.errorFields) {
return;
}
throw new Error(error);
}
}
return {
handleSubmit,
clearValidate,
validate,
validateFields,
getFieldsValue,
updateSchema,
resetSchema,
appendSchemaByField,
removeSchemaByField,
resetFields,
setFieldsValue,
scrollToField,
itemIsDateType,
};
}

View File

@@ -0,0 +1,141 @@
import { isArray, isFunction, isObject, isString, isNullOrUnDef } from '@/utils/is';
import { dateUtil } from '@/utils/dateUtil';
import { unref } from 'vue';
import type { Ref, ComputedRef } from 'vue';
import type { FormProps, FormSchema } from '../types/form';
import { cloneDeep, set } from 'lodash-es';
interface UseFormValuesContext {
defaultValueRef: Ref<any>;
getSchema: ComputedRef<FormSchema[]>;
getProps: ComputedRef<FormProps>;
formModel: Recordable;
}
/**
* @desription deconstruct array-link key. This method will mutate the target.
*/
function tryDeconstructArray(key: string, value: any, target: Recordable) {
const pattern = /^\[(.+)\]$/;
if (pattern.test(key)) {
const match = key.match(pattern);
if (match && match[1]) {
const keys = match[1].split(',');
value = Array.isArray(value) ? value : [value];
keys.forEach((k, index) => {
set(target, k.trim(), value[index]);
});
return true;
}
}
}
/**
* @desription deconstruct object-link key. This method will mutate the target.
*/
function tryDeconstructObject(key: string, value: any, target: Recordable) {
const pattern = /^\{(.+)\}$/;
if (pattern.test(key)) {
const match = key.match(pattern);
if (match && match[1]) {
const keys = match[1].split(',');
value = isObject(value) ? value : {};
keys.forEach(k => {
set(target, k.trim(), value[k.trim()]);
});
return true;
}
}
}
export function useFormValues({ defaultValueRef, getSchema, formModel, getProps }: UseFormValuesContext) {
// Processing form values
function handleFormValues(values: Recordable) {
if (!isObject(values)) {
return {};
}
const res: Recordable = {};
for (const item of Object.entries(values)) {
let [, value] = item;
const [key] = item;
if (!key || (isArray(value) && value.length === 0) || isFunction(value)) {
continue;
}
const transformDateFunc = unref(getProps).transformDateFunc;
if (isObject(value)) {
value = transformDateFunc?.(value);
}
if (isArray(value) && value[0]?.format && value[1]?.format) {
value = value.map(item => transformDateFunc?.(item));
}
// Remove spaces
if (isString(value)) {
// remove params from URL
if (value === '') {
value = undefined;
} else {
value = value.trim();
}
}
if (!tryDeconstructArray(key, value, res) && !tryDeconstructObject(key, value, res)) {
// 没有解构成功的,按原样赋值
set(res, key, value);
}
}
return handleRangeTimeValue(res);
}
/**
* @description: Processing time interval parameters
*/
function handleRangeTimeValue(values: Recordable) {
const fieldMapToTime = unref(getProps).fieldMapToTime;
if (!fieldMapToTime || !Array.isArray(fieldMapToTime)) {
return values;
}
for (const [field, [startTimeKey, endTimeKey], format = ''] of fieldMapToTime) {
if (!field || !startTimeKey || !endTimeKey) {
continue;
}
// If the value to be converted is empty, remove the field
if (!values[field]) {
Reflect.deleteProperty(values, field);
continue;
}
const [startTime, endTime]: string[] = values[field];
if (format) {
const [startTimeFormat, endTimeFormat] = Array.isArray(format) ? format : [format, format];
values[startTimeKey] = dateUtil(startTime).format(startTimeFormat);
values[endTimeKey] = dateUtil(endTime).format(endTimeFormat);
} else {
values[startTimeKey] = startTime;
values[endTimeKey] = endTime;
}
Reflect.deleteProperty(values, field);
}
return values;
}
function initDefault() {
const schemas = unref(getSchema);
const obj: Recordable = {};
schemas.forEach(item => {
const { defaultValue } = item;
if (!isNullOrUnDef(defaultValue)) {
obj[item.field] = defaultValue;
if (formModel[item.field] === undefined) {
formModel[item.field] = defaultValue;
}
}
});
defaultValueRef.value = cloneDeep(obj);
}
return { handleFormValues, initDefault };
}

View File

@@ -0,0 +1,37 @@
import type { Ref } from 'vue';
import { computed, unref } from 'vue';
import type { FormProps, FormSchema } from '../types/form';
import { isNumber } from '@/utils/is';
export function useItemLabelWidth(schemaItemRef: Ref<FormSchema>, propsRef: Ref<FormProps>) {
return computed(() => {
const schemaItem = unref(schemaItemRef);
const { labelCol = {}, wrapperCol = {} } = schemaItem.itemProps || {};
const { labelWidth, disabledLabelWidth } = schemaItem;
const { labelWidth: globalLabelWidth, labelCol: globalLabelCol, wrapperCol: globWrapperCol, layout } = unref(propsRef);
// If labelWidth is set globally, all items setting
if ((!globalLabelWidth && !labelWidth && !globalLabelCol) || disabledLabelWidth) {
labelCol.style = {
textAlign: 'left',
};
return { labelCol, wrapperCol };
}
let width = labelWidth || globalLabelWidth;
const col = { ...globalLabelCol, ...labelCol };
const wrapCol = { ...globWrapperCol, ...wrapperCol };
if (width) {
width = isNumber(width) ? `${width}px` : width;
}
return {
labelCol: { style: { width }, ...col },
wrapperCol: {
style: { width: layout === 'vertical' ? '100%' : `calc(100% - ${width})` },
...wrapCol,
},
};
});
}

View File

@@ -0,0 +1,104 @@
import type { FieldMapToTime, FormSchema } from './types/form';
import type { CSSProperties, PropType } from 'vue';
import type { ColEx } from './types';
import type { TableActionType } from '@/components/Table';
import type { ButtonProps } from 'ant-design-vue/es/button/buttonTypes';
import type { RowProps } from 'ant-design-vue/lib/grid/Row';
import { propTypes } from '@/utils/propTypes';
export const basicProps = {
model: {
type: Object as PropType<Recordable>,
default: () => ({}),
},
// 标签宽度 固定宽度
labelWidth: {
type: [Number, String] as PropType<number | string>,
default: 80,
},
fieldMapToTime: {
type: Array as PropType<FieldMapToTime>,
default: () => [],
},
compact: propTypes.bool,
// 表单配置规则
schemas: {
type: Array as PropType<FormSchema[]>,
default: () => [],
},
mergeDynamicData: {
type: Object as PropType<Recordable>,
default: null,
},
baseRowStyle: {
type: Object as PropType<CSSProperties>,
},
baseColProps: {
type: Object as PropType<Partial<ColEx>>,
default: () => ({ span: 24 }),
},
autoSetPlaceHolder: propTypes.bool.def(true),
// 在INPUT组件上单击回车时是否自动提交
autoSubmitOnEnter: propTypes.bool.def(false),
submitOnReset: propTypes.bool,
submitOnChange: propTypes.bool,
size: propTypes.oneOf(['default', 'small', 'large']).def('default'),
// 禁用表单
disabled: propTypes.bool,
emptySpan: {
type: [Number, Object] as PropType<number | Recordable>,
default: 0,
},
// 是否显示收起展开按钮
showAdvancedButton: propTypes.bool,
// 转化时间
transformDateFunc: {
type: Function as PropType<Fn>,
default: (date: any) => {
return date?.format?.('YYYY-MM-DD HH:mm:ss') ?? date;
},
},
rulesMessageJoinLabel: propTypes.bool.def(true),
// 超过3行自动折叠
autoAdvancedLine: propTypes.number.def(30),
// 不受折叠影响的行数
alwaysShowLines: propTypes.number.def(1),
// 是否显示操作按钮
showActionButtonGroup: propTypes.bool.def(false),
// 操作列Col配置
actionColOptions: Object as PropType<Partial<ColEx>>,
// 显示重置按钮
showResetButton: propTypes.bool.def(true),
// 是否聚焦第一个输入框只在第一个表单项为input的时候作用
autoFocusFirstItem: propTypes.bool,
// 重置按钮配置
resetButtonOptions: Object as PropType<Partial<ButtonProps>>,
// 显示确认按钮
showSubmitButton: propTypes.bool.def(true),
// 确认按钮配置
submitButtonOptions: Object as PropType<Partial<ButtonProps>>,
// 自定义重置函数
resetFunc: Function as PropType<() => Promise<void>>,
submitFunc: Function as PropType<() => Promise<void>>,
// 以下为默认props
hideRequiredMark: propTypes.bool,
labelCol: Object as PropType<Partial<ColEx>>,
layout: propTypes.oneOf(['horizontal', 'vertical', 'inline']).def('horizontal'),
tableAction: {
type: Object as PropType<TableActionType>,
},
wrapperCol: Object as PropType<Partial<ColEx>>,
colon: propTypes.bool,
labelAlign: propTypes.string,
rowProps: Object as PropType<RowProps>,
};

View File

@@ -0,0 +1,209 @@
import type { NamePath, RuleObject } from 'ant-design-vue/lib/form/interface';
import type { VNode } from 'vue';
import type { ButtonProps as AntdButtonProps } from '@/components/Button';
import type { FormItem } from './formItem';
import type { ColEx, ComponentType } from './index';
import type { TableActionType } from '@/components/Table/src/types/table';
import type { CSSProperties } from 'vue';
import type { RowProps } from 'ant-design-vue/lib/grid/Row';
export type FieldMapToTime = [string, [string, string], (string | [string, string])?][];
export type Rule = RuleObject & {
trigger?: 'blur' | 'change' | ['change', 'blur'];
};
export interface RenderCallbackParams {
schema: FormSchema;
values: Recordable;
model: Recordable;
field: string;
}
export interface ButtonProps extends AntdButtonProps {
text?: string;
}
export interface FormActionType {
submit: () => Promise<void>;
setFieldsValue: <T extends Recordable<any>>(values: T) => Promise<void>;
resetFields: () => Promise<void>;
getFieldsValue: () => Recordable;
clearValidate: (name?: string | string[]) => Promise<void>;
updateSchema: (data: Partial<FormSchema> | Partial<FormSchema>[]) => Promise<void>;
resetSchema: (data: Partial<FormSchema> | Partial<FormSchema>[]) => Promise<void>;
setProps: (formProps: Partial<FormProps>) => Promise<void>;
removeSchemaByField: (field: string | string[]) => Promise<void>;
appendSchemaByField: (schema: FormSchema | FormSchema[], prefixField: string | undefined, first?: boolean | undefined) => Promise<void>;
validateFields: (nameList?: NamePath[]) => Promise<any>;
validate: (nameList?: NamePath[]) => Promise<any>;
scrollToField: (name: NamePath, options?: ScrollOptions) => Promise<void>;
}
export type RegisterFn = (formInstance: FormActionType) => void;
export type UseFormReturnType = [RegisterFn, FormActionType];
export interface FormProps {
name?: string;
layout?: 'vertical' | 'inline' | 'horizontal';
// Form value
model?: Recordable;
// The width of all items in the entire form
labelWidth?: number | string;
// alignment
labelAlign?: 'left' | 'right';
// Row configuration for the entire form
rowProps?: RowProps;
// Submit form on reset
submitOnReset?: boolean;
// Submit form on form changing
submitOnChange?: boolean;
// Col configuration for the entire form
labelCol?: Partial<ColEx>;
// Col configuration for the entire form
wrapperCol?: Partial<ColEx>;
// General row style
baseRowStyle?: CSSProperties;
// General col configuration
baseColProps?: Partial<ColEx>;
// Form configuration rules
schemas?: FormSchema[];
// Function values used to merge into dynamic control form items
mergeDynamicData?: Recordable;
// Compact mode for search forms
compact?: boolean;
// Blank line span
emptySpan?: number | Partial<ColEx>;
// Internal component size of the form
size?: 'default' | 'small' | 'large';
// Whether to disable
disabled?: boolean;
// Time interval fields are mapped into multiple
fieldMapToTime?: FieldMapToTime;
// Placeholder is set automatically
autoSetPlaceHolder?: boolean;
// Auto submit on press enter on input
autoSubmitOnEnter?: boolean;
// Check whether the information is added to the label
rulesMessageJoinLabel?: boolean;
// Whether to show collapse and expand buttons
showAdvancedButton?: boolean;
// Whether to focus on the first input box, only works when the first form item is input
autoFocusFirstItem?: boolean;
// Automatically collapse over the specified number of rows
autoAdvancedLine?: number;
// Always show lines
alwaysShowLines?: number;
// Whether to show the operation button
showActionButtonGroup?: boolean;
// Reset button configuration
resetButtonOptions?: Partial<ButtonProps>;
// Confirm button configuration
submitButtonOptions?: Partial<ButtonProps>;
// Operation column configuration
actionColOptions?: Partial<ColEx>;
// Show reset button
showResetButton?: boolean;
// Show confirmation button
showSubmitButton?: boolean;
resetFunc?: () => Promise<void>;
submitFunc?: () => Promise<void>;
transformDateFunc?: (date: any) => string;
colon?: boolean;
}
export interface FormSchema {
// Field name
field: string;
// Event name triggered by internal value change, default change
changeEvent?: string;
// Variable name bound to v-model Default value
valueField?: string;
className?: string | string[];
// Label name
label: string | VNode;
extra?: string;
// 权限编码控制是否显示
auth?: string;
// Auxiliary text
subLabel?: string;
// Help text on the right side of the text
helpMessage?: string | string[] | ((renderCallbackParams: RenderCallbackParams) => string | string[]);
// BaseHelp component props
helpComponentProps?: Partial<HelpComponentProps>;
// Label width, if it is passed, the labelCol and WrapperCol configured by itemProps will be invalid
labelWidth?: string | number;
// Disable the adjustment of labelWidth with global settings of formModel, and manually set labelCol and wrapperCol by yourself
disabledLabelWidth?: boolean;
// render component
component: ComponentType;
// Component parameters
componentProps?: ((opt: { schema: FormSchema; tableAction: TableActionType; formActionType: FormActionType; formModel: Recordable }) => Recordable) | object;
// Required
required?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean);
suffix?: string | number | ((values: RenderCallbackParams) => string | number);
// Validation rules
rules?: Rule[];
// Check whether the information is added to the label
rulesMessageJoinLabel?: boolean;
// Reference formModelItem
itemProps?: Partial<FormItem>;
// col configuration outside formModelItem
colProps?: Partial<ColEx>;
// 默认值
defaultValue?: any;
isAdvanced?: boolean;
// Matching details components
span?: number;
ifShow?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean);
show?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean);
// Render the content in the form-item tag
render?: (renderCallbackParams: RenderCallbackParams) => VNode | VNode[] | string;
// Rendering col content requires outer wrapper form-item
renderColContent?: (renderCallbackParams: RenderCallbackParams) => VNode | VNode[] | string;
renderComponentContent?: ((renderCallbackParams: RenderCallbackParams) => any) | VNode | VNode[] | string;
// Custom slot, in from-item
slot?: string;
// Custom slot, similar to renderColContent
colSlot?: string;
dynamicDisabled?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean);
dynamicRules?: (renderCallbackParams: RenderCallbackParams) => Rule[];
}
export interface HelpComponentProps {
maxWidth: string;
// Whether to display the serial number
showIndex: boolean;
// Text list
text: any;
// colour
color: string;
// font size
fontSize: string;
icon: string;
absolute: boolean;
// Positioning
position: any;
}

View File

@@ -0,0 +1,91 @@
import type { NamePath } from 'ant-design-vue/lib/form/interface';
import type { ColProps } from 'ant-design-vue/lib/grid/Col';
import type { HTMLAttributes, VNodeChild } from 'vue';
export interface FormItem {
/**
* Used with label, whether to display : after label text.
* @default true
* @type boolean
*/
colon?: boolean;
/**
* The extra prompt message. It is similar to help. Usage example: to display error message and prompt message at the same time.
* @type any (string | slot)
*/
extra?: string | VNodeChild | JSX.Element;
/**
* Used with validateStatus, this option specifies the validation status icon. Recommended to be used only with Input.
* @default false
* @type boolean
*/
hasFeedback?: boolean;
/**
* The prompt message. If not provided, the prompt message will be generated by the validation rule.
* @type any (string | slot)
*/
help?: string | VNodeChild | JSX.Element;
/**
* Label test
* @type any (string | slot)
*/
label?: string | VNodeChild | JSX.Element;
/**
* The layout of label. You can set span offset to something like {span: 3, offset: 12} or sm: {span: 3, offset: 12} same as with <Col>
* @type Col
*/
labelCol?: ColProps & HTMLAttributes;
/**
* Whether provided or not, it will be generated by the validation rule.
* @default false
* @type boolean
*/
required?: boolean;
/**
* The validation status. If not provided, it will be generated by validation rule. options: 'success' 'warning' 'error' 'validating'
* @type string
*/
validateStatus?: '' | 'success' | 'warning' | 'error' | 'validating';
/**
* The layout for input controls, same as labelCol
* @type Col
*/
wrapperCol?: ColProps;
/**
* Set sub label htmlFor.
*/
htmlFor?: string;
/**
* text align of label
*/
labelAlign?: 'left' | 'right';
/**
* a key of model. In the setting of validate and resetFields method, the attribute is required
*/
name?: NamePath;
/**
* validation rules of form
*/
rules?: object | object[];
/**
* Whether to automatically associate form fields. In most cases, you can setting automatic association.
* If the conditions for automatic association are not met, you can manually associate them. See the notes below.
*/
autoLink?: boolean;
/**
* Whether stop validate on first rule of error for this field.
*/
validateFirst?: boolean;
/**
* When to validate the value of children node
*/
validateTrigger?: string | string[] | false;
}

View File

@@ -0,0 +1,6 @@
export interface AdvanceState {
isAdvanced: boolean;
hideAdvanceBtn: boolean;
isLoad: boolean;
actionSpan: number;
}

View File

@@ -0,0 +1,150 @@
type ColSpanType = number | string;
export interface ColEx {
style?: any;
/**
* raster number of cells to occupy, 0 corresponds to display: none
* @default none (0)
* @type ColSpanType
*/
span?: ColSpanType;
/**
* raster order, used in flex layout mode
* @default 0
* @type ColSpanType
*/
order?: ColSpanType;
/**
* the layout fill of flex
* @default none
* @type ColSpanType
*/
flex?: ColSpanType;
/**
* the number of cells to offset Col from the left
* @default 0
* @type ColSpanType
*/
offset?: ColSpanType;
/**
* the number of cells that raster is moved to the right
* @default 0
* @type ColSpanType
*/
push?: ColSpanType;
/**
* the number of cells that raster is moved to the left
* @default 0
* @type ColSpanType
*/
pull?: ColSpanType;
/**
* <576px and also default setting, could be a span value or an object containing above props
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
*/
xs?: { span: ColSpanType; offset: ColSpanType } | ColSpanType;
/**
* ≥576px, could be a span value or an object containing above props
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
*/
sm?: { span: ColSpanType; offset: ColSpanType } | ColSpanType;
/**
* ≥768px, could be a span value or an object containing above props
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
*/
md?: { span: ColSpanType; offset: ColSpanType } | ColSpanType;
/**
* ≥992px, could be a span value or an object containing above props
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
*/
lg?: { span: ColSpanType; offset: ColSpanType } | ColSpanType;
/**
* ≥1200px, could be a span value or an object containing above props
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
*/
xl?: { span: ColSpanType; offset: ColSpanType } | ColSpanType;
/**
* ≥1600px, could be a span value or an object containing above props
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
*/
xxl?: { span: ColSpanType; offset: ColSpanType } | ColSpanType;
}
export type ComponentType =
| 'InputGroup'
| 'InputSearch'
| 'InputCountDown'
| 'AutoComplete'
| 'MonthPicker'
| 'WeekPicker'
| 'StrengthMeter'
| 'IconPicker'
| 'Render'
| 'Alert'
| 'AreaSelect'
| 'Button'
| 'Cron'
| 'Cascader'
| 'ColorPicker'
| 'Checkbox'
| 'YunzhupaasCheckboxSingle'
| 'DatePicker'
| 'DateRange'
| 'TimePicker'
| 'TimeRange'
| 'Divider'
| 'Editor'
| 'GroupTitle'
| 'Input'
| 'InputPassword'
| 'Textarea'
| 'InputNumber'
| 'Link'
| 'OrganizeSelect'
| 'DepSelect'
| 'PosSelect'
| 'GroupSelect'
| 'RoleSelect'
| 'UserSelect'
| 'UsersSelect'
| 'Qrcode'
| 'Barcode'
| 'Radio'
| 'Rate'
| 'Select'
| 'Slider'
| 'Sign'
| 'Signature'
| 'Switch'
| 'Text'
| 'TreeSelect'
| 'UploadFile'
| 'UploadImg'
| 'UploadImgSingle'
| 'RelationForm'
| 'RelationFormAttr'
| 'PopupSelect'
| 'PopupTableSelect'
| 'PopupAttr'
| 'NumberRange'
| 'Calculate'
| 'InputTable'
| 'BillRule'
| 'ModifyUser'
| 'ModifyTime'
| 'CreateUser'
| 'CreateTime'
| 'CurrOrganize'
| 'CurrPosition'
| 'Location'
| 'Iframe';