mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2026-02-01 16:15:21 +08:00
前端和后端源码,合并到一个git仓库中,方便用户下载,避免前后端不匹配的问题
This commit is contained in:
35
jeecgboot-vue3/src/components/Form/index.ts
Normal file
35
jeecgboot-vue3/src/components/Form/index.ts
Normal file
@ -0,0 +1,35 @@
|
||||
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 { default as ApiSelect } from './src/components/ApiSelect.vue';
|
||||
export { default as RadioButtonGroup } from './src/components/RadioButtonGroup.vue';
|
||||
export { default as ApiTreeSelect } from './src/components/ApiTreeSelect.vue';
|
||||
export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue';
|
||||
//Jeecg自定义组件
|
||||
export { default as JAreaLinkage } from './src/jeecg/components/JAreaLinkage.vue';
|
||||
export { default as JSelectUser } from './src/jeecg/components/JSelectUser.vue';
|
||||
export { default as JSelectDept } from './src/jeecg/components/JSelectDept.vue';
|
||||
export { default as JCodeEditor } from './src/jeecg/components/JCodeEditor.vue';
|
||||
export { default as JCategorySelect } from './src/jeecg/components/JCategorySelect.vue';
|
||||
export { default as JSelectMultiple } from './src/jeecg/components/JSelectMultiple.vue';
|
||||
export { default as JPopup } from './src/jeecg/components/JPopup.vue';
|
||||
export { default as JAreaSelect } from './src/jeecg/components/JAreaSelect.vue';
|
||||
export { JEasyCron, JEasyCronInner, JEasyCronModal } from '/@/components/Form/src/jeecg/components/JEasyCron';
|
||||
export { default as JCheckbox } from './src/jeecg/components/JCheckbox.vue';
|
||||
export { default as JInput } from './src/jeecg/components/JInput.vue';
|
||||
export { default as JEllipsis } from './src/jeecg/components/JEllipsis.vue';
|
||||
export { default as JDictSelectTag } from './src/jeecg/components/JDictSelectTag.vue';
|
||||
export { default as JTreeSelect } from './src/jeecg/components/JTreeSelect.vue';
|
||||
export { default as JSearchSelect } from './src/jeecg/components/JSearchSelect.vue';
|
||||
export { default as JSelectUserByDept } from './src/jeecg/components/JSelectUserByDept.vue';
|
||||
export { default as JEditor } from './src/jeecg/components/JEditor.vue';
|
||||
export { default as JImageUpload } from './src/jeecg/components/JImageUpload.vue';
|
||||
// Jeecg自定义校验
|
||||
export { JCronValidator } from '/@/components/Form/src/jeecg/components/JEasyCron';
|
||||
|
||||
export { BasicForm };
|
||||
420
jeecgboot-vue3/src/components/Form/src/BasicForm.vue
Normal file
420
jeecgboot-vue3/src/components/Form/src/BasicForm.vue
Normal file
@ -0,0 +1,420 @@
|
||||
<template>
|
||||
<Form v-bind="getBindValue" :class="getFormClass" ref="formElRef" :model="formModel" @keypress.enter="handleEnterPress">
|
||||
<Row v-bind="getRow">
|
||||
<slot name="formHeader"></slot>
|
||||
<template v-for="schema in getSchema" :key="schema.field">
|
||||
<FormItem
|
||||
:tableAction="tableAction"
|
||||
:formActionType="formActionType"
|
||||
:schema="schema"
|
||||
:formProps="getProps"
|
||||
:allDefaultValues="defaultValueRef"
|
||||
:formModel="formModel"
|
||||
:setFormModel="setFormModel"
|
||||
:validateFields="validateFields"
|
||||
:clearValidate="clearValidate"
|
||||
v-auth="schema.auth"
|
||||
>
|
||||
<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 { dateItemType } from './helper';
|
||||
import { dateUtil } from '/@/utils/dateUtil';
|
||||
|
||||
// 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 { basicProps } from './props';
|
||||
import componentSetting from '/@/settings/componentSetting';
|
||||
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import dayjs from 'dayjs';
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BasicForm',
|
||||
components: { FormItem, Form, Row, FormAction },
|
||||
props: basicProps,
|
||||
emits: ['advanced-change', 'reset', 'submit', 'register'],
|
||||
setup(props, { emit, attrs }) {
|
||||
const formModel = reactive<Recordable>({});
|
||||
const modalFn = useModalContext();
|
||||
|
||||
const advanceState = reactive<AdvanceState>({
|
||||
// 默认是收起状态
|
||||
isAdvanced: false,
|
||||
hideAdvanceBtn: true,
|
||||
isLoad: false,
|
||||
actionSpan: 6,
|
||||
});
|
||||
|
||||
const defaultValueRef = 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');
|
||||
|
||||
// Get the basic configuration of the form
|
||||
const getProps = computed((): FormProps => {
|
||||
let mergeProps = { ...props, ...unref(propsRef) } as FormProps;
|
||||
//update-begin-author:sunjianlei date:20220923 for: 如果用户设置了labelWidth,则使labelCol失效,解决labelWidth设置无效的问题
|
||||
if (mergeProps.labelWidth) {
|
||||
mergeProps.labelCol = undefined;
|
||||
}
|
||||
//update-end-author:sunjianlei date:20220923 for: 如果用户设置了labelWidth,则使labelCol失效,解决labelWidth设置无效的问题
|
||||
// update-begin--author:liaozhiyang---date:20231017---for:【QQYUN-6566】BasicForm支持一行显示(inline)
|
||||
if (mergeProps.layout === 'inline') {
|
||||
if (mergeProps.labelCol === componentSetting.form.labelCol) {
|
||||
mergeProps.labelCol = undefined;
|
||||
}
|
||||
if (mergeProps.wrapperCol === componentSetting.form.wrapperCol) {
|
||||
mergeProps.wrapperCol = undefined;
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20231017---for:【QQYUN-6566】BasicForm支持一行显示(inline)
|
||||
return mergeProps;
|
||||
});
|
||||
|
||||
const getFormClass = computed(() => {
|
||||
return [
|
||||
prefixCls,
|
||||
{
|
||||
[`${prefixCls}--compact`]: unref(getProps).compact,
|
||||
'jeecg-form-detail-effect': unref(getProps).disabled
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
// Get uniform row style and Row configuration for the entire form
|
||||
const getRow = computed((): Recordable => {
|
||||
const { baseRowStyle = {}, rowProps } = unref(getProps);
|
||||
return {
|
||||
style: baseRowStyle,
|
||||
...rowProps,
|
||||
};
|
||||
});
|
||||
|
||||
const getBindValue = computed(() => ({ ...attrs, ...props, ...unref(getProps) } as Recordable));
|
||||
|
||||
const getSchema = computed((): FormSchema[] => {
|
||||
const schemas: FormSchema[] = unref(schemaRef) || (unref(getProps).schemas as any);
|
||||
for (const schema of schemas) {
|
||||
const { defaultValue, component, componentProps } = schema;
|
||||
// handle date type
|
||||
if (defaultValue && dateItemType.includes(component)) {
|
||||
//update-begin---author:wangshuai ---date:20230410 for:【issues/435】代码生成的日期控件赋默认值报错------------
|
||||
let valueFormat:string = "";
|
||||
if(componentProps){
|
||||
valueFormat = componentProps?.valueFormat;
|
||||
}
|
||||
if(!valueFormat){
|
||||
console.warn("未配置valueFormat,可能导致格式化错误!");
|
||||
}
|
||||
//update-end---author:wangshuai ---date:20230410 for:【issues/435】代码生成的日期控件赋默认值报错------------
|
||||
if (!Array.isArray(defaultValue)) {
|
||||
//update-begin---author:wangshuai ---date:20221124 for:[issues/215]列表页查询框(日期选择框)设置初始时间,一进入页面时,后台报日期转换类型错误的------------
|
||||
if(valueFormat){
|
||||
// schema.defaultValue = dateUtil(defaultValue).format(valueFormat);
|
||||
// update-begin--author:liaozhiyang---date:20240529---for:【TV360X-346 】时间组件填写默认值有问题
|
||||
schema.defaultValue = dateUtil(defaultValue, valueFormat).format(valueFormat);
|
||||
// update-end--author:liaozhiyang---date:20240529---for:【TV360X-346 】时间组件填写默认值有问题
|
||||
}else{
|
||||
schema.defaultValue = dateUtil(defaultValue);
|
||||
}
|
||||
//update-end---author:wangshuai ---date:20221124 for:[issues/215]列表页查询框(日期选择框)设置初始时间,一进入页面时,后台报日期转换类型错误的------------
|
||||
} else {
|
||||
const def: dayjs.Dayjs[] = [];
|
||||
defaultValue.forEach((item) => {
|
||||
//update-begin---author:wangshuai ---date:20221124 for:[issues/215]列表页查询框(日期选择框)设置初始时间,一进入页面时,后台报日期转换类型错误的------------
|
||||
if(valueFormat){
|
||||
// update-begin--author:liaozhiyang---date:20240529---for:【TV360X-346 】时间组件填写默认值有问题
|
||||
def.push(dateUtil(item, valueFormat).format(valueFormat));
|
||||
// update-end--author:liaozhiyang---date:20240529---for:【TV360X-346 】时间组件填写默认值有问题
|
||||
}else{
|
||||
def.push(dateUtil(item));
|
||||
}
|
||||
//update-end---author:wangshuai ---date:20221124 for:[issues/215]列表页查询框(日期选择框)设置初始时间,一进入页面时,后台报日期转换类型错误的------------
|
||||
});
|
||||
// update-begin--author:liaozhiyang---date:20240328---for:【issues/1114】rangepicker等时间控件报错(vue3.4以上版本有问题)
|
||||
def.forEach((item, index) => {
|
||||
defaultValue[index] = item;
|
||||
});
|
||||
// update-end--author:liaozhiyang---date:20240328---for:【issues/1114】rangepicker等时间控件报错(vue3.4以上版本有问题)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unref(getProps).showAdvancedButton) {
|
||||
return schemas.filter((schema) => schema.component !== 'Divider') as FormSchema[];
|
||||
} else {
|
||||
return schemas as FormSchema[];
|
||||
}
|
||||
});
|
||||
|
||||
const { handleToggleAdvanced } = 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,
|
||||
removeSchemaByFiled,
|
||||
resetFields,
|
||||
scrollToField,
|
||||
} = useFormEvents({
|
||||
emit,
|
||||
getProps,
|
||||
formModel,
|
||||
getSchema,
|
||||
defaultValueRef,
|
||||
formElRef: formElRef as Ref<FormActionType>,
|
||||
schemaRef: schemaRef as Ref<FormSchema[]>,
|
||||
handleFormValues,
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
async function setProps(formProps: Partial<FormProps>): Promise<void> {
|
||||
propsRef.value = deepMerge(unref(propsRef) || {}, formProps);
|
||||
}
|
||||
|
||||
//update-begin-author:taoyan date:2022-11-28 for: QQYUN-3121 【优化】表单视图问题#scott测试 8、此功能未实现
|
||||
const onFormSubmitWhenChange = useDebounceFn(handleSubmit, 300);
|
||||
function setFormModel(key: string, value: any) {
|
||||
formModel[key] = value;
|
||||
// update-begin--author:liaozhiyang---date:20230922---for:【issues/752】表单校验dynamicRules 无法 使用失去焦点后校验 trigger: 'blur'
|
||||
// const { validateTrigger } = unref(getBindValue);
|
||||
// if (!validateTrigger || validateTrigger === 'change') {
|
||||
// validateFields([key]).catch((_) => {});
|
||||
// }
|
||||
// update-end--author:liaozhiyang---date:20230922---for:【issues/752】表单校验dynamicRules 无法 使用失去焦点后校验 trigger: 'blur'
|
||||
if(props.autoSearch === true){
|
||||
onFormSubmitWhenChange();
|
||||
}
|
||||
}
|
||||
//update-end-author:taoyan date:2022-11-28 for: QQYUN-3121 【优化】表单视图问题#scott测试 8、此功能未实现
|
||||
|
||||
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,
|
||||
getProps,
|
||||
removeSchemaByFiled,
|
||||
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 })),
|
||||
...formActionType,
|
||||
};
|
||||
},
|
||||
});
|
||||
</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;
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20240514---for:【QQYUN-9241】form表单上下间距大点
|
||||
//&:not(.ant-form-item-with-help) {
|
||||
// margin-bottom: 24px;
|
||||
//}
|
||||
// update-begin--author:liaozhiyang---date:20240514---for:【QQYUN-9241】form表单上下间距大点
|
||||
// update-begin--author:liaozhiyang---date:20240620---for:【TV360X-1420】校验时闪动
|
||||
&-has-error {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240620---for:【TV360X-1420】校验时闪动
|
||||
&.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
/*【美化表单】form的字体改小一号*/
|
||||
/* .ant-form-item-label > label{
|
||||
font-size: 13px;
|
||||
}
|
||||
.ant-form-item .ant-select {
|
||||
font-size: 13px;
|
||||
}
|
||||
.ant-select-item-option-selected {
|
||||
font-size: 13px;
|
||||
}
|
||||
.ant-select-item-option-content {
|
||||
font-size: 13px;
|
||||
}
|
||||
.ant-input {
|
||||
font-size: 13px;
|
||||
}*/
|
||||
/*【美化表单】form的字体改小一号*/
|
||||
|
||||
.ant-form-explain {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&--compact {
|
||||
.ant-form-item {
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20231017---for:【QQYUN-6566】BasicForm支持一行显示(inline)
|
||||
&.ant-form-inline {
|
||||
& > .ant-row {
|
||||
.ant-col { width:auto !important; }
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20231017---for:【QQYUN-6566】BasicForm支持一行显示(inline)
|
||||
}
|
||||
</style>
|
||||
183
jeecgboot-vue3/src/components/Form/src/componentMap.ts
Normal file
183
jeecgboot-vue3/src/components/Form/src/componentMap.ts
Normal file
@ -0,0 +1,183 @@
|
||||
/**
|
||||
* 目前实现了异步加载的组件清单 :
|
||||
* JAreaLinkage
|
||||
* JEditor
|
||||
* JMarkdownEditor
|
||||
* JCodeEditor
|
||||
* JEasyCron
|
||||
*/
|
||||
import type { Component } from 'vue';
|
||||
import type { ComponentType } from './types/index';
|
||||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||
/**
|
||||
* Component list, register here to setting it in the form
|
||||
*/
|
||||
import {
|
||||
Input,
|
||||
Select,
|
||||
Radio,
|
||||
Checkbox,
|
||||
AutoComplete,
|
||||
Cascader,
|
||||
DatePicker,
|
||||
InputNumber,
|
||||
Switch,
|
||||
TimePicker,
|
||||
TreeSelect,
|
||||
Slider,
|
||||
Rate,
|
||||
Divider,
|
||||
} from 'ant-design-vue';
|
||||
import ApiRadioGroup from './components/ApiRadioGroup.vue';
|
||||
import RadioButtonGroup from './components/RadioButtonGroup.vue';
|
||||
import ApiSelect from './components/ApiSelect.vue';
|
||||
import ApiTreeSelect from './components/ApiTreeSelect.vue';
|
||||
import { BasicUpload } from '/@/components/Upload';
|
||||
import { StrengthMeter } from '/@/components/StrengthMeter';
|
||||
import { IconPicker } from '/@/components/Icon';
|
||||
import { CountdownInput } from '/@/components/CountDown';
|
||||
//自定义组件
|
||||
// import JAreaLinkage from './jeecg/components/JAreaLinkage.vue';
|
||||
import JSelectUser from './jeecg/components/JSelectUser.vue';
|
||||
import JSelectPosition from './jeecg/components/JSelectPosition.vue';
|
||||
import JSelectRole from './jeecg/components/JSelectRole.vue';
|
||||
import JImageUpload from './jeecg/components/JImageUpload.vue';
|
||||
import JDictSelectTag from './jeecg/components/JDictSelectTag.vue';
|
||||
import JSelectDept from './jeecg/components/JSelectDept.vue';
|
||||
import JAreaSelect from './jeecg/components/JAreaSelect.vue';
|
||||
import JEditor from './jeecg/components/JEditor.vue';
|
||||
// import JMarkdownEditor from './jeecg/components/JMarkdownEditor.vue';
|
||||
import JSelectInput from './jeecg/components/JSelectInput.vue';
|
||||
// import JCodeEditor from './jeecg/components/JCodeEditor.vue';
|
||||
import JCategorySelect from './jeecg/components/JCategorySelect.vue';
|
||||
import JSelectMultiple from './jeecg/components/JSelectMultiple.vue';
|
||||
import JPopup from './jeecg/components/JPopup.vue';
|
||||
// update-begin--author:liaozhiyang---date:20240130---for:【QQYUN-7961】popupDict字典
|
||||
import JPopupDict from './jeecg/components/JPopupDict.vue';
|
||||
// update-end--author:liaozhiyang---date:20240130---for:【QQYUN-7961】popupDict字典
|
||||
import JSwitch from './jeecg/components/JSwitch.vue';
|
||||
import JTreeDict from './jeecg/components/JTreeDict.vue';
|
||||
import JInputPop from './jeecg/components/JInputPop.vue';
|
||||
// import { JEasyCron } from './jeecg/components/JEasyCron';
|
||||
import JCheckbox from './jeecg/components/JCheckbox.vue';
|
||||
import JInput from './jeecg/components/JInput.vue';
|
||||
import JTreeSelect from './jeecg/components/JTreeSelect.vue';
|
||||
import JEllipsis from './jeecg/components/JEllipsis.vue';
|
||||
import JSelectUserByDept from './jeecg/components/JSelectUserByDept.vue';
|
||||
import JUpload from './jeecg/components/JUpload/JUpload.vue';
|
||||
import JSearchSelect from './jeecg/components/JSearchSelect.vue';
|
||||
import JAddInput from './jeecg/components/JAddInput.vue';
|
||||
import { Time } from '/@/components/Time';
|
||||
import JRangeNumber from './jeecg/components/JRangeNumber.vue';
|
||||
import UserSelect from './jeecg/components/userSelect/index.vue';
|
||||
import JRangeDate from './jeecg/components/JRangeDate.vue'
|
||||
import JRangeTime from './jeecg/components/JRangeTime.vue'
|
||||
import JInputSelect from './jeecg/components/JInputSelect.vue'
|
||||
import RoleSelectInput from './jeecg/components/roleSelect/RoleSelectInput.vue';
|
||||
import {DatePickerInFilter, CascaderPcaInFilter} from "@/components/InFilter";
|
||||
|
||||
const componentMap = new Map<ComponentType, Component>();
|
||||
|
||||
componentMap.set('Time', Time);
|
||||
componentMap.set('Input', Input);
|
||||
componentMap.set('InputGroup', Input.Group);
|
||||
componentMap.set('InputPassword', Input.Password);
|
||||
componentMap.set('InputSearch', Input.Search);
|
||||
componentMap.set('InputTextArea', Input.TextArea);
|
||||
componentMap.set('InputNumber', InputNumber);
|
||||
componentMap.set('AutoComplete', AutoComplete);
|
||||
|
||||
componentMap.set('Select', Select);
|
||||
componentMap.set('ApiSelect', ApiSelect);
|
||||
componentMap.set('TreeSelect', TreeSelect);
|
||||
componentMap.set('ApiTreeSelect', ApiTreeSelect);
|
||||
componentMap.set('ApiRadioGroup', ApiRadioGroup);
|
||||
componentMap.set('Switch', Switch);
|
||||
componentMap.set('RadioButtonGroup', RadioButtonGroup);
|
||||
componentMap.set('RadioGroup', Radio.Group);
|
||||
componentMap.set('Checkbox', Checkbox);
|
||||
componentMap.set('CheckboxGroup', Checkbox.Group);
|
||||
componentMap.set('Cascader', Cascader);
|
||||
componentMap.set('Slider', Slider);
|
||||
componentMap.set('Rate', Rate);
|
||||
|
||||
componentMap.set('DatePicker', DatePicker);
|
||||
componentMap.set('MonthPicker', DatePicker.MonthPicker);
|
||||
componentMap.set('RangePicker', DatePicker.RangePicker);
|
||||
componentMap.set('WeekPicker', DatePicker.WeekPicker);
|
||||
componentMap.set('TimePicker', TimePicker);
|
||||
componentMap.set('DatePickerInFilter', DatePickerInFilter);
|
||||
componentMap.set('StrengthMeter', StrengthMeter);
|
||||
componentMap.set('IconPicker', IconPicker);
|
||||
componentMap.set('InputCountDown', CountdownInput);
|
||||
|
||||
componentMap.set('Upload', BasicUpload);
|
||||
componentMap.set('Divider', Divider);
|
||||
|
||||
//注册自定义组件
|
||||
|
||||
componentMap.set(
|
||||
'JAreaLinkage',
|
||||
createAsyncComponent(() => import('./jeecg/components/JAreaLinkage.vue'))
|
||||
);
|
||||
componentMap.set('JSelectPosition', JSelectPosition);
|
||||
componentMap.set('JSelectUser', JSelectUser);
|
||||
componentMap.set('JSelectRole', JSelectRole);
|
||||
componentMap.set('JImageUpload', JImageUpload);
|
||||
componentMap.set('JDictSelectTag', JDictSelectTag);
|
||||
componentMap.set('JSelectDept', JSelectDept);
|
||||
componentMap.set('JAreaSelect', JAreaSelect);
|
||||
// componentMap.set(
|
||||
// 'JEditor',
|
||||
// createAsyncComponent(() => import('./jeecg/components/JEditor.vue'))
|
||||
// );
|
||||
componentMap.set('JEditor', JEditor);
|
||||
componentMap.set(
|
||||
'JMarkdownEditor',
|
||||
createAsyncComponent(() => import('./jeecg/components/JMarkdownEditor.vue'))
|
||||
);
|
||||
componentMap.set('JSelectInput', JSelectInput);
|
||||
componentMap.set(
|
||||
'JCodeEditor',
|
||||
createAsyncComponent(() => import('./jeecg/components/JCodeEditor.vue'))
|
||||
);
|
||||
componentMap.set('JCategorySelect', JCategorySelect);
|
||||
componentMap.set('JSelectMultiple', JSelectMultiple);
|
||||
componentMap.set('JPopup', JPopup);
|
||||
// update-begin--author:liaozhiyang---date:20240130---for:【QQYUN-7961】popupDict字典
|
||||
componentMap.set('JPopupDict', JPopupDict);
|
||||
// update-end--author:liaozhiyang---date:20240130---for:【QQYUN-7961】popupDict字典
|
||||
componentMap.set('JSwitch', JSwitch);
|
||||
componentMap.set('JTreeDict', JTreeDict);
|
||||
componentMap.set('JInputPop', JInputPop);
|
||||
componentMap.set(
|
||||
'JEasyCron',
|
||||
createAsyncComponent(() => import('./jeecg/components/JEasyCron/EasyCronInput.vue'))
|
||||
);
|
||||
componentMap.set('JCheckbox', JCheckbox);
|
||||
componentMap.set('JInput', JInput);
|
||||
componentMap.set('JTreeSelect', JTreeSelect);
|
||||
componentMap.set('JEllipsis', JEllipsis);
|
||||
componentMap.set('JSelectUserByDept', JSelectUserByDept);
|
||||
componentMap.set('JUpload', JUpload);
|
||||
componentMap.set('JSearchSelect', JSearchSelect);
|
||||
componentMap.set('JAddInput', JAddInput);
|
||||
componentMap.set('JRangeNumber', JRangeNumber);
|
||||
componentMap.set('CascaderPcaInFilter', CascaderPcaInFilter);
|
||||
componentMap.set('UserSelect', UserSelect);
|
||||
componentMap.set('RangeDate', JRangeDate);
|
||||
componentMap.set('RangeTime', JRangeTime);
|
||||
componentMap.set('RoleSelect', RoleSelectInput);
|
||||
componentMap.set('JInputSelect', JInputSelect);
|
||||
|
||||
|
||||
|
||||
export function add(compName: ComponentType, component: Component) {
|
||||
componentMap.set(compName, component);
|
||||
}
|
||||
|
||||
export function del(compName: ComponentType) {
|
||||
componentMap.delete(compName);
|
||||
}
|
||||
|
||||
export { componentMap };
|
||||
@ -0,0 +1,130 @@
|
||||
<!--
|
||||
* @Description:It is troublesome to implement radio button group in the form. So it is extracted independently as a separate component
|
||||
-->
|
||||
<template>
|
||||
<RadioGroup v-bind="attrs" v-model:value="state" button-style="solid" @change="handleChange">
|
||||
<template v-for="item in getOptions" :key="`${item.value}`">
|
||||
<RadioButton v-if="props.isBtn" :value="item.value" :disabled="item.disabled">
|
||||
{{ item.label }}
|
||||
</RadioButton>
|
||||
<Radio v-else :value="item.value" :disabled="item.disabled">
|
||||
{{ item.label }}
|
||||
</Radio>
|
||||
</template>
|
||||
</RadioGroup>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, ref, watchEffect, computed, unref, watch } from 'vue';
|
||||
import { Radio } from 'ant-design-vue';
|
||||
import { isFunction } from '/@/utils/is';
|
||||
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { get, omit } from 'lodash-es';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
type OptionsItem = { label: string; value: string | number | boolean; disabled?: boolean };
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ApiRadioGroup',
|
||||
components: {
|
||||
RadioGroup: Radio.Group,
|
||||
RadioButton: Radio.Button,
|
||||
Radio,
|
||||
},
|
||||
props: {
|
||||
api: {
|
||||
type: Function as PropType<(arg?: Recordable | string) => Promise<OptionsItem[]>>,
|
||||
default: null,
|
||||
},
|
||||
params: {
|
||||
type: [Object, String] as PropType<Recordable | string>,
|
||||
default: () => ({}),
|
||||
},
|
||||
value: {
|
||||
type: [String, Number, Boolean] as PropType<string | number | boolean>,
|
||||
},
|
||||
isBtn: {
|
||||
type: [Boolean] as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
numberToString: propTypes.bool,
|
||||
resultField: propTypes.string.def(''),
|
||||
labelField: propTypes.string.def('label'),
|
||||
valueField: propTypes.string.def('value'),
|
||||
immediate: propTypes.bool.def(true),
|
||||
},
|
||||
emits: ['options-change', 'change'],
|
||||
setup(props, { emit }) {
|
||||
const options = ref<OptionsItem[]>([]);
|
||||
const loading = ref(false);
|
||||
const isFirstLoad = ref(true);
|
||||
const emitData = ref<any[]>([]);
|
||||
const attrs = useAttrs();
|
||||
const { t } = useI18n();
|
||||
// Embedded in the form, just use the hook binding to perform form verification
|
||||
const [state] = useRuleFormItem(props);
|
||||
|
||||
// Processing options value
|
||||
const getOptions = computed(() => {
|
||||
const { labelField, valueField, numberToString } = props;
|
||||
|
||||
return unref(options).reduce((prev, next: Recordable) => {
|
||||
if (next) {
|
||||
const value = next[valueField];
|
||||
prev.push({
|
||||
label: next[labelField],
|
||||
value: numberToString ? `${value}` : value,
|
||||
...omit(next, [labelField, valueField]),
|
||||
});
|
||||
}
|
||||
return prev;
|
||||
}, [] as OptionsItem[]);
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.immediate && fetch();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.params,
|
||||
() => {
|
||||
!unref(isFirstLoad) && fetch();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
async function fetch() {
|
||||
const api = props.api;
|
||||
if (!api || !isFunction(api)) return;
|
||||
options.value = [];
|
||||
try {
|
||||
loading.value = true;
|
||||
const res = await api(props.params);
|
||||
if (Array.isArray(res)) {
|
||||
options.value = res;
|
||||
emitChange();
|
||||
return;
|
||||
}
|
||||
if (props.resultField) {
|
||||
options.value = get(res, props.resultField) || [];
|
||||
}
|
||||
emitChange();
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function emitChange() {
|
||||
emit('options-change', unref(getOptions));
|
||||
}
|
||||
|
||||
function handleChange(_, ...args) {
|
||||
emitData.value = args;
|
||||
}
|
||||
|
||||
return { state, getOptions, attrs, loading, t, handleChange, props };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
176
jeecgboot-vue3/src/components/Form/src/components/ApiSelect.vue
Normal file
176
jeecgboot-vue3/src/components/Form/src/components/ApiSelect.vue
Normal file
@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<Select @dropdownVisibleChange="handleFetch" v-bind="attrs_" @change="handleChange" :options="getOptions" v-model:value="state">
|
||||
<template #[item]="data" v-for="item in Object.keys($slots)">
|
||||
<slot :name="item" v-bind="data || {}"></slot>
|
||||
</template>
|
||||
<template #suffixIcon v-if="loading">
|
||||
<LoadingOutlined spin />
|
||||
</template>
|
||||
<template #notFoundContent v-if="loading">
|
||||
<span>
|
||||
<LoadingOutlined spin class="mr-1" />
|
||||
{{ t('component.form.apiSelectNotFound') }}
|
||||
</span>
|
||||
</template>
|
||||
</Select>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, ref, watchEffect, computed, unref, watch } from 'vue';
|
||||
import { Select } from 'ant-design-vue';
|
||||
import { isFunction } from '/@/utils/is';
|
||||
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { get, omit } from 'lodash-es';
|
||||
import { LoadingOutlined } from '@ant-design/icons-vue';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
|
||||
type OptionsItem = { label: string; value: string; disabled?: boolean };
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ApiSelect',
|
||||
components: {
|
||||
Select,
|
||||
LoadingOutlined,
|
||||
},
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
value: [Array, Object, String, Number],
|
||||
numberToString: propTypes.bool,
|
||||
api: {
|
||||
type: Function as PropType<(arg?: Recordable) => Promise<OptionsItem[]>>,
|
||||
default: null,
|
||||
},
|
||||
// api params
|
||||
params: {
|
||||
type: Object as PropType<Recordable>,
|
||||
default: () => ({}),
|
||||
},
|
||||
// support xxx.xxx.xx
|
||||
resultField: propTypes.string.def(''),
|
||||
labelField: propTypes.string.def('label'),
|
||||
valueField: propTypes.string.def('value'),
|
||||
immediate: propTypes.bool.def(true),
|
||||
},
|
||||
emits: ['options-change', 'change'],
|
||||
setup(props, { emit }) {
|
||||
const options = ref<OptionsItem[]>([]);
|
||||
const loading = ref(false);
|
||||
const isFirstLoad = ref(true);
|
||||
const emitData = ref<any[]>([]);
|
||||
const attrs = useAttrs();
|
||||
const { t } = useI18n();
|
||||
|
||||
// Embedded in the form, just use the hook binding to perform form verification
|
||||
const [state, setState] = useRuleFormItem(props, 'value', 'change', emitData);
|
||||
// update-begin--author:liaozhiyang---date:20230830---for:【QQYUN-6308】解决警告
|
||||
let vModalValue: any;
|
||||
const attrs_ = computed(() => {
|
||||
let obj: any = unref(attrs) || {};
|
||||
if (obj && obj['onUpdate:value']) {
|
||||
vModalValue = obj['onUpdate:value'];
|
||||
delete obj['onUpdate:value'];
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20231017---for:【issues/5467】ApiSelect修复覆盖了用户传递的方法
|
||||
if (obj['filterOption'] === undefined) {
|
||||
// update-begin--author:liaozhiyang---date:20230904---for:【issues/5305】无法按照预期进行搜索
|
||||
obj['filterOption'] = (inputValue, option) => {
|
||||
if (typeof option['label'] === 'string') {
|
||||
return option['label'].toLowerCase().indexOf(inputValue.toLowerCase()) != -1;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
// update-end--author:liaozhiyang---date:20230904---for:【issues/5305】无法按照预期进行搜索
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20231017---for:【issues/5467】ApiSelect修复覆盖了用户传递的方法
|
||||
return obj;
|
||||
});
|
||||
// update-begin--author:liaozhiyang---date:20230830---for:【QQYUN-6308】解决警告
|
||||
const getOptions = computed(() => {
|
||||
const { labelField, valueField, numberToString } = props;
|
||||
return unref(options).reduce((prev, next: Recordable) => {
|
||||
if (next) {
|
||||
const value = next[valueField];
|
||||
prev.push({
|
||||
...omit(next, [labelField, valueField]),
|
||||
label: next[labelField],
|
||||
value: numberToString ? `${value}` : value,
|
||||
});
|
||||
}
|
||||
return prev;
|
||||
}, [] as OptionsItem[]);
|
||||
});
|
||||
// update-begin--author:liaozhiyang---date:20240509---for:【issues/6191】apiSelect多次请求
|
||||
props.immediate && fetch();
|
||||
// update-end--author:liaozhiyang---date:20240509---for:【issues/6191】apiSelect多次请求
|
||||
|
||||
watch(
|
||||
() => props.params,
|
||||
() => {
|
||||
!unref(isFirstLoad) && fetch();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
//监听数值修改,查询数据
|
||||
watchEffect(() => {
|
||||
props.value && handleFetch();
|
||||
});
|
||||
|
||||
async function fetch() {
|
||||
const api = props.api;
|
||||
if (!api || !isFunction(api)) return;
|
||||
options.value = [];
|
||||
try {
|
||||
loading.value = true;
|
||||
const res = await api(props.params);
|
||||
if (Array.isArray(res)) {
|
||||
options.value = res;
|
||||
emitChange();
|
||||
return;
|
||||
}
|
||||
if (props.resultField) {
|
||||
options.value = get(res, props.resultField) || [];
|
||||
}
|
||||
emitChange();
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
//--@updateBy-begin----author:liusq---date:20210914------for:判断选择模式,multiple多选情况下的value值空的情况下需要设置为数组------
|
||||
unref(attrs).mode == 'multiple' && !Array.isArray(unref(state)) && setState([]);
|
||||
//--@updateBy-end----author:liusq---date:20210914------for:判断选择模式,multiple多选情况下的value值空的情况下需要设置为数组------
|
||||
|
||||
//update-begin---author:wangshuai ---date:20230505 for:初始化value值,如果是多选字符串的情况下显示不出来------------
|
||||
initValue();
|
||||
//update-end---author:wangshuai ---date:20230505 for:初始化value值,如果是多选字符串的情况下显示不出来------------
|
||||
}
|
||||
}
|
||||
|
||||
function initValue() {
|
||||
let value = props.value;
|
||||
if (value && typeof value === 'string' && value != 'null' && value != 'undefined') {
|
||||
state.value = value.split(',');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFetch() {
|
||||
if (!props.immediate && unref(isFirstLoad)) {
|
||||
await fetch();
|
||||
isFirstLoad.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function emitChange() {
|
||||
emit('options-change', unref(getOptions));
|
||||
}
|
||||
|
||||
function handleChange(_, ...args) {
|
||||
vModalValue && vModalValue(_);
|
||||
emitData.value = args;
|
||||
}
|
||||
|
||||
return { state, attrs_, attrs, getOptions, loading, t, handleFetch, handleChange };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<a-tree-select v-bind="getAttrs" @change="handleChange">
|
||||
<template #[item]="data" v-for="item in Object.keys($slots)">
|
||||
<slot :name="item" v-bind="data || {}"></slot>
|
||||
</template>
|
||||
<template #suffixIcon v-if="loading">
|
||||
<LoadingOutlined spin />
|
||||
</template>
|
||||
</a-tree-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, watch, ref, onMounted, unref } from 'vue';
|
||||
import { TreeSelect } from 'ant-design-vue';
|
||||
import { isArray, isFunction } from '/@/utils/is';
|
||||
import { get } from 'lodash-es';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { LoadingOutlined } from '@ant-design/icons-vue';
|
||||
export default defineComponent({
|
||||
name: 'ApiTreeSelect',
|
||||
components: { ATreeSelect: TreeSelect, LoadingOutlined },
|
||||
props: {
|
||||
api: { type: Function as PropType<(arg?: Recordable) => Promise<Recordable>> },
|
||||
params: { type: Object },
|
||||
immediate: { type: Boolean, default: true },
|
||||
resultField: propTypes.string.def(''),
|
||||
},
|
||||
emits: ['options-change', 'change'],
|
||||
setup(props, { attrs, emit }) {
|
||||
const treeData = ref<Recordable[]>([]);
|
||||
const isFirstLoaded = ref<Boolean>(false);
|
||||
const loading = ref(false);
|
||||
const getAttrs = computed(() => {
|
||||
return {
|
||||
...(props.api ? { treeData: unref(treeData) } : {}),
|
||||
...attrs,
|
||||
};
|
||||
});
|
||||
|
||||
function handleChange(...args) {
|
||||
emit('change', ...args);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.params,
|
||||
() => {
|
||||
//update-begin---author:wangshuai---date:2024-02-28---for:【QQYUN-8346】 ApiTreeSelect组件入参变化时,不及时刷新数据 #1054---
|
||||
unref(isFirstLoaded) && fetch();
|
||||
//update-end---author:wangshuai---date:2024-02-28---for:【QQYUN-8346】 ApiTreeSelect组件入参变化时,不及时刷新数据 #1054---
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.immediate,
|
||||
(v) => {
|
||||
v && !isFirstLoaded.value && fetch();
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
props.immediate && fetch();
|
||||
});
|
||||
|
||||
async function fetch() {
|
||||
const { api } = props;
|
||||
if (!api || !isFunction(api)) return;
|
||||
loading.value = true;
|
||||
treeData.value = [];
|
||||
let result;
|
||||
try {
|
||||
result = await api(props.params);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
loading.value = false;
|
||||
if (!result) return;
|
||||
if (!isArray(result)) {
|
||||
result = get(result, props.resultField);
|
||||
}
|
||||
treeData.value = (result as Recordable[]) || [];
|
||||
isFirstLoaded.value = true;
|
||||
emit('options-change', treeData.value);
|
||||
}
|
||||
return { getAttrs, loading, handleChange };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
128
jeecgboot-vue3/src/components/Form/src/components/FormAction.vue
Normal file
128
jeecgboot-vue3/src/components/Form/src/components/FormAction.vue
Normal file
@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<a-col v-bind="actionColOpt" v-if="showActionButtonGroup">
|
||||
<div style="width: 100%" :style="{ textAlign: actionColOpt.style.textAlign }">
|
||||
<FormItem>
|
||||
<!-- update-begin-author:zyf Date:20211213 for:调换按钮前后位置-->
|
||||
<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>
|
||||
<!-- update-end-author:zyf Date:20211213 for:调换按钮前后位置-->
|
||||
|
||||
<slot name="advanceBefore"></slot>
|
||||
<Button type="link" size="small" @click="toggleAdvanced" v-if="showAdvancedButton && !hideAdvanceBtn">
|
||||
{{ isAdvanced ? t('component.form.putAway') : 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,
|
||||
layout: propTypes.oneOf(['horizontal', 'vertical', 'inline']).def('horizontal'),
|
||||
},
|
||||
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 } : {};
|
||||
// update-begin--author:liaozhiyang---date:20240105---for:【QQYUN-6566】BasicForm支持一行显示(inline)
|
||||
const defaultSpan = props.layout == 'inline' ? {} : { span: showAdvancedButton ? 6 : 4 };
|
||||
// update-end--author:liaozhiyang---date:20240105---for:【QQYUN-6566】BasicForm支持一行显示(inline)
|
||||
const actionColOpt: Partial<ColEx> = {
|
||||
style: { textAlign: 'right' },
|
||||
...defaultSpan,
|
||||
...advancedSpanObj,
|
||||
...actionColOptions,
|
||||
};
|
||||
|
||||
|
||||
|
||||
return actionColOpt;
|
||||
});
|
||||
|
||||
const getResetBtnOptions = computed((): ButtonOptions => {
|
||||
return Object.assign(
|
||||
{
|
||||
text: t('common.resetText'),
|
||||
preIcon: 'ic:baseline-restart-alt',
|
||||
},
|
||||
props.resetButtonOptions
|
||||
);
|
||||
});
|
||||
|
||||
const getSubmitBtnOptions = computed(() => {
|
||||
return Object.assign(
|
||||
{},
|
||||
{
|
||||
text: t('common.queryText'),
|
||||
preIcon: 'ant-design:search-outlined',
|
||||
},
|
||||
props.submitButtonOptions
|
||||
);
|
||||
});
|
||||
|
||||
function toggleAdvanced() {
|
||||
emit('toggle-advanced');
|
||||
}
|
||||
|
||||
return {
|
||||
t,
|
||||
actionColOpt,
|
||||
getResetBtnOptions,
|
||||
getSubmitBtnOptions,
|
||||
toggleAdvanced,
|
||||
...useFormContext(),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
513
jeecgboot-vue3/src/components/Form/src/components/FormItem.vue
Normal file
513
jeecgboot-vue3/src/components/Form/src/components/FormItem.vue
Normal file
@ -0,0 +1,513 @@
|
||||
<script lang="tsx">
|
||||
import { NamePath, ValidateOptions } from 'ant-design-vue/lib/form/interface';
|
||||
import type { PropType, Ref } from 'vue';
|
||||
import type { FormActionType, FormProps } from '../types/form';
|
||||
import type { FormSchema } from '../types/form';
|
||||
import type { ValidationRule } from 'ant-design-vue/lib/form/Form';
|
||||
import type { TableActionType } from '/@/components/Table';
|
||||
import { defineComponent, computed, unref, toRefs } from 'vue';
|
||||
import { Form, Col, Divider } from 'ant-design-vue';
|
||||
import { componentMap } from '../componentMap';
|
||||
import { BasicHelp } from '/@/components/Basic';
|
||||
import { isBoolean, isFunction, isNull } from '/@/utils/is';
|
||||
import { getSlot } from '/@/utils/helper/tsxHelper';
|
||||
import { createPlaceholderMessage, setComponentRuleType } from '../helper';
|
||||
import { upperFirst, cloneDeep } from 'lodash-es';
|
||||
import { useItemLabelWidth } from '../hooks/useLabelWidth';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { useAppInject } from '/@/hooks/web/useAppInject';
|
||||
import { usePermission } from '/@/hooks/web/usePermission';
|
||||
import Middleware from './Middleware.vue';
|
||||
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) => void>,
|
||||
default: null,
|
||||
},
|
||||
validateFields: {
|
||||
type: Function as PropType<(nameList?: NamePath[] | undefined, options?: ValidateOptions) => Promise<any>>,
|
||||
default: null,
|
||||
},
|
||||
tableAction: {
|
||||
type: Object as PropType<TableActionType>,
|
||||
},
|
||||
formActionType: {
|
||||
type: Object as PropType<FormActionType>,
|
||||
},
|
||||
// update-begin--author:liaozhiyang---date:20240605---for:【TV360X-857】解决禁用状态下触发校验
|
||||
clearValidate: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
// update-end-author:liaozhiyang---date:20240605---for:【TV360X-857】解决禁用状态下触发校验
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
const { t } = useI18n();
|
||||
|
||||
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 }) ?? {};
|
||||
}
|
||||
if (schema.component === 'Divider') {
|
||||
//update-begin---author:wangshuai---date:2023-09-22---for:【QQYUN-6603】分割线标题位置显示不正确---
|
||||
componentProps = Object.assign({ type: 'horizontal',orientation:'left', plain: true, }, componentProps);
|
||||
//update-end---author:wangshuai---date:2023-09-22---for:【QQYUN-6603】分割线标题位置显示不正确---
|
||||
}
|
||||
return componentProps as Recordable;
|
||||
});
|
||||
|
||||
const getDisable = computed(() => {
|
||||
const { disabled: globDisabled } = props.formProps;
|
||||
// update-begin--author:liaozhiyang---date:20240530---for:【TV360X-594】表单全局禁用则dynamicDisabled不生效
|
||||
if (!!globDisabled) {
|
||||
return globDisabled;
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240530---for:【TV360X-594】表单全局禁用则dynamicDisabled不生效
|
||||
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;
|
||||
});
|
||||
|
||||
// update-begin--author:liaozhiyang---date:20240308---for:【QQYUN-8377】formSchema props支持动态修改
|
||||
const getDynamicPropsValue = computed(() => {
|
||||
const { dynamicPropsVal, dynamicPropskey } = props.schema;
|
||||
if (dynamicPropskey == null) {
|
||||
return null;
|
||||
} else {
|
||||
const { [dynamicPropskey]: itemValue } = unref(getComponentsProps);
|
||||
let value = itemValue;
|
||||
if (isFunction(dynamicPropsVal)) {
|
||||
value = dynamicPropsVal(unref(getValues));
|
||||
return value;
|
||||
}
|
||||
}
|
||||
});
|
||||
// update-end--author:liaozhiyang---date:20240308---for:【QQYUN-8377】formSchema props支持动态修改
|
||||
|
||||
function getShow(): { isShow: boolean; isIfShow: boolean } {
|
||||
const { show, ifShow } = props.schema;
|
||||
const { showAdvancedButton } = props.formProps;
|
||||
const itemIsAdvanced = showAdvancedButton ? (isBoolean(props.schema.isAdvanced) ? props.schema.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;
|
||||
return { isShow, isIfShow };
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20240530---for:【TV360X-434】validator校验执行两次
|
||||
let vSwitchArr: any = [],
|
||||
prevValidatorArr: any = [];
|
||||
const hijackValidator = (rules) => {
|
||||
vSwitchArr = [];
|
||||
prevValidatorArr = [];
|
||||
rules.forEach((item, index) => {
|
||||
const fn = item.validator;
|
||||
vSwitchArr.push(true);
|
||||
prevValidatorArr.push(null);
|
||||
if (isFunction(fn)) {
|
||||
item.validator = (rule, value, callback) => {
|
||||
if (vSwitchArr[index]) {
|
||||
vSwitchArr[index] = false;
|
||||
setTimeout(() => {
|
||||
vSwitchArr[index] = true;
|
||||
}, 100);
|
||||
const result = fn(rule, value, callback);
|
||||
prevValidatorArr[index] = result;
|
||||
return result;
|
||||
} else {
|
||||
return prevValidatorArr[index];
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
// update-end--author:liaozhiyang---date:20240530---for:【TV360X-434】validator校验执行两次
|
||||
function handleRules(): ValidationRule[] {
|
||||
const { rules: defRules = [], component, rulesMessageJoinLabel, label, dynamicRules, required, auth, field } = props.schema;
|
||||
// update-begin--author:liaozhiyang---date:20240605---for:【TV360X-857】解决禁用状态下触发校验
|
||||
const { disabled: globDisabled } = props.formProps;
|
||||
const { disabled: itemDisabled = false } = unref(getComponentsProps);
|
||||
if (!!globDisabled || !!itemDisabled) {
|
||||
props.clearValidate(field);
|
||||
return [];
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240605---for:【TV360X-857】解决禁用状态下触发校验
|
||||
// update-begin--author:liaozhiyang---date:20240531---for:【TV360X-842】必填项v-auth、show隐藏的情况下表单无法提交
|
||||
const { hasPermission } = usePermission();
|
||||
const { isShow } = getShow();
|
||||
if ((auth && !hasPermission(auth)) || !isShow) {
|
||||
return [];
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240531---for:【TV360X-842】必填项v-auth、show隐藏的情况下表单无法提交
|
||||
if (isFunction(dynamicRules)) {
|
||||
// update-begin--author:liaozhiyang---date:20240514---for:【issues/1244】标识了必填,但是必填标识没显示
|
||||
const ruleArr = dynamicRules(unref(getValues)) as ValidationRule[];
|
||||
if (required) {
|
||||
ruleArr.unshift({ required: true });
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20240530---for:【TV360X-434】validator校验执行两次
|
||||
hijackValidator(ruleArr);
|
||||
// update-end--author:liaozhiyang---date:20240530---for:【TV360X-434】validator校验执行两次
|
||||
return ruleArr;
|
||||
// update-end--author:liaozhiyang---date:20240514---for:【issues/1244】标识了必填,但是必填标识没显示
|
||||
}
|
||||
|
||||
let rules: ValidationRule[] = cloneDeep(defRules) as ValidationRule[];
|
||||
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;
|
||||
|
||||
if ((!rules || rules.length === 0) && getRequired) {
|
||||
rules = [{ 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) {
|
||||
//update-begin---author:wangshuai---date:2024-02-01---for:【QQYUN-8176】编辑表单中,校验必填时,如果组件是ApiSelect,打开编辑页面时,即使该字段有值,也会提示请选择---
|
||||
//https://github.com/vbenjs/vue-vben-admin/pull/3082 github修复原文
|
||||
/*if (!Reflect.has(rule, 'type')) {
|
||||
rule.type = component === 'InputNumber' ? 'number' : 'string';
|
||||
}*/
|
||||
//update-end---author:wangshuai---date:2024-02-01---for:【QQYUN-8176】编辑表单中,校验必填时,如果组件是ApiSelect,打开编辑页面时,即使该字段有值,也会提示请选择---
|
||||
|
||||
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);
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20241226---for:【QQYUN-7495】pattern由字符串改成正则传递给antd(因使用InputNumber时发现正则无效)
|
||||
rules.forEach((item) => {
|
||||
if (typeof item.pattern === 'string') {
|
||||
try {
|
||||
const reg = new Function('item', `return ${item.pattern}`)(item);
|
||||
if (Object.prototype.toString.call(reg) === '[object RegExp]') {
|
||||
item.pattern = reg;
|
||||
} else {
|
||||
item.pattern = new RegExp(item.pattern);
|
||||
}
|
||||
} catch (error) {
|
||||
item.pattern = new RegExp(item.pattern);
|
||||
}
|
||||
}
|
||||
});
|
||||
// update-end--author:liaozhiyang---date:20231226---for:【QQYUN-7495】pattern由字符串改成正则传递给antd(因使用InputNumber时发现正则无效)
|
||||
// update-begin--author:liaozhiyang---date:20240530---for:【TV360X-434】validator校验执行两次
|
||||
hijackValidator(rules);
|
||||
// update-end--author:liaozhiyang---date:20240530---for:【TV360X-434】validator校验执行两次
|
||||
return rules;
|
||||
}
|
||||
|
||||
function renderComponent() {
|
||||
const { renderComponentContent, component, field, changeEvent = 'change', valueField, componentProps, dynamicRules } = props.schema;
|
||||
|
||||
const isCheck = component && ['Switch', 'Checkbox'].includes(component);
|
||||
// update-begin--author:liaozhiyang---date:20231013---for:【QQYUN-6679】input去空格
|
||||
let isTrim = false;
|
||||
if (component === 'Input' && componentProps && componentProps.trim) {
|
||||
isTrim = true;
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20231013---for:【QQYUN-6679】input去空格
|
||||
const eventKey = `on${upperFirst(changeEvent)}`;
|
||||
// update-begin--author:liaozhiyang---date:20230922---for:【issues/752】表单校验dynamicRules 无法 使用失去焦点后校验 trigger: 'blur'
|
||||
const on = {
|
||||
[eventKey]: (...args: Nullable<Recordable>[]) => {
|
||||
const [e] = args;
|
||||
if (propsData[eventKey]) {
|
||||
propsData[eventKey](...args);
|
||||
}
|
||||
const target = e ? e.target : null;
|
||||
// update-begin--author:liaozhiyang---date:20231013---for:【QQYUN-6679】input去空格
|
||||
let value;
|
||||
if (target) {
|
||||
if (isCheck) {
|
||||
value = target.checked;
|
||||
} else {
|
||||
value = isTrim ? target.value.trim() : target.value;
|
||||
}
|
||||
} else {
|
||||
value = e;
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20231013---for:【QQYUN-6679】input去空格
|
||||
props.setFormModel(field, value);
|
||||
// update-begin--author:liaozhiyang---date:20240522---for:【TV360X-341】有值之后必填校验不消失
|
||||
props.validateFields([field]).catch((_) => {});
|
||||
// update-end--author:liaozhiyang---date:20240522--for:【TV360X-341】有值之后必填校验不消失
|
||||
},
|
||||
// onBlur: () => {
|
||||
// props.validateFields([field], { triggerName: 'blur' }).catch((_) => {});
|
||||
// },
|
||||
};
|
||||
// update-end--author:liaozhiyang---date:20230922---for:【issues/752】表单校验dynamicRules 无法 使用失去焦点后校验 trigger: 'blur'
|
||||
const Comp = componentMap.get(component) as ReturnType<typeof defineComponent>;
|
||||
|
||||
const { autoSetPlaceHolder, size } = props.formProps;
|
||||
const propsData: Recordable = {
|
||||
allowClear: true,
|
||||
getPopupContainer: (trigger: Element) => {
|
||||
|
||||
return trigger?.parentNode;
|
||||
},
|
||||
size,
|
||||
...unref(getComponentsProps),
|
||||
disabled: unref(getDisable),
|
||||
};
|
||||
// update-begin--author:liaozhiyang---date:20240308---for:【QQYUN-8377】formSchema props支持动态修改
|
||||
const dynamicPropskey = props.schema.dynamicPropskey;
|
||||
if (dynamicPropskey) {
|
||||
propsData[dynamicPropskey] = unref(getDynamicPropsValue);
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240308---for:【QQYUN-8377】formSchema props支持动态修改
|
||||
|
||||
const isCreatePlaceholder = !propsData.disabled && autoSetPlaceHolder;
|
||||
// RangePicker place是一个数组
|
||||
if (isCreatePlaceholder && component !== 'RangePicker' && component) {
|
||||
//自动设置placeholder
|
||||
propsData.placeholder = unref(getComponentsProps)?.placeholder || createPlaceholderMessage(component) + props.schema.label;
|
||||
}
|
||||
propsData.codeField = field;
|
||||
propsData.formValues = unref(getValues);
|
||||
|
||||
const bindValue: Recordable = {
|
||||
[valueField || (isCheck ? 'checked' : 'value')]: props.formModel[field],
|
||||
};
|
||||
|
||||
const compAttr: Recordable = {
|
||||
...propsData,
|
||||
...on,
|
||||
...bindValue,
|
||||
};
|
||||
|
||||
if (!renderComponentContent) {
|
||||
return <Comp {...compAttr} />;
|
||||
}
|
||||
const compSlot = isFunction(renderComponentContent)
|
||||
? { ...renderComponentContent(unref(getValues)) }
|
||||
: {
|
||||
default: () => renderComponentContent,
|
||||
};
|
||||
return <Comp {...compAttr}>{compSlot}</Comp>;
|
||||
}
|
||||
|
||||
/**
|
||||
*渲染Label
|
||||
* @updateBy:zyf
|
||||
*/
|
||||
function renderLabelHelpMessage() {
|
||||
//update-begin-author:taoyan date:2022-9-7 for: VUEN-2061【样式】online表单超出4个 .. 省略显示
|
||||
//label宽度支持自定义
|
||||
const { label, helpMessage, helpComponentProps, subLabel, labelLength } = props.schema;
|
||||
let showLabel: string = label + '';
|
||||
// update-begin--author:liaozhiyang---date:20240517---for:【TV360X-98】label展示的文字必须和labelLength配置一致
|
||||
if (labelLength) {
|
||||
showLabel = showLabel.substr(0, labelLength);
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240517---for:【TV360X-98】label展示的文字必须和labelLength配置一致
|
||||
const titleObj = { title: label };
|
||||
const renderLabel = subLabel ? (
|
||||
<span>
|
||||
{label} <span class="text-secondary">{subLabel}</span>
|
||||
</span>
|
||||
) : labelLength ? (
|
||||
<label {...titleObj}>{showLabel}</label>
|
||||
) : (
|
||||
label
|
||||
);
|
||||
//update-end-author:taoyan date:2022-9-7 for: VUEN-2061【样式】online表单超出4个 .. 省略显示
|
||||
const getHelpMessage = isFunction(helpMessage) ? helpMessage(unref(getValues)) : helpMessage;
|
||||
if (!getHelpMessage || (Array.isArray(getHelpMessage) && getHelpMessage.length === 0)) {
|
||||
return renderLabel;
|
||||
}
|
||||
return (
|
||||
<span>
|
||||
{renderLabel}
|
||||
<BasicHelp placement="top" class="mx-1" text={getHelpMessage} {...helpComponentProps} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function renderItem() {
|
||||
const { itemProps, slot, render, field, suffix, component } = props.schema;
|
||||
const { labelCol, wrapperCol } = unref(itemLabelWidthProp);
|
||||
const { colon } = props.formProps;
|
||||
|
||||
if (component === 'Divider') {
|
||||
return (
|
||||
<Col span={24}>
|
||||
<Divider {...unref(getComponentsProps)}>{renderLabelHelpMessage()}</Divider>
|
||||
</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;
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
name={field}
|
||||
colon={colon}
|
||||
class={{ 'suffix-item': showSuffix }}
|
||||
{...(itemProps as Recordable)}
|
||||
label={renderLabelHelpMessage()}
|
||||
rules={handleRules()}
|
||||
// update-begin--author:liaozhiyang---date:20240514---for:【issues/1244】标识了必填,但是必填标识没显示
|
||||
validateFirst = { true }
|
||||
// update-end--author:liaozhiyang---date:20240514---for:【issues/1244】标识了必填,但是必填标识没显示
|
||||
labelCol={labelCol}
|
||||
wrapperCol={wrapperCol}
|
||||
>
|
||||
<div style="display:flex">
|
||||
{/* author: sunjianlei for: 【VUEN-744】此处加上 width: 100%; 因为要防止组件宽度超出 FormItem */}
|
||||
{/* update-begin--author:liaozhiyang---date:20240510---for:【TV360X-719】表单校验不通过项滚动到可视区内 */}
|
||||
<Middleware>{getContent()}</Middleware>
|
||||
{/* update-end--author:liaozhiyang---date:20240510---for:【TV360X-719】表单校验不通过项滚动到可视区内 */}
|
||||
{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;
|
||||
// update-begin--author:liaozhiyang---date:20230803---for:【issues-641】调整表格搜索表单的span配置无效
|
||||
const { getIsMobile } = useAppInject();
|
||||
let realColProps;
|
||||
realColProps = { ...baseColProps, ...colProps };
|
||||
if (colProps['span'] && !unref(getIsMobile)) {
|
||||
['xs', 'sm', 'md', 'lg', 'xl', 'xxl'].forEach((name) => delete realColProps[name]);
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20230803---for:【issues-641】调整表格搜索表单的span配置无效
|
||||
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>
|
||||
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div :id="formItemId" style="flex: 1; width: 100%">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Form } from 'ant-design-vue';
|
||||
import { computed } from 'vue';
|
||||
const formItemContext = Form.useInjectFormItemContext();
|
||||
const formItemId = computed(() => {
|
||||
return formItemContext.id.value;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@ -0,0 +1,57 @@
|
||||
<!--
|
||||
* @Description:It is troublesome to implement radio button group in the form. So it is extracted independently as a separate component
|
||||
-->
|
||||
<template>
|
||||
<RadioGroup v-bind="attrs" v-model:value="state" button-style="solid">
|
||||
<template v-for="item in getOptions" :key="`${item.value}`">
|
||||
<RadioButton :value="item.value" :disabled="item.disabled">
|
||||
{{ item.label }}
|
||||
</RadioButton>
|
||||
</template>
|
||||
</RadioGroup>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, computed } from 'vue';
|
||||
import { Radio } from 'ant-design-vue';
|
||||
import { isString } from '/@/utils/is';
|
||||
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
|
||||
type OptionsItem = { label: string; value: string | number | boolean; disabled?: boolean };
|
||||
type RadioItem = string | OptionsItem;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'RadioButtonGroup',
|
||||
components: {
|
||||
RadioGroup: Radio.Group,
|
||||
RadioButton: Radio.Button,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number, Boolean] as PropType<string | number | boolean>,
|
||||
},
|
||||
options: {
|
||||
type: Array as PropType<RadioItem[]>,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const attrs = useAttrs();
|
||||
// Embedded in the form, just use the hook binding to perform form verification
|
||||
const [state] = useRuleFormItem(props);
|
||||
|
||||
// Processing options value
|
||||
const getOptions = computed((): OptionsItem[] => {
|
||||
const { options } = props;
|
||||
if (!options || options?.length === 0) return [];
|
||||
|
||||
const isStringArr = options.some((item) => isString(item));
|
||||
if (!isStringArr) return options as OptionsItem[];
|
||||
|
||||
return options.map((item) => ({ label: item, value: item })) as OptionsItem[];
|
||||
});
|
||||
|
||||
return { state, getOptions, attrs };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<div :class="formDisabled ? 'jeecg-form-container-disabled jeecg-form-detail-effect' : ''">
|
||||
<fieldset :disabled="formDisabled">
|
||||
<slot name="detail"></slot>
|
||||
</fieldset>
|
||||
<slot name="edit"> </slot>
|
||||
<fieldset disabled>
|
||||
<slot></slot>
|
||||
</fieldset>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, watch } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JForm',
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const formDisabled = ref<boolean>(props.disabled);
|
||||
|
||||
watch(
|
||||
() => props.disabled,
|
||||
(value) => {
|
||||
formDisabled.value = value;
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
formDisabled,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.jeecg-form-container-disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.jeecg-form-container-disabled fieldset[disabled] {
|
||||
-ms-pointer-events: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
.jeecg-form-container-disabled :deep(.ant-select) {
|
||||
-ms-pointer-events: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20240605---for:【TV360X-857】online代码生成详情样式调整
|
||||
// begin antdv 禁用样式
|
||||
// .jeecg-form-container-disabled :deep(.ant-input-number),
|
||||
// .jeecg-form-container-disabled :deep(.ant-input),
|
||||
// .jeecg-form-container-disabled :deep(.ant-input-password),
|
||||
// .jeecg-form-container-disabled :deep(.ant-select-single .ant-select-selector),
|
||||
// .jeecg-form-container-disabled :deep(.ant-radio-wrapper .ant-radio-checked .ant-radio-inner),
|
||||
// .jeecg-form-container-disabled :deep(.ant-switch),
|
||||
// .jeecg-form-container-disabled :deep(.ant-picker),
|
||||
// .jeecg-form-container-disabled :deep(.ant-select:not(.ant-select-customize-input) .ant-select-selector),
|
||||
// .jeecg-form-container-disabled :deep(.ant-input-affix-wrapper),
|
||||
// .jeecg-form-container-disabled :deep(.tox .tox-toolbar__group),
|
||||
// .jeecg-form-container-disabled :deep(.tox .tox-edit-area__iframe),
|
||||
// .jeecg-form-container-disabled :deep(.vditor-toolbar),
|
||||
// .jeecg-form-container-disabled :deep(.vditor-preview),
|
||||
// .jeecg-form-container-disabled :deep(.jeecg-tinymce-img-upload) {
|
||||
// background: rgba(51, 51, 51, 0.04);
|
||||
// }
|
||||
|
||||
// .jeecg-form-container-disabled :deep(.ant-radio-wrapper),
|
||||
// .jeecg-form-container-disabled :deep(.ant-checkbox-wrapper),
|
||||
// .jeecg-form-container-disabled :deep(.ant-btn) {
|
||||
// color: rgba(0, 0, 0, 0.65);
|
||||
// }
|
||||
// .jeecg-form-container-disabled :deep(.ant-radio-wrapper .ant-radio-inner:after),
|
||||
// .jeecg-form-container-disabled :deep(.ant-checkbox-checked .ant-checkbox-inner) {
|
||||
// background-color: rgba(51, 51, 51, 0.25);
|
||||
// }
|
||||
// .jeecg-form-container-disabled :deep(.ant-radio-inner),
|
||||
// .jeecg-form-container-disabled :deep(.ant-checkbox-inner) {
|
||||
// border-color: rgba(51, 51, 51, 0.25) !important;
|
||||
// }
|
||||
// .jeecg-form-container-disabled :deep(.ant-input-password > .ant-input),
|
||||
// .jeecg-form-container-disabled :deep(.ant-input-affix-wrapper .ant-input) {
|
||||
// background: none;
|
||||
// }
|
||||
html[data-theme='light'] {
|
||||
.jeecg-form-detail-effect {
|
||||
:deep(.ant-select-selector),
|
||||
:deep(.ant-btn),
|
||||
:deep(.ant-input),
|
||||
:deep(.ant-input-affix-wrapper),
|
||||
:deep(.ant-picker),
|
||||
:deep(.ant-input-number) {
|
||||
color: #606266 !important;
|
||||
}
|
||||
:deep(.ant-select) {
|
||||
color: #606266 !important;
|
||||
}
|
||||
:deep(.ant-select-selection-item-content),:deep(.ant-select-selection-item),:deep(input) {
|
||||
color: #606266 !important;
|
||||
}
|
||||
|
||||
:deep(.ant-radio-wrapper),
|
||||
:deep(.ant-checkbox-wrapper),
|
||||
:deep(.ant-btn) {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
:deep(.ant-radio-wrapper .ant-radio-inner:after),
|
||||
:deep(.ant-checkbox-checked .ant-checkbox-inner) {
|
||||
color: #606266 !important;
|
||||
}
|
||||
:deep(.ant-radio-inner),
|
||||
:deep(.ant-checkbox-inner) {
|
||||
border-color: rgba(51, 51, 51, 0.25) !important;
|
||||
background-color: rgba(51, 51, 51, 0.04) !important;
|
||||
}
|
||||
:deep(.ant-checkbox-checked .ant-checkbox-inner::after), :deep(.ant-tree-checkbox-checked .ant-tree-checkbox-inner::after){
|
||||
border-color: rgba(51, 51, 51, 0.25) !important;
|
||||
}
|
||||
:deep(.ant-switch) {
|
||||
background-color: rgba(51, 51, 51, 0.25);
|
||||
}
|
||||
:deep(.tox .tox-toolbar__group),
|
||||
:deep(.tox .tox-edit-area__iframe),
|
||||
:deep(.vditor-toolbar),
|
||||
:deep(.vditor-preview),
|
||||
:deep(.jeecg-tinymce-img-upload) {
|
||||
background: rgba(51, 51, 51, 0.04);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme='dark'] {
|
||||
.jeecg-form-detail-effect {
|
||||
:deep(.ant-select-selector),
|
||||
:deep(.ant-btn),
|
||||
:deep(.ant-input),
|
||||
:deep(.ant-input-affix-wrapper),
|
||||
:deep(.ant-picker),
|
||||
:deep(.ant-input-number) {
|
||||
color: rgba(255, 255, 255, 0.25) !important;
|
||||
//background-color: rgba(255, 255, 255, 0.08) !important;
|
||||
}
|
||||
:deep(.ant-select) {
|
||||
color: rgba(255, 255, 255, 0.25) !important;
|
||||
}
|
||||
:deep(.ant-select-selection-item-content),:deep(.ant-select-selection-item),:deep(input) {
|
||||
color: rgba(255, 255, 255, 0.25) !important;
|
||||
}
|
||||
|
||||
:deep(.ant-radio-wrapper),
|
||||
:deep(.ant-checkbox-wrapper){
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
:deep(.ant-radio-wrapper .ant-radio-inner:after),
|
||||
:deep(.ant-checkbox-checked .ant-checkbox-inner) {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
:deep(.ant-radio-inner),
|
||||
:deep(.ant-checkbox-inner) {
|
||||
border-color: #424242 !important;
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
:deep(.ant-switch) {
|
||||
background-color: rgba(51, 51, 51, 0.25);
|
||||
opacity: 0.65;
|
||||
}
|
||||
:deep(.tox .tox-toolbar__group),
|
||||
:deep(.tox .tox-edit-area__iframe),
|
||||
:deep(.vditor-toolbar),
|
||||
:deep(.vditor-preview),
|
||||
:deep(.jeecg-tinymce-img-upload) {
|
||||
background: rgba(51, 51, 51, 0.04);
|
||||
}
|
||||
}
|
||||
}
|
||||
// end antdv 禁用样式
|
||||
// update-begin--author:liaozhiyang---date:20240605---for:【TV360X-857】online代码生成详情样式调整
|
||||
.jeecg-form-container-disabled :deep(.ant-upload-select) {
|
||||
cursor: grabbing;
|
||||
}
|
||||
.jeecg-form-container-disabled :deep(.ant-upload-list) {
|
||||
cursor: grabbing;
|
||||
}
|
||||
.jeecg-form-container-disabled fieldset[disabled] :deep(.ant-upload-list){
|
||||
// -ms-pointer-events: auto !important;
|
||||
// pointer-events: auto !important;
|
||||
}
|
||||
.jeecg-form-container-disabled fieldset[disabled] iframe {
|
||||
-ms-pointer-events: auto !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
.jeecg-form-container-disabled :deep(.ant-upload-list-item-actions .anticon-delete),
|
||||
.jeecg-form-container-disabled :deep(.ant-upload-list-item .anticon-close) {
|
||||
display: none;
|
||||
}
|
||||
.jeecg-form-container-disabled :deep(.vditor-sv) {
|
||||
display: none !important;
|
||||
background: rgba(51, 51, 51, 0.04);
|
||||
}
|
||||
</style>
|
||||
88
jeecgboot-vue3/src/components/Form/src/helper.ts
Normal file
88
jeecgboot-vue3/src/components/Form/src/helper.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import type { ValidationRule } from 'ant-design-vue/lib/form/Form';
|
||||
import type { ComponentType } from './types/index';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { dateUtil } 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', 'TimePicker'];
|
||||
|
||||
function genType() {
|
||||
return [...DATE_TYPE, 'RangePicker'];
|
||||
}
|
||||
|
||||
export function setComponentRuleType(rule: ValidationRule, component: ComponentType, valueFormat: string) {
|
||||
//update-begin---author:wangshuai---date:2024-02-01---for:【QQYUN-8176】编辑表单中,校验必填时,如果组件是ApiSelect,打开编辑页面时,即使该字段有值,也会提示请选择---
|
||||
//https://github.com/vbenjs/vue-vben-admin/pull/3082 github修复原文
|
||||
if (Reflect.has(rule, 'type')) {
|
||||
return;
|
||||
}
|
||||
//update-end---author:wangshuai---date:2024-02-01---for:【QQYUN-8176】编辑表单中,校验必填时,如果组件是ApiSelect,打开编辑页面时,即使该字段有值,也会提示请选择---
|
||||
if (['DatePicker', 'MonthPicker', 'WeekPicker', 'TimePicker'].includes(component)) {
|
||||
rule.type = valueFormat ? 'string' : 'object';
|
||||
} else if (['RangePicker', 'Upload', 'CheckboxGroup', 'TimePicker'].includes(component)) {
|
||||
rule.type = 'array';
|
||||
} else if (['InputNumber'].includes(component)) {
|
||||
rule.type = 'number';
|
||||
}
|
||||
}
|
||||
|
||||
export function processDateValue(attr: Recordable, component: string) {
|
||||
const { valueFormat, value } = attr;
|
||||
if (valueFormat) {
|
||||
attr.value = isObject(value) ? dateUtil(value).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', 'InputTextArea'].includes(component)) {
|
||||
return val && isNumber(val) ? `${val}` : val;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
/**
|
||||
*liaozhiyang
|
||||
*2023-12-26
|
||||
*某些组件的传值需要把字符串类型转成数值类型
|
||||
*/
|
||||
export function handleInputStringValue(component?: ComponentType, val?: any) {
|
||||
if (!component) return val;
|
||||
// update-begin--author:liaozhiyang---date:20240517---for:【TV360X-13】InputNumber设置精确3位小数传入''变成了0.00
|
||||
if (['InputNumber'].includes(component) && typeof val === 'string' && val != '') {
|
||||
return Number(val);
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240517---for:【TV360X-13】InputNumber设置精确3位小数传入''变成了0.00
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间字段
|
||||
*/
|
||||
export const dateItemType = genType();
|
||||
164
jeecgboot-vue3/src/components/Form/src/hooks/useAdvanced.ts
Normal file
164
jeecgboot-vue3/src/components/Form/src/hooks/useAdvanced.ts
Normal file
@ -0,0 +1,164 @@
|
||||
import type { ColEx } from '../types';
|
||||
import type { AdvanceState } from '../types/hooks';
|
||||
import type { ComputedRef, Ref } 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 { 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, 30);
|
||||
|
||||
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, index = 0) {
|
||||
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;
|
||||
}
|
||||
|
||||
let autoAdvancedCol = unref(getProps).autoAdvancedCol ?? 3;
|
||||
|
||||
if (isLastAction) {
|
||||
advanceState.hideAdvanceBtn = unref(getSchema).length <= autoAdvancedCol;
|
||||
// update-begin--author:sunjianlei---date:20211108---for: 注释掉该逻辑,使小于等于2行时,也显示展开收起按钮
|
||||
/* if (itemColSum <= BASIC_COL_LEN * 2) {
|
||||
// 小于等于2行时,不显示折叠和展开按钮
|
||||
advanceState.hideAdvanceBtn = true;
|
||||
advanceState.isAdvanced = true;
|
||||
} else */
|
||||
// update-end--author:sunjianlei---date:20211108---for: 注释掉该逻辑,使小于等于2行时,也显示展开收起按钮
|
||||
if (itemColSum > BASIC_COL_LEN * 2 && itemColSum <= BASIC_COL_LEN * (unref(getProps).autoAdvancedLine || 3)) {
|
||||
advanceState.hideAdvanceBtn = false;
|
||||
|
||||
// 默认超过 3 行折叠
|
||||
} else if (!advanceState.isLoad) {
|
||||
advanceState.isLoad = true;
|
||||
advanceState.isAdvanced = !advanceState.isAdvanced;
|
||||
// update-begin--author:sunjianlei---date:20211108---for: 如果总列数大于 autoAdvancedCol,就默认折叠
|
||||
if (unref(getSchema).length > autoAdvancedCol) {
|
||||
advanceState.hideAdvanceBtn = false;
|
||||
advanceState.isAdvanced = false;
|
||||
}
|
||||
// update-end--author:sunjianlei---date:20211108---for: 如果总列数大于 autoAdvancedCol,就默认折叠
|
||||
}
|
||||
return { isAdvanced: advanceState.isAdvanced, itemColSum };
|
||||
}
|
||||
if (itemColSum > BASIC_COL_LEN * (unref(getProps).alwaysShowLines || 1)) {
|
||||
return { isAdvanced: advanceState.isAdvanced, itemColSum };
|
||||
} else if (!advanceState.isAdvanced && index + 1 > autoAdvancedCol) {
|
||||
// 如果当前是收起状态,并且当前列下标 > autoAdvancedCol,就隐藏
|
||||
return { isAdvanced: false, itemColSum };
|
||||
} else {
|
||||
// The first line is always displayed
|
||||
return { isAdvanced: true, itemColSum };
|
||||
}
|
||||
}
|
||||
|
||||
function updateAdvanced() {
|
||||
let itemColSum = 0;
|
||||
let realItemColSum = 0;
|
||||
const { baseColProps = {} } = unref(getProps);
|
||||
|
||||
const schemas = unref(getSchema);
|
||||
for (let i = 0; i < schemas.length; i++) {
|
||||
const schema = schemas[i];
|
||||
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, false, i);
|
||||
|
||||
itemColSum = sum || 0;
|
||||
if (isAdvanced) {
|
||||
realItemColSum = itemColSum;
|
||||
}
|
||||
schema.isAdvanced = isAdvanced;
|
||||
}
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
35
jeecgboot-vue3/src/components/Form/src/hooks/useAutoFocus.ts
Normal file
35
jeecgboot-vue3/src/components/Form/src/hooks/useAutoFocus.ts
Normal file
@ -0,0 +1,35 @@
|
||||
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();
|
||||
});
|
||||
}
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
159
jeecgboot-vue3/src/components/Form/src/hooks/useForm.ts
Normal file
159
jeecgboot-vue3/src/components/Form/src/hooks/useForm.ts
Normal file
@ -0,0 +1,159 @@
|
||||
import type { FormProps, FormActionType, UseFormReturnType, FormSchema } from '../types/form';
|
||||
import type { NamePath, ValidateOptions } from 'ant-design-vue/lib/form/interface';
|
||||
import type { DynamicProps } from '/#/utils';
|
||||
import { handleRangeValue } from '../utils/formUtils';
|
||||
import { ref, onUnmounted, unref, nextTick, watch } from 'vue';
|
||||
import { isProdMode } from '/@/utils/env';
|
||||
import { error } from '/@/utils/log';
|
||||
import { getDynamicProps, getValueType } from '/@/utils';
|
||||
import { add } from "/@/components/Form/src/componentMap";
|
||||
//集成online专用控件
|
||||
import { OnlineSelectCascade, LinkTableCard, LinkTableSelect } from '@jeecg/online';
|
||||
|
||||
export declare type ValidateFields = (nameList?: NamePath[], options?: ValidateOptions) => 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);
|
||||
|
||||
//集成online专用控件
|
||||
add("OnlineSelectCascade", OnlineSelectCascade)
|
||||
add("LinkTableCard", LinkTableCard)
|
||||
add("LinkTableSelect", LinkTableSelect)
|
||||
|
||||
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();
|
||||
});
|
||||
},
|
||||
|
||||
removeSchemaByFiled: async (field: string | string[]) => {
|
||||
unref(formRef)?.removeSchemaByFiled(field);
|
||||
},
|
||||
|
||||
// TODO promisify
|
||||
getFieldsValue: <T>() => {
|
||||
//update-begin-author:taoyan date:2022-7-5 for: VUEN-1341【流程】编码方式 流程节点编辑表单时,填写数据报错 包括用户组件、部门组件、省市区
|
||||
let values = unref(formRef)?.getFieldsValue() as T;
|
||||
if(values){
|
||||
Object.keys(values).map(key=>{
|
||||
if (values[key] instanceof Array) {
|
||||
// update-begin-author:sunjianlei date:20221205 for: 【issues/4330】判断如果是对象数组,则不拼接
|
||||
let isObject = typeof (values[key][0] || '') === 'object';
|
||||
if (!isObject) {
|
||||
values[key] = values[key].join(',');
|
||||
}
|
||||
// update-end-author:sunjianlei date:20221205 for: 【issues/4330】判断如果是对象数组,则不拼接
|
||||
}
|
||||
});
|
||||
}
|
||||
return values;
|
||||
//update-end-author:taoyan date:2022-7-5 for: VUEN-1341【流程】编码方式 流程节点编辑表单时,填写数据报错 包括用户组件、部门组件、省市区
|
||||
},
|
||||
|
||||
setFieldsValue: async <T>(values: T) => {
|
||||
const form = await getForm();
|
||||
form.setFieldsValue<T>(values);
|
||||
},
|
||||
|
||||
appendSchemaByField: async (schema: 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();
|
||||
},
|
||||
|
||||
/**
|
||||
* 表单验证并返回表单值
|
||||
* @update:添加表单值转换逻辑
|
||||
* @updateBy:zyf
|
||||
* @updateDate:2021-09-02
|
||||
*/
|
||||
validate: async (nameList?: NamePath[]): Promise<Recordable> => {
|
||||
const form = await getForm();
|
||||
let getProps = props || form.getProps;
|
||||
let values = form.validate(nameList).then((values) => {
|
||||
for (let key in values) {
|
||||
if (values[key] instanceof Array) {
|
||||
let valueType = getValueType(getProps, key);
|
||||
if (valueType === 'string') {
|
||||
values[key] = values[key].join(',');
|
||||
}
|
||||
}
|
||||
}
|
||||
//--@updateBy-begin----author:liusq---date:20210916------for:处理区域事件字典信息------
|
||||
return handleRangeValue(getProps, values);
|
||||
//--@updateBy-end----author:liusq---date:20210916------for:处理区域事件字典信息------
|
||||
});
|
||||
return values;
|
||||
},
|
||||
validateFields: async (nameList?: NamePath[], options?: ValidateOptions): Promise<Recordable> => {
|
||||
const form = await getForm();
|
||||
return form.validateFields(nameList, options);
|
||||
},
|
||||
};
|
||||
|
||||
return [register, methods];
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
279
jeecgboot-vue3/src/components/Form/src/hooks/useFormEvents.ts
Normal file
279
jeecgboot-vue3/src/components/Form/src/hooks/useFormEvents.ts
Normal file
@ -0,0 +1,279 @@
|
||||
import type { ComputedRef, Ref } from 'vue';
|
||||
import type { FormProps, FormSchema, FormActionType } from '../types/form';
|
||||
import type { NamePath, ValidateOptions } from 'ant-design-vue/lib/form/interface';
|
||||
import { unref, toRaw } from 'vue';
|
||||
import { isArray, isFunction, isObject, isString } from '/@/utils/is';
|
||||
import { deepMerge, getValueType } from '/@/utils';
|
||||
import { dateItemType, handleInputNumberValue, handleInputStringValue } from '../helper';
|
||||
import { dateUtil } from '/@/utils/dateUtil';
|
||||
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>;
|
||||
formElRef: Ref<FormActionType>;
|
||||
schemaRef: Ref<FormSchema[]>;
|
||||
handleFormValues: Fn;
|
||||
}
|
||||
export function useFormEvents({
|
||||
emit,
|
||||
getProps,
|
||||
formModel,
|
||||
getSchema,
|
||||
defaultValueRef,
|
||||
formElRef,
|
||||
schemaRef,
|
||||
handleFormValues,
|
||||
}: UseFormActionContext) {
|
||||
async function resetFields(): Promise<void> {
|
||||
const { resetFunc, submitOnReset } = unref(getProps);
|
||||
resetFunc && isFunction(resetFunc) && (await resetFunc());
|
||||
|
||||
const formEl = unref(formElRef);
|
||||
if (!formEl) return;
|
||||
|
||||
Object.keys(formModel).forEach((key) => {
|
||||
formModel[key] = defaultValueRef.value[key];
|
||||
});
|
||||
clearValidate();
|
||||
emit('reset', toRaw(formModel));
|
||||
submitOnReset && handleSubmit();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Set form value
|
||||
*/
|
||||
async function setFieldsValue(values: Recordable): Promise<void> {
|
||||
const fields = unref(getSchema)
|
||||
.map((item) => item.field)
|
||||
.filter(Boolean);
|
||||
|
||||
const validKeys: string[] = [];
|
||||
Object.keys(values).forEach((key) => {
|
||||
const schema = unref(getSchema).find((item) => item.field === key);
|
||||
let value = values[key];
|
||||
|
||||
//antd3升级后,online表单时间控件选中值报js错 TypeError: Reflect.has called on non-object
|
||||
if(!(values instanceof Object)){
|
||||
return;
|
||||
}
|
||||
|
||||
const hasKey = Reflect.has(values, key);
|
||||
|
||||
value = handleInputNumberValue(schema?.component, value);
|
||||
// update-begin--author:liaozhiyang---date:20231226---for:【QQYUN-7535】popup回填字段inputNumber组件验证错误
|
||||
value = handleInputStringValue(schema?.component, value);
|
||||
// update-end--author:liaozhiyang---date:20231226---for:【QQYUN-7535】popup回填字段inputNumber组件验证错误
|
||||
// 0| '' is allow
|
||||
if (hasKey && fields.includes(key)) {
|
||||
// time type
|
||||
if (itemIsDateType(key)) {
|
||||
if (Array.isArray(value)) {
|
||||
const arr: any[] = [];
|
||||
for (const ele of value) {
|
||||
arr.push(ele ? dateUtil(ele) : null);
|
||||
}
|
||||
formModel[key] = arr;
|
||||
} else {
|
||||
const { componentProps } = schema || {};
|
||||
let _props = componentProps as any;
|
||||
if (typeof componentProps === 'function') {
|
||||
_props = _props({ formModel });
|
||||
}
|
||||
formModel[key] = value ? (_props?.valueFormat ? value : dateUtil(value)) : null;
|
||||
}
|
||||
} else {
|
||||
formModel[key] = value;
|
||||
}
|
||||
validKeys.push(key);
|
||||
}
|
||||
});
|
||||
validateFields(validKeys).catch((_) => {});
|
||||
}
|
||||
/**
|
||||
* @description: Delete based on field name
|
||||
*/
|
||||
async function removeSchemaByFiled(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) {
|
||||
_removeSchemaByFiled(field, schemaList);
|
||||
}
|
||||
schemaRef.value = schemaList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Delete based on field name
|
||||
*/
|
||||
function _removeSchemaByFiled(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, prefixField?: string, first = false) {
|
||||
const schemaList: FormSchema[] = cloneDeep(unref(getSchema));
|
||||
|
||||
const index = schemaList.findIndex((schema) => schema.field === prefixField);
|
||||
const hasInList = schemaList.some((item) => item.field === prefixField || schema.field);
|
||||
|
||||
if (!hasInList) return;
|
||||
|
||||
if (!prefixField || index === -1 || first) {
|
||||
first ? schemaList.unshift(schema) : schemaList.push(schema);
|
||||
schemaRef.value = schemaList;
|
||||
return;
|
||||
}
|
||||
if (index !== -1) {
|
||||
schemaList.splice(index + 1, 0, 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) => item.component === 'Divider' || (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) => item.component === 'Divider' || (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[] = [];
|
||||
updateData.forEach((item) => {
|
||||
unref(getSchema).forEach((val) => {
|
||||
if (val.field === item.field) {
|
||||
const newSchema = deepMerge(val, item);
|
||||
schema.push(newSchema as FormSchema);
|
||||
} else {
|
||||
schema.push(val);
|
||||
}
|
||||
});
|
||||
});
|
||||
schemaRef.value = uniqBy(schema, 'field');
|
||||
}
|
||||
|
||||
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, options?: ValidateOptions) {
|
||||
return unref(formElRef)?.validateFields(nameList, options);
|
||||
}
|
||||
|
||||
async function validate(nameList?: NamePath[] | undefined) {
|
||||
return await unref(formElRef)?.validate(nameList);
|
||||
}
|
||||
|
||||
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();
|
||||
//update-begin---author:zhangdaihao Date:20140212 for:[bug号]树机构调整------------
|
||||
//--updateBy-begin----author:zyf---date:20211206------for:对查询表单提交的数组处理成字符串------
|
||||
for (let key in values) {
|
||||
if (values[key] instanceof Array) {
|
||||
let valueType = getValueType(getProps, key);
|
||||
if (valueType === 'string') {
|
||||
values[key] = values[key].join(',');
|
||||
}
|
||||
}
|
||||
}
|
||||
//--updateBy-end----author:zyf---date:20211206------for:对查询表单提交的数组处理成字符串------
|
||||
const res = handleFormValues(values);
|
||||
emit('submit', res);
|
||||
} catch (error) {
|
||||
//update-begin-author:taoyan date:2022-11-4 for: 列表查询表单会触发校验错误导致重置失败,原因不明
|
||||
emit('submit', {});
|
||||
console.error('query form validate error, please ignore!', error)
|
||||
//throw new Error(error);
|
||||
//update-end-author:taoyan date:2022-11-4 for: 列表查询表单会触发校验错误导致重置失败,原因不明
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handleSubmit,
|
||||
clearValidate,
|
||||
validate,
|
||||
validateFields,
|
||||
getFieldsValue,
|
||||
updateSchema,
|
||||
resetSchema,
|
||||
appendSchemaByField,
|
||||
removeSchemaByFiled,
|
||||
resetFields,
|
||||
setFieldsValue,
|
||||
scrollToField,
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
import { isArray, isFunction, isObject, isString, isNullOrUnDef } from '/@/utils/is';
|
||||
import { unref } from 'vue';
|
||||
import type { Ref, ComputedRef } from 'vue';
|
||||
import type { FormProps, FormSchema } from '../types/form';
|
||||
import dayjs from "dayjs";
|
||||
import { set } from 'lodash-es';
|
||||
import { handleRangeValue } from '/@/components/Form/src/utils/formUtils';
|
||||
|
||||
interface UseFormValuesContext {
|
||||
defaultValueRef: Ref<any>;
|
||||
getSchema: ComputedRef<FormSchema[]>;
|
||||
getProps: ComputedRef<FormProps>;
|
||||
formModel: Recordable;
|
||||
}
|
||||
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);
|
||||
}
|
||||
// 判断是否是dayjs实例
|
||||
if (isArray(value) && dayjs.isDayjs(value[0]) && dayjs.isDayjs(value[1])) {
|
||||
value = value.map((item) => transformDateFunc?.(item));
|
||||
}
|
||||
// Remove spaces
|
||||
if (isString(value)) {
|
||||
value = value.trim();
|
||||
}
|
||||
set(res, key, value);
|
||||
}
|
||||
return handleRangeValue(getProps, res);
|
||||
}
|
||||
|
||||
function initDefault() {
|
||||
const schemas = unref(getSchema);
|
||||
const obj: Recordable = {};
|
||||
schemas.forEach((item) => {
|
||||
const { defaultValue } = item;
|
||||
if (!isNullOrUnDef(defaultValue)) {
|
||||
obj[item.field] = defaultValue;
|
||||
formModel[item.field] = defaultValue;
|
||||
}
|
||||
});
|
||||
defaultValueRef.value = obj;
|
||||
}
|
||||
|
||||
return { handleFormValues, initDefault };
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
import type { Ref } from 'vue';
|
||||
import type { FormProps, FormSchema } from '../types/form';
|
||||
|
||||
import { computed, unref } from 'vue';
|
||||
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);
|
||||
|
||||
// update-begin--author:sunjianlei---date:20211104---for: 禁用全局 labelWidth,不自动设置 textAlign --------
|
||||
if (disabledLabelWidth) {
|
||||
return { labelCol, wrapperCol };
|
||||
}
|
||||
// update-begin--author:sunjianlei---date:20211104---for: 禁用全局 labelWidth,不自动设置 textAlign --------
|
||||
|
||||
// If labelWidth is set globally, all items setting
|
||||
if (!globalLabelWidth && !labelWidth && !globalLabelCol) {
|
||||
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: width ? width : '100%' }, ...col },
|
||||
wrapperCol: {
|
||||
style: { width: layout === 'vertical' ? '100%' : `calc(100% - ${width})` },
|
||||
...wrapCol,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div v-for="(param, index) in dynamicInput.params" :key="index" style="display: flex">
|
||||
<a-input placeholder="请输入参数key" v-model:value="param.label" style="width: 30%; margin-bottom: 5px" @input="emitChange" />
|
||||
<a-input placeholder="请输入参数value" v-model:value="param.value" style="width: 30%; margin: 0 0 5px 5px" @input="emitChange" />
|
||||
<MinusCircleOutlined
|
||||
v-if="dynamicInput.params.length > min"
|
||||
class="dynamic-delete-button"
|
||||
@click="remove(param)"
|
||||
style="width: 50px"
|
||||
></MinusCircleOutlined>
|
||||
</div>
|
||||
<div>
|
||||
<a-button type="dashed" style="width: 60%" @click="add">
|
||||
<PlusOutlined />
|
||||
新增
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons-vue';
|
||||
import { defineComponent, reactive, ref, UnwrapRef, watchEffect } from 'vue';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { isEmpty } from '/@/utils/is';
|
||||
import { tryOnMounted, tryOnUnmounted } from '@vueuse/core';
|
||||
interface Params {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JAddInput',
|
||||
props: {
|
||||
value: propTypes.string.def(''),
|
||||
//update-begin---author:wangshuai ---date:20220516 for:[VUEN-1043]系统编码规则,最后一个输入框不能删除------------
|
||||
//自定义删除按钮多少才会显示
|
||||
min: propTypes.integer.def(1),
|
||||
//update-end---author:wangshuai ---date:20220516 for:[VUEN-1043]系统编码规则,最后一个输入框不能删除--------------
|
||||
},
|
||||
emits: ['change', 'update:value'],
|
||||
setup(props, { emit }) {
|
||||
//input动态数据
|
||||
const dynamicInput: UnwrapRef<{ params: Params[] }> = reactive({ params: [] });
|
||||
//删除Input
|
||||
const remove = (item: Params) => {
|
||||
let index = dynamicInput.params.indexOf(item);
|
||||
if (index !== -1) {
|
||||
dynamicInput.params.splice(index, 1);
|
||||
}
|
||||
emitChange();
|
||||
};
|
||||
//新增Input
|
||||
const add = () => {
|
||||
dynamicInput.params.push({
|
||||
label: '',
|
||||
value: '',
|
||||
});
|
||||
emitChange();
|
||||
};
|
||||
|
||||
//监听传入数据value
|
||||
watchEffect(() => {
|
||||
initVal();
|
||||
});
|
||||
|
||||
/**
|
||||
* 初始化数值
|
||||
*/
|
||||
function initVal() {
|
||||
console.log('props.value', props.value);
|
||||
dynamicInput.params = [];
|
||||
if (props.value && props.value.indexOf('{') == 0) {
|
||||
let jsonObj = JSON.parse(props.value);
|
||||
Object.keys(jsonObj).forEach((key) => {
|
||||
dynamicInput.params.push({ label: key, value: jsonObj[key] });
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 数值改变
|
||||
*/
|
||||
function emitChange() {
|
||||
let obj = {};
|
||||
if (dynamicInput.params.length > 0) {
|
||||
dynamicInput.params.forEach((item) => {
|
||||
obj[item['label']] = item['value'];
|
||||
});
|
||||
}
|
||||
emit('change', isEmpty(obj) ? '' : JSON.stringify(obj));
|
||||
emit('update:value', isEmpty(obj) ? '' : JSON.stringify(obj));
|
||||
}
|
||||
|
||||
return {
|
||||
dynamicInput,
|
||||
emitChange,
|
||||
remove,
|
||||
add,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
MinusCircleOutlined,
|
||||
PlusOutlined,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.dynamic-delete-button {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
top: 4px;
|
||||
font-size: 24px;
|
||||
color: #999;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.dynamic-delete-button:hover {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.dynamic-delete-button[disabled] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<Cascader v-bind="attrs" :value="cascaderValue" :options="getOptions" @change="handleChange" />
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, ref, reactive, watchEffect, computed, unref, watch, onMounted } from 'vue';
|
||||
import { Cascader } from 'ant-design-vue';
|
||||
import { provinceAndCityData, regionData, provinceAndCityDataPlus, regionDataPlus } from '../../utils/areaDataUtil';
|
||||
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { isArray } from '/@/utils/is';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JAreaLinkage',
|
||||
components: {
|
||||
Cascader,
|
||||
},
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
value: propTypes.oneOfType([propTypes.object, propTypes.array, propTypes.string]),
|
||||
//是否显示区县
|
||||
showArea: propTypes.bool.def(true),
|
||||
//是否是全部
|
||||
showAll: propTypes.bool.def(false),
|
||||
// 存储数据
|
||||
saveCode: propTypes.oneOf(['province', 'city', 'region', 'all']).def('all'),
|
||||
},
|
||||
emits: ['options-change', 'change', 'update:value'],
|
||||
setup(props, { emit, refs }) {
|
||||
const emitData = ref<any[]>([]);
|
||||
const attrs = useAttrs();
|
||||
// const [state] = useRuleFormItem(props, 'value', 'change', emitData);
|
||||
const cascaderValue = ref([]);
|
||||
const getOptions = computed(() => {
|
||||
if (props.showArea && props.showAll) {
|
||||
return regionDataPlus;
|
||||
}
|
||||
if (props.showArea && !props.showAll) {
|
||||
return regionData;
|
||||
}
|
||||
if (!props.showArea && !props.showAll) {
|
||||
return provinceAndCityData;
|
||||
}
|
||||
if (!props.showArea && props.showAll) {
|
||||
return provinceAndCityDataPlus;
|
||||
}
|
||||
});
|
||||
/**
|
||||
* 监听value变化
|
||||
*/
|
||||
watchEffect(() => {
|
||||
// update-begin--author:liaozhiyang---date:20240612--for:【TV360X-1223】省市区换新组件
|
||||
if (props.value) {
|
||||
initValue();
|
||||
} else {
|
||||
cascaderValue.value = [];
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240612---for:【TV360X-1223】省市区换新组件
|
||||
});
|
||||
|
||||
/**
|
||||
* 将字符串值转化为数组
|
||||
*/
|
||||
function initValue() {
|
||||
let value = props.value ? props.value : [];
|
||||
// update-begin--author:liaozhiyang---date:20240607---for:【TV360X-501】省市区换新组件
|
||||
if (value && typeof value === 'string' && value != 'null' && value != 'undefined') {
|
||||
const arr = value.split(',');
|
||||
cascaderValue.value = transform(arr);
|
||||
} else if (isArray(value)) {
|
||||
if (value.length) {
|
||||
cascaderValue.value = transform(value);
|
||||
} else {
|
||||
cascaderValue.value = [];
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240607---for:【TV360X-501】省市区换新组件
|
||||
}
|
||||
function transform(arr) {
|
||||
let result: any = [];
|
||||
if (props.saveCode === 'region') {
|
||||
// 81 香港、82 澳门
|
||||
const regionCode = arr[0];
|
||||
if (['82', '81'].includes(regionCode.substring(0, 2))) {
|
||||
result = [`${regionCode.substring(0, 2)}0000`, regionCode];
|
||||
} else {
|
||||
result = [`${regionCode.substring(0, 2)}0000`, `${regionCode.substring(0, 2)}${regionCode.substring(2, 4)}00`, regionCode];
|
||||
}
|
||||
} else if (props.saveCode === 'city') {
|
||||
const cityCode = arr[0];
|
||||
result = [`${cityCode.substring(0, 2)}0000`, cityCode];
|
||||
} else if (props.saveCode === 'province') {
|
||||
const provinceCode = arr[0];
|
||||
result = [provinceCode];
|
||||
} else {
|
||||
result = arr;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function handleChange(arr, ...args) {
|
||||
// update-begin--author:liaozhiyang---date:20240607---for:【TV360X-501】省市区换新组件
|
||||
if (arr?.length) {
|
||||
let result: any = [];
|
||||
if (props.saveCode === 'region') {
|
||||
// 可能只有两位(选择香港时,只有省区)
|
||||
result = [arr[arr.length - 1]];
|
||||
} else if (props.saveCode === 'city') {
|
||||
result = [arr[1]];
|
||||
} else if (props.saveCode === 'province') {
|
||||
result = [arr[0]];
|
||||
} else {
|
||||
result = arr;
|
||||
}
|
||||
emit('change', result);
|
||||
emit('update:value', result);
|
||||
} else {
|
||||
emit('change', arr);
|
||||
emit('update:value', arr);
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240607---for:【TV360X-501】省市区换新组件
|
||||
// emitData.value = args;
|
||||
//update-begin-author:taoyan date:2022-6-27 for: VUEN-1424【vue3】树表、单表、jvxe、erp 、内嵌子表省市县 选择不上
|
||||
// 上面改的v-model:value导致选中数据没有显示
|
||||
// state.value = result;
|
||||
//update-end-author:taoyan date:2022-6-27 for: VUEN-1424【vue3】树表、单表、jvxe、erp 、内嵌子表省市县 选择不上
|
||||
}
|
||||
return {
|
||||
cascaderValue,
|
||||
attrs,
|
||||
regionData,
|
||||
getOptions,
|
||||
handleChange,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<a-form-item-rest>
|
||||
<div class="area-select">
|
||||
<!--省份-->
|
||||
<a-select v-model:value="province" @change="proChange" allowClear :disabled="disabled">
|
||||
<template v-for="item in provinceOptions" :key="`${item.value}`">
|
||||
<a-select-option :value="item.value">{{ item.label }}</a-select-option>
|
||||
</template>
|
||||
</a-select>
|
||||
<!--城市-->
|
||||
<a-select v-if="level >= 2" v-model:value="city" @change="cityChange" :disabled="disabled">
|
||||
<template v-for="item in cityOptions" :key="`${item.value}`">
|
||||
<a-select-option :value="item.value">{{ item.label }}</a-select-option>
|
||||
</template>
|
||||
</a-select>
|
||||
<!--地区-->
|
||||
<a-select v-if="level >= 3" v-model:value="area" @change="areaChange" :disabled="disabled">
|
||||
<template v-for="item in areaOptions" :key="`${item.value}`">
|
||||
<a-select-option :value="item.value">{{ item.label }}</a-select-option>
|
||||
</template>
|
||||
</a-select>
|
||||
</div>
|
||||
</a-form-item-rest>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, ref, reactive, watchEffect, computed, unref, watch, onMounted, onUnmounted, toRefs } from 'vue';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
|
||||
import { provinceOptions, getDataByCode, getRealCode } from '../../utils/areaDataUtil';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JAreaSelect',
|
||||
props: {
|
||||
value: [Array, String],
|
||||
province: [String],
|
||||
city: [String],
|
||||
area: [String],
|
||||
level: propTypes.number.def(3),
|
||||
disabled: propTypes.bool.def(false),
|
||||
codeField: propTypes.string,
|
||||
size: propTypes.string,
|
||||
placeholder: propTypes.string,
|
||||
formValues: propTypes.any,
|
||||
allowClear: propTypes.bool.def(false),
|
||||
getPopupContainer: {
|
||||
type: Function,
|
||||
default: (node) => node?.parentNode,
|
||||
},
|
||||
},
|
||||
emits: ['change', 'update:value','update:area','update:city','update:province'],
|
||||
setup(props, { emit, refs }) {
|
||||
const emitData = ref<any[]>([]);
|
||||
//下拉框的选择值
|
||||
const pca = reactive({
|
||||
province: '',
|
||||
city: '',
|
||||
area: '',
|
||||
});
|
||||
//表单值
|
||||
const [state] = useRuleFormItem(props, 'value', 'change', emitData);
|
||||
//城市下拉框的选项
|
||||
const cityOptions = computed(() => {
|
||||
return pca.province ? getDataByCode(pca.province) : [];
|
||||
});
|
||||
//地区下拉框的选项
|
||||
const areaOptions = computed(() => {
|
||||
return pca.city ? getDataByCode(pca.city) : [];
|
||||
});
|
||||
/**
|
||||
* 监听props值
|
||||
*/
|
||||
watchEffect(() => {
|
||||
props && initValue();
|
||||
});
|
||||
|
||||
/**
|
||||
* 监听组件值变化
|
||||
*/
|
||||
watch(pca, (newVal) => {
|
||||
if (!props.value) {
|
||||
emit('update:province', pca.province);
|
||||
emit('update:city', pca.city);
|
||||
emit('update:area', pca.area);
|
||||
}
|
||||
});
|
||||
/**
|
||||
* 数据初始化
|
||||
*/
|
||||
function initValue() {
|
||||
if (props.value) {
|
||||
//传参是数组的情况下的处理
|
||||
if (Array.isArray(props.value)) {
|
||||
pca.province = props.value[0];
|
||||
pca.city = props.value[1] ? props.value[1] : '';
|
||||
pca.area = props.value[2] ? props.value[2] : '';
|
||||
} else {
|
||||
//传参是数值
|
||||
let valueArr = getRealCode(props.value, props.level);
|
||||
if (valueArr) {
|
||||
pca.province = valueArr[0];
|
||||
pca.city = props.level >= 2 && valueArr[1] ? valueArr[1] : '';
|
||||
pca.area = props.level >= 3 && valueArr[2] ? valueArr[2] : '';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//绑定三个数据的情况
|
||||
pca.province = props.province ? props.province : '';
|
||||
pca.city = props.city ? props.city : '';
|
||||
pca.area = props.area ? props.area : '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 省份change事件
|
||||
*/
|
||||
function proChange(val) {
|
||||
pca.city = val && getDataByCode(val)[0]?.value;
|
||||
pca.area = pca.city && getDataByCode(pca.city)[0]?.value;
|
||||
state.value = props.level <= 1 ? val : props.level <= 2 ? pca.city : pca.area;
|
||||
emit('update:value', unref(state));
|
||||
}
|
||||
|
||||
/**
|
||||
* 城市change事件
|
||||
*/
|
||||
function cityChange(val) {
|
||||
pca.area = val && getDataByCode(val)[0]?.value;
|
||||
state.value = props.level <= 2 ? val : pca.area;
|
||||
emit('update:value', unref(state));
|
||||
}
|
||||
|
||||
/**
|
||||
* 区域change事件
|
||||
*/
|
||||
function areaChange(val) {
|
||||
state.value = val;
|
||||
emit('update:value', unref(state));
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(pca),
|
||||
provinceOptions,
|
||||
cityOptions,
|
||||
areaOptions,
|
||||
proChange,
|
||||
cityChange,
|
||||
areaChange,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.area-select {
|
||||
width: 100%;
|
||||
|
||||
/* update-begin-author:taoyan date:2023-2-18 for: QQYUN-4292【online表单】高级查询 2.省市县样式问题 */
|
||||
/* display: flex;*/
|
||||
|
||||
.ant-select {
|
||||
width: calc(33.3% - 7px)
|
||||
}
|
||||
/* update-end-author:taoyan date:2023-2-18 for: QQYUN-4292【online表单】高级查询 2.省市县样式问题 */
|
||||
|
||||
.ant-select:not(:first-child) {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,264 @@
|
||||
<!--下拉树-->
|
||||
<template>
|
||||
<a-tree-select
|
||||
allowClear
|
||||
labelInValue
|
||||
style="width: 100%"
|
||||
:disabled="disabled"
|
||||
:dropdownStyle="{ maxHeight: '400px', overflow: 'auto' }"
|
||||
:placeholder="placeholder"
|
||||
:loadData="asyncLoadTreeData"
|
||||
:value="treeValue"
|
||||
:treeData="treeData"
|
||||
:multiple="multiple"
|
||||
@change="onChange"
|
||||
>
|
||||
</a-tree-select>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, unref, watch, nextTick } from 'vue';
|
||||
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { loadDictItem, loadTreeData } from '/@/api/common/api';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
const { createMessage, createErrorModal } = useMessage();
|
||||
export default defineComponent({
|
||||
name: 'JCategorySelect',
|
||||
components: {},
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请选择',
|
||||
required: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
condition: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: false,
|
||||
},
|
||||
// 是否支持多选
|
||||
multiple: {
|
||||
type: [Boolean, String],
|
||||
default: false,
|
||||
},
|
||||
loadTriggleChange: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
pid: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: false,
|
||||
},
|
||||
pcode: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: false,
|
||||
},
|
||||
back: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
emits: ['options-change', 'change', 'update:value'],
|
||||
setup(props, { emit, refs }) {
|
||||
console.info(props);
|
||||
const emitData = ref<any[]>([]);
|
||||
const treeData = ref<any[]>([]);
|
||||
const treeValue = ref();
|
||||
const attrs = useAttrs();
|
||||
const [state, , , formItemContext] = useRuleFormItem(props, 'value', 'change', emitData);
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
loadItemByCode();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
watch(
|
||||
() => props.pcode,
|
||||
() => {
|
||||
loadRoot();
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
function loadRoot() {
|
||||
let param = {
|
||||
pid: props.pid,
|
||||
pcode: !props.pcode ? '0' : props.pcode,
|
||||
condition: props.condition,
|
||||
};
|
||||
console.info(param);
|
||||
loadTreeData(param).then((res) => {
|
||||
if(res && res.length>0){
|
||||
for (let i of res) {
|
||||
i.value = i.key;
|
||||
if (i.leaf == false) {
|
||||
i.isLeaf = false;
|
||||
} else if (i.leaf == true) {
|
||||
i.isLeaf = true;
|
||||
}
|
||||
}
|
||||
treeData.value = res;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadItemByCode() {
|
||||
if (!props.value || props.value == '0') {
|
||||
if(props.multiple){
|
||||
treeValue.value = [];
|
||||
}else{
|
||||
treeValue.value = { value: null, label: null };
|
||||
}
|
||||
} else {
|
||||
loadDictItem({ ids: props.value }).then((res) => {
|
||||
let values = props.value.split(',');
|
||||
treeValue.value = res.map((item, index) => ({
|
||||
key: values[index],
|
||||
value: values[index],
|
||||
label: item,
|
||||
}));
|
||||
if(!props.multiple){
|
||||
treeValue.value = treeValue.value[0];
|
||||
}
|
||||
onLoadTriggleChange(res[0]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onLoadTriggleChange(text) {
|
||||
//只有单选才会触发
|
||||
if (!props.multiple && props.loadTriggleChange) {
|
||||
backValue(props.value, text);
|
||||
}
|
||||
}
|
||||
|
||||
function backValue(value, label) {
|
||||
let obj = {};
|
||||
if (props.back) {
|
||||
obj[props.back] = label;
|
||||
}
|
||||
emit('change', value, obj);
|
||||
emit("update:value",value)
|
||||
}
|
||||
|
||||
function asyncLoadTreeData(treeNode) {
|
||||
let dataRef = treeNode.dataRef;
|
||||
return new Promise<void>((resolve) => {
|
||||
if (treeNode.children && treeNode.children.length > 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
let pid = dataRef.key;
|
||||
let param = {
|
||||
pid: pid,
|
||||
condition: props.condition,
|
||||
};
|
||||
loadTreeData(param).then((res) => {
|
||||
if (res) {
|
||||
for (let i of res) {
|
||||
i.value = i.key;
|
||||
if (i.leaf == false) {
|
||||
i.isLeaf = false;
|
||||
} else if (i.leaf == true) {
|
||||
i.isLeaf = true;
|
||||
}
|
||||
}
|
||||
addChildren(pid, res, treeData.value);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addChildren(pid, children, treeArray) {
|
||||
if (treeArray && treeArray.length > 0) {
|
||||
for (let item of treeArray) {
|
||||
if (item.key == pid) {
|
||||
if (!children || children.length == 0) {
|
||||
item.isLeaf = true;
|
||||
} else {
|
||||
item.children = children;
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
addChildren(pid, children, item.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onChange(value) {
|
||||
if (!value) {
|
||||
emit('change', '');
|
||||
treeValue.value = '';
|
||||
emit("update:value",'')
|
||||
} else if (Array.isArray(value)) {
|
||||
let labels = [];
|
||||
let values = value.map((item) => {
|
||||
labels.push(item.label);
|
||||
return item.value;
|
||||
});
|
||||
backValue(values.join(','), labels.join(','));
|
||||
treeValue.value = value;
|
||||
} else {
|
||||
backValue(value.value, value.label);
|
||||
treeValue.value = value;
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20240429---for:【QQYUN-9110】组件有值校验没消失
|
||||
nextTick(() => {
|
||||
formItemContext?.onFieldChange();
|
||||
});
|
||||
// update-end--author:liaozhiyang---date:20240429---for:【QQYUN-9110】组件有值校验没消失
|
||||
}
|
||||
|
||||
function getCurrTreeData() {
|
||||
return treeData;
|
||||
}
|
||||
|
||||
function validateProp() {
|
||||
let mycondition = props.condition;
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!mycondition) {
|
||||
resolve();
|
||||
} else {
|
||||
try {
|
||||
let test = JSON.parse(mycondition);
|
||||
if (typeof test == 'object' && test) {
|
||||
resolve();
|
||||
} else {
|
||||
createMessage.error('组件JTreeSelect-condition传值有误,需要一个json字符串!');
|
||||
reject();
|
||||
}
|
||||
} catch (e) {
|
||||
createMessage.error('组件JTreeSelect-condition传值有误,需要一个json字符串!');
|
||||
reject();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
attrs,
|
||||
onChange,
|
||||
treeData,
|
||||
treeValue,
|
||||
asyncLoadTreeData,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<a-checkbox-group v-bind="attrs" v-model:value="checkboxArray" :options="checkOptions" @change="handleChange">
|
||||
<template #label="{label, value}">
|
||||
<span :class="[useDicColor && getDicColor(value) ? 'colorText' : '']" :style="{ backgroundColor: `${getDicColor(value)}` }">{{ label }}</span>
|
||||
</template>
|
||||
</a-checkbox-group>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, watch, watchEffect, ref, unref } from 'vue';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { initDictOptions } from '/@/utils/dict/index';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JCheckbox',
|
||||
props: {
|
||||
value:propTypes.oneOfType([propTypes.string, propTypes.number]),
|
||||
dictCode: propTypes.string,
|
||||
useDicColor: propTypes.bool.def(false),
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
emits: ['change', 'update:value'],
|
||||
setup(props, { emit }) {
|
||||
const attrs = useAttrs();
|
||||
//checkbox选项
|
||||
const checkOptions = ref<any[]>([]);
|
||||
//checkbox数值
|
||||
const checkboxArray = ref<any[]>([]);
|
||||
/**
|
||||
* 监听value
|
||||
*/
|
||||
watchEffect(() => {
|
||||
//update-begin-author:taoyan date:2022-7-4 for:issues/I5E7YX AUTO在线表单进入功能测试之后一直卡在功能测试界面
|
||||
let temp = props.value;
|
||||
if(!temp && temp!==0){
|
||||
checkboxArray.value = []
|
||||
}else{
|
||||
temp = temp + '';
|
||||
checkboxArray.value = temp.split(',')
|
||||
}
|
||||
//update-end-author:taoyan date:2022-7-4 for:issues/I5E7YX AUTO在线表单进入功能测试之后一直卡在功能测试界面
|
||||
//update-begin-author:taoyan date:20220401 for: 调用表单的 resetFields不会清空当前信息,界面显示上一次的数据
|
||||
if (props.value === '' || props.value === undefined) {
|
||||
checkboxArray.value = [];
|
||||
}
|
||||
//update-end-author:taoyan date:20220401 for: 调用表单的 resetFields不会清空当前信息,界面显示上一次的数据
|
||||
});
|
||||
/**
|
||||
* 监听字典code
|
||||
*/
|
||||
watchEffect(() => {
|
||||
props && initOptions();
|
||||
});
|
||||
|
||||
/**
|
||||
* 初始化选项
|
||||
*/
|
||||
async function initOptions() {
|
||||
//根据options, 初始化选项
|
||||
if (props.options && props.options.length > 0) {
|
||||
checkOptions.value = props.options;
|
||||
return;
|
||||
}
|
||||
//根据字典Code, 初始化选项
|
||||
if (props.dictCode) {
|
||||
const dictData = await initDictOptions(props.dictCode);
|
||||
checkOptions.value = dictData.reduce((prev, next) => {
|
||||
if (next) {
|
||||
const value = next['value'];
|
||||
prev.push({
|
||||
label: next['text'],
|
||||
value: value,
|
||||
color: next['color'],
|
||||
});
|
||||
}
|
||||
return prev;
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* change事件
|
||||
* @param $event
|
||||
*/
|
||||
function handleChange($event) {
|
||||
emit('update:value', $event.join(','));
|
||||
emit('change', $event.join(','));
|
||||
}
|
||||
const getDicColor = (value) => {
|
||||
if (props.useDicColor) {
|
||||
const findItem = checkOptions.value.find((item) => item.value == value);
|
||||
if (findItem) {
|
||||
return findItem.color;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return { checkboxArray, checkOptions, attrs, handleChange, getDicColor };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
// update-begin--author:liaozhiyang---date:20230110---for:【QQYUN-7799】字典组件(原生组件除外)加上颜色配置
|
||||
.colorText {
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
padding: 0 6px;
|
||||
border-radius: 8px;
|
||||
background-color: red;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20230110---for:【QQYUN-7799】字典组件(原生组件除外)加上颜色配置
|
||||
</style>
|
||||
@ -0,0 +1,375 @@
|
||||
<template>
|
||||
<div ref="containerRef" v-bind="boxBindProps">
|
||||
<!-- 全屏按钮 -->
|
||||
<a-icon v-if="fullScreen" class="full-screen-icon" :type="fullScreenIcon" @click="onToggleFullScreen" />
|
||||
<textarea ref="textarea" v-bind="getBindValue"></textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, reactive, ref, watch, unref, computed } from 'vue';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
|
||||
// 引入全局实例
|
||||
import _CodeMirror, { EditorFromTextArea } from 'codemirror';
|
||||
// 核心样式
|
||||
import 'codemirror/lib/codemirror.css';
|
||||
// 引入主题后还需要在 options 中指定主题才会生效
|
||||
import 'codemirror/theme/idea.css';
|
||||
// 需要引入具体的语法高亮库才会有对应的语法高亮效果
|
||||
import 'codemirror/mode/javascript/javascript.js';
|
||||
import 'codemirror/mode/css/css.js';
|
||||
import 'codemirror/mode/xml/xml.js';
|
||||
import 'codemirror/mode/clike/clike.js';
|
||||
import 'codemirror/mode/markdown/markdown.js';
|
||||
import 'codemirror/mode/python/python.js';
|
||||
import 'codemirror/mode/r/r.js';
|
||||
import 'codemirror/mode/shell/shell.js';
|
||||
import 'codemirror/mode/sql/sql.js';
|
||||
import 'codemirror/mode/swift/swift.js';
|
||||
import 'codemirror/mode/vue/vue.js';
|
||||
// 折叠资源引入:开始
|
||||
import 'codemirror/addon/fold/foldgutter.css';
|
||||
import 'codemirror/addon/fold/foldcode.js';
|
||||
import 'codemirror/addon/fold/brace-fold.js';
|
||||
import 'codemirror/addon/fold/comment-fold.js';
|
||||
import 'codemirror/addon/fold/indent-fold.js';
|
||||
import 'codemirror/addon/fold/foldgutter.js';
|
||||
// 折叠资源引入:结束
|
||||
//光标行背景高亮,配置里面也需要styleActiveLine设置为true
|
||||
import 'codemirror/addon/selection/active-line.js';
|
||||
// 支持代码自动补全
|
||||
import 'codemirror/addon/hint/show-hint.css';
|
||||
import 'codemirror/addon/hint/show-hint.js';
|
||||
import 'codemirror/addon/hint/anyword-hint.js';
|
||||
// 匹配括号
|
||||
import 'codemirror/addon/edit/matchbrackets';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { isJsonObjectString } from '/@/utils/is.ts';
|
||||
// 代码提示
|
||||
import { useCodeHinting } from '../hooks/useCodeHinting';
|
||||
|
||||
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
|
||||
import { ThemeEnum } from '/@/enums/appEnum';
|
||||
export default defineComponent({
|
||||
name: 'JCodeEditor',
|
||||
// 不将 attrs 的属性绑定到 html 标签上
|
||||
inheritAttrs: false,
|
||||
components: {},
|
||||
props: {
|
||||
value: propTypes.string.def(''),
|
||||
height: propTypes.string.def('auto'),
|
||||
disabled: propTypes.bool.def(false),
|
||||
// 是否显示全屏按钮
|
||||
fullScreen: propTypes.bool.def(false),
|
||||
// 全屏以后的z-index
|
||||
zIndex: propTypes.any.def(1500),
|
||||
theme: propTypes.string.def('idea'),
|
||||
language: propTypes.string.def(''),
|
||||
// 代码提示
|
||||
keywords: propTypes.array.def([]),
|
||||
},
|
||||
emits: ['change', 'update:value'],
|
||||
setup(props, { emit }) {
|
||||
const { getDarkMode } = useRootSetting();
|
||||
const containerRef = ref(null);
|
||||
const { prefixCls } = useDesign('code-editer');
|
||||
const CodeMirror = window.CodeMirror || _CodeMirror;
|
||||
const emitData = ref<object>();
|
||||
//表单值
|
||||
const [state] = useRuleFormItem(props, 'value', 'change', emitData);
|
||||
const textarea = ref<HTMLTextAreaElement>();
|
||||
let coder: Nullable<EditorFromTextArea> = null;
|
||||
const attrs = useAttrs();
|
||||
const height = ref(props.height);
|
||||
const options = reactive({
|
||||
// 缩进格式
|
||||
tabSize: 2,
|
||||
// 主题,对应主题库 JS 需要提前引入
|
||||
// update-begin--author:liaozhiyang---date:20240327---for:【QQYUN-8639】暗黑主题适配
|
||||
theme: getDarkMode.value == ThemeEnum.DARK ? 'monokai' : props.theme,
|
||||
// update-end--author:liaozhiyang---date:20240327---for:【QQYUN-8639】暗黑主题适配
|
||||
smartIndent: true, // 是否智能缩进
|
||||
// 显示行号
|
||||
lineNumbers: true,
|
||||
line: true,
|
||||
// 启用代码折叠相关功能:开始
|
||||
foldGutter: true,
|
||||
lineWrapping: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
|
||||
// 启用代码折叠相关功能:结束
|
||||
// 光标行高亮
|
||||
styleActiveLine: true,
|
||||
// update-begin--author:liaozhiyang---date:20231201---for:【issues/869】JCodeEditor组件初始化时没有设置mode
|
||||
mode: props.language,
|
||||
// update-begin--author:liaozhiyang---date:20231201---for:【issues/869】JCodeEditor组件初始化时没有设置mode
|
||||
// update-begin--author:liaozhiyang---date:20240603---for:【TV360X-898】代码生成之后的预览改成只读
|
||||
readOnly: props.disabled,
|
||||
// update-end--author:liaozhiyang---date:20240603---for:【TV360X-898】代码生成之后的预览改成只读
|
||||
// 匹配括号
|
||||
matchBrackets: true,
|
||||
extraKeys: {
|
||||
// Tab: function autoFormat(editor) {
|
||||
// //var totalLines = editor.lineCount();
|
||||
// //editor.autoFormatRange({line:0, ch:0}, {line:totalLines});
|
||||
// setValue(innerValue, false);
|
||||
// },
|
||||
'Cmd-/': (cm) => comment(cm),
|
||||
'Ctrl-/': (cm) => comment(cm),
|
||||
},
|
||||
});
|
||||
// 内部存储值,初始为 props.value
|
||||
let innerValue = props.value ?? '';
|
||||
// 全屏状态
|
||||
const isFullScreen = ref(false);
|
||||
const fullScreenIcon = computed(() => (isFullScreen.value ? 'fullscreen-exit' : 'fullscreen'));
|
||||
// 外部盒子参数
|
||||
const boxBindProps = computed(() => {
|
||||
let _props = {
|
||||
class: [
|
||||
prefixCls,
|
||||
'full-screen-parent',
|
||||
'auto-height',
|
||||
{
|
||||
'full-screen': isFullScreen.value,
|
||||
},
|
||||
],
|
||||
style: {},
|
||||
};
|
||||
if (isFullScreen.value) {
|
||||
_props.style['z-index'] = props.zIndex;
|
||||
}
|
||||
return _props;
|
||||
});
|
||||
// update-begin--author:liaozhiyang---date:20230904---for:【QQYUN-5955】online js增强,加入代码提示
|
||||
const { codeHintingMount, codeHintingRegistry } = useCodeHinting(CodeMirror, props.keywords, props.language);
|
||||
codeHintingRegistry();
|
||||
// update-end--author:liaozhiyang---date:20230904---for:【QQYUN-5955】online js增强,加入代码提示
|
||||
/**
|
||||
* 监听组件值
|
||||
*/
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
if (innerValue != props.value) {
|
||||
setValue(props.value, false);
|
||||
}
|
||||
}
|
||||
);
|
||||
onMounted(() => {
|
||||
initialize();
|
||||
// update-begin--author:liaozhiyang---date:20240318---for:【QQYUN-8473】代码编辑器首次加载会有遮挡
|
||||
setTimeout(() => {
|
||||
refresh();
|
||||
}, 150);
|
||||
// update-end--author:liaozhiyang---date:20240318---for:【QQYUN-8473】代码编辑器首次加载会有遮挡
|
||||
});
|
||||
|
||||
/**
|
||||
* 组件赋值
|
||||
* @param value
|
||||
* @param trigger 是否触发 change 事件
|
||||
*/
|
||||
function setValue(value: string, trigger = true) {
|
||||
if (value && isJsonObjectString(value)) {
|
||||
value = JSON.stringify(JSON.parse(value), null, 2);
|
||||
}
|
||||
coder?.setValue(value ?? '');
|
||||
innerValue = value;
|
||||
trigger && emitChange(innerValue);
|
||||
// update-begin--author:liaozhiyang---date:20240510---for:【QQYUN-9231】代码编辑器有遮挡
|
||||
setTimeout(() => {
|
||||
refresh();
|
||||
// 再次刷下防止小概率下遮挡问题
|
||||
setTimeout(() => {
|
||||
refresh();
|
||||
}, 600);
|
||||
}, 400);
|
||||
// update-end--author:liaozhiyang---date:20240510---for:【QQYUN-9231】代码编辑器有遮挡
|
||||
}
|
||||
|
||||
//编辑器值修改事件
|
||||
function onChange(obj) {
|
||||
let value = obj.getValue();
|
||||
innerValue = value || '';
|
||||
if (props.value != innerValue) {
|
||||
emitChange(innerValue);
|
||||
}
|
||||
}
|
||||
|
||||
function emitChange(value) {
|
||||
emit('change', value);
|
||||
emit('update:value', value);
|
||||
}
|
||||
|
||||
//组件初始化
|
||||
function initialize() {
|
||||
coder = CodeMirror.fromTextArea(textarea.value!, options);
|
||||
//绑定值修改事件
|
||||
coder.on('change', onChange);
|
||||
// 初始化成功时赋值一次
|
||||
setValue(innerValue, false);
|
||||
// update-begin--author:liaozhiyang---date:20230904---for:【QQYUN-5955】online js增强,加入代码提示
|
||||
codeHintingMount(coder);
|
||||
// update-end--author:liaozhiyang---date:20230904---for:【QQYUN-5955】online js增强,加入代码提示
|
||||
}
|
||||
|
||||
// 切换全屏状态
|
||||
function onToggleFullScreen() {
|
||||
isFullScreen.value = !isFullScreen.value;
|
||||
}
|
||||
|
||||
//update-begin-author:taoyan date:2022-5-9 for: codeEditor禁用功能
|
||||
watch(
|
||||
() => props.disabled,
|
||||
(val) => {
|
||||
if (coder) {
|
||||
coder.setOption('readOnly', val);
|
||||
}
|
||||
}
|
||||
);
|
||||
//update-end-author:taoyan date:2022-5-9 for: codeEditor禁用功能
|
||||
|
||||
// 支持动态设置语言
|
||||
watch(()=>props.language, (val)=>{
|
||||
if(val && coder){
|
||||
coder.setOption('mode', val);
|
||||
}
|
||||
});
|
||||
|
||||
const getBindValue = Object.assign({}, unref(props), unref(attrs));
|
||||
|
||||
//update-begin-author:taoyan date:2022-10-18 for: VUEN-2480【严重bug】online vue3测试的问题 8、online js增强样式问题
|
||||
function refresh(){
|
||||
if(coder){
|
||||
coder.refresh();
|
||||
}
|
||||
}
|
||||
//update-end-author:taoyan date:2022-10-18 for: VUEN-2480【严重bug】online vue3测试的问题 8、online js增强样式问题
|
||||
|
||||
/**
|
||||
* 2024-04-01
|
||||
* liaozhiyang
|
||||
* 代码批量注释
|
||||
*/
|
||||
function comment(cm) {
|
||||
var selection = cm.getSelection();
|
||||
var start = cm.getCursor('start');
|
||||
var end = cm.getCursor('end');
|
||||
var isCommented = selection.startsWith('//');
|
||||
if (isCommented) {
|
||||
// 如果已经被注释,取消注释
|
||||
cm.replaceRange(selection.replace(/\n\/\/\s/g, '\n').replace(/^\/\/\s/, ''), start, end);
|
||||
} else {
|
||||
// 添加注释
|
||||
cm.replaceRange('// ' + selection.replace(/\n(?=.)/g, '\n// '), start, end);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
textarea,
|
||||
boxBindProps,
|
||||
getBindValue,
|
||||
setValue,
|
||||
isFullScreen,
|
||||
fullScreenIcon,
|
||||
onToggleFullScreen,
|
||||
refresh,
|
||||
containerRef,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
//noinspection LessUnresolvedVariable
|
||||
@prefix-cls: ~'@{namespace}-code-editer';
|
||||
.@{prefix-cls} {
|
||||
&.auto-height {
|
||||
.CodeMirror {
|
||||
height: v-bind(height) !important;
|
||||
min-height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 全屏样式 */
|
||||
|
||||
&.full-screen-parent {
|
||||
position: relative;
|
||||
|
||||
.full-screen-icon {
|
||||
opacity: 0;
|
||||
color: black;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
line-height: 24px;
|
||||
background-color: white;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
z-index: 9;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.3s;
|
||||
padding: 2px 0 0 1.5px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.full-screen-icon {
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.full-screen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 8px;
|
||||
background-color: #f5f5f5;
|
||||
|
||||
.full-screen-icon {
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.full-screen-child,
|
||||
.CodeMirror {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.full-screen-child {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/** VUEN-2344【vue3】这个样式有问题,是不是加个边框 */
|
||||
.CodeMirror{
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
}
|
||||
.CodeMirror-hints.idea,
|
||||
.CodeMirror-hints.monokai {
|
||||
z-index: 1001;
|
||||
max-width: 600px;
|
||||
max-height: 300px;
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20240327---for:【QQYUN-8639】暗黑主题适配
|
||||
html[data-theme='dark'] {
|
||||
.@{prefix-cls} {
|
||||
.CodeMirror {
|
||||
border: 1px solid #3a3a3a;
|
||||
}
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240327---for:【QQYUN-8639】暗黑主题适配
|
||||
</style>
|
||||
@ -0,0 +1,237 @@
|
||||
<template>
|
||||
<a-radio-group v-if="compType === CompTypeEnum.Radio" v-bind="attrs" v-model:value="state" @change="handleChangeRadio">
|
||||
<template v-for="item in dictOptions" :key="`${item.value}`">
|
||||
<a-radio :value="item.value">
|
||||
<span :class="[useDicColor && item.color ? 'colorText' : '']" :style="{ backgroundColor: `${useDicColor && item.color}` }">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</a-radio>
|
||||
</template>
|
||||
</a-radio-group>
|
||||
|
||||
<a-radio-group
|
||||
v-else-if="compType === CompTypeEnum.RadioButton"
|
||||
v-bind="attrs"
|
||||
v-model:value="state"
|
||||
buttonStyle="solid"
|
||||
@change="handleChangeRadio"
|
||||
>
|
||||
<template v-for="item in dictOptions" :key="`${item.value}`">
|
||||
<a-radio-button :value="item.value">
|
||||
{{ item.label }}
|
||||
</a-radio-button>
|
||||
</template>
|
||||
</a-radio-group>
|
||||
|
||||
<template v-else-if="compType === CompTypeEnum.Select">
|
||||
<!-- 显示加载效果 -->
|
||||
<a-input v-if="loadingEcho" readOnly placeholder="加载中…">
|
||||
<template #prefix>
|
||||
<LoadingOutlined />
|
||||
</template>
|
||||
</a-input>
|
||||
<a-select
|
||||
v-else
|
||||
:placeholder="placeholder"
|
||||
v-bind="attrs"
|
||||
v-model:value="state"
|
||||
:filterOption="handleFilterOption"
|
||||
:getPopupContainer="getPopupContainer"
|
||||
:style="style"
|
||||
@change="handleChange"
|
||||
>
|
||||
<a-select-option v-if="showChooseOption" :value="null">请选择…</a-select-option>
|
||||
<template v-for="item in dictOptions" :key="`${item.value}`">
|
||||
<a-select-option :value="item.value">
|
||||
<span
|
||||
:class="[useDicColor && item.color ? 'colorText' : '']"
|
||||
:style="{ backgroundColor: `${useDicColor && item.color}` }"
|
||||
:title="item.label"
|
||||
>
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</a-select-option>
|
||||
</template>
|
||||
</a-select>
|
||||
</template>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, ref, reactive, watchEffect, computed, unref, watch, onMounted, nextTick } from 'vue';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { initDictOptions } from '/@/utils/dict';
|
||||
import { get, omit } from 'lodash-es';
|
||||
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
|
||||
import { CompTypeEnum } from '/@/enums/CompTypeEnum';
|
||||
import { LoadingOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JDictSelectTag',
|
||||
inheritAttrs: false,
|
||||
components: { LoadingOutlined },
|
||||
props: {
|
||||
value: propTypes.oneOfType([propTypes.string, propTypes.number, propTypes.array]),
|
||||
dictCode: propTypes.string,
|
||||
type: propTypes.string,
|
||||
placeholder: propTypes.string,
|
||||
stringToNumber: propTypes.bool,
|
||||
useDicColor: propTypes.bool.def(false),
|
||||
getPopupContainer: {
|
||||
type: Function,
|
||||
default: (node) => node?.parentNode,
|
||||
},
|
||||
// 是否显示【请选择】选项
|
||||
showChooseOption: propTypes.bool.def(true),
|
||||
// 下拉项-online使用
|
||||
options: {
|
||||
type: Array,
|
||||
default: [],
|
||||
required: false,
|
||||
},
|
||||
style: propTypes.any,
|
||||
},
|
||||
emits: ['options-change', 'change','update:value'],
|
||||
setup(props, { emit, refs }) {
|
||||
const dictOptions = ref<any[]>([]);
|
||||
const attrs = useAttrs();
|
||||
const [state, , , formItemContext] = useRuleFormItem(props, 'value', 'change');
|
||||
const getBindValue = Object.assign({}, unref(props), unref(attrs));
|
||||
// 是否正在加载回显数据
|
||||
const loadingEcho = ref<boolean>(false);
|
||||
// 是否是首次加载回显,只有首次加载,才会显示 loading
|
||||
let isFirstLoadEcho = true;
|
||||
|
||||
//组件类型
|
||||
const compType = computed(() => {
|
||||
return !props.type || props.type === 'list' ? 'select' : props.type;
|
||||
});
|
||||
/**
|
||||
* 监听字典code
|
||||
*/
|
||||
watchEffect(() => {
|
||||
if (props.dictCode) {
|
||||
loadingEcho.value = isFirstLoadEcho;
|
||||
isFirstLoadEcho = false;
|
||||
initDictData().finally(() => {
|
||||
loadingEcho.value = isFirstLoadEcho;
|
||||
});
|
||||
}
|
||||
//update-begin-author:taoyan date: 如果没有提供dictCode 可以走options的配置--
|
||||
if (!props.dictCode) {
|
||||
dictOptions.value = props.options;
|
||||
}
|
||||
//update-end-author:taoyan date: 如果没有提供dictCode 可以走options的配置--
|
||||
});
|
||||
|
||||
//update-begin-author:taoyan date:20220404 for: 使用useRuleFormItem定义的value,会有一个问题,如果不是操作设置的值而是代码设置的控件值而不能触发change事件
|
||||
// 此处添加空值的change事件,即当组件调用地代码设置value为''也能触发change事件
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
if (props.value === '') {
|
||||
emit('change', '');
|
||||
nextTick(() => formItemContext.onFieldChange());
|
||||
}
|
||||
}
|
||||
);
|
||||
//update-end-author:taoyan date:20220404 for: 使用useRuleFormItem定义的value,会有一个问题,如果不是操作设置的值而是代码设置的控件值而不能触发change事件
|
||||
|
||||
async function initDictData() {
|
||||
let { dictCode, stringToNumber } = props;
|
||||
//根据字典Code, 初始化字典数组
|
||||
const dictData = await initDictOptions(dictCode);
|
||||
dictOptions.value = dictData.reduce((prev, next) => {
|
||||
if (next) {
|
||||
const value = next['value'];
|
||||
prev.push({
|
||||
label: next['text'] || next['label'],
|
||||
value: stringToNumber ? +value : value,
|
||||
color: next['color'],
|
||||
...omit(next, ['text', 'value', 'color']),
|
||||
});
|
||||
}
|
||||
return prev;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function handleChange(e) {
|
||||
const { mode } = unref<Recordable>(getBindValue);
|
||||
let changeValue:any;
|
||||
// 兼容多选模式
|
||||
|
||||
//update-begin---author:wangshuai ---date:20230216 for:[QQYUN-4290]公文发文:选择机关代字报错,是因为值改变触发了change事件三次,导致数据发生改变------------
|
||||
//采用一个值,不然的话state值变换触发多个change
|
||||
if (mode === 'multiple') {
|
||||
changeValue = e?.target?.value ?? e;
|
||||
// 过滤掉空值
|
||||
if (changeValue == null || changeValue === '') {
|
||||
changeValue = [];
|
||||
}
|
||||
if (Array.isArray(changeValue)) {
|
||||
changeValue = changeValue.filter((item) => item != null && item !== '');
|
||||
}
|
||||
} else {
|
||||
changeValue = e?.target?.value ?? e;
|
||||
}
|
||||
state.value = changeValue;
|
||||
|
||||
//update-begin---author:wangshuai ---date:20230403 for:【issues/4507】JDictSelectTag组件使用时,浏览器给出警告提示:Expected Function, got Array------------
|
||||
emit('update:value',changeValue)
|
||||
//update-end---author:wangshuai ---date:20230403 for:【issues/4507】JDictSelectTag组件使用时,浏览器给出警告提示:Expected Function, got Array述------------
|
||||
//update-end---author:wangshuai ---date:20230216 for:[QQYUN-4290]公文发文:选择机关代字报错,是因为值改变触发了change事件三次,导致数据发生改变------------
|
||||
|
||||
// nextTick(() => formItemContext.onFieldChange());
|
||||
}
|
||||
|
||||
/** 单选radio的值变化事件 */
|
||||
function handleChangeRadio(e) {
|
||||
state.value = e?.target?.value ?? e;
|
||||
//update-begin---author:wangshuai ---date:20230504 for:【issues/506】JDictSelectTag 组件 type="radio" 没有返回值------------
|
||||
emit('update:value',e?.target?.value ?? e)
|
||||
//update-end---author:wangshuai ---date:20230504 for:【issues/506】JDictSelectTag 组件 type="radio" 没有返回值------------
|
||||
}
|
||||
|
||||
/** 用于搜索下拉框中的内容 */
|
||||
function handleFilterOption(input, option) {
|
||||
// update-begin--author:liaozhiyang---date:20230914---for:【QQYUN-6514】 配置的时候,Y轴不能输入多个字段了,控制台报错
|
||||
if (typeof option.children === 'function') {
|
||||
// 在 label 中搜索
|
||||
let labelIf = option.children()[0]?.children.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
if (labelIf) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20230914---for:【QQYUN-6514】 配置的时候,Y轴不能输入多个字段了,控制台报错
|
||||
// 在 value 中搜索
|
||||
return (option.value || '').toString().toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
compType,
|
||||
attrs,
|
||||
loadingEcho,
|
||||
getBindValue,
|
||||
dictOptions,
|
||||
CompTypeEnum,
|
||||
handleChange,
|
||||
handleChangeRadio,
|
||||
handleFilterOption,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
// update-begin--author:liaozhiyang---date:20230110---for:【QQYUN-7799】字典组件(原生组件除外)加上颜色配置
|
||||
.colorText {
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
padding: 0 6px;
|
||||
border-radius: 8px;
|
||||
background-color: red;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20230110---for:【QQYUN-7799】字典组件(原生组件除外)加上颜色配置
|
||||
</style>
|
||||
@ -0,0 +1,319 @@
|
||||
<template>
|
||||
<div :class="`${prefixCls}`">
|
||||
<div class="content">
|
||||
<a-tabs :size="`small`" v-model:activeKey="activeKey">
|
||||
<a-tab-pane tab="秒" key="second" v-if="!hideSecond">
|
||||
<SecondUI v-model:value="second" :disabled="disabled" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="分" key="minute">
|
||||
<MinuteUI v-model:value="minute" :disabled="disabled" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="时" key="hour">
|
||||
<HourUI v-model:value="hour" :disabled="disabled" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="日" key="day">
|
||||
<DayUI v-model:value="day" :week="week" :disabled="disabled" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="月" key="month">
|
||||
<MonthUI v-model:value="month" :disabled="disabled" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="周" key="week">
|
||||
<WeekUI v-model:value="week" :day="day" :disabled="disabled" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="年" key="year" v-if="!hideYear && !hideSecond">
|
||||
<YearUI v-model:value="year" :disabled="disabled" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
<a-divider />
|
||||
<!-- 执行时间预览 -->
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="18" style="margin-top: 22px">
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="8" style="margin-bottom: 12px">
|
||||
<a-input v-model:value="inputValues.second" @blur="onInputBlur">
|
||||
<template #addonBefore>
|
||||
<span class="allow-click" @click="activeKey = 'second'">秒</span>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-col>
|
||||
<a-col :span="8" style="margin-bottom: 12px">
|
||||
<a-input v-model:value="inputValues.minute" @blur="onInputBlur">
|
||||
<template #addonBefore>
|
||||
<span class="allow-click" @click="activeKey = 'minute'">分</span>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-col>
|
||||
<a-col :span="8" style="margin-bottom: 12px">
|
||||
<a-input v-model:value="inputValues.hour" @blur="onInputBlur">
|
||||
<template #addonBefore>
|
||||
<span class="allow-click" @click="activeKey = 'hour'">时</span>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-col>
|
||||
<a-col :span="8" style="margin-bottom: 12px">
|
||||
<a-input v-model:value="inputValues.day" @blur="onInputBlur">
|
||||
<template #addonBefore>
|
||||
<span class="allow-click" @click="activeKey = 'day'">日</span>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-col>
|
||||
<a-col :span="8" style="margin-bottom: 12px">
|
||||
<a-input v-model:value="inputValues.month" @blur="onInputBlur">
|
||||
<template #addonBefore>
|
||||
<span class="allow-click" @click="activeKey = 'month'">月</span>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-col>
|
||||
<a-col :span="8" style="margin-bottom: 12px">
|
||||
<a-input v-model:value="inputValues.week" @blur="onInputBlur">
|
||||
<template #addonBefore>
|
||||
<span class="allow-click" @click="activeKey = 'week'">周</span>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-input v-model:value="inputValues.year" @blur="onInputBlur">
|
||||
<template #addonBefore>
|
||||
<span class="allow-click" @click="activeKey = 'year'">年</span>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-col>
|
||||
<a-col :span="16">
|
||||
<a-input v-model:value="inputValues.cron" @blur="onInputCronBlur">
|
||||
<template #addonBefore>
|
||||
<a-tooltip title="Cron表达式">式</a-tooltip>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<div>近十次执行时间(不含年)</div>
|
||||
<a-textarea type="textarea" :value="preTimeList" :rows="5" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref, watch, provide } from 'vue';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import CronParser from 'cron-parser';
|
||||
import SecondUI from './tabs/SecondUI.vue';
|
||||
import MinuteUI from './tabs/MinuteUI.vue';
|
||||
import HourUI from './tabs/HourUI.vue';
|
||||
import DayUI from './tabs/DayUI.vue';
|
||||
import MonthUI from './tabs/MonthUI.vue';
|
||||
import WeekUI from './tabs/WeekUI.vue';
|
||||
import YearUI from './tabs/YearUI.vue';
|
||||
import { cronEmits, cronProps } from './easy.cron.data';
|
||||
import { dateFormat, simpleDebounce } from '/@/utils/common/compUtils';
|
||||
|
||||
const { prefixCls } = useDesign('easy-cron-inner');
|
||||
provide('prefixCls', prefixCls);
|
||||
const emit = defineEmits([...cronEmits]);
|
||||
const props = defineProps({ ...cronProps });
|
||||
const activeKey = ref(props.hideSecond ? 'minute' : 'second');
|
||||
const second = ref('*');
|
||||
const minute = ref('*');
|
||||
const hour = ref('*');
|
||||
const day = ref('*');
|
||||
const month = ref('*');
|
||||
const week = ref('?');
|
||||
const year = ref('*');
|
||||
const inputValues = reactive({
|
||||
second: '',
|
||||
minute: '',
|
||||
hour: '',
|
||||
day: '',
|
||||
month: '',
|
||||
week: '',
|
||||
year: '',
|
||||
cron: '',
|
||||
});
|
||||
const preTimeList = ref('执行预览,会忽略年份参数。');
|
||||
|
||||
// cron表达式
|
||||
const cronValueInner = computed(() => {
|
||||
let result: string[] = [];
|
||||
if (!props.hideSecond) {
|
||||
result.push(second.value ? second.value : '*');
|
||||
}
|
||||
result.push(minute.value ? minute.value : '*');
|
||||
result.push(hour.value ? hour.value : '*');
|
||||
result.push(day.value ? day.value : '*');
|
||||
result.push(month.value ? month.value : '*');
|
||||
result.push(week.value ? week.value : '?');
|
||||
if (!props.hideYear && !props.hideSecond) result.push(year.value ? year.value : '*');
|
||||
return result.join(' ');
|
||||
});
|
||||
// 不含年
|
||||
const cronValueNoYear = computed(() => {
|
||||
const v = cronValueInner.value;
|
||||
if (props.hideYear || props.hideSecond) return v;
|
||||
const vs = v.split(' ');
|
||||
if (vs.length >= 6) {
|
||||
// 转成 Quartz 的规则
|
||||
vs[5] = convertWeekToQuartz(vs[5]);
|
||||
}
|
||||
return vs.slice(0, vs.length - 1).join(' ');
|
||||
});
|
||||
const calTriggerList = simpleDebounce(calTriggerListInner, 500);
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(newVal) => {
|
||||
if (newVal === cronValueInner.value) {
|
||||
return;
|
||||
}
|
||||
formatValue();
|
||||
}
|
||||
);
|
||||
|
||||
watch(cronValueInner, (newValue) => {
|
||||
calTriggerList();
|
||||
emitValue(newValue);
|
||||
assignInput();
|
||||
});
|
||||
|
||||
// watch(minute, () => {
|
||||
// if (second.value === '*') {
|
||||
// second.value = '0'
|
||||
// }
|
||||
// })
|
||||
// watch(hour, () => {
|
||||
// if (minute.value === '*') {
|
||||
// minute.value = '0'
|
||||
// }
|
||||
// })
|
||||
// watch(day, () => {
|
||||
// if (day.value !== '?' && hour.value === '*') {
|
||||
// hour.value = '0'
|
||||
// }
|
||||
// })
|
||||
// watch(week, () => {
|
||||
// if (week.value !== '?' && hour.value === '*') {
|
||||
// hour.value = '0'
|
||||
// }
|
||||
// })
|
||||
// watch(month, () => {
|
||||
// if (day.value === '?' && week.value === '*') {
|
||||
// week.value = '1'
|
||||
// } else if (week.value === '?' && day.value === '*') {
|
||||
// day.value = '1'
|
||||
// }
|
||||
// })
|
||||
// watch(year, () => {
|
||||
// if (month.value === '*') {
|
||||
// month.value = '1'
|
||||
// }
|
||||
// })
|
||||
|
||||
assignInput();
|
||||
formatValue();
|
||||
calTriggerListInner();
|
||||
|
||||
function assignInput() {
|
||||
inputValues.second = second.value;
|
||||
inputValues.minute = minute.value;
|
||||
inputValues.hour = hour.value;
|
||||
inputValues.day = day.value;
|
||||
inputValues.month = month.value;
|
||||
inputValues.week = week.value;
|
||||
inputValues.year = year.value;
|
||||
inputValues.cron = cronValueInner.value;
|
||||
}
|
||||
|
||||
function formatValue() {
|
||||
if (!props.value) return;
|
||||
const values = props.value.split(' ').filter((item) => !!item);
|
||||
if (!values || values.length <= 0) return;
|
||||
let i = 0;
|
||||
if (!props.hideSecond) second.value = values[i++];
|
||||
if (values.length > i) minute.value = values[i++];
|
||||
if (values.length > i) hour.value = values[i++];
|
||||
if (values.length > i) day.value = values[i++];
|
||||
if (values.length > i) month.value = values[i++];
|
||||
if (values.length > i) week.value = values[i++];
|
||||
if (values.length > i) year.value = values[i];
|
||||
assignInput();
|
||||
}
|
||||
|
||||
// Quartz 的规则:
|
||||
// 1 = 周日,2 = 周一,3 = 周二,4 = 周三,5 = 周四,6 = 周五,7 = 周六
|
||||
function convertWeekToQuartz(week: string) {
|
||||
let convert = (v: string) => {
|
||||
if (v === '0') {
|
||||
return '1';
|
||||
}
|
||||
if (v === '1') {
|
||||
return '0';
|
||||
}
|
||||
return (Number.parseInt(v) - 1).toString();
|
||||
};
|
||||
// 匹配示例 1-7 or 1/7
|
||||
let patten1 = /^([0-7])([-/])([0-7])$/;
|
||||
// 匹配示例 1,4,7
|
||||
let patten2 = /^([0-7])(,[0-7])+$/;
|
||||
if (/^[0-7]$/.test(week)) {
|
||||
return convert(week);
|
||||
} else if (patten1.test(week)) {
|
||||
return week.replace(patten1, ($0, before, separator, after) => {
|
||||
if (separator === '/') {
|
||||
return convert(before) + separator + after;
|
||||
} else {
|
||||
return convert(before) + separator + convert(after);
|
||||
}
|
||||
});
|
||||
} else if (patten2.test(week)) {
|
||||
return week
|
||||
.split(',')
|
||||
.map((v) => convert(v))
|
||||
.join(',');
|
||||
}
|
||||
return week;
|
||||
}
|
||||
|
||||
function calTriggerListInner() {
|
||||
// 设置了回调函数
|
||||
if (props.remote) {
|
||||
props.remote(cronValueInner.value, +new Date(), (v) => {
|
||||
preTimeList.value = v;
|
||||
});
|
||||
return;
|
||||
}
|
||||
const format = 'yyyy-MM-dd hh:mm:ss';
|
||||
const options = {
|
||||
currentDate: dateFormat(new Date(), format),
|
||||
};
|
||||
const iter = CronParser.parseExpression(cronValueNoYear.value, options);
|
||||
const result: string[] = [];
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
result.push(dateFormat(new Date(iter.next() as any), format));
|
||||
}
|
||||
preTimeList.value = result.length > 0 ? result.join('\n') : '无执行时间';
|
||||
}
|
||||
|
||||
function onInputBlur() {
|
||||
second.value = inputValues.second;
|
||||
minute.value = inputValues.minute;
|
||||
hour.value = inputValues.hour;
|
||||
day.value = inputValues.day;
|
||||
month.value = inputValues.month;
|
||||
week.value = inputValues.week;
|
||||
year.value = inputValues.year;
|
||||
}
|
||||
|
||||
function onInputCronBlur(event) {
|
||||
emitValue(event.target.value);
|
||||
}
|
||||
|
||||
function emitValue(value) {
|
||||
emit('change', value);
|
||||
emit('update:value', value);
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
@import 'easy.cron.inner';
|
||||
</style>
|
||||
@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div :class="`${prefixCls}`">
|
||||
<a-input :placeholder="placeholder" v-model:value="editCronValue" :disabled="disabled">
|
||||
<template #addonAfter>
|
||||
<a class="open-btn" :disabled="disabled ? 'disabled' : null" @click="showConfigModal">
|
||||
<Icon icon="ant-design:setting-outlined" />
|
||||
<span>选择</span>
|
||||
</a>
|
||||
</template>
|
||||
</a-input>
|
||||
<EasyCronModal
|
||||
@register="registerModal"
|
||||
v-model:value="editCronValue"
|
||||
:exeStartTime="exeStartTime"
|
||||
:hideYear="hideYear"
|
||||
:remote="remote"
|
||||
:hideSecond="hideSecond"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import Icon from '/@/components/Icon/src/Icon.vue';
|
||||
import EasyCronModal from './EasyCronModal.vue';
|
||||
import { cronEmits, cronProps } from './easy.cron.data';
|
||||
|
||||
const { prefixCls } = useDesign('easy-cron-input');
|
||||
const emit = defineEmits([...cronEmits]);
|
||||
const props = defineProps({
|
||||
...cronProps,
|
||||
placeholder: propTypes.string.def('请输入cron表达式'),
|
||||
exeStartTime: propTypes.oneOfType([propTypes.number, propTypes.string, propTypes.object]).def(0),
|
||||
});
|
||||
const [registerModal, { openModal }] = useModal();
|
||||
const editCronValue = ref(props.value);
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(newVal) => {
|
||||
if (newVal !== editCronValue.value) {
|
||||
editCronValue.value = newVal;
|
||||
}
|
||||
}
|
||||
);
|
||||
watch(editCronValue, (newVal) => {
|
||||
emit('change', newVal);
|
||||
emit('update:value', newVal);
|
||||
});
|
||||
|
||||
function showConfigModal() {
|
||||
if (!props.disabled) {
|
||||
openModal();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@import 'easy.cron.input';
|
||||
</style>
|
||||
@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<BasicModal @register="registerModal" title="Cron表达式" width="800px" @ok="onOk">
|
||||
<EasyCron v-bind="attrs" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import EasyCron from './EasyCronInner.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'EasyCronModal',
|
||||
inheritAttrs: false,
|
||||
components: { BasicModal, EasyCron },
|
||||
setup() {
|
||||
const attrs = useAttrs();
|
||||
const [registerModal, { closeModal }] = useModalInner();
|
||||
|
||||
function onOk() {
|
||||
closeModal();
|
||||
}
|
||||
|
||||
return { attrs, registerModal, onOk };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 知行合一
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@ -0,0 +1,10 @@
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
|
||||
export const cronEmits = ['change', 'update:value'];
|
||||
export const cronProps = {
|
||||
value: propTypes.string.def(''),
|
||||
disabled: propTypes.bool.def(false),
|
||||
hideSecond: propTypes.bool.def(false),
|
||||
hideYear: propTypes.bool.def(false),
|
||||
remote: propTypes.func,
|
||||
};
|
||||
@ -0,0 +1,59 @@
|
||||
//noinspection LessUnresolvedVariable
|
||||
@prefix-cls: ~'@{namespace}-easy-cron-inner';
|
||||
|
||||
.@{prefix-cls} {
|
||||
.content {
|
||||
.ant-checkbox-wrapper + .ant-checkbox-wrapper {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-config-list {
|
||||
text-align: left;
|
||||
margin: 0 10px 10px 10px;
|
||||
|
||||
.item {
|
||||
margin-top: 5px;
|
||||
font-size: 14px;
|
||||
|
||||
span {
|
||||
padding: 0 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.choice {
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.w60 {
|
||||
width: 60px;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.w80 {
|
||||
width: 80px;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.list-check-item {
|
||||
padding: 1px 3px;
|
||||
width: 4em;
|
||||
}
|
||||
|
||||
.list-cn .list-check-item {
|
||||
width: 5em;
|
||||
}
|
||||
|
||||
.tip-info {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.allow-click {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
//noinspection LessUnresolvedVariable
|
||||
@prefix-cls: ~'@{namespace}-easy-cron-input';
|
||||
|
||||
.@{prefix-cls} {
|
||||
a.open-btn {
|
||||
cursor: pointer;
|
||||
|
||||
.app-iconify {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
right: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
// 原开源项目地址:https://gitee.com/toktok/easy-cron
|
||||
|
||||
export { default as JEasyCron } from './EasyCronInput.vue';
|
||||
export { default as JEasyCronInner } from './EasyCronInner.vue';
|
||||
export { default as JEasyCronModal } from './EasyCronModal.vue';
|
||||
export { default as JCronValidator } from './validator';
|
||||
@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div :class="`${prefixCls}-config-list`">
|
||||
<a-radio-group v-model:value="type">
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.unset" v-bind="beforeRadioAttrs">不设置</a-radio>
|
||||
<span class="tip-info">日和周只能设置其中之一</span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每日</a-radio>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
|
||||
<span> 从 </span>
|
||||
<InputNumber v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
|
||||
<span> 日 至 </span>
|
||||
<InputNumber v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
|
||||
<span> 日 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
|
||||
<span> 从 </span>
|
||||
<InputNumber v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
|
||||
<span> 日开始,间隔 </span>
|
||||
<InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
|
||||
<span> 日 </span>
|
||||
</div>
|
||||
<!-- 工作日暂不支持,会报错,先隐藏了 -->
|
||||
<!-- <div class="item">-->
|
||||
<!-- <a-radio :value="TypeEnum.work" v-bind="beforeRadioAttrs">工作日</a-radio>-->
|
||||
<!-- <span> 本月 </span>-->
|
||||
<!-- <InputNumber v-model:value="valueWork" v-bind="typeWorkAttrs" />-->
|
||||
<!-- <span> 日,最近的工作日 </span>-->
|
||||
<!-- </div>-->
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.last" v-bind="beforeRadioAttrs">最后一日</a-radio>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
|
||||
<div class="list">
|
||||
<a-checkbox-group v-model:value="valueList">
|
||||
<template v-for="i in specifyRange" :key="i">
|
||||
<a-checkbox :value="i" v-bind="typeSpecifyAttrs">{{ i }}</a-checkbox>
|
||||
</template>
|
||||
</a-checkbox-group>
|
||||
</div>
|
||||
</div>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, watch } from 'vue';
|
||||
import { InputNumber } from 'ant-design-vue';
|
||||
import { TypeEnum, useTabEmits, useTabProps, useTabSetup } from './useTabMixin';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DayUI',
|
||||
components: { InputNumber },
|
||||
props: useTabProps({
|
||||
defaultValue: '*',
|
||||
props: {
|
||||
week: { type: String, default: '?' },
|
||||
},
|
||||
}),
|
||||
emits: useTabEmits(),
|
||||
setup(props, context) {
|
||||
const disabledChoice = computed(() => {
|
||||
return (props.week && props.week !== '?') || props.disabled;
|
||||
});
|
||||
const setup = useTabSetup(props, context, {
|
||||
defaultValue: '*',
|
||||
valueWork: 1,
|
||||
minValue: 1,
|
||||
maxValue: 31,
|
||||
valueRange: { start: 1, end: 31 },
|
||||
valueLoop: { start: 1, interval: 1 },
|
||||
disabled: disabledChoice,
|
||||
});
|
||||
const typeWorkAttrs = computed(() => ({
|
||||
disabled: setup.type.value !== TypeEnum.work || props.disabled || disabledChoice.value,
|
||||
...setup.inputNumberAttrs.value,
|
||||
}));
|
||||
|
||||
watch(
|
||||
() => props.week,
|
||||
() => {
|
||||
setup.updateValue(disabledChoice.value ? '?' : setup.computeValue.value);
|
||||
}
|
||||
);
|
||||
|
||||
return { ...setup, typeWorkAttrs };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div :class="`${prefixCls}-config-list`">
|
||||
<a-radio-group v-model:value="type">
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每时</a-radio>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
|
||||
<span> 从 </span>
|
||||
<InputNumber v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
|
||||
<span> 时 至 </span>
|
||||
<InputNumber v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
|
||||
<span> 时 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
|
||||
<span> 从 </span>
|
||||
<InputNumber v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
|
||||
<span> 时开始,间隔 </span>
|
||||
<InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
|
||||
<span> 时 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
|
||||
<div class="list">
|
||||
<a-checkbox-group v-model:value="valueList">
|
||||
<template v-for="i in specifyRange" :key="i">
|
||||
<a-checkbox :value="i" v-bind="typeSpecifyAttrs">{{ i }}</a-checkbox>
|
||||
</template>
|
||||
</a-checkbox-group>
|
||||
</div>
|
||||
</div>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { InputNumber } from 'ant-design-vue';
|
||||
import { useTabProps, useTabEmits, useTabSetup } from './useTabMixin';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'HourUI',
|
||||
components: { InputNumber },
|
||||
props: useTabProps({
|
||||
defaultValue: '*',
|
||||
}),
|
||||
emits: useTabEmits(),
|
||||
setup(props, context) {
|
||||
return useTabSetup(props, context, {
|
||||
defaultValue: '*',
|
||||
minValue: 0,
|
||||
maxValue: 23,
|
||||
valueRange: { start: 0, end: 23 },
|
||||
valueLoop: { start: 0, interval: 1 },
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div :class="`${prefixCls}-config-list`">
|
||||
<a-radio-group v-model:value="type">
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每分</a-radio>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
|
||||
<span> 从 </span>
|
||||
<InputNumber v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
|
||||
<span> 分 至 </span>
|
||||
<InputNumber v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
|
||||
<span> 分 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
|
||||
<span> 从 </span>
|
||||
<InputNumber v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
|
||||
<span> 分开始,间隔 </span>
|
||||
<InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
|
||||
<span> 分 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
|
||||
<div class="list">
|
||||
<a-checkbox-group v-model:value="valueList">
|
||||
<template v-for="i in specifyRange" :key="i">
|
||||
<a-checkbox :value="i" v-bind="typeSpecifyAttrs">{{ i }}</a-checkbox>
|
||||
</template>
|
||||
</a-checkbox-group>
|
||||
</div>
|
||||
</div>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { InputNumber } from 'ant-design-vue';
|
||||
import { useTabProps, useTabEmits, useTabSetup } from './useTabMixin';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MinuteUI',
|
||||
components: { InputNumber },
|
||||
props: useTabProps({
|
||||
defaultValue: '*',
|
||||
}),
|
||||
emits: useTabEmits(),
|
||||
setup(props, context) {
|
||||
return useTabSetup(props, context, {
|
||||
defaultValue: '*',
|
||||
minValue: 0,
|
||||
maxValue: 59,
|
||||
valueRange: { start: 0, end: 59 },
|
||||
valueLoop: { start: 0, interval: 1 },
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div :class="`${prefixCls}-config-list`">
|
||||
<a-radio-group v-model:value="type">
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每月</a-radio>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
|
||||
<span> 从 </span>
|
||||
<InputNumber v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
|
||||
<span> 月 至 </span>
|
||||
<InputNumber v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
|
||||
<span> 月 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
|
||||
<span> 从 </span>
|
||||
<InputNumber v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
|
||||
<span> 月开始,间隔 </span>
|
||||
<InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
|
||||
<span> 月 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
|
||||
<div class="list">
|
||||
<a-checkbox-group v-model:value="valueList">
|
||||
<template v-for="i in specifyRange" :key="i">
|
||||
<a-checkbox :value="i" v-bind="typeSpecifyAttrs">{{ i }}</a-checkbox>
|
||||
</template>
|
||||
</a-checkbox-group>
|
||||
</div>
|
||||
</div>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { InputNumber } from 'ant-design-vue';
|
||||
import { useTabProps, useTabEmits, useTabSetup } from './useTabMixin';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MonthUI',
|
||||
components: { InputNumber },
|
||||
props: useTabProps({
|
||||
defaultValue: '*',
|
||||
}),
|
||||
emits: useTabEmits(),
|
||||
setup(props, context) {
|
||||
return useTabSetup(props, context, {
|
||||
defaultValue: '*',
|
||||
minValue: 1,
|
||||
maxValue: 12,
|
||||
valueRange: { start: 1, end: 12 },
|
||||
valueLoop: { start: 1, interval: 1 },
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div :class="`${prefixCls}-config-list`">
|
||||
<a-radio-group v-model:value="type">
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每秒</a-radio>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
|
||||
<span> 从 </span>
|
||||
<InputNumber v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
|
||||
<span> 秒 至 </span>
|
||||
<InputNumber v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
|
||||
<span> 秒 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
|
||||
<span> 从 </span>
|
||||
<InputNumber v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
|
||||
<span> 秒开始,间隔 </span>
|
||||
<InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
|
||||
<span> 秒 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
|
||||
<div class="list">
|
||||
<a-checkbox-group v-model:value="valueList">
|
||||
<template v-for="i in specifyRange" :key="i">
|
||||
<a-checkbox :value="i" v-bind="typeSpecifyAttrs">{{ i }}</a-checkbox>
|
||||
</template>
|
||||
</a-checkbox-group>
|
||||
</div>
|
||||
</div>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { InputNumber } from 'ant-design-vue';
|
||||
import { useTabProps, useTabEmits, useTabSetup } from './useTabMixin';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SecondUI',
|
||||
components: { InputNumber },
|
||||
props: useTabProps({
|
||||
defaultValue: '*',
|
||||
}),
|
||||
emits: useTabEmits(),
|
||||
setup(props, context) {
|
||||
return useTabSetup(props, context, {
|
||||
defaultValue: '*',
|
||||
minValue: 0,
|
||||
maxValue: 59,
|
||||
valueRange: { start: 0, end: 59 },
|
||||
valueLoop: { start: 0, interval: 1 },
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div :class="`${prefixCls}-config-list`">
|
||||
<a-radio-group v-model:value="type">
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.unset" v-bind="beforeRadioAttrs">不设置</a-radio>
|
||||
<span class="tip-info">日和周只能设置其中之一</span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
|
||||
<span> 从 </span>
|
||||
<a-select v-model:value="valueRange.start" :options="weekOptions" v-bind="typeRangeSelectAttrs" />
|
||||
<span> 至 </span>
|
||||
<a-select v-model:value="valueRange.end" :options="weekOptions" v-bind="typeRangeSelectAttrs" />
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
|
||||
<span> 从 </span>
|
||||
<a-select v-model:value="valueLoop.start" :options="weekOptions" v-bind="typeLoopSelectAttrs" />
|
||||
<span> 开始,间隔 </span>
|
||||
<InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
|
||||
<span> 天 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
|
||||
<div class="list list-cn">
|
||||
<a-checkbox-group v-model:value="valueList">
|
||||
<template v-for="opt in weekOptions" :key="i">
|
||||
<a-checkbox :value="opt.value" v-bind="typeSpecifyAttrs">{{ opt.label }}</a-checkbox>
|
||||
</template>
|
||||
</a-checkbox-group>
|
||||
</div>
|
||||
</div>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, watch, defineComponent } from 'vue';
|
||||
import { InputNumber } from 'ant-design-vue';
|
||||
import { useTabProps, useTabEmits, useTabSetup, TypeEnum } from './useTabMixin';
|
||||
|
||||
const WEEK_MAP_EN = {
|
||||
'1': 'SUN',
|
||||
'2': 'MON',
|
||||
'3': 'TUE',
|
||||
'4': 'WED',
|
||||
'5': 'THU',
|
||||
'6': 'FRI',
|
||||
'7': 'SAT',
|
||||
};
|
||||
|
||||
const WEEK_MAP_CN = {
|
||||
'1': '周日',
|
||||
'2': '周一',
|
||||
'3': '周二',
|
||||
'4': '周三',
|
||||
'5': '周四',
|
||||
'6': '周五',
|
||||
'7': '周六',
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'WeekUI',
|
||||
components: { InputNumber },
|
||||
props: useTabProps({
|
||||
defaultValue: '?',
|
||||
props: {
|
||||
day: { type: String, default: '*' },
|
||||
},
|
||||
}),
|
||||
emits: useTabEmits(),
|
||||
setup(props, context) {
|
||||
const disabledChoice = computed(() => {
|
||||
return (props.day && props.day !== '?') || props.disabled;
|
||||
});
|
||||
const setup = useTabSetup(props, context, {
|
||||
defaultType: TypeEnum.unset,
|
||||
defaultValue: '?',
|
||||
minValue: 1,
|
||||
maxValue: 7,
|
||||
// 0,7表示周日 1表示周一
|
||||
valueRange: { start: 1, end: 7 },
|
||||
valueLoop: { start: 2, interval: 1 },
|
||||
disabled: disabledChoice,
|
||||
});
|
||||
const weekOptions = computed(() => {
|
||||
let options: { label: string; value: number }[] = [];
|
||||
for (let weekKey of Object.keys(WEEK_MAP_CN)) {
|
||||
let weekName: string = WEEK_MAP_CN[weekKey];
|
||||
options.push({
|
||||
value: Number.parseInt(weekKey),
|
||||
label: weekName,
|
||||
});
|
||||
}
|
||||
return options;
|
||||
});
|
||||
|
||||
const typeRangeSelectAttrs = computed(() => ({
|
||||
class: ['w80'],
|
||||
disabled: setup.typeRangeAttrs.value.disabled,
|
||||
}));
|
||||
|
||||
const typeLoopSelectAttrs = computed(() => ({
|
||||
class: ['w80'],
|
||||
disabled: setup.typeLoopAttrs.value.disabled,
|
||||
}));
|
||||
|
||||
watch(
|
||||
() => props.day,
|
||||
() => {
|
||||
setup.updateValue(disabledChoice.value ? '?' : setup.computeValue.value);
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
...setup,
|
||||
weekOptions,
|
||||
typeLoopSelectAttrs,
|
||||
typeRangeSelectAttrs,
|
||||
WEEK_MAP_CN,
|
||||
WEEK_MAP_EN,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div :class="`${prefixCls}-config-list`">
|
||||
<a-radio-group v-model:value="type">
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每年</a-radio>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
|
||||
<span> 从 </span>
|
||||
<InputNumber class="w80" v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
|
||||
<span> 年 至 </span>
|
||||
<InputNumber class="w80" v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
|
||||
<span> 年 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
|
||||
<span> 从 </span>
|
||||
<InputNumber class="w80" v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
|
||||
<span> 年开始,间隔 </span>
|
||||
<InputNumber class="w80" v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
|
||||
<span> 年 </span>
|
||||
</div>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { InputNumber } from 'ant-design-vue';
|
||||
import { useTabProps, useTabEmits, useTabSetup } from './useTabMixin';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'YearUI',
|
||||
components: { InputNumber },
|
||||
props: useTabProps({
|
||||
defaultValue: '*',
|
||||
}),
|
||||
emits: useTabEmits(),
|
||||
setup(props, context) {
|
||||
const nowYear = new Date().getFullYear();
|
||||
return useTabSetup(props, context, {
|
||||
defaultValue: '*',
|
||||
minValue: 0,
|
||||
valueRange: { start: nowYear, end: nowYear + 100 },
|
||||
valueLoop: { start: nowYear, interval: 1 },
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,199 @@
|
||||
// 主要用于日和星期的互斥使用
|
||||
import { computed, inject, reactive, ref, unref, watch } from 'vue';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
|
||||
export enum TypeEnum {
|
||||
unset = 'UNSET',
|
||||
every = 'EVERY',
|
||||
range = 'RANGE',
|
||||
loop = 'LOOP',
|
||||
work = 'WORK',
|
||||
last = 'LAST',
|
||||
specify = 'SPECIFY',
|
||||
}
|
||||
|
||||
// use 公共 props
|
||||
export function useTabProps(options) {
|
||||
const defaultValue = options?.defaultValue ?? '?';
|
||||
return {
|
||||
value: propTypes.string.def(defaultValue),
|
||||
disabled: propTypes.bool.def(false),
|
||||
...options?.props,
|
||||
};
|
||||
}
|
||||
|
||||
// use 公共 emits
|
||||
export function useTabEmits() {
|
||||
return ['change', 'update:value'];
|
||||
}
|
||||
|
||||
// use 公共 setup
|
||||
export function useTabSetup(props, context, options) {
|
||||
const { emit } = context;
|
||||
const prefixCls = inject('prefixCls');
|
||||
const defaultValue = ref(options?.defaultValue ?? '?');
|
||||
// 类型
|
||||
const type = ref(options.defaultType ?? TypeEnum.every);
|
||||
const valueList = ref<any[]>([]);
|
||||
// 对于不同的类型,所定义的值也有所不同
|
||||
const valueRange = reactive(options.valueRange);
|
||||
const valueLoop = reactive(options.valueLoop);
|
||||
const valueWeek = reactive(options.valueWeek);
|
||||
const valueWork = ref(options.valueWork);
|
||||
const maxValue = ref(options.maxValue);
|
||||
const minValue = ref(options.minValue);
|
||||
|
||||
// 根据不同的类型计算出的value
|
||||
const computeValue = computed(() => {
|
||||
let valueArray: any[] = [];
|
||||
switch (type.value) {
|
||||
case TypeEnum.unset:
|
||||
valueArray.push('?');
|
||||
break;
|
||||
case TypeEnum.every:
|
||||
valueArray.push('*');
|
||||
break;
|
||||
case TypeEnum.range:
|
||||
valueArray.push(`${valueRange.start}-${valueRange.end}`);
|
||||
break;
|
||||
case TypeEnum.loop:
|
||||
valueArray.push(`${valueLoop.start}/${valueLoop.interval}`);
|
||||
break;
|
||||
case TypeEnum.work:
|
||||
valueArray.push(`${valueWork.value}W`);
|
||||
break;
|
||||
case TypeEnum.last:
|
||||
valueArray.push('L');
|
||||
break;
|
||||
case TypeEnum.specify:
|
||||
if (valueList.value.length === 0) {
|
||||
valueList.value.push(minValue.value);
|
||||
}
|
||||
valueArray.push(valueList.value.join(','));
|
||||
break;
|
||||
default:
|
||||
valueArray.push(defaultValue.value);
|
||||
break;
|
||||
}
|
||||
return valueArray.length > 0 ? valueArray.join('') : defaultValue.value;
|
||||
});
|
||||
// 指定值范围区间,介于最小值和最大值之间
|
||||
const specifyRange = computed(() => {
|
||||
let range: number[] = [];
|
||||
if (maxValue.value != null) {
|
||||
for (let i = minValue.value; i <= maxValue.value; i++) {
|
||||
range.push(i);
|
||||
}
|
||||
}
|
||||
return range;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(val) => {
|
||||
if (val !== computeValue.value) {
|
||||
parseValue(val);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(computeValue, (v) => updateValue(v));
|
||||
|
||||
function updateValue(value) {
|
||||
emit('change', value);
|
||||
emit('update:value', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* parseValue
|
||||
* @param value
|
||||
*/
|
||||
function parseValue(value) {
|
||||
if (value === computeValue.value) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!value || value === defaultValue.value) {
|
||||
type.value = TypeEnum.every;
|
||||
} else if (value.indexOf('?') >= 0) {
|
||||
type.value = TypeEnum.unset;
|
||||
} else if (value.indexOf('-') >= 0) {
|
||||
type.value = TypeEnum.range;
|
||||
const values = value.split('-');
|
||||
if (values.length >= 2) {
|
||||
valueRange.start = parseInt(values[0]);
|
||||
valueRange.end = parseInt(values[1]);
|
||||
}
|
||||
} else if (value.indexOf('/') >= 0) {
|
||||
type.value = TypeEnum.loop;
|
||||
const values = value.split('/');
|
||||
if (values.length >= 2) {
|
||||
valueLoop.start = value[0] === '*' ? 0 : parseInt(values[0]);
|
||||
valueLoop.interval = parseInt(values[1]);
|
||||
}
|
||||
} else if (value.indexOf('W') >= 0) {
|
||||
type.value = TypeEnum.work;
|
||||
const values = value.split('W');
|
||||
if (!values[0] && !isNaN(values[0])) {
|
||||
valueWork.value = parseInt(values[0]);
|
||||
}
|
||||
} else if (value.indexOf('L') >= 0) {
|
||||
type.value = TypeEnum.last;
|
||||
} else if (value.indexOf(',') >= 0 || !isNaN(value)) {
|
||||
type.value = TypeEnum.specify;
|
||||
valueList.value = value.split(',').map((item) => parseInt(item));
|
||||
} else {
|
||||
type.value = TypeEnum.every;
|
||||
}
|
||||
} catch (e) {
|
||||
type.value = TypeEnum.every;
|
||||
}
|
||||
}
|
||||
|
||||
const beforeRadioAttrs = computed(() => ({
|
||||
class: ['choice'],
|
||||
disabled: props.disabled || unref(options.disabled),
|
||||
}));
|
||||
const inputNumberAttrs = computed(() => ({
|
||||
class: ['w60'],
|
||||
max: maxValue.value,
|
||||
min: minValue.value,
|
||||
precision: 0,
|
||||
}));
|
||||
const typeRangeAttrs = computed(() => ({
|
||||
disabled: type.value !== TypeEnum.range || props.disabled || unref(options.disabled),
|
||||
...inputNumberAttrs.value,
|
||||
}));
|
||||
const typeLoopAttrs = computed(() => ({
|
||||
disabled: type.value !== TypeEnum.loop || props.disabled || unref(options.disabled),
|
||||
...inputNumberAttrs.value,
|
||||
}));
|
||||
const typeSpecifyAttrs = computed(() => ({
|
||||
disabled: type.value !== TypeEnum.specify || props.disabled || unref(options.disabled),
|
||||
class: ['list-check-item'],
|
||||
}));
|
||||
|
||||
return {
|
||||
type,
|
||||
TypeEnum,
|
||||
prefixCls,
|
||||
defaultValue,
|
||||
valueRange,
|
||||
valueLoop,
|
||||
valueWeek,
|
||||
valueList,
|
||||
valueWork,
|
||||
maxValue,
|
||||
minValue,
|
||||
computeValue,
|
||||
specifyRange,
|
||||
updateValue,
|
||||
parseValue,
|
||||
beforeRadioAttrs,
|
||||
inputNumberAttrs,
|
||||
typeRangeAttrs,
|
||||
typeLoopAttrs,
|
||||
typeSpecifyAttrs,
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
import CronParser from 'cron-parser';
|
||||
import type { ValidatorRule } from 'ant-design-vue/lib/form/interface';
|
||||
|
||||
const cronRule: ValidatorRule = {
|
||||
validator({}, value) {
|
||||
// 没填写就不校验
|
||||
if (!value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const values: string[] = value.split(' ').filter((item) => !!item);
|
||||
if (values.length > 7) {
|
||||
return Promise.reject('Cron表达式最多7项!');
|
||||
}
|
||||
// 检查第7项
|
||||
let val: string = value;
|
||||
if (values.length === 7) {
|
||||
const year = values[6];
|
||||
if (year !== '*' && year !== '?') {
|
||||
let yearValues: string[] = [];
|
||||
if (year.indexOf('-') >= 0) {
|
||||
yearValues = year.split('-');
|
||||
} else if (year.indexOf('/')) {
|
||||
yearValues = year.split('/');
|
||||
} else {
|
||||
yearValues = [year];
|
||||
}
|
||||
// 判断是否都是数字
|
||||
const checkYear = yearValues.some((item) => isNaN(Number(item)));
|
||||
if (checkYear) {
|
||||
return Promise.reject('Cron表达式参数[年]错误:' + year);
|
||||
}
|
||||
}
|
||||
// 取其中的前六项
|
||||
val = values.slice(0, 6).join(' ');
|
||||
}
|
||||
// 6位 没有年
|
||||
// 5位没有秒、年
|
||||
try {
|
||||
const iter = CronParser.parseExpression(val);
|
||||
iter.next();
|
||||
return Promise.resolve();
|
||||
} catch (e) {
|
||||
return Promise.reject('Cron表达式错误:' + e);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default cronRule.validator;
|
||||
@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<Tinymce v-bind="bindProps" @change="onChange" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, nextTick } from 'vue';
|
||||
|
||||
import { Tinymce } from '/@/components/Tinymce';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { Form } from 'ant-design-vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JEditor',
|
||||
// 不将 attrs 的属性绑定到 html 标签上
|
||||
inheritAttrs: false,
|
||||
components: { Tinymce },
|
||||
props: {
|
||||
value: propTypes.string.def(''),
|
||||
disabled: propTypes.bool.def(false),
|
||||
},
|
||||
emits: ['change', 'update:value'],
|
||||
setup(props, { emit, attrs }) {
|
||||
// 合并 props 和 attrs
|
||||
const bindProps = computed(() => Object.assign({}, props, attrs));
|
||||
const formItemContext = Form.useInjectFormItemContext();
|
||||
// value change 事件
|
||||
function onChange(value) {
|
||||
emit('change', value);
|
||||
emit('update:value', value);
|
||||
// update-begin--author:liaozhiyang---date:20240429---for:【QQYUN-9110】组件有值校验没消失
|
||||
nextTick(() => {
|
||||
formItemContext?.onFieldChange();
|
||||
});
|
||||
// update-end--author:liaozhiyang---date:20240429---for:【QQYUN-9110】组件有值校验没消失
|
||||
}
|
||||
|
||||
return {
|
||||
bindProps,
|
||||
onChange,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<a-tooltip placement="topLeft">
|
||||
<template #title>
|
||||
<span>{{ value }}</span>
|
||||
</template>
|
||||
{{ showText }}
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
|
||||
const props = defineProps({
|
||||
value: propTypes.oneOfType([propTypes.string, propTypes.number, propTypes.array]),
|
||||
length: propTypes.number.def(25),
|
||||
});
|
||||
//显示的文本
|
||||
const showText = computed(() =>
|
||||
props.value ? (props.value.length > props.length ? props.value.slice(0, props.length) + '...' : props.value) : props.value
|
||||
);
|
||||
</script>
|
||||
@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div :class="disabled ? 'jeecg-form-container-disabled' : ''">
|
||||
<fieldset :disabled="disabled">
|
||||
<slot></slot>
|
||||
</fieldset>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JFormContainer',
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.jeecg-form-container-disabled {
|
||||
cursor: not-allowed;
|
||||
|
||||
fieldset[disabled] {
|
||||
-ms-pointer-events: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ant-select {
|
||||
-ms-pointer-events: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ant-upload-select {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ant-upload-list {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
fieldset[disabled]{
|
||||
.anticon-delete{
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.jeecg-form-container-disabled fieldset[disabled] .ant-upload-list {
|
||||
-ms-pointer-events: auto !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.jeecg-form-container-disabled .ant-upload-list-item-actions .anticon-delete,
|
||||
.jeecg-form-container-disabled .ant-upload-list-item .anticon-close {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<div class="clearfix">
|
||||
<a-upload
|
||||
:listType="listType"
|
||||
accept="image/*"
|
||||
:multiple="multiple"
|
||||
:action="uploadUrl"
|
||||
:headers="headers"
|
||||
:data="{ biz: bizPath }"
|
||||
v-model:fileList="uploadFileList"
|
||||
:beforeUpload="beforeUpload"
|
||||
:disabled="disabled"
|
||||
@change="handleChange"
|
||||
@preview="handlePreview"
|
||||
>
|
||||
<div v-if="uploadVisible">
|
||||
<div v-if="listType == 'picture-card'">
|
||||
<LoadingOutlined v-if="loading" />
|
||||
<UploadOutlined v-else />
|
||||
<div class="ant-upload-text">{{ text }}</div>
|
||||
</div>
|
||||
<a-button v-if="listType == 'picture'" :disabled="disabled">
|
||||
<UploadOutlined></UploadOutlined>
|
||||
{{ text }}
|
||||
</a-button>
|
||||
</div>
|
||||
</a-upload>
|
||||
<a-modal :open="previewVisible" :footer="null" @cancel="handleCancel()">
|
||||
<img alt="example" style="width: 100%" :src="previewImage" />
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, ref, reactive, watchEffect, computed, unref, watch, onMounted, nextTick } from 'vue';
|
||||
import { LoadingOutlined, UploadOutlined } from '@ant-design/icons-vue';
|
||||
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { getFileAccessHttpUrl, getHeaders, getRandom } from '/@/utils/common/compUtils';
|
||||
import { uploadUrl } from '/@/api/common/api';
|
||||
import { getToken } from '/@/utils/auth';
|
||||
|
||||
const { createMessage, createErrorModal } = useMessage();
|
||||
export default defineComponent({
|
||||
name: 'JImageUpload',
|
||||
components: { LoadingOutlined, UploadOutlined },
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
//绑定值
|
||||
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
|
||||
//按钮文本
|
||||
listType: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'picture-card',
|
||||
},
|
||||
//按钮文本
|
||||
text: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '上传',
|
||||
},
|
||||
//这个属性用于控制文件上传的业务路径
|
||||
bizPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'temp',
|
||||
},
|
||||
//是否禁用
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
//上传数量
|
||||
fileMax: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
emits: ['options-change', 'change', 'update:value'],
|
||||
setup(props, { emit, refs }) {
|
||||
const emitData = ref<any[]>([]);
|
||||
const attrs = useAttrs();
|
||||
const [state] = useRuleFormItem(props, 'value', 'change', emitData);
|
||||
//获取文件名
|
||||
const getFileName = (path) => {
|
||||
if (path.lastIndexOf('\\') >= 0) {
|
||||
let reg = new RegExp('\\\\', 'g');
|
||||
path = path.replace(reg, '/');
|
||||
}
|
||||
return path.substring(path.lastIndexOf('/') + 1);
|
||||
};
|
||||
//token
|
||||
const headers = getHeaders();
|
||||
//上传状态
|
||||
const loading = ref<boolean>(false);
|
||||
//是否是初始化加载
|
||||
const initTag = ref<boolean>(true);
|
||||
//文件列表
|
||||
let uploadFileList = ref<any[]>([]);
|
||||
//预览图
|
||||
const previewImage = ref<string | undefined>('');
|
||||
//预览框状态
|
||||
const previewVisible = ref<boolean>(false);
|
||||
|
||||
//计算是否开启多图上传
|
||||
const multiple = computed(() => {
|
||||
return props['fileMax'] > 1 || props['fileMax'] === 0;
|
||||
});
|
||||
|
||||
//计算是否可以继续上传
|
||||
const uploadVisible = computed(() => {
|
||||
if (props['fileMax'] === 0) {
|
||||
return true;
|
||||
}
|
||||
return uploadFileList.value.length < props['fileMax'];
|
||||
});
|
||||
|
||||
/**
|
||||
* 监听value变化
|
||||
*/
|
||||
watch(
|
||||
() => props.value,
|
||||
(val, prevCount) => {
|
||||
//update-begin---author:liusq ---date:20230601 for:【issues/556】JImageUpload组件value赋初始值没显示图片------------
|
||||
if (val && val instanceof Array) {
|
||||
val = val.join(',');
|
||||
}
|
||||
if (initTag.value == true) {
|
||||
initFileList(val);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
//update-end---author:liusq ---date:20230601 for:【issues/556】JImageUpload组件value赋初始值没显示图片------------
|
||||
);
|
||||
|
||||
/**
|
||||
* 初始化文件列表
|
||||
*/
|
||||
function initFileList(paths) {
|
||||
if (!paths || paths.length == 0) {
|
||||
uploadFileList.value = [];
|
||||
return;
|
||||
}
|
||||
let files = [];
|
||||
let arr = paths.split(',');
|
||||
arr.forEach((value) => {
|
||||
let url = getFileAccessHttpUrl(value);
|
||||
files.push({
|
||||
uid: getRandom(10),
|
||||
name: getFileName(value),
|
||||
status: 'done',
|
||||
url: url,
|
||||
response: {
|
||||
status: 'history',
|
||||
message: value,
|
||||
},
|
||||
});
|
||||
});
|
||||
uploadFileList.value = files;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传前校验
|
||||
*/
|
||||
function beforeUpload(file) {
|
||||
let fileType = file.type;
|
||||
if (fileType.indexOf('image') < 0) {
|
||||
createMessage.info('请上传图片');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 文件上传结果回调
|
||||
*/
|
||||
function handleChange({ file, fileList, event }) {
|
||||
initTag.value = false;
|
||||
// update-begin--author:liaozhiyang---date:20231116---for:【issues/846】上传多个列表只显示一个
|
||||
// uploadFileList.value = fileList;
|
||||
if (file.status === 'error') {
|
||||
createMessage.error(`${file.name} 上传失败.`);
|
||||
}
|
||||
let fileUrls = [];
|
||||
let noUploadingFileCount = 0;
|
||||
if (file.status != 'uploading') {
|
||||
fileList.forEach((file) => {
|
||||
if (file.status === 'done') {
|
||||
fileUrls.push(file.response.message);
|
||||
}
|
||||
if (file.status != 'uploading') {
|
||||
noUploadingFileCount++;
|
||||
}
|
||||
});
|
||||
if (file.status === 'removed') {
|
||||
handleDelete(file);
|
||||
}
|
||||
if (noUploadingFileCount == fileList.length) {
|
||||
state.value = fileUrls.join(',');
|
||||
emit('update:value', fileUrls.join(','));
|
||||
// update-begin---author:wangshuai ---date:20221121 for:[issues/248]原生表单内使用图片组件,关闭弹窗图片组件值不会被清空------------
|
||||
nextTick(() => {
|
||||
initTag.value = true;
|
||||
});
|
||||
// update-end---author:wangshuai ---date:20221121 for:[issues/248]原生表单内使用图片组件,关闭弹窗图片组件值不会被清空------------
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20231116---for:【issues/846】上传多个列表只显示一个
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除图片
|
||||
*/
|
||||
function handleDelete(file) {
|
||||
//如有需要新增 删除逻辑
|
||||
console.log(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览图片
|
||||
*/
|
||||
function handlePreview(file) {
|
||||
previewImage.value = file.url || file.thumbUrl;
|
||||
previewVisible.value = true;
|
||||
}
|
||||
|
||||
function getAvatarView() {
|
||||
if (uploadFileList.length > 0) {
|
||||
let url = uploadFileList[0].url;
|
||||
return getFileAccessHttpUrl(url, null);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
previewVisible.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
attrs,
|
||||
previewImage,
|
||||
previewVisible,
|
||||
uploadFileList,
|
||||
multiple,
|
||||
headers,
|
||||
loading,
|
||||
uploadUrl,
|
||||
beforeUpload,
|
||||
uploadVisible,
|
||||
handlePreview,
|
||||
handleCancel,
|
||||
handleChange,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.ant-upload-select-picture-card i {
|
||||
font-size: 32px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.ant-upload-select-picture-card .ant-upload-text {
|
||||
margin-top: 8px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<div>
|
||||
<BasicModal v-bind="$attrs" @register="register" title="导入EXCEL" :width="600" @cancel="handleClose" :confirmLoading="uploading" destroyOnClose>
|
||||
<!--是否校验-->
|
||||
<div style="margin: 0 5px 5px" v-if="online">
|
||||
<span style="display: inline-block; height: 32px; line-height: 32px; vertical-align: middle">是否开启校验:</span>
|
||||
<span style="margin-left: 6px">
|
||||
<a-switch :checked="validateStatus == 1" @change="handleChangeValidateStatus" checked-children="是" un-checked-children="否" />
|
||||
</span>
|
||||
</div>
|
||||
<!--上传-->
|
||||
<a-upload name="file" accept=".xls,.xlsx" :multiple="true" :fileList="fileList" @remove="handleRemove" :beforeUpload="beforeUpload">
|
||||
<a-button preIcon="ant-design:upload-outlined">选择导入文件</a-button>
|
||||
</a-upload>
|
||||
<!--页脚-->
|
||||
<template #footer>
|
||||
<a-button @click="handleClose">关闭</a-button>
|
||||
<a-button type="primary" @click="handleImport" :disabled="uploadDisabled" :loading="uploading">{{
|
||||
uploading ? '上传中...' : '开始上传'
|
||||
}}</a-button>
|
||||
</template>
|
||||
</BasicModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, unref, watchEffect, computed } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { useGlobSetting } from '/@/hooks/setting';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { isObject } from '/@/utils/is';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JImportModal',
|
||||
components: {
|
||||
BasicModal,
|
||||
},
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: false,
|
||||
},
|
||||
biz: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: false,
|
||||
},
|
||||
//是否online导入
|
||||
online: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
emits: ['ok', 'register'],
|
||||
setup(props, { emit, refs }) {
|
||||
const { createMessage, createWarningModal } = useMessage();
|
||||
//注册弹框
|
||||
const [register, { closeModal }] = useModalInner((data) => {
|
||||
reset(data);
|
||||
});
|
||||
const glob = useGlobSetting();
|
||||
const attrs = useAttrs();
|
||||
const uploading = ref(false);
|
||||
//文件集合
|
||||
const fileList = ref([]);
|
||||
//上传url
|
||||
const uploadAction = ref('');
|
||||
const foreignKeys = ref('');
|
||||
//校验状态
|
||||
const validateStatus = ref(0);
|
||||
const getBindValue = Object.assign({}, unref(props), unref(attrs));
|
||||
//监听url
|
||||
watchEffect(() => {
|
||||
props.url && (uploadAction.value = `${glob.uploadUrl}${props.url}`);
|
||||
});
|
||||
//按钮disabled状态
|
||||
const uploadDisabled = computed(() => !(unref(fileList).length > 0));
|
||||
|
||||
//关闭方法
|
||||
function handleClose() {
|
||||
// update-begin--author:liaozhiyang---date:20231226---for:【QQYUN-7477】关闭弹窗清空内容(之前上传失败关闭后不会清除)
|
||||
closeModal();
|
||||
reset();
|
||||
// update-end--author:liaozhiyang---date:20231226---for:【QQYUN-7477】关闭弹窗清空内容(之前上传失败关闭后不会清除)
|
||||
}
|
||||
|
||||
//校验状态切换
|
||||
function handleChangeValidateStatus(checked) {
|
||||
validateStatus.value = !!checked ? 1 : 0;
|
||||
}
|
||||
|
||||
//移除上传文件
|
||||
function handleRemove(file) {
|
||||
const index = unref(fileList).indexOf(file);
|
||||
const newFileList = unref(fileList).slice();
|
||||
newFileList.splice(index, 1);
|
||||
fileList.value = newFileList;
|
||||
}
|
||||
|
||||
//上传前处理
|
||||
function beforeUpload(file) {
|
||||
fileList.value = [...unref(fileList), file];
|
||||
return false;
|
||||
}
|
||||
|
||||
//文件上传
|
||||
function handleImport() {
|
||||
let { biz, online } = props;
|
||||
const formData = new FormData();
|
||||
if (biz) {
|
||||
formData.append('isSingleTableImport', biz);
|
||||
}
|
||||
if (unref(foreignKeys) && unref(foreignKeys).length > 0) {
|
||||
formData.append('foreignKeys', unref(foreignKeys));
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20240429---for:【issues/6124】当用户没有【Online表单开发】页面的权限时用户无权导入从表数据
|
||||
if (isObject(foreignKeys.value)) {
|
||||
formData.append('foreignKeys', JSON.stringify(foreignKeys.value));
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240429---for:【issues/6124】当用户没有【Online表单开发】页面的权限时用户无权导入从表数据
|
||||
if (!!online) {
|
||||
formData.append('validateStatus', unref(validateStatus));
|
||||
}
|
||||
unref(fileList).forEach((file) => {
|
||||
formData.append('files[]', file);
|
||||
});
|
||||
uploading.value = true;
|
||||
|
||||
//TODO 请求怎样处理的问题
|
||||
let headers = {
|
||||
'Content-Type': 'multipart/form-data;boundary = ' + new Date().getTime(),
|
||||
};
|
||||
defHttp.post({ url: props.url, params: formData, headers }, { isTransformResponse: false }).then((res) => {
|
||||
uploading.value = false;
|
||||
if (res.success) {
|
||||
if (res.code == 201) {
|
||||
errorTip(res.message, res.result);
|
||||
} else {
|
||||
createMessage.success(res.message);
|
||||
}
|
||||
handleClose();
|
||||
reset();
|
||||
emit('ok');
|
||||
} else {
|
||||
createMessage.warning(res.message);
|
||||
}
|
||||
}).catch(() => {
|
||||
uploading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
//错误信息提示
|
||||
function errorTip(tipMessage, fileUrl) {
|
||||
let href = glob.uploadUrl + fileUrl;
|
||||
createWarningModal({
|
||||
title: '导入成功,但是有错误数据!',
|
||||
centered: false,
|
||||
content: `<div>
|
||||
<span>${tipMessage}</span><br/>
|
||||
<span>具体详情请<a href = ${href} target="_blank"> 点击下载 </a> </span>
|
||||
</div>`,
|
||||
});
|
||||
}
|
||||
|
||||
//重置
|
||||
function reset(arg?) {
|
||||
fileList.value = [];
|
||||
uploading.value = false;
|
||||
foreignKeys.value = arg;
|
||||
validateStatus.value = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
register,
|
||||
getBindValue,
|
||||
uploadDisabled,
|
||||
fileList,
|
||||
uploading,
|
||||
validateStatus,
|
||||
handleClose,
|
||||
handleChangeValidateStatus,
|
||||
handleRemove,
|
||||
beforeUpload,
|
||||
handleImport,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<a-input v-bind="getBindValue" v-model:value="showText" @input="backValue"></a-input>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, ref, watchEffect, unref, watch, computed } from 'vue';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { JInputTypeEnum } from '/@/enums/jeecgEnum.ts';
|
||||
import { omit } from 'lodash-es';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JInput',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
value: propTypes.string.def(''),
|
||||
type: propTypes.string.def(JInputTypeEnum.JINPUT_QUERY_LIKE),
|
||||
placeholder: propTypes.string.def(''),
|
||||
trim: propTypes.bool.def(false),
|
||||
},
|
||||
emits: ['change', 'update:value'],
|
||||
setup(props, { emit }) {
|
||||
const attrs = useAttrs();
|
||||
//表单值
|
||||
const showText = ref('');
|
||||
// update-begin--author:liaozhiyang---date:20231026---for:【issues/803】JIput updateSchema不生效
|
||||
//绑定属性
|
||||
const getBindValue = computed(() => {
|
||||
return omit(Object.assign({}, unref(props), unref(attrs)), ['value']);
|
||||
});
|
||||
// update-end--author:liaozhiyang---date:20231026---for:【issues/803】JIput updateSchema不生效
|
||||
//监听类型变化
|
||||
watch(
|
||||
() => props.type,
|
||||
(val) => {
|
||||
val && backValue({ target: { value: unref(showText) } });
|
||||
}
|
||||
);
|
||||
//监听value变化
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
initVal();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* 初始化数值
|
||||
*/
|
||||
function initVal() {
|
||||
if (!props.value) {
|
||||
showText.value = '';
|
||||
} else {
|
||||
let text = props.value;
|
||||
switch (props.type) {
|
||||
case JInputTypeEnum.JINPUT_QUERY_LIKE:
|
||||
//修复路由传参的值传送到jinput框被前后各截取了一位 #1336
|
||||
if (text.indexOf('*') != -1) {
|
||||
text = text.substring(1, text.length - 1);
|
||||
}
|
||||
break;
|
||||
case JInputTypeEnum.JINPUT_QUERY_NE:
|
||||
text = text.substring(1);
|
||||
break;
|
||||
case JInputTypeEnum.JINPUT_QUERY_GE:
|
||||
text = text.substring(2);
|
||||
break;
|
||||
case JInputTypeEnum.JINPUT_QUERY_LE:
|
||||
text = text.substring(2);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
showText.value = text;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回值
|
||||
*/
|
||||
function backValue(e) {
|
||||
let text = e?.target?.value ?? '';
|
||||
if (text && !!props.trim) {
|
||||
text = text.trim();
|
||||
}
|
||||
switch (props.type) {
|
||||
case JInputTypeEnum.JINPUT_QUERY_LIKE:
|
||||
text = '*' + text + '*';
|
||||
break;
|
||||
case JInputTypeEnum.JINPUT_QUERY_NE:
|
||||
text = '!' + text;
|
||||
break;
|
||||
case JInputTypeEnum.JINPUT_QUERY_GE:
|
||||
text = '>=' + text;
|
||||
break;
|
||||
case JInputTypeEnum.JINPUT_QUERY_LE:
|
||||
text = '<=' + text;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
emit('change', text);
|
||||
emit('update:value', text);
|
||||
}
|
||||
|
||||
return { showText, attrs, getBindValue, backValue };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<a-popover
|
||||
trigger="contextmenu"
|
||||
v-model:open="visible"
|
||||
:overlayClassName="`${prefixCls}-popover`"
|
||||
:getPopupContainer="getPopupContainer"
|
||||
:placement="position"
|
||||
>
|
||||
<template #title>
|
||||
<span :class="title ? 'title' : 'emptyTitle'">{{ title }}</span>
|
||||
<span style="float: right" title="关闭">
|
||||
<Icon icon="ant-design:close-outlined" @click="visible = false" />
|
||||
</span>
|
||||
</template>
|
||||
<template #content>
|
||||
<a-textarea ref="textareaRef" :value="innerValue" :disabled="disabled" :style="textareaStyle" v-bind="attrs" @input="onInputChange" />
|
||||
</template>
|
||||
<a-input :class="`${prefixCls}-input`" :value="innerValue" :disabled="disabled" v-bind="attrs" @change="onInputChange">
|
||||
<template #suffix>
|
||||
<Icon icon="ant-design:fullscreen-outlined" @click.stop="onShowPopup" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-popover>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import Icon from '/@/components/Icon/src/Icon.vue';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
|
||||
const { prefixCls } = useDesign('j-input-popup');
|
||||
const props = defineProps({
|
||||
// v-model:value
|
||||
value: propTypes.string.def(''),
|
||||
title: propTypes.string.def(''),
|
||||
// 弹出框显示位置
|
||||
position: propTypes.string.def('right'),
|
||||
width: propTypes.number.def(300),
|
||||
height: propTypes.number.def(150),
|
||||
disabled: propTypes.bool.def(false),
|
||||
// 弹出框挂载的元素ID
|
||||
popContainer: propTypes.oneOfType([propTypes.string, propTypes.func]).def(''),
|
||||
});
|
||||
const attrs = useAttrs();
|
||||
const emit = defineEmits(['change', 'update:value']);
|
||||
|
||||
const visible = ref<boolean>(false);
|
||||
const innerValue = ref<string>('');
|
||||
// textarea ref对象
|
||||
const textareaRef = ref();
|
||||
// textarea 样式
|
||||
const textareaStyle = computed(() => ({
|
||||
height: `${props.height}px`,
|
||||
width: `${props.width}px`,
|
||||
}));
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(value) => {
|
||||
if (value && value.length > 0) {
|
||||
innerValue.value = value;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
function onInputChange(event) {
|
||||
innerValue.value = event.target.value;
|
||||
emitValue(innerValue.value);
|
||||
}
|
||||
|
||||
async function onShowPopup() {
|
||||
visible.value = true;
|
||||
await nextTick();
|
||||
textareaRef.value?.focus();
|
||||
}
|
||||
|
||||
// 获取弹出框挂载的元素
|
||||
function getPopupContainer(node) {
|
||||
if (!props.popContainer) {
|
||||
return node?.parentNode;
|
||||
} else if (typeof props.popContainer === 'function') {
|
||||
return props.popContainer(node);
|
||||
} else {
|
||||
return document.getElementById(props.popContainer);
|
||||
}
|
||||
}
|
||||
|
||||
function emitValue(value) {
|
||||
emit('change', value);
|
||||
emit('update:value', value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
//noinspection LessUnresolvedVariable
|
||||
@prefix-cls: ~'@{namespace}-j-input-popup';
|
||||
|
||||
.@{prefix-cls} {
|
||||
&-popover {
|
||||
// update-begin--author:liaozhiyang---date:20240520---for:【TV360X-144】jVxetable中的多行文本组件当title没有时去掉多余的线
|
||||
.ant-popover-title:has(.emptyTitle) {
|
||||
border-bottom: none;
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240520---for:【TV360X-144】jVxetable中的多行文本组件当title没有时去掉多余的线
|
||||
}
|
||||
|
||||
&-input {
|
||||
.app-iconify {
|
||||
cursor: pointer;
|
||||
color: #666666;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="JInputSelect">
|
||||
<a-input-group compact>
|
||||
<a-select
|
||||
v-bind="$attrs"
|
||||
:placeholder="selectPlaceholder"
|
||||
v-if="selectLocation === 'left'"
|
||||
v-model:value="selectVal"
|
||||
@change="handleSelectChange"
|
||||
>
|
||||
<a-select-option v-for="item in options" :key="item.value">{{ item.label }}</a-select-option>
|
||||
</a-select>
|
||||
<a-input v-bind="$attrs" :placeholder="inputPlaceholder" v-model:value="inputVal" @change="handleInputChange" />
|
||||
<a-select
|
||||
v-bind="$attrs"
|
||||
:placeholder="selectPlaceholder"
|
||||
v-if="selectLocation === 'right'"
|
||||
v-model:value="selectVal"
|
||||
@change="handleSelectChange"
|
||||
>
|
||||
<a-select-option v-for="item in options" :key="item.value">{{ item.label }}</a-select-option>
|
||||
</a-select>
|
||||
</a-input-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="JInputSelect" lang="ts">
|
||||
import { ref, watchEffect } from 'vue';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
const props = defineProps({
|
||||
value: propTypes.string.def(''),
|
||||
options: propTypes.array.def([]),
|
||||
selectLocation: propTypes.oneOf(['left', 'right']).def('right'),
|
||||
selectPlaceholder: propTypes.string.def(''),
|
||||
inputPlaceholder: propTypes.string.def(''),
|
||||
});
|
||||
const emit = defineEmits(['update:value', 'change']);
|
||||
const selectVal = ref<string>();
|
||||
const inputVal = ref<string>();
|
||||
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const val = e.target.value;
|
||||
setSelectValByInputVal(val);
|
||||
emits(val);
|
||||
};
|
||||
const handleSelectChange = (val) => {
|
||||
inputVal.value = val;
|
||||
emits(val);
|
||||
};
|
||||
const setSelectValByInputVal = (val) => {
|
||||
const findItem = props.options.find((item) => item.value === val);
|
||||
if (findItem) {
|
||||
selectVal.value = val;
|
||||
} else {
|
||||
selectVal.value = undefined;
|
||||
}
|
||||
}
|
||||
watchEffect(() => {
|
||||
inputVal.value = props.value;
|
||||
setSelectValByInputVal(props.value);
|
||||
});
|
||||
const emits = (val) => {
|
||||
emit('update:value', val);
|
||||
emit('change', val);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.JInputSelect {
|
||||
.ant-input-group {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<MarkDown v-bind="bindProps" @change="onChange" @get="onGetVditor" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, watch, nextTick } from 'vue';
|
||||
import { MarkDown } from '/@/components/Markdown';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { Form } from 'ant-design-vue';
|
||||
export default defineComponent({
|
||||
name: 'JMarkdownEditor',
|
||||
// 不将 attrs 的属性绑定到 html 标签上
|
||||
inheritAttrs: false,
|
||||
components: { MarkDown },
|
||||
props: {
|
||||
value: propTypes.string.def(''),
|
||||
disabled: propTypes.bool.def(false),
|
||||
},
|
||||
emits: ['change', 'update:value'],
|
||||
setup(props, { emit, attrs }) {
|
||||
// markdown 组件实例
|
||||
let mdRef: any = null;
|
||||
// vditor 组件实例
|
||||
let vditorRef: any = null;
|
||||
// 合并 props 和 attrs
|
||||
const bindProps = computed(() => Object.assign({}, props, attrs));
|
||||
const formItemContext = Form.useInjectFormItemContext();
|
||||
// 相当于 onMounted
|
||||
function onGetVditor(instance) {
|
||||
mdRef = instance;
|
||||
vditorRef = mdRef.getVditor();
|
||||
|
||||
// 监听禁用,切换编辑器禁用状态
|
||||
watch(
|
||||
() => props.disabled,
|
||||
(disabled) => (disabled ? vditorRef.disabled() : vditorRef.enable()),
|
||||
{ immediate: true }
|
||||
);
|
||||
}
|
||||
|
||||
// value change 事件
|
||||
function onChange(value) {
|
||||
emit('change', value);
|
||||
emit('update:value', value);
|
||||
// update-begin--author:liaozhiyang---date:20240429---for:【QQYUN-9110】组件有值校验没消失
|
||||
nextTick(() => {
|
||||
formItemContext?.onFieldChange();
|
||||
});
|
||||
// update-end--author:liaozhiyang---date:20240429---for:【QQYUN-9110】组件有值校验没消失
|
||||
}
|
||||
|
||||
return {
|
||||
bindProps,
|
||||
|
||||
onChange,
|
||||
onGetVditor,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
@ -0,0 +1,180 @@
|
||||
<!--popup组件-->
|
||||
<template>
|
||||
<div class="JPopup components-input-demo-presuffix" v-if="avalid">
|
||||
<!--输入框-->
|
||||
<a-input @click="handleOpen" v-model:value="showText" :placeholder="placeholder" readOnly v-bind="attrs">
|
||||
<template #prefix>
|
||||
<Icon icon="ant-design:cluster-outlined"></Icon>
|
||||
</template>
|
||||
<!-- update-begin-author:taoyan date:2022-5-31 for: VUEN-1157 popup 选中后,有两个清除图标;后边这个清除,只是把输入框中数据清除,实际值并没有清除 -->
|
||||
<!-- <template #suffix>
|
||||
<Icon icon="ant-design:close-circle-outlined" @click="handleEmpty" title="清空" v-if="showText"></Icon>
|
||||
</template>-->
|
||||
<!-- update-begin-author:taoyan date:2022-5-31 for: VUEN-1157 popup 选中后,有两个清除图标;后边这个清除,只是把输入框中数据清除,实际值并没有清除 -->
|
||||
</a-input>
|
||||
<!-- update-begin--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式 -->
|
||||
<a-form-item>
|
||||
<!--popup弹窗-->
|
||||
<JPopupOnlReportModal
|
||||
@register="regModal"
|
||||
:code="code"
|
||||
:multi="multi"
|
||||
:sorter="sorter"
|
||||
:groupId="uniqGroupId"
|
||||
:param="param"
|
||||
:showAdvancedButton="showAdvancedButton"
|
||||
@ok="callBack"
|
||||
:getContainer="getContainer"
|
||||
></JPopupOnlReportModal>
|
||||
</a-form-item>
|
||||
<!-- update-end--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式 -->
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import JPopupOnlReportModal from './modal/JPopupOnlReportModal.vue';
|
||||
import { defineComponent, ref, reactive, onMounted, watchEffect, watch, computed, unref } from 'vue';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JPopup',
|
||||
components: {
|
||||
JPopupOnlReportModal,
|
||||
},
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
code: propTypes.string.def(''),
|
||||
value: propTypes.string.def(''),
|
||||
sorter: propTypes.string.def(''),
|
||||
width: propTypes.number.def(1200),
|
||||
placeholder: propTypes.string.def('请选择'),
|
||||
multi: propTypes.bool.def(false),
|
||||
param: propTypes.object.def({}),
|
||||
spliter: propTypes.string.def(','),
|
||||
groupId: propTypes.string.def(''),
|
||||
formElRef: propTypes.object,
|
||||
setFieldsValue: propTypes.func,
|
||||
getContainer: propTypes.func,
|
||||
fieldConfig: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
showAdvancedButton: propTypes.bool.def(true),
|
||||
},
|
||||
emits: ['update:value', 'register', 'popUpChange', 'focus'],
|
||||
setup(props, { emit, refs }) {
|
||||
const { createMessage } = useMessage();
|
||||
const attrs = useAttrs();
|
||||
//pop是否展示
|
||||
const avalid = ref(true);
|
||||
const showText = ref('');
|
||||
//注册model
|
||||
const [regModal, { openModal }] = useModal();
|
||||
//表单值
|
||||
let {code, fieldConfig } = props;
|
||||
// update-begin--author:liaozhiyang---date:20230811---for:【issues/675】子表字段Popup弹框数据不更新
|
||||
//唯一分组groupId
|
||||
const uniqGroupId = computed(() => (props.groupId ? `${props.groupId}_${code}_${fieldConfig[0]['source']}_${fieldConfig[0]['target']}` : ''));
|
||||
// update-begin--author:liaozhiyang---date:20230811---for:【issues/675】子表字段Popup弹框数据不更新
|
||||
/**
|
||||
* 判断popup配置项是否正确
|
||||
*/
|
||||
onMounted(() => {
|
||||
if (props.fieldConfig.length == 0) {
|
||||
createMessage.error('popup参数未正确配置!');
|
||||
avalid.value = false;
|
||||
}
|
||||
});
|
||||
/**
|
||||
* 监听value数值
|
||||
*/
|
||||
watch(
|
||||
() => props.value,
|
||||
(val) => {
|
||||
showText.value = val && val.length > 0 ? val.split(props.spliter).join(',') : '';
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* 打开pop弹出框
|
||||
*/
|
||||
function handleOpen() {
|
||||
emit('focus');
|
||||
// update-begin--author:liaozhiyang---date:20240528---for:【TV360X-317】禁用后JPopup和JPopupdic还可以点击出弹窗
|
||||
!attrs.value.disabled && openModal(true);
|
||||
// update-end--author:liaozhiyang---date:20240528---for:【TV360X-317】禁用后JPopup和JPopupdic还可以点击出弹窗
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO 清空
|
||||
*/
|
||||
function handleEmpty() {
|
||||
showText.value = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 传值回调
|
||||
*/
|
||||
function callBack(rows) {
|
||||
let { fieldConfig } = props;
|
||||
//匹配popup设置的回调值
|
||||
let values = {};
|
||||
for (let item of fieldConfig) {
|
||||
let val = rows.map((row) => row[item.source]);
|
||||
// update-begin--author:liaozhiyang---date:20230831---for:【QQYUN-7535】数组只有一个且是number类型,join会改变值的类型为string
|
||||
val = val.length == 1 ? val[0] : val.join(',');
|
||||
// update-begin--author:liaozhiyang---date:20230831---for:【QQYUN-7535】数组只有一个且是number类型,join会改变值的类型为string
|
||||
item.target.split(',').forEach((target) => {
|
||||
values[target] = val;
|
||||
});
|
||||
}
|
||||
//传入表单示例方式赋值
|
||||
props.formElRef && props.formElRef.setFieldsValue(values);
|
||||
//传入赋值方法方式赋值
|
||||
props.setFieldsValue && props.setFieldsValue(values);
|
||||
// update-begin--author:liaozhiyang---date:20230831---for:【issues/5288】popup弹框,无法将选择的数据填充到自身
|
||||
// update-begin--author:liaozhiyang---date:20230811---for:【issues/5213】JPopup抛出change事件
|
||||
emit('popUpChange', values);
|
||||
// update-end--author:liaozhiyang---date:20230811---for:【issues/5213】JPopup抛出change事件
|
||||
// update-begin--author:liaozhiyang---date:20230831---for:【issues/5288】popup弹框,无法将选择的数据填充到自身
|
||||
}
|
||||
|
||||
return {
|
||||
showText,
|
||||
avalid,
|
||||
uniqGroupId,
|
||||
attrs,
|
||||
regModal,
|
||||
handleOpen,
|
||||
handleEmpty,
|
||||
callBack,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
// update-begin--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
|
||||
.JPopup {
|
||||
> .ant-form-item {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
|
||||
.components-input-demo-presuffix .anticon-close-circle {
|
||||
cursor: pointer;
|
||||
color: #ccc;
|
||||
transition: color 0.3s;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.components-input-demo-presuffix .anticon-close-circle:hover {
|
||||
color: #f5222d;
|
||||
}
|
||||
|
||||
.components-input-demo-presuffix .anticon-close-circle:active {
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,232 @@
|
||||
<!--popup组件-->
|
||||
<template>
|
||||
<div class="JPopupDict components-input-demo-presuffix">
|
||||
<!--输入框-->
|
||||
<a-select v-model:value="showText" v-bind="attrs" :mode="multi ? 'multiple' : ''" @click="handleOpen" readOnly :loading="loading">
|
||||
<a-select-option v-for="item in options" :value="item.value">{{ item.text }}</a-select-option>
|
||||
</a-select>
|
||||
<!-- update-begin--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式 -->
|
||||
<a-form-item>
|
||||
<!--popup弹窗-->
|
||||
<JPopupOnlReportModal
|
||||
@register="regModal"
|
||||
:code="code"
|
||||
:multi="multi"
|
||||
:sorter="sorter"
|
||||
:groupId="''"
|
||||
:param="param"
|
||||
@ok="callBack"
|
||||
:getContainer="getContainer"
|
||||
:showAdvancedButton="showAdvancedButton"
|
||||
/>
|
||||
</a-form-item>
|
||||
<!-- update-end--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式 -->
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import JPopupOnlReportModal from './modal/JPopupOnlReportModal.vue';
|
||||
import { defineComponent, ref, nextTick, watch, reactive, unref } from 'vue';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
//定义请求url信息
|
||||
const configUrl = reactive({
|
||||
getColumns: '/online/cgreport/api/getRpColumns/',
|
||||
getData: '/online/cgreport/api/getData/',
|
||||
});
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JPopupDict',
|
||||
components: {
|
||||
JPopupOnlReportModal,
|
||||
},
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
/**
|
||||
* 示例:demo,name,id
|
||||
* demo: online报表编码
|
||||
* name: online报表的字段,用户显示的label
|
||||
* id: online报表的字段,用于存储key
|
||||
*/
|
||||
dictCode: propTypes.string.def(''),
|
||||
value: propTypes.string.def(''),
|
||||
sorter: propTypes.string.def(''),
|
||||
multi: propTypes.bool.def(false),
|
||||
param: propTypes.object.def({}),
|
||||
spliter: propTypes.string.def(','),
|
||||
getContainer: propTypes.func,
|
||||
showAdvancedButton: propTypes.bool.def(true),
|
||||
},
|
||||
emits: ['update:value', 'register', 'change'],
|
||||
setup(props, { emit }) {
|
||||
const { createMessage } = useMessage();
|
||||
const attrs = useAttrs();
|
||||
const showText = ref<any>(props.multi ? [] : '');
|
||||
const options = ref<any>([]);
|
||||
const cgRpConfigId = ref('');
|
||||
const loading = ref(false);
|
||||
const code = props.dictCode.split(',')[0];
|
||||
const labelFiled = props.dictCode.split(',')[1];
|
||||
const valueFiled = props.dictCode.split(',')[2];
|
||||
if (!code || !valueFiled || !labelFiled) {
|
||||
createMessage.error('popupDict参数未正确配置!');
|
||||
}
|
||||
//注册model
|
||||
const [regModal, { openModal }] = useModal();
|
||||
|
||||
/**
|
||||
* 打开pop弹出框
|
||||
*/
|
||||
function handleOpen() {
|
||||
// update-begin--author:liaozhiyang---date:20240528---for:【TV360X-317】禁用后JPopup和JPopupdic还可以点击出弹窗
|
||||
!attrs.value.disabled && openModal(true);
|
||||
// update-end--author:liaozhiyang---date:20240528---for:【TV360X-317】禁用后JPopup和JPopupdic还可以点击出弹窗
|
||||
}
|
||||
/**
|
||||
* 监听value数值
|
||||
*/
|
||||
watch(
|
||||
() => props.value,
|
||||
(val) => {
|
||||
const callBack = () => {
|
||||
if (props.multi) {
|
||||
showText.value = val && val.length > 0 ? val.split(props.spliter) : [];
|
||||
} else {
|
||||
showText.value = val ?? '';
|
||||
}
|
||||
};
|
||||
if (props.value || props.defaultValue) {
|
||||
if (cgRpConfigId.value) {
|
||||
loadData({ callBack });
|
||||
} else {
|
||||
loadColumnsInfo({ callBack });
|
||||
}
|
||||
} else {
|
||||
callBack();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
watch(
|
||||
() => showText.value,
|
||||
(val) => {
|
||||
let result;
|
||||
if (props.multi) {
|
||||
result = val.join(',');
|
||||
} else {
|
||||
result = val;
|
||||
}
|
||||
nextTick(() => {
|
||||
emit('change', result);
|
||||
emit('update:value', result);
|
||||
});
|
||||
}
|
||||
);
|
||||
/**
|
||||
* 加载列信息
|
||||
*/
|
||||
function loadColumnsInfo({ callBack }) {
|
||||
loading.value = true;
|
||||
let url = `${configUrl.getColumns}${code}`;
|
||||
defHttp
|
||||
.get({ url }, { isTransformResponse: false, successMessageMode: 'none' })
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
cgRpConfigId.value = res.result.cgRpConfigId;
|
||||
loadData({ callBack });
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
loading.value = false;
|
||||
callBack?.();
|
||||
});
|
||||
}
|
||||
function loadData({ callBack }) {
|
||||
loading.value = true;
|
||||
let url = `${configUrl.getData}${unref(cgRpConfigId)}`;
|
||||
defHttp
|
||||
.get(
|
||||
{ url, params: { ['force_' + valueFiled]: props.value || props.defaultValue } },
|
||||
{ isTransformResponse: false, successMessageMode: 'none' }
|
||||
)
|
||||
.then((res) => {
|
||||
let data = res.result;
|
||||
if (data.records?.length) {
|
||||
options.value = data.records.map((item) => {
|
||||
return { value: item[valueFiled], text: item[labelFiled] };
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
callBack?.();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 传值回调
|
||||
*/
|
||||
function callBack(rows) {
|
||||
const dataOptions: any = [];
|
||||
const dataValue: any = [];
|
||||
let result;
|
||||
rows.forEach((item) => {
|
||||
dataOptions.push({ value: item[valueFiled], text: item[labelFiled] });
|
||||
dataValue.push(item[valueFiled]);
|
||||
});
|
||||
options.value = dataOptions;
|
||||
if (props.multi) {
|
||||
showText.value = dataValue;
|
||||
result = dataValue.join(props.spliter);
|
||||
} else {
|
||||
showText.value = dataValue[0];
|
||||
result = dataValue[0];
|
||||
}
|
||||
nextTick(() => {
|
||||
emit('change', result);
|
||||
emit('update:value', result);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
showText,
|
||||
attrs,
|
||||
regModal,
|
||||
handleOpen,
|
||||
callBack,
|
||||
code,
|
||||
options,
|
||||
loading,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
// update-begin--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
|
||||
.JPopupDict {
|
||||
> .ant-form-item {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
|
||||
.components-input-demo-presuffix {
|
||||
:deep(.ant-select-dropdown) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
.components-input-demo-presuffix .anticon-close-circle {
|
||||
cursor: pointer;
|
||||
color: #ccc;
|
||||
transition: color 0.3s;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.components-input-demo-presuffix .anticon-close-circle:hover {
|
||||
color: #f5222d;
|
||||
}
|
||||
|
||||
.components-input-demo-presuffix .anticon-close-circle:active {
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<a-range-picker v-model:value="rangeValue" @change="handleChange" :show-time="datetime" :placeholder="placeholder" :valueFormat="valueFormat"/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, ref, watch, computed } from 'vue';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { Form } from 'ant-design-vue';
|
||||
|
||||
const placeholder = ['开始日期', '结束日期']
|
||||
/**
|
||||
* 用于范围查询
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: "JRangeDate",
|
||||
props:{
|
||||
value: propTypes.string.def(''),
|
||||
datetime: propTypes.bool.def(false),
|
||||
placeholder: propTypes.string.def(''),
|
||||
},
|
||||
emits:['change', 'update:value'],
|
||||
setup(props, {emit}){
|
||||
const rangeValue = ref([])
|
||||
const formItemContext = Form.useInjectFormItemContext();
|
||||
|
||||
watch(()=>props.value, (val)=>{
|
||||
if(val){
|
||||
rangeValue.value = val.split(',')
|
||||
}else{
|
||||
rangeValue.value = []
|
||||
}
|
||||
}, {immediate: true});
|
||||
|
||||
const valueFormat = computed(()=>{
|
||||
if(props.datetime === true){
|
||||
return 'YYYY-MM-DD HH:mm:ss'
|
||||
}else{
|
||||
return 'YYYY-MM-DD'
|
||||
}
|
||||
});
|
||||
|
||||
function handleChange(arr){
|
||||
let str = ''
|
||||
if(arr && arr.length>0){
|
||||
if(arr[1] && arr[0]){
|
||||
str = arr.join(',')
|
||||
}
|
||||
}
|
||||
emit('change', str);
|
||||
emit('update:value', str);
|
||||
formItemContext.onFieldChange();
|
||||
}
|
||||
return {
|
||||
rangeValue,
|
||||
placeholder,
|
||||
valueFormat,
|
||||
handleChange
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<a-input-group>
|
||||
<a-input-number v-bind="attrs" :value="beginValue" style="width: calc(50% - 15px)" placeholder="最小值" @change="handleChangeBegin" />
|
||||
<a-input style="width: 30px; border-left: 0; pointer-events: none; background-color: #fff" placeholder="~" disabled />
|
||||
<a-input-number v-bind="attrs" :value="endValue" style="width: calc(50% - 15px); border-left: 0" placeholder="最大值" @change="handleChangeEnd" />
|
||||
</a-input-group>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 查询条件用-数值范围查询
|
||||
*/
|
||||
import { ref, watch } from 'vue';
|
||||
import { Form } from 'ant-design-vue';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
|
||||
export default {
|
||||
name: 'JRangeNumber',
|
||||
props: {
|
||||
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
|
||||
},
|
||||
emits: ['change', 'update:value', 'blur'],
|
||||
setup(props, { emit }) {
|
||||
const beginValue = ref('');
|
||||
const endValue = ref('');
|
||||
const attrs = useAttrs();
|
||||
const formItemContext = Form.useInjectFormItemContext();
|
||||
|
||||
function handleChangeBegin(e) {
|
||||
beginValue.value = e;
|
||||
emitArray();
|
||||
}
|
||||
|
||||
function handleChangeEnd(e) {
|
||||
endValue.value = e;
|
||||
emitArray();
|
||||
}
|
||||
|
||||
function emitArray() {
|
||||
let arr = [];
|
||||
let begin = beginValue.value || '';
|
||||
let end = endValue.value || '';
|
||||
arr.push(begin);
|
||||
arr.push(end);
|
||||
emit('change', arr);
|
||||
emit('update:value', arr);
|
||||
formItemContext.onFieldChange();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(val) => {
|
||||
if (val && val.length == 2) {
|
||||
beginValue.value = val[0];
|
||||
endValue.value = val[1];
|
||||
} else {
|
||||
beginValue.value = '';
|
||||
endValue.value = '';
|
||||
}
|
||||
}, {immediate: true}
|
||||
);
|
||||
|
||||
return {
|
||||
beginValue,
|
||||
endValue,
|
||||
handleChangeBegin,
|
||||
handleChangeEnd,
|
||||
attrs,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
// update-begin--author:liaozhiyang---date:20240607---for:【TV360X-214】范围查询控件没有根据配置格式化
|
||||
.ant-input-group {
|
||||
display: flex;
|
||||
.ant-input-number {
|
||||
&:first-child {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
&:last-child {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240607---for:【TV360X-214】范围查询控件没有根据配置格式化
|
||||
</style>
|
||||
@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<a-time-range-picker v-model:value="rangeValue" @change="handleChange" :placeholder="placeholder" :valueFormat="format" :format="format"/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, ref, watch } from 'vue';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { Form } from 'ant-design-vue';
|
||||
|
||||
const placeholder = ['开始时间', '结束时间']
|
||||
/**
|
||||
* 用于时间-time组件的范围查询
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: "JRangeTime",
|
||||
props:{
|
||||
value: propTypes.string.def(''),
|
||||
format: propTypes.string.def('HH:mm:ss'),
|
||||
placeholder: propTypes.string.def(''),
|
||||
},
|
||||
emits:['change', 'update:value'],
|
||||
setup(props, {emit}){
|
||||
const rangeValue = ref([])
|
||||
const formItemContext = Form.useInjectFormItemContext();
|
||||
|
||||
watch(()=>props.value, (val)=>{
|
||||
if(val){
|
||||
rangeValue.value = val.split(',')
|
||||
}else{
|
||||
rangeValue.value = []
|
||||
}
|
||||
}, {immediate: true});
|
||||
|
||||
|
||||
function handleChange(arr){
|
||||
let str = ''
|
||||
if(arr && arr.length>0){
|
||||
if(arr[1] && arr[0]){
|
||||
str = arr.join(',')
|
||||
}
|
||||
}
|
||||
emit('change', str);
|
||||
emit('update:value', str);
|
||||
formItemContext.onFieldChange();
|
||||
}
|
||||
return {
|
||||
rangeValue,
|
||||
placeholder,
|
||||
handleChange
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,378 @@
|
||||
<template>
|
||||
<!--异步字典下拉搜素-->
|
||||
<a-select
|
||||
v-if="async"
|
||||
v-bind="attrs"
|
||||
v-model:value="selectedAsyncValue"
|
||||
showSearch
|
||||
labelInValue
|
||||
allowClear
|
||||
:getPopupContainer="getParentContainer"
|
||||
:placeholder="placeholder"
|
||||
:filterOption="isDictTable ? false : filterOption"
|
||||
:notFoundContent="loading ? undefined : null"
|
||||
@focus="handleAsyncFocus"
|
||||
@search="loadData"
|
||||
@change="handleAsyncChange"
|
||||
>
|
||||
<template #notFoundContent>
|
||||
<a-spin size="small" />
|
||||
</template>
|
||||
<a-select-option v-for="d in options" :key="d.value" :value="d.value">{{ d.text }}</a-select-option>
|
||||
</a-select>
|
||||
<!--字典下拉搜素-->
|
||||
<a-select
|
||||
v-else
|
||||
v-model:value="selectedValue"
|
||||
v-bind="attrs"
|
||||
showSearch
|
||||
:getPopupContainer="getParentContainer"
|
||||
:placeholder="placeholder"
|
||||
:filterOption="filterOption"
|
||||
:notFoundContent="loading ? undefined : null"
|
||||
:dropdownAlign="{overflow: {adjustY: adjustY }}"
|
||||
@change="handleChange"
|
||||
>
|
||||
<template #notFoundContent>
|
||||
<a-spin v-if="loading" size="small" />
|
||||
</template>
|
||||
<a-select-option v-for="d in options" :key="d.value" :value="d.value">{{ d.text }}</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
import { defineComponent, PropType, ref, reactive, watchEffect, computed, unref, watch, onMounted } from 'vue';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { initDictOptions } from '/@/utils/dict/index';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { setPopContainer } from '/@/utils';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JSearchSelect',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
value: propTypes.oneOfType([propTypes.string, propTypes.number]),
|
||||
dict: propTypes.string,
|
||||
dictOptions: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
async: propTypes.bool.def(false),
|
||||
placeholder: propTypes.string,
|
||||
popContainer: propTypes.string,
|
||||
pageSize: propTypes.number.def(10),
|
||||
getPopupContainer: {
|
||||
type: Function,
|
||||
default: (node) => node?.parentNode,
|
||||
},
|
||||
//默认开启Y轴溢出位置调整,因此在可视空间不足时下拉框位置会自动上移,导致Select的输入框被遮挡。需要注意的是,默认情况是是可视空间,而不是所拥有的空间
|
||||
//update-begin-author:liusq date:2023-04-04 for:[issue/286]下拉搜索框遮挡问题
|
||||
adjustY:propTypes.bool.def(true),
|
||||
//update-end-author:liusq date:2023-04-04 for:[issue/286]下拉搜索框遮挡问题
|
||||
//是否在有值后立即触发change
|
||||
immediateChange: propTypes.bool.def(false),
|
||||
//update-begin-author:taoyan date:2022-8-15 for: VUEN-1971 【online 专项测试】关联记录和他表字段 1
|
||||
//支持传入查询参数,如排序信息
|
||||
params:{
|
||||
type: Object,
|
||||
default: ()=>{}
|
||||
},
|
||||
//update-end-author:taoyan date:2022-8-15 for: VUEN-1971 【online 专项测试】关联记录和他表字段 1
|
||||
},
|
||||
emits: ['change', 'update:value'],
|
||||
setup(props, { emit, refs }) {
|
||||
const options = ref<any[]>([]);
|
||||
const loading = ref(false);
|
||||
// update-begin--author:liaozhiyang---date:20231205---for:【issues/897】JSearchSelect组件添加class/style样式不生效
|
||||
const attrs = useAttrs({'excludeDefaultKeys': false});
|
||||
// update-end--author:liaozhiyang---date:20231205---for:【issues/897】JSearchSelect组件添加class/style样式不生效
|
||||
const selectedValue = ref([]);
|
||||
const selectedAsyncValue = ref([]);
|
||||
const lastLoad = ref(0);
|
||||
// 是否根据value加载text
|
||||
const loadSelectText = ref(true);
|
||||
|
||||
// 是否是字典表
|
||||
const isDictTable = computed(() => {
|
||||
if (props.dict) {
|
||||
return props.dict.split(',').length >= 2
|
||||
}
|
||||
return false;
|
||||
})
|
||||
|
||||
/**
|
||||
* 监听字典code
|
||||
*/
|
||||
watch(() => props.dict, () => {
|
||||
if (!props.dict) {
|
||||
return
|
||||
}
|
||||
if (isDictTable.value) {
|
||||
initDictTableData();
|
||||
} else {
|
||||
initDictCodeData();
|
||||
}
|
||||
}, {immediate: true});
|
||||
|
||||
/**
|
||||
* 监听value
|
||||
*/
|
||||
watch(
|
||||
() => props.value,
|
||||
(val) => {
|
||||
if (val || val === 0) {
|
||||
initSelectValue();
|
||||
} else {
|
||||
selectedValue.value = [];
|
||||
selectedAsyncValue.value = [];
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
/**
|
||||
* 监听dictOptions
|
||||
*/
|
||||
watch(
|
||||
() => props.dictOptions,
|
||||
(val) => {
|
||||
if (val && val.length >= 0) {
|
||||
options.value = [...val];
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
/**
|
||||
* 异步查询数据
|
||||
*/
|
||||
const loadData = debounce(async function loadData(value) {
|
||||
if (!isDictTable.value) {
|
||||
return;
|
||||
}
|
||||
lastLoad.value += 1;
|
||||
const currentLoad = unref(lastLoad);
|
||||
options.value = [];
|
||||
loading.value = true;
|
||||
let keywordInfo = getKeywordParam(value);
|
||||
//update-begin---author:chenrui ---date:2024/4/7 for:[QQYUN-8800]JSearchSelect的search事件在中文输入还没拼字成功时会触发,导致后端SQL注入 #6049------------
|
||||
keywordInfo = keywordInfo.replaceAll("'", '');
|
||||
//update-end---author:chenrui ---date:2024/4/7 for:[QQYUN-8800]JSearchSelect的search事件在中文输入还没拼字成功时会触发,导致后端SQL注入 #6049------------
|
||||
// 字典code格式:table,text,code
|
||||
defHttp
|
||||
.get({
|
||||
url: `/sys/dict/loadDict/${props.dict}`,
|
||||
params: { keyword: keywordInfo, pageSize: props.pageSize },
|
||||
})
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
if (res && res.length > 0) {
|
||||
if (currentLoad != unref(lastLoad)) {
|
||||
return;
|
||||
}
|
||||
options.value = res;
|
||||
}
|
||||
});
|
||||
}, 300);
|
||||
/**
|
||||
* 初始化value
|
||||
*/
|
||||
function initSelectValue() {
|
||||
//update-begin-author:taoyan date:2022-4-24 for: 下拉搜索组件每次选中值会触发value的监听事件,触发此方法,但是实际不需要
|
||||
if (loadSelectText.value === false) {
|
||||
loadSelectText.value = true;
|
||||
return;
|
||||
}
|
||||
//update-end-author:taoyan date:2022-4-24 for: 下拉搜索组件每次选中值会触发value的监听事件,触发此方法,但是实际不需要
|
||||
let { async, value, dict } = props;
|
||||
if (async) {
|
||||
if (!selectedAsyncValue || !selectedAsyncValue.key || selectedAsyncValue.key !== value) {
|
||||
defHttp.get({ url: `/sys/dict/loadDictItem/${dict}`, params: { key: value } }).then((res) => {
|
||||
if (res && res.length > 0) {
|
||||
let obj = {
|
||||
key: value,
|
||||
label: res,
|
||||
};
|
||||
selectedAsyncValue.value = { ...obj };
|
||||
//update-begin-author:taoyan date:2022-8-11 for: 值改变触发change事件--用于online关联记录配置页面
|
||||
if(props.immediateChange == true){
|
||||
emit('change', value);
|
||||
}
|
||||
//update-end-author:taoyan date:2022-8-11 for: 值改变触发change事件--用于online关联记录配置页面
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
selectedValue.value = value.toString();
|
||||
//update-begin-author:taoyan date:2022-8-11 for: 值改变触发change事件--用于online他表字段配置界面
|
||||
if(props.immediateChange == true){
|
||||
emit('change', value.toString());
|
||||
}
|
||||
//update-end-author:taoyan date:2022-8-11 for: 值改变触发change事件--用于online他表字段配置界面
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化字典下拉数据
|
||||
*/
|
||||
async function initDictTableData() {
|
||||
let { dict, async, dictOptions, pageSize } = props;
|
||||
if (!async) {
|
||||
//如果字典项集合有数据
|
||||
if (dictOptions && dictOptions.length > 0) {
|
||||
options.value = dictOptions;
|
||||
} else {
|
||||
//根据字典Code, 初始化字典数组
|
||||
let dictStr = '';
|
||||
if (dict) {
|
||||
let arr = dict.split(',');
|
||||
if (arr[0].indexOf('where') > 0) {
|
||||
let tbInfo = arr[0].split('where');
|
||||
dictStr = tbInfo[0].trim() + ',' + arr[1] + ',' + arr[2] + ',' + encodeURIComponent(tbInfo[1]);
|
||||
} else {
|
||||
dictStr = dict;
|
||||
}
|
||||
//根据字典Code, 初始化字典数组
|
||||
const dictData = await initDictOptions(dictStr);
|
||||
options.value = dictData;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!dict) {
|
||||
console.error('搜索组件未配置字典项');
|
||||
} else {
|
||||
//异步一开始也加载一点数据
|
||||
loading.value = true;
|
||||
let keywordInfo = getKeywordParam('');
|
||||
defHttp
|
||||
.get({
|
||||
url: `/sys/dict/loadDict/${dict}`,
|
||||
params: { pageSize: pageSize, keyword: keywordInfo },
|
||||
})
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
if (res && res.length > 0) {
|
||||
options.value = res;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询数据字典
|
||||
*/
|
||||
async function initDictCodeData() {
|
||||
options.value = await initDictOptions(props.dict);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步改变事件
|
||||
* */
|
||||
function handleChange(value) {
|
||||
selectedValue.value = value;
|
||||
callback();
|
||||
}
|
||||
/**
|
||||
* 异步改变事件
|
||||
* */
|
||||
function handleAsyncChange(selectedObj) {
|
||||
if (selectedObj) {
|
||||
selectedAsyncValue.value = selectedObj;
|
||||
selectedValue.value = selectedObj.key;
|
||||
} else {
|
||||
selectedAsyncValue.value = null;
|
||||
selectedValue.value = null;
|
||||
options.value = null;
|
||||
loadData('');
|
||||
}
|
||||
callback();
|
||||
// update-begin--author:liaozhiyang---date:20240524---for:【TV360X-426】下拉搜索设置了默认值,把查询条件删掉,再点击重置,没附上值
|
||||
// 点x清空时需要把loadSelectText设置true
|
||||
selectedObj ?? (loadSelectText.value = true);
|
||||
// update-end--author:liaozhiyang---date:20240524---for:【TV360X-426】下拉搜索设置了默认值,把查询条件删掉,再点击重置,没附上值
|
||||
}
|
||||
/**
|
||||
*回调方法
|
||||
* */
|
||||
function callback() {
|
||||
loadSelectText.value = false;
|
||||
emit('change', unref(selectedValue));
|
||||
emit('update:value', unref(selectedValue));
|
||||
}
|
||||
/**
|
||||
* 过滤选中option
|
||||
*/
|
||||
function filterOption(input, option) {
|
||||
//update-begin-author:taoyan date:2022-11-8 for: issues/218 所有功能表单的下拉搜索框搜索无效
|
||||
let value = '', label = '';
|
||||
try {
|
||||
value = option.value;
|
||||
label = option.children()[0].children;
|
||||
}catch (e) {
|
||||
console.log('获取下拉项失败', e)
|
||||
}
|
||||
let str = input.toLowerCase();
|
||||
return value.toLowerCase().indexOf(str) >= 0 || label.toLowerCase().indexOf(str) >= 0;
|
||||
//update-end-author:taoyan date:2022-11-8 for: issues/218 所有功能表单的下拉搜索框搜索无效
|
||||
}
|
||||
|
||||
function getParentContainer(node) {
|
||||
// update-begin-author:taoyan date:20220407 for: getPopupContainer一直有值 导致popContainer的逻辑永远走不进去,把它挪到前面判断
|
||||
if (props.popContainer) {
|
||||
// update-begin--author:liaozhiyang---date:20240517---for:【QQYUN-9339】有多个modal弹窗内都有下拉字典多选和下拉搜索组件时,打开另一个modal时组件的options不展示
|
||||
return setPopContainer(node, props.popContainer);
|
||||
// update-end--author:liaozhiyang---date:20240517---for:【QQYUN-9339】有多个modal弹窗内都有下拉字典多选和下拉搜索组件时,打开另一个modal时组件的options不展示
|
||||
} else {
|
||||
if (typeof props.getPopupContainer === 'function') {
|
||||
return props.getPopupContainer(node);
|
||||
} else {
|
||||
return node?.parentNode;
|
||||
}
|
||||
}
|
||||
// update-end-author:taoyan date:20220407 for: getPopupContainer一直有值 导致popContainer的逻辑永远走不进去,把它挪到前面判断
|
||||
}
|
||||
|
||||
//update-begin-author:taoyan date:2022-8-15 for: VUEN-1971 【online 专项测试】关联记录和他表字段 1
|
||||
//获取关键词参数 支持设置排序信息
|
||||
function getKeywordParam(text){
|
||||
// 如果设定了排序信息,需要写入排序信息,在关键词后加 [orderby:create_time,desc]
|
||||
if(props.params && props.params.column && props.params.order){
|
||||
let temp = text||''
|
||||
|
||||
//update-begin-author:taoyan date:2023-5-22 for: /issues/4905 表单生成器字段配置时,选择关联字段,在进行高级配置时,无法加载数据库列表,提示 Sgin签名校验错误! #4905
|
||||
temp = temp+'[orderby:'+props.params.column+','+props.params.order+']'
|
||||
return encodeURI(temp);
|
||||
//update-end-author:taoyan date:2023-5-22 for: /issues/4905 表单生成器字段配置时,选择关联字段,在进行高级配置时,无法加载数据库列表,提示 Sgin签名校验错误! #4905
|
||||
|
||||
}else{
|
||||
return text;
|
||||
}
|
||||
}
|
||||
//update-end-author:taoyan date:2022-8-15 for: VUEN-1971 【online 专项测试】关联记录和他表字段 1
|
||||
// update-begin--author:liaozhiyang---date:20240523---for:【TV360X-26】下拉搜索控件选中选项后再次点击下拉应该显示初始的下拉选项,而不是只展示选中结果
|
||||
const handleAsyncFocus = () => {
|
||||
options.value.length && initDictCodeData();
|
||||
attrs.onFocus?.();
|
||||
};
|
||||
// update-end--author:liaozhiyang---date:20240523---for:【TV360X-26】下拉搜索控件选中选项后再次点击下拉应该显示初始的下拉选项,而不是只展示选中结果
|
||||
return {
|
||||
attrs,
|
||||
options,
|
||||
loading,
|
||||
isDictTable,
|
||||
selectedValue,
|
||||
selectedAsyncValue,
|
||||
loadData: useDebounceFn(loadData, 800),
|
||||
getParentContainer,
|
||||
filterOption,
|
||||
handleChange,
|
||||
handleAsyncChange,
|
||||
handleAsyncFocus,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@ -0,0 +1,202 @@
|
||||
<!--部门选择组件-->
|
||||
<template>
|
||||
<div class="JSelectDept">
|
||||
<JSelectBiz @change="handleSelectChange" @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs"/>
|
||||
<!-- update-begin--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式 -->
|
||||
<a-form-item>
|
||||
<DeptSelectModal @register="regModal" @getSelectResult="setValue" v-bind="getBindValue" :multiple="multiple" @close="handleClose"/>
|
||||
</a-form-item>
|
||||
<!-- update-end--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式 -->
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import DeptSelectModal from './modal/DeptSelectModal.vue';
|
||||
import JSelectBiz from './base/JSelectBiz.vue';
|
||||
import { defineComponent, ref, reactive, watchEffect, watch, provide, unref, toRaw } from 'vue';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { SelectValue } from 'ant-design-vue/es/select';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JSelectDept',
|
||||
components: {
|
||||
DeptSelectModal,
|
||||
JSelectBiz,
|
||||
},
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
|
||||
// 是否允许多选,默认 true
|
||||
multiple: propTypes.bool.def(true),
|
||||
},
|
||||
emits: ['options-change', 'change', 'select', 'update:value'],
|
||||
setup(props, { emit, refs }) {
|
||||
const emitData = ref<any[]>();
|
||||
//注册model
|
||||
const [regModal, { openModal }] = useModal();
|
||||
//表单值
|
||||
// const [state] = useRuleFormItem(props, 'value', 'change', emitData);
|
||||
//下拉框选项值
|
||||
const selectOptions = ref<SelectValue>([]);
|
||||
//下拉框选中值
|
||||
let selectValues = reactive<Recordable>({
|
||||
value: [],
|
||||
});
|
||||
let tempSave: any = [];
|
||||
|
||||
// 是否正在加载回显数据
|
||||
const loadingEcho = ref<boolean>(false);
|
||||
//下发 selectOptions,xxxBiz组件接收
|
||||
provide('selectOptions', selectOptions);
|
||||
//下发 selectValues,xxxBiz组件接收
|
||||
provide('selectValues', selectValues);
|
||||
//下发 loadingEcho,xxxBiz组件接收
|
||||
provide('loadingEcho', loadingEcho);
|
||||
|
||||
const tag = ref(false);
|
||||
const attrs = useAttrs();
|
||||
|
||||
/**
|
||||
* 监听组件值
|
||||
*/
|
||||
watchEffect(() => {
|
||||
// update-begin--author:liaozhiyang---date:20240611---for:【TV360X-576】已选中了数据,再次选择打开弹窗点击取消,数据清空了(同步JSelectDept改法)
|
||||
//update-begin-author:liusq---date:2024-06-03--for: [TV360X-840]用户授权,没有选择,点取消,也会回显一个选过的用户
|
||||
tempSave = [];
|
||||
//update-end-author:liusq---date:2024-06-03--for:[TV360X-840]用户授权,没有选择,点取消,也会回显一个选过的用户
|
||||
// update-end--author:liaozhiyang---date:20240611---for:【TV360X-576】已选中了数据,再次选择打开弹窗点击取消,数据清空了(同步JSelectDept改法)
|
||||
props.value && initValue();
|
||||
});
|
||||
|
||||
//update-begin-author:liusq---date:20220609--for: 为了解决弹窗form初始化赋值问题 ---
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
initValue();
|
||||
}
|
||||
);
|
||||
//update-end-author:liusq---date:20220609--for: 为了解决弹窗form初始化赋值问题 ---
|
||||
/**
|
||||
* 监听selectValues变化
|
||||
*/
|
||||
// update-begin--author:liaozhiyang---date:20240527---for:【TV360X-414】部门设置了默认值,查询重置变成空了(同步JSelectUser组件改法)
|
||||
// watch(selectValues, () => {
|
||||
// if (selectValues) {
|
||||
// state.value = selectValues.value;
|
||||
// }
|
||||
// });
|
||||
// update-end--author:liaozhiyang---date:20240527---for:【TV360X-414】部门设置了默认值,查询重置变成空了(同步JSelectUser组件改法)
|
||||
/**
|
||||
* 监听selectOptions变化
|
||||
*/
|
||||
watch(selectOptions, () => {
|
||||
if (selectOptions) {
|
||||
emit('select', toRaw(unref(selectOptions)), toRaw(unref(selectValues)));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 打卡弹出框
|
||||
*/
|
||||
function handleOpen() {
|
||||
tag.value = true;
|
||||
openModal(true, {
|
||||
isUpdate: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字符串值转化为数组
|
||||
*/
|
||||
function initValue() {
|
||||
let value = props.value ? props.value : [];
|
||||
if (value && typeof value === 'string') {
|
||||
// state.value = value.split(',');
|
||||
selectValues.value = value.split(',');
|
||||
tempSave = value.split(',');
|
||||
} else {
|
||||
// 【VUEN-857】兼容数组(行编辑的用法问题)
|
||||
selectValues.value = value;
|
||||
tempSave = cloneDeep(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置下拉框的值
|
||||
*/
|
||||
function setValue(options, values) {
|
||||
selectOptions.value = options;
|
||||
//emitData.value = values.join(",");
|
||||
// state.value = values;
|
||||
selectValues.value = values;
|
||||
send(values);
|
||||
}
|
||||
const getBindValue = Object.assign({}, unref(props), unref(attrs));
|
||||
|
||||
// update-begin--author:liaozhiyang---date:20240527---for:【TV360X-414】部门设置了默认值,查询重置变成空了(同步JSelectUser组件改法)
|
||||
const handleClose = () => {
|
||||
if (tempSave.length) {
|
||||
selectValues.value = cloneDeep(tempSave);
|
||||
} else {
|
||||
send(tempSave);
|
||||
}
|
||||
};
|
||||
const handleSelectChange = (values) => {
|
||||
tempSave = cloneDeep(values);
|
||||
send(tempSave);
|
||||
};
|
||||
const send = (values) => {
|
||||
let result = typeof props.value == 'string' ? values.join(',') : values;
|
||||
emit('update:value', result);
|
||||
emit('change', result);
|
||||
};
|
||||
// update-end--author:liaozhiyang---date:20240527---for:【TV360X-414】部门设置了默认值,查询重置变成空了(同步JSelectUser组件改法)
|
||||
|
||||
return {
|
||||
// state,
|
||||
attrs,
|
||||
selectOptions,
|
||||
selectValues,
|
||||
loadingEcho,
|
||||
getBindValue,
|
||||
tag,
|
||||
regModal,
|
||||
setValue,
|
||||
handleOpen,
|
||||
handleClose,
|
||||
handleSelectChange,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
// update-begin--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
|
||||
.JSelectDept {
|
||||
> .ant-form-item {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
|
||||
.j-select-row {
|
||||
@width: 82px;
|
||||
|
||||
.left {
|
||||
width: calc(100% - @width - 8px);
|
||||
}
|
||||
|
||||
.right {
|
||||
width: @width;
|
||||
}
|
||||
|
||||
.full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-select-search__field) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<a-select v-bind="bindProps" @change="onChange" @search="onSearch" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { defineComponent, ref, watch, computed } from 'vue';
|
||||
|
||||
// 可以输入的下拉框(此组件暂时没有人用)
|
||||
export default defineComponent({
|
||||
name: 'JSelectInput',
|
||||
props: {
|
||||
options: propTypes.array.def(() => []),
|
||||
},
|
||||
emits: ['change', 'update:value'],
|
||||
setup(props, { emit, attrs }) {
|
||||
// 内部 options 选项
|
||||
const options = ref<any[]>([]);
|
||||
// 监听外部 options 变化,并覆盖内部 options
|
||||
watch(
|
||||
() => props.options,
|
||||
() => {
|
||||
options.value = [...props.options];
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
// 合并 props 和 attrs
|
||||
const bindProps: any = computed(() =>
|
||||
Object.assign(
|
||||
{
|
||||
showSearch: true,
|
||||
},
|
||||
props,
|
||||
attrs,
|
||||
{
|
||||
options: options.value,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
function onChange(...args: any[]) {
|
||||
deleteSearchAdd(args[0]);
|
||||
emit('change', ...args);
|
||||
emit('update:value', args[0]);
|
||||
}
|
||||
|
||||
function onSearch(value) {
|
||||
// 是否找到了对应的项,找不到则添加这一项
|
||||
let foundIt =
|
||||
options.value.findIndex((option) => {
|
||||
return option.value.toString() === value.toString();
|
||||
}) !== -1;
|
||||
// !!value :不添加空值
|
||||
if (!foundIt && !!value) {
|
||||
deleteSearchAdd(value);
|
||||
// searchAdd 是否是通过搜索添加的
|
||||
options.value.push({ value: value, searchAdd: true });
|
||||
//onChange(value,{ value })
|
||||
} else if (foundIt) {
|
||||
onChange(value);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除无用的因搜索(用户输入)而创建的项
|
||||
function deleteSearchAdd(value = '') {
|
||||
let indexes: any[] = [];
|
||||
options.value.forEach((option, index) => {
|
||||
if (option.searchAdd) {
|
||||
if ((option.value ?? '').toString() !== value.toString()) {
|
||||
indexes.push(index);
|
||||
}
|
||||
}
|
||||
});
|
||||
// 翻转删除数组中的项
|
||||
for (let index of indexes.reverse()) {
|
||||
options.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
bindProps,
|
||||
onChange,
|
||||
onSearch,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@ -0,0 +1,188 @@
|
||||
<!--字典下拉多选-->
|
||||
<template>
|
||||
<a-select
|
||||
:value="arrayValue"
|
||||
@change="onChange"
|
||||
mode="multiple"
|
||||
:filter-option="filterOption"
|
||||
:disabled="disabled"
|
||||
:placeholder="placeholder"
|
||||
allowClear
|
||||
:getPopupContainer="getParentContainer"
|
||||
>
|
||||
<a-select-option v-for="(item, index) in dictOptions" :key="index" :getPopupContainer="getParentContainer" :value="item.value">
|
||||
<span :class="[useDicColor && item.color ? 'colorText' : '']" :style="{ backgroundColor: `${useDicColor && item.color}` }">{{ item.text || item.label }}</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onMounted, ref, nextTick, watch } from 'vue';
|
||||
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { getDictItems } from '/@/api/common/api';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { setPopContainer } from '/@/utils';
|
||||
|
||||
const { createMessage, createErrorModal } = useMessage();
|
||||
export default defineComponent({
|
||||
name: 'JSelectMultiple',
|
||||
components: {},
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请选择',
|
||||
required: false,
|
||||
},
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: false,
|
||||
},
|
||||
triggerChange: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
spliter: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ',',
|
||||
},
|
||||
popContainer: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: false,
|
||||
},
|
||||
dictCode: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
useDicColor: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['options-change', 'change', 'input', 'update:value'],
|
||||
setup(props, { emit, refs }) {
|
||||
//console.info(props);
|
||||
const emitData = ref<any[]>([]);
|
||||
const arrayValue = ref<any[]>(!props.value ? [] : props.value.split(props.spliter));
|
||||
const dictOptions = ref<any[]>([]);
|
||||
const attrs = useAttrs();
|
||||
const [state, , , formItemContext] = useRuleFormItem(props, 'value', 'change', emitData);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.dictCode) {
|
||||
loadDictOptions();
|
||||
} else {
|
||||
dictOptions.value = props.options;
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(val) => {
|
||||
if (!val) {
|
||||
arrayValue.value = [];
|
||||
} else {
|
||||
arrayValue.value = props.value.split(props.spliter);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
//适用于 动态改变下拉选项的操作
|
||||
watch(()=>props.options, ()=>{
|
||||
if (props.dictCode) {
|
||||
// nothing to do
|
||||
} else {
|
||||
dictOptions.value = props.options;
|
||||
}
|
||||
});
|
||||
|
||||
function onChange(selectedValue) {
|
||||
if (props.triggerChange) {
|
||||
emit('change', selectedValue.join(props.spliter));
|
||||
emit('update:value', selectedValue.join(props.spliter));
|
||||
} else {
|
||||
emit('input', selectedValue.join(props.spliter));
|
||||
emit('update:value', selectedValue.join(props.spliter));
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20240429---for:【QQYUN-9110】组件有值校验没消失
|
||||
nextTick(() => {
|
||||
formItemContext?.onFieldChange();
|
||||
});
|
||||
// update-end--author:liaozhiyang---date:20240429---for:【QQYUN-9110】组件有值校验没消失
|
||||
}
|
||||
|
||||
function getParentContainer(node) {
|
||||
if (!props.popContainer) {
|
||||
return node?.parentNode;
|
||||
} else {
|
||||
// update-begin--author:liaozhiyang---date:20240517---for:【QQYUN-9339】有多个modal弹窗内都有下拉字典多选和下拉搜索组件时,打开另一个modal时组件的options不展示
|
||||
return setPopContainer(node, props.popContainer);
|
||||
// update-end--author:liaozhiyang---date:20240517---for:【QQYUN-9339】有多个modal弹窗内都有下拉字典多选和下拉搜索组件时,打开另一个modal时组件的options不展示
|
||||
}
|
||||
}
|
||||
|
||||
// 根据字典code查询字典项
|
||||
function loadDictOptions() {
|
||||
//update-begin-author:taoyan date:2022-6-21 for: 字典数据请求前将参数编码处理,但是不能直接编码,因为可能之前已经编码过了
|
||||
let temp = props.dictCode || '';
|
||||
if (temp.indexOf(',') > 0 && temp.indexOf(' ') > 0) {
|
||||
// 编码后 是不包含空格的
|
||||
temp = encodeURI(temp);
|
||||
}
|
||||
//update-end-author:taoyan date:2022-6-21 for: 字典数据请求前将参数编码处理,但是不能直接编码,因为可能之前已经编码过了
|
||||
getDictItems(temp).then((res) => {
|
||||
if (res) {
|
||||
dictOptions.value = res.map((item) => ({ value: item.value, label: item.text, color:item.color }));
|
||||
//console.info('res', dictOptions.value);
|
||||
} else {
|
||||
console.error('getDictItems error: : ', res);
|
||||
dictOptions.value = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//update-begin-author:taoyan date:2022-5-31 for: VUEN-1145 下拉多选,搜索时,查不到数据
|
||||
function filterOption(input, option) {
|
||||
return option.children()[0].children.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
}
|
||||
//update-end-author:taoyan date:2022-5-31 for: VUEN-1145 下拉多选,搜索时,查不到数据
|
||||
|
||||
return {
|
||||
state,
|
||||
attrs,
|
||||
dictOptions,
|
||||
onChange,
|
||||
arrayValue,
|
||||
getParentContainer,
|
||||
filterOption,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style scoped lang='less'>
|
||||
.colorText{
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
padding: 0 6px;
|
||||
border-radius: 8px;
|
||||
background-color: red;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,164 @@
|
||||
<!--职务选择组件-->
|
||||
<template>
|
||||
<div class="JSelectPosition">
|
||||
<JSelectBiz @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs"></JSelectBiz>
|
||||
<!-- update-begin--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式 -->
|
||||
<a-form-item>
|
||||
<PositionSelectModal @register="regModal" @getSelectResult="setValue" v-bind="getBindValue"></PositionSelectModal>
|
||||
</a-form-item>
|
||||
<!-- update-end--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式 -->
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import PositionSelectModal from './modal/PositionSelectModal.vue';
|
||||
import JSelectBiz from './base/JSelectBiz.vue';
|
||||
import { defineComponent, ref, reactive, watchEffect, watch, provide, computed, unref } from 'vue';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { SelectValue } from 'ant-design-vue/es/select';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JSelectPosition',
|
||||
components: {
|
||||
PositionSelectModal,
|
||||
JSelectBiz,
|
||||
},
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
|
||||
labelKey: {
|
||||
type: String,
|
||||
default: 'name',
|
||||
},
|
||||
rowKey: {
|
||||
type: String,
|
||||
default: 'id',
|
||||
},
|
||||
params: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
emits: ['options-change', 'change', 'update:value'],
|
||||
setup(props, { emit, refs }) {
|
||||
const emitData = ref<any[]>();
|
||||
//注册model
|
||||
const [regModal, { openModal }] = useModal();
|
||||
//表单值
|
||||
const [state] = useRuleFormItem(props, 'value', 'change', emitData);
|
||||
//下拉框选项值
|
||||
const selectOptions = ref<SelectValue>([]);
|
||||
//下拉框选中值
|
||||
let selectValues = reactive<object>({
|
||||
value: [],
|
||||
change: false,
|
||||
});
|
||||
// 是否正在加载回显数据
|
||||
const loadingEcho = ref<boolean>(false);
|
||||
//下发 selectOptions,xxxBiz组件接收
|
||||
provide('selectOptions', selectOptions);
|
||||
//下发 selectValues,xxxBiz组件接收
|
||||
provide('selectValues', selectValues);
|
||||
//下发 loadingEcho,xxxBiz组件接收
|
||||
provide('loadingEcho', loadingEcho);
|
||||
|
||||
const tag = ref(false);
|
||||
const attrs = useAttrs();
|
||||
|
||||
/**
|
||||
* 监听组件值
|
||||
*/
|
||||
watchEffect(() => {
|
||||
props.value && initValue();
|
||||
});
|
||||
|
||||
/**
|
||||
* 监听selectValues变化
|
||||
*/
|
||||
watch(selectValues, () => {
|
||||
if (selectValues) {
|
||||
state.value = selectValues.value;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 打卡弹出框
|
||||
*/
|
||||
function handleOpen() {
|
||||
tag.value = true;
|
||||
openModal(true, {
|
||||
isUpdate: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字符串值转化为数组
|
||||
*/
|
||||
function initValue() {
|
||||
let value = props.value ? props.value : [];
|
||||
if (value && typeof value === 'string' && value != 'null' && value != 'undefined') {
|
||||
state.value = value.split(',');
|
||||
selectValues.value = value.split(',');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置下拉框的值
|
||||
*/
|
||||
function setValue(options, values) {
|
||||
selectOptions.value = options;
|
||||
//emitData.value = values.join(",");
|
||||
state.value = values;
|
||||
selectValues.value = values;
|
||||
//update-begin-author:liusq date:20230517 for:选择职务组件v-model方式绑定值不生效
|
||||
emit('update:value', values.join(','));
|
||||
//update-begin-author:liusq date:20230517 for:选择职务组件v-model方式绑定值不生效
|
||||
|
||||
}
|
||||
|
||||
const getBindValue = Object.assign({}, unref(props), unref(attrs));
|
||||
return {
|
||||
state,
|
||||
getBindValue,
|
||||
attrs,
|
||||
selectOptions,
|
||||
selectValues,
|
||||
loadingEcho,
|
||||
tag,
|
||||
regModal,
|
||||
setValue,
|
||||
handleOpen,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
// update-begin--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
|
||||
.JSelectPosition {
|
||||
> .ant-form-item {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
|
||||
.j-select-row {
|
||||
@width: 82px;
|
||||
|
||||
.left {
|
||||
width: calc(100% - @width - 8px);
|
||||
}
|
||||
|
||||
.right {
|
||||
width: @width;
|
||||
}
|
||||
|
||||
.full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-select-search__field) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,165 @@
|
||||
<!--角色选择组件-->
|
||||
<template>
|
||||
<div class="JSelectRole">
|
||||
<JSelectBiz @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs"></JSelectBiz>
|
||||
<!-- update-begin--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式 -->
|
||||
<a-form-item>
|
||||
<RoleSelectModal @register="regModal" @getSelectResult="setValue" v-bind="getBindValue"></RoleSelectModal>
|
||||
</a-form-item>
|
||||
<!-- update-end--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式 -->
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import RoleSelectModal from './modal/RoleSelectModal.vue';
|
||||
import JSelectBiz from './base/JSelectBiz.vue';
|
||||
import { defineComponent, ref, unref, reactive, watchEffect, watch, provide } from 'vue';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { SelectValue } from 'ant-design-vue/es/select';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JSelectRole',
|
||||
components: {
|
||||
RoleSelectModal,
|
||||
JSelectBiz,
|
||||
},
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
|
||||
labelKey: {
|
||||
type: String,
|
||||
default: 'roleName',
|
||||
},
|
||||
rowKey: {
|
||||
type: String,
|
||||
default: 'id',
|
||||
},
|
||||
params: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
emits: ['options-change', 'change'],
|
||||
setup(props, { emit, refs }) {
|
||||
const emitData = ref<any[]>();
|
||||
//注册model
|
||||
const [regModal, { openModal }] = useModal();
|
||||
//表单值
|
||||
const [state] = useRuleFormItem(props, 'value', 'change', emitData);
|
||||
//下拉框选项值
|
||||
const selectOptions = ref<SelectValue>([]);
|
||||
//下拉框选中值
|
||||
let selectValues = reactive<Recordable>({
|
||||
value: [],
|
||||
change: false,
|
||||
});
|
||||
// 是否正在加载回显数据
|
||||
const loadingEcho = ref<boolean>(false);
|
||||
//下发 selectOptions,xxxBiz组件接收
|
||||
provide('selectOptions', selectOptions);
|
||||
//下发 selectValues,xxxBiz组件接收
|
||||
provide('selectValues', selectValues);
|
||||
//下发 loadingEcho,xxxBiz组件接收
|
||||
provide('loadingEcho', loadingEcho);
|
||||
|
||||
const tag = ref(false);
|
||||
const attrs = useAttrs();
|
||||
|
||||
/**
|
||||
* 监听组件值
|
||||
*/
|
||||
watchEffect(() => {
|
||||
props.value && initValue();
|
||||
// 查询条件重置的时候,清空界面显示
|
||||
if (!props.value) {
|
||||
selectValues.value = [];
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 监听selectValues变化
|
||||
*/
|
||||
watch(selectValues, () => {
|
||||
if (selectValues) {
|
||||
state.value = selectValues.value;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 打卡弹出框
|
||||
*/
|
||||
function handleOpen() {
|
||||
tag.value = true;
|
||||
openModal(true, {
|
||||
isUpdate: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字符串值转化为数组
|
||||
*/
|
||||
function initValue() {
|
||||
let value = props.value ? props.value : [];
|
||||
if (value && typeof value === 'string' && value != 'null' && value != 'undefined') {
|
||||
state.value = value.split(',');
|
||||
selectValues.value = value.split(',');
|
||||
} else {
|
||||
selectValues.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置下拉框的值
|
||||
*/
|
||||
function setValue(options, values) {
|
||||
selectOptions.value = options;
|
||||
//emitData.value = values.join(",");
|
||||
state.value = values;
|
||||
selectValues.value = values;
|
||||
}
|
||||
const getBindValue = Object.assign({}, unref(props), unref(attrs));
|
||||
return {
|
||||
state,
|
||||
attrs,
|
||||
getBindValue,
|
||||
selectOptions,
|
||||
selectValues,
|
||||
loadingEcho,
|
||||
tag,
|
||||
regModal,
|
||||
setValue,
|
||||
handleOpen,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
// update-begin--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
|
||||
.JSelectRole {
|
||||
> .ant-form-item {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
|
||||
.j-select-row {
|
||||
@width: 82px;
|
||||
|
||||
.left {
|
||||
width: calc(100% - @width - 8px);
|
||||
}
|
||||
|
||||
.right {
|
||||
width: @width;
|
||||
}
|
||||
|
||||
.full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-select-search__field) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,222 @@
|
||||
<!--用户选择组件-->
|
||||
<template>
|
||||
<div class="JselectUser">
|
||||
<JSelectBiz @change="handleSelectChange" @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs"></JSelectBiz>
|
||||
<!-- update-begin--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式 -->
|
||||
<a-form-item>
|
||||
<UserSelectModal
|
||||
:rowKey="rowKey"
|
||||
@register="regModal"
|
||||
@getSelectResult="setValue"
|
||||
v-bind="getBindValue"
|
||||
:excludeUserIdList="excludeUserIdList"
|
||||
@close="handleClose"
|
||||
/>
|
||||
</a-form-item>
|
||||
<!-- update-end--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式 -->
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { unref } from 'vue';
|
||||
import UserSelectModal from './modal/UserSelectModal.vue';
|
||||
import JSelectBiz from './base/JSelectBiz.vue';
|
||||
import { defineComponent, ref, reactive, watchEffect, watch, provide } from 'vue';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { SelectValue } from 'ant-design-vue/es/select';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
export default defineComponent({
|
||||
name: 'JSelectUser',
|
||||
components: {
|
||||
UserSelectModal,
|
||||
JSelectBiz,
|
||||
},
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
|
||||
labelKey: {
|
||||
type: String,
|
||||
default: 'realname',
|
||||
},
|
||||
rowKey: {
|
||||
type: String,
|
||||
default: 'username',
|
||||
},
|
||||
params: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
//update-begin---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
|
||||
//排除用户id的集合
|
||||
excludeUserIdList:{
|
||||
type: Array,
|
||||
default: () => [],
|
||||
}
|
||||
//update-end---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
|
||||
},
|
||||
emits: ['options-change', 'change', 'update:value'],
|
||||
setup(props, { emit }) {
|
||||
const emitData = ref<any[]>();
|
||||
//注册model
|
||||
const [regModal, { openModal }] = useModal();
|
||||
//表单值
|
||||
// const [state] = useRuleFormItem(props, 'value', 'change', emitData);
|
||||
//下拉框选项值
|
||||
const selectOptions = ref<SelectValue>([]);
|
||||
//下拉框选中值
|
||||
let selectValues = reactive<Recordable>({
|
||||
value: [],
|
||||
change: false,
|
||||
});
|
||||
let tempSave: any = [];
|
||||
// 是否正在加载回显数据
|
||||
const loadingEcho = ref<boolean>(false);
|
||||
//下发 selectOptions,xxxBiz组件接收
|
||||
provide('selectOptions', selectOptions);
|
||||
//下发 selectValues,xxxBiz组件接收
|
||||
provide('selectValues', selectValues);
|
||||
//下发 loadingEcho,xxxBiz组件接收
|
||||
provide('loadingEcho', loadingEcho);
|
||||
|
||||
const tag = ref(false);
|
||||
const attrs = useAttrs();
|
||||
|
||||
/**
|
||||
* 监听组件值
|
||||
*/
|
||||
watchEffect(() => {
|
||||
// update-begin--author:liaozhiyang---date:20240611---for:【TV360X-576】已选中了数据,再次选择打开弹窗点击取消,数据清空了
|
||||
//update-begin-author:liusq---date:2024-06-03--for: [TV360X-840]用户授权,没有选择,点取消,也会回显一个选过的用户
|
||||
tempSave = [];
|
||||
//update-end-author:liusq---date:2024-06-03--for:[TV360X-840]用户授权,没有选择,点取消,也会回显一个选过的用户
|
||||
// update-end--author:liaozhiyang---date:20240611---for:【TV360X-576】已选中了数据,再次选择打开弹窗点击取消,数据清空了
|
||||
props.value && initValue();
|
||||
// 查询条件重置的时候 界面显示未清空
|
||||
if (!props.value) {
|
||||
selectValues.value = [];
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 监听selectValues变化
|
||||
*/
|
||||
// watch(selectValues, () => {
|
||||
// if (selectValues) {
|
||||
// state.value = selectValues.value;
|
||||
// }
|
||||
// });
|
||||
|
||||
//update-begin---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
|
||||
const excludeUserIdList = ref<any>([]);
|
||||
|
||||
/**
|
||||
* 需要监听一下excludeUserIdList,否则modal获取不到
|
||||
*/
|
||||
watch(()=>props.excludeUserIdList,(data)=>{
|
||||
excludeUserIdList.value = data;
|
||||
},{ immediate: true })
|
||||
//update-end---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
|
||||
|
||||
/**
|
||||
* 打卡弹出框
|
||||
*/
|
||||
function handleOpen() {
|
||||
tag.value = true;
|
||||
openModal(true, {
|
||||
isUpdate: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字符串值转化为数组
|
||||
*/
|
||||
function initValue() {
|
||||
let value = props.value ? props.value : [];
|
||||
if (value && typeof value === 'string' && value != 'null' && value != 'undefined') {
|
||||
// state.value = value.split(',');
|
||||
selectValues.value = value.split(',');
|
||||
tempSave = value.split(',');
|
||||
} else {
|
||||
// 【VUEN-857】兼容数组(行编辑的用法问题)
|
||||
selectValues.value = value;
|
||||
tempSave = cloneDeep(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置下拉框的值
|
||||
*/
|
||||
function setValue(options, values) {
|
||||
selectOptions.value = options;
|
||||
//emitData.value = values.join(",");
|
||||
// state.value = values;
|
||||
selectValues.value = values;
|
||||
send(values);
|
||||
}
|
||||
const getBindValue = Object.assign({}, unref(props), unref(attrs));
|
||||
// update-begin--author:liaozhiyang---date:20240517---for:【QQYUN-9366】用户选择组件取消和关闭会把选择数据带入
|
||||
const handleClose = () => {
|
||||
if (tempSave.length) {
|
||||
selectValues.value = cloneDeep(tempSave);
|
||||
} else {
|
||||
send(tempSave);
|
||||
}
|
||||
};
|
||||
const handleSelectChange = (values) => {
|
||||
tempSave = cloneDeep(values);
|
||||
send(tempSave);
|
||||
};
|
||||
const send = (values) => {
|
||||
let result = typeof props.value == "string" ? values.join(',') : values;
|
||||
emit('update:value', result);
|
||||
emit('change', result);
|
||||
};
|
||||
// update-end--author:liaozhiyang---date:20240517---for:【QQYUN-9366】用户选择组件取消和关闭会把选择数据带入
|
||||
return {
|
||||
// state,
|
||||
attrs,
|
||||
selectOptions,
|
||||
getBindValue,
|
||||
selectValues,
|
||||
loadingEcho,
|
||||
tag,
|
||||
regModal,
|
||||
setValue,
|
||||
handleOpen,
|
||||
excludeUserIdList,
|
||||
handleClose,
|
||||
handleSelectChange,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
// update-begin--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
|
||||
.JselectUser {
|
||||
> .ant-form-item {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
|
||||
.j-select-row {
|
||||
@width: 82px;
|
||||
|
||||
.left {
|
||||
width: calc(100% - @width - 8px);
|
||||
}
|
||||
|
||||
.right {
|
||||
width: @width;
|
||||
}
|
||||
|
||||
.full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-select-search__field) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,153 @@
|
||||
<!--用户选择组件-->
|
||||
<template>
|
||||
<div>
|
||||
<JSelectBiz @change="handleChange" @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs"></JSelectBiz>
|
||||
<UserSelectByDepModal :rowKey="rowKey" @register="regModal" @getSelectResult="setValue" v-bind="getBindValue"></UserSelectByDepModal>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { unref } from 'vue';
|
||||
import UserSelectByDepModal from './modal/UserSelectByDepModal.vue';
|
||||
import JSelectBiz from './base/JSelectBiz.vue';
|
||||
import { defineComponent, ref, reactive, watchEffect, watch, provide } from 'vue';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { SelectValue } from 'ant-design-vue/es/select';
|
||||
export default defineComponent({
|
||||
name: 'JSelectUserByDept',
|
||||
components: {
|
||||
UserSelectByDepModal,
|
||||
JSelectBiz,
|
||||
},
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
|
||||
rowKey: {
|
||||
type: String,
|
||||
default: 'username',
|
||||
},
|
||||
labelKey: {
|
||||
type: String,
|
||||
default: 'realname',
|
||||
},
|
||||
},
|
||||
emits: ['options-change', 'change', 'update:value'],
|
||||
setup(props, { emit, refs }) {
|
||||
const emitData = ref<any[]>();
|
||||
//注册model
|
||||
const [regModal, { openModal }] = useModal();
|
||||
//表单值
|
||||
const [state] = useRuleFormItem(props, 'value', 'change', emitData);
|
||||
//下拉框选项值
|
||||
const selectOptions = ref<SelectValue>([]);
|
||||
//下拉框选中值
|
||||
let selectValues = reactive<object>({
|
||||
value: [],
|
||||
change: false,
|
||||
});
|
||||
// 是否正在加载回显数据
|
||||
const loadingEcho = ref<boolean>(false);
|
||||
//下发 selectOptions,xxxBiz组件接收
|
||||
provide('selectOptions', selectOptions);
|
||||
//下发 selectValues,xxxBiz组件接收
|
||||
provide('selectValues', selectValues);
|
||||
//下发 loadingEcho,xxxBiz组件接收
|
||||
provide('loadingEcho', loadingEcho);
|
||||
|
||||
const tag = ref(false);
|
||||
const attrs = useAttrs();
|
||||
|
||||
/**
|
||||
* 监听组件值
|
||||
*/
|
||||
watchEffect(() => {
|
||||
initValue();
|
||||
});
|
||||
|
||||
/**
|
||||
* 监听selectValues变化
|
||||
*/
|
||||
watch(selectValues, () => {
|
||||
if (selectValues) {
|
||||
state.value = selectValues.value;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 打卡弹出框
|
||||
*/
|
||||
function handleOpen() {
|
||||
tag.value = true;
|
||||
openModal(true, {
|
||||
isUpdate: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字符串值转化为数组
|
||||
*/
|
||||
function initValue() {
|
||||
let value = props.value ? props.value : [];
|
||||
if (value && typeof value === 'string' && value != 'null' && value != 'undefined') {
|
||||
state.value = value.split(',');
|
||||
selectValues.value = value.split(',');
|
||||
} else {
|
||||
selectValues.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置下拉框的值
|
||||
*/
|
||||
function setValue(options, values) {
|
||||
selectOptions.value = options;
|
||||
//emitData.value = values.join(",");
|
||||
state.value = values;
|
||||
selectValues.value = values;
|
||||
emit('update:value', values);
|
||||
emit('options-change', options);
|
||||
}
|
||||
|
||||
function handleChange(values) {
|
||||
emit('update:value', values);
|
||||
}
|
||||
const getBindValue = Object.assign({}, unref(props), unref(attrs));
|
||||
return {
|
||||
state,
|
||||
attrs,
|
||||
selectOptions,
|
||||
getBindValue,
|
||||
selectValues,
|
||||
loadingEcho,
|
||||
tag,
|
||||
regModal,
|
||||
setValue,
|
||||
handleOpen,
|
||||
handleChange,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.j-select-row {
|
||||
@width: 82px;
|
||||
|
||||
.left {
|
||||
width: calc(100% - @width - 8px);
|
||||
}
|
||||
|
||||
.right {
|
||||
width: @width;
|
||||
}
|
||||
|
||||
.full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-select-search__field) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div :class="prefixCls">
|
||||
<a-select
|
||||
v-if="query"
|
||||
v-model:value="state"
|
||||
:options="selectOptions"
|
||||
:disabled="disabled"
|
||||
style="width: 100%"
|
||||
v-bind="attrs"
|
||||
@change="onSelectChange"
|
||||
/>
|
||||
<a-switch v-else v-model:checked="checked" :disabled="disabled" v-bind="attrs" @change="onSwitchChange" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
|
||||
const { prefixCls } = useDesign('j-switch');
|
||||
const props = defineProps({
|
||||
// v-model:value
|
||||
value: propTypes.oneOfType([propTypes.string, propTypes.number]),
|
||||
// 取值 options
|
||||
options: propTypes.array.def(() => ['Y', 'N']),
|
||||
// 文本 options
|
||||
labelOptions: propTypes.array.def(() => ['是', '否']),
|
||||
// 是否使用下拉
|
||||
query: propTypes.bool.def(false),
|
||||
// 是否禁用
|
||||
disabled: propTypes.bool.def(false),
|
||||
});
|
||||
const attrs = useAttrs();
|
||||
const emit = defineEmits(['change', 'update:value']);
|
||||
|
||||
const checked = ref<boolean>(false);
|
||||
const [state] = useRuleFormItem(props, 'value', 'change');
|
||||
watch(
|
||||
() => props.value,
|
||||
(val) => {
|
||||
if (!props.query) {
|
||||
// update-begin--author:liaozhiyang---date:20231226---for:【QQYUN-7473】options使用[0,1],导致开关无法切换
|
||||
if (!val && !props.options.includes(val)) {
|
||||
checked.value = false;
|
||||
emitValue(props.options[1]);
|
||||
} else {
|
||||
checked.value = props.options[0] == val;
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20231226---for:【QQYUN-7473】options使用[0,1],导致开关无法切换
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const selectOptions = computed(() => {
|
||||
let options: any[] = [];
|
||||
options.push({ value: props.options[0], label: props.labelOptions[0] });
|
||||
options.push({ value: props.options[1], label: props.labelOptions[1] });
|
||||
return options;
|
||||
});
|
||||
|
||||
function onSwitchChange(checked) {
|
||||
let flag = checked === false ? props.options[1] : props.options[0];
|
||||
emitValue(flag);
|
||||
}
|
||||
|
||||
function onSelectChange(value) {
|
||||
emitValue(value);
|
||||
}
|
||||
|
||||
function emitValue(value) {
|
||||
emit('change', value);
|
||||
emit('update:value', value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
//noinspection LessUnresolvedVariable
|
||||
@prefix-cls: ~'@{namespace}-j-switch';
|
||||
|
||||
.@{prefix-cls} {
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<TreeSelect
|
||||
:class="prefixCls"
|
||||
:value="treeValue"
|
||||
:treeData="treeData"
|
||||
:loadData="asyncLoadTreeData"
|
||||
allowClear
|
||||
labelInValue
|
||||
:dropdownStyle="{ maxHeight: '400px', overflow: 'auto' }"
|
||||
style="width: 100%"
|
||||
v-bind="attrs"
|
||||
@change="onChange"
|
||||
>
|
||||
</TreeSelect>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { TreeSelect } from 'ant-design-vue';
|
||||
|
||||
enum Api {
|
||||
view = '/sys/category/loadOne',
|
||||
root = '/sys/category/loadTreeRoot',
|
||||
children = '/sys/category/loadTreeChildren',
|
||||
}
|
||||
|
||||
const { prefixCls } = useDesign('j-tree-dict');
|
||||
const props = defineProps({
|
||||
// v-model:value
|
||||
value: propTypes.string.def(''),
|
||||
field: propTypes.string.def('id'),
|
||||
parentCode: propTypes.string.def(''),
|
||||
async: propTypes.bool.def(false),
|
||||
});
|
||||
const attrs = useAttrs();
|
||||
const emit = defineEmits(['change', 'update:value']);
|
||||
|
||||
const treeData = ref<any[]>([]);
|
||||
const treeValue = ref<any>(null);
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
() => loadViewInfo(),
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
watch(
|
||||
() => props.parentCode,
|
||||
() => loadRoot(),
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
async function loadViewInfo() {
|
||||
if (!props.value || props.value == '0') {
|
||||
treeValue.value = { value: null, label: null };
|
||||
} else {
|
||||
let params = { field: props.field, val: props.value };
|
||||
let result = await defHttp.get({ url: Api.view, params });
|
||||
treeValue.value = {
|
||||
value: props.value,
|
||||
label: result.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRoot() {
|
||||
let params = {
|
||||
async: props.async,
|
||||
pcode: props.parentCode,
|
||||
};
|
||||
let result = await defHttp.get({ url: Api.root, params });
|
||||
treeData.value = [...result];
|
||||
handleTreeNodeValue(result);
|
||||
}
|
||||
|
||||
async function asyncLoadTreeData(treeNode) {
|
||||
if (!props.async || treeNode.dataRef.children) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
let pid = treeNode.dataRef.key;
|
||||
let params = { pid: pid };
|
||||
let result = await defHttp.get({ url: Api.children, params });
|
||||
handleTreeNodeValue(result);
|
||||
addChildren(pid, result, treeData.value);
|
||||
treeData.value = [...treeData.value];
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function addChildren(pid, children, treeArray) {
|
||||
if (treeArray && treeArray.length > 0) {
|
||||
for (let item of treeArray) {
|
||||
if (item.key == pid) {
|
||||
if (!children || children.length == 0) {
|
||||
item.leaf = true;
|
||||
} else {
|
||||
item.children = children;
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
addChildren(pid, children, item.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleTreeNodeValue(result) {
|
||||
let storeField = props.field == 'code' ? 'code' : 'key';
|
||||
for (let i of result) {
|
||||
i.value = i[storeField];
|
||||
i.isLeaf = i.leaf;
|
||||
if (i.children && i.children.length > 0) {
|
||||
handleTreeNodeValue(i.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onChange(value) {
|
||||
if (!value) {
|
||||
emitValue('');
|
||||
} else {
|
||||
emitValue(value.value);
|
||||
}
|
||||
treeValue.value = value;
|
||||
}
|
||||
|
||||
function emitValue(value) {
|
||||
emit('change', value);
|
||||
emit('update:value', value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
//noinspection LessUnresolvedVariable
|
||||
@prefix-cls: ~'@{namespace}-j-tree-dict';
|
||||
|
||||
.@{prefix-cls} {
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,405 @@
|
||||
<template>
|
||||
<a-tree-select
|
||||
v-if="show"
|
||||
allowClear
|
||||
labelInValue
|
||||
style="width: 100%"
|
||||
:getPopupContainer="(node) => node?.parentNode"
|
||||
:dropdownStyle="{ maxHeight: '400px', overflow: 'auto' }"
|
||||
:placeholder="placeholder"
|
||||
:loadData="asyncLoadTreeData"
|
||||
:value="treeValue"
|
||||
:treeData="treeData"
|
||||
:multiple="multiple"
|
||||
v-bind="attrs"
|
||||
@change="onChange"
|
||||
@search="onSearch"
|
||||
:tree-checkable="treeCheckAble"
|
||||
>
|
||||
</a-tree-select>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
/*
|
||||
* 异步树加载组件 通过传入表名 显示字段 存储字段 加载一个树控件
|
||||
* <j-tree-select dict="aa_tree_test,aad,id" pid-field="pid" ></j-tree-select>
|
||||
* */
|
||||
import { ref, watch, unref, nextTick } from 'vue';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { TreeSelect } from 'ant-design-vue';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
enum Api {
|
||||
url = '/sys/dict/loadTreeData',
|
||||
view = '/sys/dict/loadDictItem/',
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
value: propTypes.string.def(''),
|
||||
placeholder: propTypes.string.def('请选择'),
|
||||
dict: propTypes.string.def('id'),
|
||||
parentCode: propTypes.string.def(''),
|
||||
pidField: propTypes.string.def('pid'),
|
||||
//update-begin---author:wangshuai ---date:20220620 for:JTreeSelect组件pidValue还原成空,否则会影响自定义组件树示例------------
|
||||
pidValue: propTypes.string.def(''),
|
||||
//update-end---author:wangshuai ---date:20220620 for:JTreeSelect组件pidValue还原成空,否则会影响自定义组件树示例--------------
|
||||
hasChildField: propTypes.string.def(''),
|
||||
converIsLeafVal: propTypes.integer.def(1),
|
||||
condition: propTypes.string.def(''),
|
||||
multiple: propTypes.bool.def(false),
|
||||
loadTriggleChange: propTypes.bool.def(false),
|
||||
reload: propTypes.number.def(1),
|
||||
//update-begin-author:taoyan date:2022-11-8 for: issues/4173 Online JTreeSelect控件changeOptions方法未生效
|
||||
url: propTypes.string.def(''),
|
||||
params: propTypes.object.def({}),
|
||||
//update-end-author:taoyan date:2022-11-8 for: issues/4173 Online JTreeSelect控件changeOptions方法未生效
|
||||
//update-begin---author:wangshuai date: 20230202 for: 新增是否有复选框
|
||||
//默认没有选择框
|
||||
treeCheckAble: propTypes.bool.def(false),
|
||||
//update-end---author:wangshuai date: 20230202 for: 新增是否有复选框
|
||||
hiddenNodeKey: propTypes.string.def(''),
|
||||
});
|
||||
const attrs = useAttrs();
|
||||
const emit = defineEmits(['change', 'update:value']);
|
||||
const { createMessage } = useMessage();
|
||||
//树形下拉数据
|
||||
const treeData = ref<any[]>([]);
|
||||
//选择数据
|
||||
const treeValue = ref<any>(null);
|
||||
const tableName = ref<any>('');
|
||||
const text = ref<any>('');
|
||||
const code = ref<any>('');
|
||||
const show = ref<boolean>(true);
|
||||
/**
|
||||
* 监听value数据并初始化
|
||||
*/
|
||||
watch(
|
||||
() => props.value,
|
||||
() => loadItemByCode(),
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
/**
|
||||
* 监听dict变化
|
||||
*/
|
||||
watch(
|
||||
() => props.dict,
|
||||
() => {
|
||||
initDictInfo();
|
||||
loadRoot();
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
// update-begin--author:liaozhiyang---date:20240529---for:【TV360X-87】树表编辑时不可选自己及子孙节点当父节点
|
||||
watch(
|
||||
() => props.hiddenNodeKey,
|
||||
() => {
|
||||
if (treeData.value?.length && props.hiddenNodeKey) {
|
||||
handleHiddenNode(treeData.value);
|
||||
treeData.value = [...treeData.value];
|
||||
}
|
||||
}
|
||||
);
|
||||
// update-end--author:liaozhiyang---date:20240529---for:【TV360X-87】树表编辑时不可选自己及子孙节点当父节点
|
||||
|
||||
//update-begin-author:taoyan date:2022-5-25 for: VUEN-1056 15、严重——online树表单,添加的时候,父亲节点是空的
|
||||
watch(
|
||||
() => props.reload,
|
||||
async () => {
|
||||
treeData.value = [];
|
||||
// update-begin--author:liaozhiyang---date:20240524---for:【TV360X-88】online树表重复新增时父节点数据加载不全且已开的子节点不重新加载
|
||||
show.value = false;
|
||||
nextTick(() => {
|
||||
show.value = true;
|
||||
});
|
||||
// update-end--author:liaozhiyang---date:20240524---for:【TV360X-88】online树表重复新增时父节点数据加载不全且已开的子节点不重新加载
|
||||
await loadRoot();
|
||||
},
|
||||
{
|
||||
immediate: false,
|
||||
}
|
||||
);
|
||||
//update-end-author:taoyan date:2022-5-25 for: VUEN-1056 15、严重——online树表单,添加的时候,父亲节点是空的
|
||||
|
||||
/**
|
||||
* 根据code获取下拉数据并回显
|
||||
*/
|
||||
async function loadItemByCode() {
|
||||
if (!props.value || props.value == '0') {
|
||||
if(props.multiple){
|
||||
treeValue.value = [];
|
||||
}else{
|
||||
treeValue.value = { label: null, value: null };
|
||||
}
|
||||
} else {
|
||||
//update-begin-author:taoyan date:2022-11-8 for: issues/4173 Online JTreeSelect控件changeOptions方法未生效
|
||||
if(props.url){
|
||||
getItemFromTreeData();
|
||||
}else{
|
||||
let params = { key: props.value };
|
||||
let result = await defHttp.get({ url: `${Api.view}${props.dict}`, params }, { isTransformResponse: false });
|
||||
if (result.success) {
|
||||
//update-start-author:liaozhiyang date:2023-7-17 for:【issues/5141】使用JtreeSelect 组件 控制台报错
|
||||
if(props.multiple){
|
||||
let values = props.value.split(',');
|
||||
treeValue.value = result.result.map((item, index) => ({
|
||||
key: values[index],
|
||||
value: values[index],
|
||||
label: item,
|
||||
}));
|
||||
}else{
|
||||
treeValue.value = { key: props.value, value: props.value, label: result.result[0] };
|
||||
}
|
||||
//update-end-author:liaozhiyang date:2023-7-17 for:【issues/5141】使用JtreeSelect 组件 控制台报错
|
||||
onLoadTriggleChange(result.result[0]);
|
||||
}
|
||||
}
|
||||
//update-end-author:taoyan date:2022-11-8 for: issues/4173 Online JTreeSelect控件changeOptions方法未生效
|
||||
}
|
||||
}
|
||||
|
||||
function onLoadTriggleChange(text) {
|
||||
//只有单选才会触发
|
||||
if (!props.multiple && props.loadTriggleChange) {
|
||||
emit('change', props.value, text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化数据
|
||||
*/
|
||||
function initDictInfo() {
|
||||
let arr = props.dict?.split(',');
|
||||
tableName.value = arr[0];
|
||||
text.value = arr[1];
|
||||
code.value = arr[2];
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载下拉树形数据
|
||||
*/
|
||||
async function loadRoot() {
|
||||
let params = {
|
||||
pid: props.pidValue,
|
||||
pidField: props.pidField,
|
||||
hasChildField: props.hasChildField,
|
||||
converIsLeafVal: props.converIsLeafVal,
|
||||
condition: props.condition,
|
||||
tableName: unref(tableName),
|
||||
text: unref(text),
|
||||
code: unref(code),
|
||||
};
|
||||
let res = await defHttp.get({ url: Api.url, params }, { isTransformResponse: false });
|
||||
if (res.success && res.result) {
|
||||
for (let i of res.result) {
|
||||
i.value = i.key;
|
||||
i.isLeaf = !!i.leaf;
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20240523---for:【TV360X-87】树表编辑时不可选自己及子孙节点当父节点
|
||||
handleHiddenNode(res.result);
|
||||
// update-end--author:liaozhiyang---date:20240523---for:【TV360X-87】树表编辑时不可选自己及子孙节点当父节点
|
||||
treeData.value = [...res.result];
|
||||
} else {
|
||||
console.log('数根节点查询结果异常', res);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步加载数据
|
||||
*/
|
||||
async function asyncLoadTreeData(treeNode) {
|
||||
if (treeNode.dataRef.children) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if(props.url){
|
||||
return Promise.resolve();
|
||||
}
|
||||
let pid = treeNode.dataRef.key;
|
||||
let params = {
|
||||
pid: pid,
|
||||
pidField: props.pidField,
|
||||
hasChildField: props.hasChildField,
|
||||
converIsLeafVal: props.converIsLeafVal,
|
||||
condition: props.condition,
|
||||
tableName: unref(tableName),
|
||||
text: unref(text),
|
||||
code: unref(code),
|
||||
};
|
||||
let res = await defHttp.get({ url: Api.url, params }, { isTransformResponse: false });
|
||||
if (res.success) {
|
||||
for (let i of res.result) {
|
||||
i.value = i.key;
|
||||
i.isLeaf = !!i.leaf;
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20240523---for:【TV360X-87】树表编辑时不可选自己及子孙节点当父节点
|
||||
handleHiddenNode(res.result);
|
||||
// update-end--author:liaozhiyang---date:20240523---for:【TV360X-87】树表编辑时不可选自己及子孙节点当父节点
|
||||
//添加子节点
|
||||
addChildren(pid, res.result, treeData.value);
|
||||
treeData.value = [...treeData.value];
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载子节点
|
||||
*/
|
||||
function addChildren(pid, children, treeArray) {
|
||||
if (treeArray && treeArray.length > 0) {
|
||||
for (let item of treeArray) {
|
||||
if (item.key == pid) {
|
||||
if (!children || children.length == 0) {
|
||||
item.isLeaf = true;
|
||||
} else {
|
||||
item.children = children;
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
addChildren(pid, children, item.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 选中树节点事件
|
||||
*/
|
||||
function onChange(value) {
|
||||
if (!value) {
|
||||
emitValue('');
|
||||
} else if (value instanceof Array) {
|
||||
emitValue(value.map((item) => item.value).join(','));
|
||||
} else {
|
||||
emitValue(value.value);
|
||||
}
|
||||
treeValue.value = value;
|
||||
}
|
||||
|
||||
function emitValue(value) {
|
||||
emit('change', value);
|
||||
emit('update:value', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本框值变化
|
||||
*/
|
||||
function onSearch(value) {
|
||||
console.log(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验条件配置是否有误
|
||||
*/
|
||||
function validateProp() {
|
||||
let mycondition = props.condition;
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!mycondition) {
|
||||
resolve();
|
||||
} else {
|
||||
try {
|
||||
let test = JSON.parse(mycondition);
|
||||
if (typeof test == 'object' && test) {
|
||||
resolve();
|
||||
} else {
|
||||
createMessage.error('组件JTreeSelect-condition传值有误,需要一个json字符串!');
|
||||
reject();
|
||||
}
|
||||
} catch (e) {
|
||||
createMessage.error('组件JTreeSelect-condition传值有误,需要一个json字符串!');
|
||||
reject();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//update-begin-author:taoyan date:2022-11-8 for: issues/4173 Online JTreeSelect控件changeOptions方法未生效
|
||||
watch(()=>props.url, async (val)=>{
|
||||
if(val){
|
||||
await loadRootByUrl();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 根据自定义的请求地址加载数据
|
||||
*/
|
||||
async function loadRootByUrl(){
|
||||
let url = props.url;
|
||||
let params = props.params;
|
||||
let res = await defHttp.get({ url, params }, { isTransformResponse: false });
|
||||
if (res.success && res.result) {
|
||||
for (let i of res.result) {
|
||||
i.key = i.value;
|
||||
i.isLeaf = !!i.leaf;
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20240523---for:【TV360X-87】树表编辑时不可选自己及子孙节点当父节点
|
||||
handleHiddenNode(res.result);
|
||||
// update-end--author:liaozhiyang---date:20240523---for:【TV360X-87】树表编辑时不可选自己及子孙节点当父节点
|
||||
treeData.value = [...res.result];
|
||||
} else {
|
||||
console.log('数根节点查询结果异常', res);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据已有的树数据 翻译选项
|
||||
*/
|
||||
function getItemFromTreeData(){
|
||||
let data = treeData.value;
|
||||
let arr = []
|
||||
findChildrenNode(data, arr);
|
||||
if(arr.length>0){
|
||||
treeValue.value = arr
|
||||
onLoadTriggleChange(arr[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归找子节点
|
||||
* @param data
|
||||
* @param arr
|
||||
*/
|
||||
function findChildrenNode(data, arr){
|
||||
let val = props.value;
|
||||
if(data && data.length){
|
||||
for(let item of data){
|
||||
if(val===item.value){
|
||||
arr.push({
|
||||
key: item.key,
|
||||
value: item.value,
|
||||
label: item.label||item.title
|
||||
})
|
||||
}else{
|
||||
findChildrenNode(item.children, arr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//update-end-author:taoyan date:2022-11-8 for: issues/4173 Online JTreeSelect控件changeOptions方法未生效
|
||||
|
||||
/**
|
||||
* 2024-05-23
|
||||
* liaozhiyang
|
||||
* 过滤掉指定节点(包含其子孙节点)
|
||||
*/
|
||||
function handleHiddenNode(data) {
|
||||
if (props.hiddenNodeKey && data?.length) {
|
||||
for (let i = 0, len = data.length; i < len; i++) {
|
||||
const item = data[i];
|
||||
if (item.key == props.hiddenNodeKey) {
|
||||
data.splice(i, 1);
|
||||
i--;
|
||||
len--;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// onCreated
|
||||
validateProp().then(() => {
|
||||
initDictInfo();
|
||||
loadRoot();
|
||||
loadItemByCode();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less"></style>
|
||||
@ -0,0 +1,461 @@
|
||||
<template>
|
||||
<div ref="containerRef" :class="`${prefixCls}-container`">
|
||||
<a-upload
|
||||
:headers="headers"
|
||||
:multiple="multiple"
|
||||
:action="uploadUrl"
|
||||
:fileList="fileList"
|
||||
:disabled="disabled"
|
||||
v-bind="bindProps"
|
||||
@remove="onRemove"
|
||||
@change="onFileChange"
|
||||
@preview="onFilePreview"
|
||||
>
|
||||
<template v-if="isImageMode">
|
||||
<div v-if="!isMaxCount">
|
||||
<Icon icon="ant-design:plus-outlined" />
|
||||
<div class="ant-upload-text">{{ text }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-button v-else-if="buttonVisible" :disabled="buttonDisabled">
|
||||
<Icon icon="ant-design:upload-outlined" />
|
||||
<span>{{ text }}</span>
|
||||
</a-button>
|
||||
</a-upload>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, watch, nextTick, createApp,unref } from 'vue';
|
||||
import { Icon } from '/@/components/Icon';
|
||||
import { getToken } from '/@/utils/auth';
|
||||
import { uploadUrl } from '/@/api/common/api';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { createImgPreview } from '/@/components/Preview/index';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { UploadTypeEnum } from './upload.data';
|
||||
import { getFileAccessHttpUrl, getHeaders } from '/@/utils/common/compUtils';
|
||||
import UploadItemActions from './components/UploadItemActions.vue';
|
||||
|
||||
const { createMessage, createConfirm } = useMessage();
|
||||
const { prefixCls } = useDesign('j-upload');
|
||||
const attrs = useAttrs();
|
||||
const emit = defineEmits(['change', 'update:value']);
|
||||
const props = defineProps({
|
||||
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
|
||||
text: propTypes.string.def('上传'),
|
||||
fileType: propTypes.string.def(UploadTypeEnum.all),
|
||||
/*这个属性用于控制文件上传的业务路径*/
|
||||
bizPath: propTypes.string.def('temp'),
|
||||
/**
|
||||
* 是否返回url,
|
||||
* true:仅返回url
|
||||
* false:返回fileName filePath fileSize
|
||||
*/
|
||||
returnUrl: propTypes.bool.def(true),
|
||||
// 最大上传数量
|
||||
maxCount: propTypes.number.def(0),
|
||||
buttonVisible: propTypes.bool.def(true),
|
||||
multiple: propTypes.bool.def(true),
|
||||
// 是否显示左右移动按钮
|
||||
mover: propTypes.bool.def(true),
|
||||
// 是否显示下载按钮
|
||||
download: propTypes.bool.def(true),
|
||||
// 删除时是否显示确认框
|
||||
removeConfirm: propTypes.bool.def(false),
|
||||
beforeUpload: propTypes.func,
|
||||
disabled: propTypes.bool.def(false),
|
||||
// 替换前一个文件,用于超出最大数量依然允许上传
|
||||
replaceLastOne: propTypes.bool.def(false),
|
||||
});
|
||||
|
||||
const headers = getHeaders();
|
||||
const fileList = ref<any[]>([]);
|
||||
const uploadGoOn = ref<boolean>(true);
|
||||
// refs
|
||||
const containerRef = ref();
|
||||
// 是否达到了最大上传数量
|
||||
const isMaxCount = computed(() => props.maxCount > 0 && fileList.value.length >= props.maxCount);
|
||||
// 当前是否是上传图片模式
|
||||
const isImageMode = computed(() => props.fileType === UploadTypeEnum.image);
|
||||
// 上传按钮是否禁用
|
||||
const buttonDisabled = computed(()=>{
|
||||
if(props.disabled === true){
|
||||
return true;
|
||||
}
|
||||
if(isMaxCount.value === true){
|
||||
if(props.replaceLastOne === true){
|
||||
return false
|
||||
}else{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false
|
||||
});
|
||||
// 合并 props 和 attrs
|
||||
const bindProps = computed(() => {
|
||||
//update-begin-author:liusq date:20220411 for: [issue/455]上传组件传入accept限制上传文件类型无效
|
||||
const bind: any = Object.assign({}, props, unref(attrs));
|
||||
//update-end-author:liusq date:20220411 for: [issue/455]上传组件传入accept限制上传文件类型无效
|
||||
|
||||
bind.name = 'file';
|
||||
bind.listType = isImageMode.value ? 'picture-card' : 'text';
|
||||
bind.class = [bind.class, { 'upload-disabled': props.disabled }];
|
||||
bind.data = { biz: props.bizPath, ...bind.data };
|
||||
//update-begin-author:taoyan date:20220407 for: 自定义beforeUpload return false,并不能中断上传过程
|
||||
if (!bind.beforeUpload) {
|
||||
bind.beforeUpload = onBeforeUpload;
|
||||
}
|
||||
//update-end-author:taoyan date:20220407 for: 自定义beforeUpload return false,并不能中断上传过程
|
||||
// 如果当前是图片上传模式,就只能上传图片
|
||||
if (isImageMode.value && !bind.accept) {
|
||||
bind.accept = 'image/*';
|
||||
}
|
||||
return bind;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(val) => {
|
||||
if (Array.isArray(val)) {
|
||||
if (props.returnUrl) {
|
||||
parsePathsValue(val.join(','));
|
||||
} else {
|
||||
parseArrayValue(val);
|
||||
}
|
||||
} else {
|
||||
//update-begin---author:liusq ---date:20230914 for:[issues/5327]Upload组件returnUrl为false时上传的字段值返回了一个'[object Object]' ------------
|
||||
if (props.returnUrl) {
|
||||
parsePathsValue(val);
|
||||
} else {
|
||||
val && parseArrayValue(JSON.parse(val));
|
||||
}
|
||||
//update-end---author:liusq ---date:20230914 for:[issues/5327]Upload组件returnUrl为false时上传的字段值返回了一个'[object Object]' ------------
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(fileList, () => nextTick(() => addActionsListener()), { immediate: true });
|
||||
|
||||
const antUploadItemCls = 'ant-upload-list-item';
|
||||
|
||||
// Listener
|
||||
function addActionsListener() {
|
||||
if (!isImageMode.value) {
|
||||
return;
|
||||
}
|
||||
const uploadItems = containerRef.value ? containerRef.value.getElementsByClassName(antUploadItemCls) : null;
|
||||
if (!uploadItems || uploadItems.length === 0) {
|
||||
return;
|
||||
}
|
||||
for (const uploadItem of uploadItems) {
|
||||
let hasActions = uploadItem.getAttribute('data-has-actions') === 'true';
|
||||
if (!hasActions) {
|
||||
uploadItem.addEventListener('mouseover', onAddActionsButton);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加可左右移动的按钮
|
||||
function onAddActionsButton(event) {
|
||||
const getUploadItem = () => {
|
||||
for (const path of event.path) {
|
||||
if (path.classList.contains(antUploadItemCls)) {
|
||||
return path;
|
||||
} else if (path.classList.contains(`${prefixCls}-container`)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const uploadItem = getUploadItem();
|
||||
if (!uploadItem) {
|
||||
return;
|
||||
}
|
||||
const actions = uploadItem.getElementsByClassName('ant-upload-list-item-actions');
|
||||
if (!actions || actions.length === 0) {
|
||||
return;
|
||||
}
|
||||
// 添加操作按钮
|
||||
const div = document.createElement('div');
|
||||
div.className = 'upload-actions-container';
|
||||
createApp(UploadItemActions, {
|
||||
element: uploadItem,
|
||||
fileList: fileList,
|
||||
mover: props.mover,
|
||||
download: props.download,
|
||||
emitValue: emitValue,
|
||||
}).mount(div);
|
||||
actions[0].appendChild(div);
|
||||
uploadItem.setAttribute('data-has-actions', 'true');
|
||||
uploadItem.removeEventListener('mouseover', onAddActionsButton);
|
||||
}
|
||||
|
||||
// 解析数据库存储的逗号分割
|
||||
function parsePathsValue(paths) {
|
||||
if (!paths || paths.length == 0) {
|
||||
fileList.value = [];
|
||||
return;
|
||||
}
|
||||
let list: any[] = [];
|
||||
for (const item of paths.split(',')) {
|
||||
let url = getFileAccessHttpUrl(item);
|
||||
list.push({
|
||||
uid: uidGenerator(),
|
||||
name: getFileName(item),
|
||||
status: 'done',
|
||||
url: url,
|
||||
response: { status: 'history', message: item },
|
||||
});
|
||||
}
|
||||
fileList.value = list;
|
||||
}
|
||||
|
||||
// 解析数组值
|
||||
function parseArrayValue(array) {
|
||||
if (!array || array.length == 0) {
|
||||
fileList.value = [];
|
||||
return;
|
||||
}
|
||||
let list: any[] = [];
|
||||
for (const item of array) {
|
||||
let url = getFileAccessHttpUrl(item.filePath);
|
||||
list.push({
|
||||
uid: uidGenerator(),
|
||||
name: item.fileName,
|
||||
url: url,
|
||||
status: 'done',
|
||||
response: { status: 'history', message: item.filePath },
|
||||
});
|
||||
}
|
||||
fileList.value = list;
|
||||
}
|
||||
|
||||
// 文件上传之前的操作
|
||||
function onBeforeUpload(file) {
|
||||
uploadGoOn.value = true;
|
||||
if (isImageMode.value) {
|
||||
if (file.type.indexOf('image') < 0) {
|
||||
createMessage.warning('请上传图片');
|
||||
uploadGoOn.value = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 扩展 beforeUpload 验证
|
||||
if (typeof props.beforeUpload === 'function') {
|
||||
return props.beforeUpload(file);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 删除处理事件
|
||||
function onRemove() {
|
||||
if (props.removeConfirm) {
|
||||
return new Promise((resolve) => {
|
||||
createConfirm({
|
||||
title: '删除',
|
||||
content: `确定要删除这${isImageMode.value ? '张图片' : '个文件'}吗?`,
|
||||
iconType: 'warning',
|
||||
onOk: () => resolve(true),
|
||||
onCancel: () => resolve(false),
|
||||
});
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// upload组件change事件
|
||||
function onFileChange(info) {
|
||||
if (!info.file.status && uploadGoOn.value === false) {
|
||||
info.fileList.pop();
|
||||
}
|
||||
let fileListTemp = info.fileList;
|
||||
// 限制最大上传数
|
||||
if (props.maxCount > 0) {
|
||||
let count = fileListTemp.length;
|
||||
if (count >= props.maxCount) {
|
||||
let diffNum = props.maxCount - fileListTemp.length;
|
||||
if (diffNum >= 0) {
|
||||
fileListTemp = fileListTemp.slice(-props.maxCount);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (info.file.status === 'done') {
|
||||
let successFileList = [];
|
||||
if (info.file.response.success) {
|
||||
successFileList = fileListTemp.map((file) => {
|
||||
if (file.response) {
|
||||
let reUrl = file.response.message;
|
||||
file.url = getFileAccessHttpUrl(reUrl);
|
||||
}
|
||||
return file;
|
||||
});
|
||||
}else{
|
||||
successFileList = fileListTemp.filter(item=>{
|
||||
return item.uid!=info.file.uid;
|
||||
});
|
||||
createMessage.error(`${info.file.name} 上传失败.`);
|
||||
}
|
||||
fileListTemp = successFileList;
|
||||
} else if (info.file.status === 'error') {
|
||||
createMessage.error(`${info.file.name} 上传失败.`);
|
||||
}
|
||||
fileList.value = fileListTemp;
|
||||
if (info.file.status === 'done' || info.file.status === 'removed') {
|
||||
//returnUrl为true时仅返回文件路径
|
||||
if (props.returnUrl) {
|
||||
handlePathChange();
|
||||
} else {
|
||||
//returnUrl为false时返回文件名称、文件路径及文件大小
|
||||
let newFileList: any[] = [];
|
||||
for (const item of fileListTemp) {
|
||||
if (item.status === 'done') {
|
||||
let fileJson = {
|
||||
fileName: item.name,
|
||||
filePath: item.response.message,
|
||||
fileSize: item.size,
|
||||
};
|
||||
newFileList.push(fileJson);
|
||||
}else{
|
||||
return;
|
||||
}
|
||||
}
|
||||
//update-begin---author:liusq ---date:20230914 for:[issues/5327]Upload组件returnUrl为false时上传的字段值返回了一个'[object Object]' ------------
|
||||
emitValue(JSON.stringify(newFileList));
|
||||
//update-end---author:liusq ---date:20230914 for:[issues/5327]Upload组件returnUrl为false时上传的字段值返回了一个'[object Object]' ------------
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handlePathChange() {
|
||||
let uploadFiles = fileList.value;
|
||||
let path = '';
|
||||
if (!uploadFiles || uploadFiles.length == 0) {
|
||||
path = '';
|
||||
}
|
||||
let pathList: string[] = [];
|
||||
for (const item of uploadFiles) {
|
||||
if (item.status === 'done') {
|
||||
pathList.push(item.response.message);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (pathList.length > 0) {
|
||||
path = pathList.join(',');
|
||||
}
|
||||
emitValue(path);
|
||||
}
|
||||
|
||||
// 预览文件、图片
|
||||
function onFilePreview(file) {
|
||||
if (isImageMode.value) {
|
||||
createImgPreview({ imageList: [file.url], maskClosable: true });
|
||||
} else {
|
||||
window.open(file.url);
|
||||
}
|
||||
}
|
||||
|
||||
function emitValue(value) {
|
||||
emit('change', value);
|
||||
emit('update:value', value);
|
||||
}
|
||||
|
||||
function uidGenerator() {
|
||||
return '-' + parseInt(Math.random() * 10000 + 1, 10);
|
||||
}
|
||||
|
||||
function getFileName(path) {
|
||||
if (path.lastIndexOf('\\') >= 0) {
|
||||
let reg = new RegExp('\\\\', 'g');
|
||||
path = path.replace(reg, '/');
|
||||
}
|
||||
return path.substring(path.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
addActionsListener,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
//noinspection LessUnresolvedVariable
|
||||
@prefix-cls: ~'@{namespace}-j-upload';
|
||||
|
||||
.@{prefix-cls} {
|
||||
&-container {
|
||||
position: relative;
|
||||
|
||||
.upload-disabled {
|
||||
.ant-upload-list-item {
|
||||
.anticon-close {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.anticon-delete {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* update-begin-author:taoyan date:2022-5-24 for:VUEN-1093详情界面 图片下载按钮显示不全*/
|
||||
.upload-download-handler {
|
||||
right: 6px !important;
|
||||
}
|
||||
/* update-end-author:taoyan date:2022-5-24 for:VUEN-1093详情界面 图片下载按钮显示不全*/
|
||||
}
|
||||
.ant-upload-text-icon {
|
||||
color: @primary-color;
|
||||
}
|
||||
.ant-upload-list-item {
|
||||
.upload-actions-container {
|
||||
position: absolute;
|
||||
top: -31px;
|
||||
left: -18px;
|
||||
z-index: 11;
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
line-height: 28px;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
|
||||
a {
|
||||
opacity: 0.9;
|
||||
margin: 0 5px;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
.anticon {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-mover-handler,
|
||||
.upload-download-handler {
|
||||
position: absolute;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.upload-mover-handler {
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.upload-download-handler {
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<BasicModal @register="registerModal" :title="modalTitle" :width="width" @ok="handleOk" v-bind="$attrs">
|
||||
<JUpload ref="uploadRef" :value="value" v-bind="uploadBinds.props" @change="emitValue" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, unref, reactive, computed, nextTick } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import JUpload from './JUpload.vue';
|
||||
import { UploadTypeEnum } from './upload.data';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const emit = defineEmits(['change', 'update:value', 'register']);
|
||||
const props = defineProps({
|
||||
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
|
||||
width: propTypes.number.def(520),
|
||||
});
|
||||
|
||||
const uploadRef = ref();
|
||||
const uploadBinds = reactive({ props: {} as any });
|
||||
const modalTitle = computed(() => (uploadBinds.props?.fileType === UploadTypeEnum.image ? '图片上传' : '文件上传'));
|
||||
|
||||
// 注册弹窗
|
||||
const [registerModal, { closeModal }] = useModalInner(async (data) => {
|
||||
uploadBinds.props = unref(data) || {};
|
||||
if ([UploadTypeEnum.image, 'img', 'picture'].includes(uploadBinds.props?.fileType)) {
|
||||
uploadBinds.props.fileType = UploadTypeEnum.image;
|
||||
} else {
|
||||
uploadBinds.props.fileType = UploadTypeEnum.file;
|
||||
}
|
||||
nextTick(() => uploadRef.value.addActionsListener());
|
||||
});
|
||||
|
||||
function handleOk() {
|
||||
closeModal();
|
||||
}
|
||||
|
||||
function emitValue(value) {
|
||||
emit('change', value);
|
||||
emit('update:value', value);
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div v-show="download" class="upload-download-handler">
|
||||
<a class="download" title="下载" @click="onDownload">
|
||||
<Icon icon="ant-design:download" />
|
||||
</a>
|
||||
</div>
|
||||
<div v-show="mover && list.length > 1" class="upload-mover-handler">
|
||||
<a title="向前移动" @click="onMoveForward">
|
||||
<Icon icon="ant-design:arrow-left" />
|
||||
</a>
|
||||
<a title="向后移动" @click="onMoveBack">
|
||||
<Icon icon="ant-design:arrow-right" />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { unref, computed } from 'vue';
|
||||
import { Icon } from '/@/components/Icon';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const props = defineProps({
|
||||
element: { type: HTMLElement, required: true },
|
||||
fileList: { type: Object, required: true },
|
||||
mover: { type: Boolean, required: true },
|
||||
download: { type: Boolean, required: true },
|
||||
emitValue: { type: Function, required: true },
|
||||
});
|
||||
const list = computed(() => unref(props.fileList));
|
||||
|
||||
// 向前移动图片
|
||||
function onMoveForward() {
|
||||
let index = getIndexByUrl();
|
||||
if (index === -1) {
|
||||
createMessage.warn('移动失败:' + index);
|
||||
return;
|
||||
}
|
||||
if (index === 0) {
|
||||
doSwap(index, unref(list).length - 1);
|
||||
return;
|
||||
}
|
||||
doSwap(index, index - 1);
|
||||
}
|
||||
|
||||
// 向后移动图片
|
||||
function onMoveBack() {
|
||||
let index = getIndexByUrl();
|
||||
if (index === -1) {
|
||||
createMessage.warn('移动失败:' + index);
|
||||
return;
|
||||
}
|
||||
if (index == unref(list).length - 1) {
|
||||
doSwap(index, 0);
|
||||
return;
|
||||
}
|
||||
doSwap(index, index + 1);
|
||||
}
|
||||
|
||||
function doSwap(oldIndex, newIndex) {
|
||||
if (oldIndex !== newIndex) {
|
||||
let array: any[] = [...(unref(list) as Array<any>)];
|
||||
let temp = array[oldIndex];
|
||||
array[oldIndex] = array[newIndex];
|
||||
array[newIndex] = temp;
|
||||
props.emitValue(array.map((i) => i.url).join(','));
|
||||
}
|
||||
}
|
||||
|
||||
function getIndexByUrl() {
|
||||
const url = props.element?.getElementsByTagName('img')[0]?.src;
|
||||
if (url) {
|
||||
const fileList: any = unref(list);
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
let current = fileList[i].url;
|
||||
const replace = url.replace(window.location.origin, '');
|
||||
if (current === replace || encodeURI(current) === replace) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function onDownload() {
|
||||
const url = props.element?.getElementsByTagName('img')[0]?.src;
|
||||
window.open(url);
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,3 @@
|
||||
export { UploadTypeEnum } from './upload.data';
|
||||
export { default as JUpload } from './JUpload.vue';
|
||||
export { default as JUploadModal } from './JUploadModal.vue';
|
||||
@ -0,0 +1,5 @@
|
||||
export enum UploadTypeEnum {
|
||||
all = 'all',
|
||||
image = 'image',
|
||||
file = 'file',
|
||||
}
|
||||
@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div>
|
||||
<a-row class="j-select-row" type="flex" :gutter="8">
|
||||
<a-col class="left" :class="{ full: !showButton }">
|
||||
<!-- 显示加载效果 -->
|
||||
<a-input v-if="loading" readOnly placeholder="加载中…">
|
||||
<template #prefix>
|
||||
<LoadingOutlined />
|
||||
</template>
|
||||
</a-input>
|
||||
<a-select
|
||||
v-else
|
||||
ref="select"
|
||||
v-model:value="selectValues.value"
|
||||
:placeholder="placeholder"
|
||||
:mode="multiple"
|
||||
:open="false"
|
||||
:disabled="disabled"
|
||||
:options="options"
|
||||
:maxTagCount="maxTagCount"
|
||||
@change="handleChange"
|
||||
style="width: 100%"
|
||||
@click="!disabled && openModal(false)"
|
||||
v-bind="attrs"
|
||||
></a-select>
|
||||
</a-col>
|
||||
<a-col v-if="showButton" class="right">
|
||||
<a-button v-if="buttonIcon" :preIcon="buttonIcon" type="primary" @click="openModal(true)" :disabled="disabled">选择</a-button>
|
||||
<a-button v-else type="primary" @click="openModal(true)" :disabled="disabled">选择</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, inject, reactive } from 'vue';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { LoadingOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JSelectBiz',
|
||||
components: { LoadingOutlined },
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
showButton: propTypes.bool.def(true),
|
||||
disabled: propTypes.bool.def(false),
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请选择',
|
||||
},
|
||||
// 是否支持多选,默认 true
|
||||
multiple: {
|
||||
type: String,
|
||||
default: 'multiple',
|
||||
},
|
||||
// 是否正在加载
|
||||
loading: propTypes.bool.def(false),
|
||||
// 最多显示多少个 tag
|
||||
maxTagCount: propTypes.number,
|
||||
// buttonIcon
|
||||
buttonIcon: propTypes.string.def(''),
|
||||
},
|
||||
emits: ['handleOpen', 'change'],
|
||||
setup(props, { emit, refs }) {
|
||||
//接收下拉框选项
|
||||
const options = inject('selectOptions') || ref([]);
|
||||
//接收选择的值
|
||||
const selectValues = inject('selectValues') || ref({});
|
||||
const attrs = useAttrs();
|
||||
|
||||
/**
|
||||
* 打开弹出框
|
||||
*/
|
||||
function openModal(isButton) {
|
||||
if (props.showButton && isButton) {
|
||||
emit('handleOpen');
|
||||
}
|
||||
if (!props.showButton && !isButton) {
|
||||
emit('handleOpen');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下拉框值改变事件
|
||||
*/
|
||||
function handleChange(value) {
|
||||
selectValues.value = value;
|
||||
selectValues.change = true;
|
||||
emit('change', value);
|
||||
}
|
||||
|
||||
return {
|
||||
attrs,
|
||||
selectValues,
|
||||
options,
|
||||
handleChange,
|
||||
openModal,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.j-select-row {
|
||||
@width: 82px;
|
||||
|
||||
.left {
|
||||
width: calc(100% - @width - 8px);
|
||||
}
|
||||
|
||||
.right {
|
||||
width: @width;
|
||||
}
|
||||
|
||||
.full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-select-search__field) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div>
|
||||
<a-row class="j-select-row" type="flex" :gutter="8">
|
||||
<a-col class="left" :class="{ full: !showButton }">
|
||||
<a-select
|
||||
ref="select"
|
||||
v-model:value="selectValues.value"
|
||||
:mode="multiple"
|
||||
:open="false"
|
||||
:options="options"
|
||||
@change="handleChange"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col v-if="showButton" class="right">
|
||||
<a-button type="primary" @click="openModal">选择</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, inject, reactive } from 'vue';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JSelectBiz',
|
||||
components: {},
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
showButton: propTypes.bool.def(true),
|
||||
// 是否支持多选,默认 true
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: 'multiple',
|
||||
},
|
||||
},
|
||||
emits: ['btnOk'],
|
||||
setup(props, { emit, refs }) {
|
||||
//接收下拉框选项
|
||||
const options = inject('selectOptions') || ref([]);
|
||||
//接收选择的值
|
||||
const selectValues = inject('selectValues') || ref({});
|
||||
const attrs = useAttrs();
|
||||
|
||||
/**
|
||||
* 打开弹出框
|
||||
*/
|
||||
function openModal() {
|
||||
emit('btnOk');
|
||||
}
|
||||
|
||||
/**
|
||||
* 下拉框值改变事件
|
||||
*/
|
||||
function handleChange(value) {
|
||||
selectValues.value = value;
|
||||
selectValues.change = true;
|
||||
}
|
||||
|
||||
return {
|
||||
attrs,
|
||||
selectValues,
|
||||
options,
|
||||
handleChange,
|
||||
openModal,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.j-select-row {
|
||||
@width: 82px;
|
||||
|
||||
.left {
|
||||
width: calc(100% - @width - 8px);
|
||||
}
|
||||
|
||||
.right {
|
||||
width: @width;
|
||||
}
|
||||
|
||||
.full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-select-search__field) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,131 @@
|
||||
<!--部门选择框-->
|
||||
<template>
|
||||
<div>
|
||||
<BasicModal v-bind="$attrs" @register="register" :title="modalTitle" width="500px" :maxHeight="maxHeight" @ok="handleOk" destroyOnClose @visible-change="visibleChange">
|
||||
<BasicTree
|
||||
ref="treeRef"
|
||||
:treeData="treeData"
|
||||
:load-data="sync == false ? null : onLoadData"
|
||||
v-bind="getBindValue"
|
||||
@select="onSelect"
|
||||
@check="onCheck"
|
||||
:fieldNames="fieldNames"
|
||||
:checkedKeys="checkedKeys"
|
||||
:multiple="multiple"
|
||||
:checkStrictly="getCheckStrictly"
|
||||
/>
|
||||
<!--树操作部分-->
|
||||
<template #insertFooter>
|
||||
<a-dropdown placement="top">
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item v-if="multiple" key="1" @click="checkALL(true)">全部勾选</a-menu-item>
|
||||
<a-menu-item v-if="multiple" key="2" @click="checkALL(false)">取消全选</a-menu-item>
|
||||
<a-menu-item key="3" @click="expandAll(true)">展开全部</a-menu-item>
|
||||
<a-menu-item key="4" @click="expandAll(false)">折叠全部</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<a-button style="float: left"> 树操作 <Icon icon="ant-design:up-outlined" /> </a-button>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
</BasicModal>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, unref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { queryDepartTreeSync, queryTreeList } from '/@/api/common/api';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { treeProps } from '/@/components/Form/src/jeecg/props/props';
|
||||
import { BasicTree, TreeActionType } from '/@/components/Tree';
|
||||
import { useTreeBiz } from '/@/components/Form/src/jeecg/hooks/useTreeBiz';
|
||||
import {propTypes} from "/@/utils/propTypes";
|
||||
import { omit } from 'lodash-es';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DeptSelectModal',
|
||||
components: {
|
||||
BasicModal,
|
||||
BasicTree,
|
||||
},
|
||||
props: {
|
||||
...treeProps,
|
||||
//选择框标题
|
||||
modalTitle: {
|
||||
type: String,
|
||||
default: '部门选择',
|
||||
},
|
||||
// update-begin--author:liaozhiyang---date:20231220---for:【QQYUN-7678】部门组件内容过多没有滚动条(给一个默认最大高)
|
||||
maxHeight: {
|
||||
type: Number,
|
||||
default: 500,
|
||||
},
|
||||
// update-end--author:liaozhiyang---date:20231220---for:【QQYUN-7678】部门组件内容过多没有滚动条(给一个默认最大高)
|
||||
value: propTypes.oneOfType([propTypes.string, propTypes.array])
|
||||
},
|
||||
emits: ['register', 'getSelectResult', 'close'],
|
||||
setup(props, { emit, refs }) {
|
||||
//注册弹框
|
||||
const [register, { closeModal }] = useModalInner();
|
||||
const attrs = useAttrs();
|
||||
const treeRef = ref<Nullable<TreeActionType>>(null);
|
||||
|
||||
//update-begin-author:taoyan date:2022-10-28 for: 部门选择警告类型不匹配
|
||||
let propValue = props.value === ''?[]:props.value;
|
||||
//update-begin-author:liusq date:2023-05-26 for: [issues/538]JSelectDept组件受 dynamicDisabled 影响
|
||||
let temp = Object.assign({}, unref(props), unref(attrs), {value: propValue},{disabled: false});
|
||||
const getBindValue = omit(temp, 'multiple');
|
||||
//update-end-author:liusq date:2023-05-26 for: [issues/538]JSelectDept组件受 dynamicDisabled 影响
|
||||
//update-end-author:taoyan date:2022-10-28 for: 部门选择警告类型不匹配
|
||||
|
||||
const queryUrl = getQueryUrl();
|
||||
const [{ visibleChange, checkedKeys, getCheckStrictly, getSelectTreeData, onCheck, onLoadData, treeData, checkALL, expandAll, onSelect }] =
|
||||
useTreeBiz(treeRef, queryUrl, getBindValue, props);
|
||||
const searchInfo = ref(props.params);
|
||||
const tree = ref([]);
|
||||
//替换treeNode中key字段为treeData中对应的字段
|
||||
const fieldNames = {
|
||||
key: props.rowKey,
|
||||
};
|
||||
// {children:'children', title:'title', key:'key' }
|
||||
/**
|
||||
* 确定选择
|
||||
*/
|
||||
function handleOk() {
|
||||
getSelectTreeData((options, values) => {
|
||||
//回传选项和已选择的值
|
||||
emit('getSelectResult', options, values);
|
||||
//关闭弹窗
|
||||
closeModal();
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取查询数据方法 */
|
||||
function getQueryUrl() {
|
||||
let queryFn = props.sync ? queryDepartTreeSync : queryTreeList;
|
||||
//update-begin-author:taoyan date:2022-7-4 for: issues/I5F3P4 online配置部门选择后编辑,查看数据应该显示部门名称,不是部门代码
|
||||
return (params) => queryFn(Object.assign({}, params, { primaryKey: props.rowKey }));
|
||||
//update-end-author:taoyan date:2022-7-4 for: issues/I5F3P4 online配置部门选择后编辑,查看数据应该显示部门名称,不是部门代码
|
||||
}
|
||||
|
||||
return {
|
||||
tree,
|
||||
handleOk,
|
||||
searchInfo,
|
||||
treeRef,
|
||||
treeData,
|
||||
onCheck,
|
||||
onSelect,
|
||||
checkALL,
|
||||
expandAll,
|
||||
fieldNames,
|
||||
checkedKeys,
|
||||
register,
|
||||
getBindValue,
|
||||
getCheckStrictly,
|
||||
visibleChange,
|
||||
onLoadData,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,293 @@
|
||||
<template>
|
||||
<!--popup选择框-->
|
||||
<div>
|
||||
<BasicModal
|
||||
v-bind="$attrs"
|
||||
@register="register"
|
||||
:title="title"
|
||||
:width="1200"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
cancelText="关闭"
|
||||
wrapClassName="j-popup-modal"
|
||||
@visible-change="visibleChange"
|
||||
>
|
||||
<div class="jeecg-basic-table-form-container" v-if="showSearchFlag">
|
||||
<a-form ref="formRef" :model="queryParam" :label-col="labelCol" :wrapper-col="wrapperCol" @keyup.enter.native="searchQuery">
|
||||
<a-row :gutter="24">
|
||||
<template v-for="(item, index) in queryInfo">
|
||||
<template v-if="item.hidden === '1'">
|
||||
<a-col :md="8" :sm="24" :key="'query' + index" v-show="toggleSearchStatus">
|
||||
<SearchFormItem :formElRef="formRef" :queryParam="queryParam" :item="item" :dictOptions="dictOptions"></SearchFormItem>
|
||||
</a-col>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-col :md="8" :sm="24" :key="'query' + index">
|
||||
<SearchFormItem :formElRef="formRef" :queryParam="queryParam" :item="item" :dictOptions="dictOptions"></SearchFormItem>
|
||||
</a-col>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<a-col :md="8" :sm="8" v-if="showAdvancedButton">
|
||||
<span style="float: left; overflow: hidden" class="table-page-search-submitButtons">
|
||||
<a-col :lg="6">
|
||||
<a-button type="primary" preIcon="ant-design:reload-outlined" @click="searchReset">重置</a-button>
|
||||
<a-button type="primary" preIcon="ant-design:search-outlined" @click="searchQuery" style="margin-left: 8px">查询</a-button>
|
||||
<a @click="handleToggleSearch" style="margin-left: 8px">
|
||||
{{ toggleSearchStatus ? '收起' : '展开' }}
|
||||
<Icon :icon="toggleSearchStatus ? 'ant-design:up-outlined' : 'ant-design:down-outlined'" />
|
||||
</a>
|
||||
</a-col>
|
||||
</span>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:canResize="false"
|
||||
:bordered="true"
|
||||
:loading="loading"
|
||||
:rowKey="combineRowKey"
|
||||
:columns="columns"
|
||||
:showIndexColumn="false"
|
||||
:dataSource="dataSource"
|
||||
:pagination="pagination"
|
||||
:rowSelection="rowSelection"
|
||||
@row-click="clickThenCheck"
|
||||
@change="handleChangeInTable"
|
||||
>
|
||||
<template #tableTitle></template>
|
||||
</BasicTable>
|
||||
</BasicModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, unref, ref, watch, watchEffect, reactive, computed } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { usePopBiz } from '/@/components/jeecg/OnLine/hooks/usePopBiz';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JPopupOnlReportModal',
|
||||
components: {
|
||||
//此处需要异步加载BasicTable
|
||||
BasicModal,
|
||||
SearchFormItem: createAsyncComponent(() => import('/@/components/jeecg/OnLine/SearchFormItem.vue'), { loading: false }),
|
||||
BasicTable: createAsyncComponent(() => import('/@/components/Table/src/BasicTable.vue'), {
|
||||
loading: true,
|
||||
}),
|
||||
},
|
||||
props: ['multi', 'code', 'sorter', 'groupId', 'param','showAdvancedButton'],
|
||||
emits: ['ok', 'register'],
|
||||
setup(props, { emit, refs }) {
|
||||
const { createMessage } = useMessage();
|
||||
const labelCol = reactive({
|
||||
xs: { span: 24 },
|
||||
sm: { span: 6 },
|
||||
});
|
||||
const wrapperCol = reactive({
|
||||
xs: { span: 24 },
|
||||
sm: { span: 18 },
|
||||
});
|
||||
//注册弹框
|
||||
const [register, { closeModal }] = useModalInner();
|
||||
const formRef = ref();
|
||||
const tableRef = ref();
|
||||
const toggleSearchStatus = ref(false);
|
||||
const attrs = useAttrs();
|
||||
const tableScroll = ref({ x: true });
|
||||
// update-begin--author:liaozhiyang---date:20230811---for:【issues/675】子表字段Popup弹框数据不更新
|
||||
const getBindValue = computed(() => {
|
||||
return Object.assign({}, unref(props), unref(attrs));
|
||||
});
|
||||
// update-end--author:liaozhiyang---date:20230811---for:【issues/675】子表字段Popup弹框数据不更新
|
||||
const [
|
||||
{
|
||||
visibleChange,
|
||||
loadColumnsInfo,
|
||||
dynamicParamHandler,
|
||||
loadData,
|
||||
handleChangeInTable,
|
||||
combineRowKey,
|
||||
clickThenCheck,
|
||||
filterUnuseSelect,
|
||||
getOkSelectRows,
|
||||
},
|
||||
{
|
||||
visible,
|
||||
rowSelection,
|
||||
checkedKeys,
|
||||
selectRows,
|
||||
pagination,
|
||||
dataSource,
|
||||
columns,
|
||||
loading,
|
||||
title,
|
||||
iSorter,
|
||||
queryInfo,
|
||||
queryParam,
|
||||
dictOptions,
|
||||
},
|
||||
] = usePopBiz(getBindValue,tableRef);
|
||||
|
||||
const showSearchFlag = computed(() => unref(queryInfo) && unref(queryInfo).length > 0);
|
||||
/**
|
||||
*监听code
|
||||
*/
|
||||
watch(
|
||||
() => props.code,
|
||||
() => {
|
||||
loadColumnsInfo();
|
||||
}
|
||||
);
|
||||
/**
|
||||
*监听popup动态参数 支持系统变量语法
|
||||
*/
|
||||
watch(
|
||||
() => props.param,
|
||||
() => {
|
||||
// update-begin--author:liaozhiyang---date:20231213---for:【issues/901】JPopup组件配置param参数后异常
|
||||
if (visible.value) {
|
||||
dynamicParamHandler();
|
||||
loadData();
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20231213---for:【issues/901】JPopup组件配置param参数后异常
|
||||
}
|
||||
);
|
||||
/**
|
||||
*监听sorter排序字段
|
||||
*/
|
||||
watchEffect(() => {
|
||||
if (props.sorter) {
|
||||
let arr = props.sorter.split('=');
|
||||
if (arr.length === 2 && ['asc', 'desc'].includes(arr[1].toLowerCase())) {
|
||||
iSorter.value = { column: arr[0], order: arr[1].toLowerCase() };
|
||||
// 排序字段受控
|
||||
unref(columns).forEach((col) => {
|
||||
if (col.dataIndex === unref(iSorter).column) {
|
||||
col['sortOrder'] = unref(iSorter).order === 'asc' ? 'ascend' : 'descend';
|
||||
} else {
|
||||
col['sortOrder'] = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn('【JPopup】sorter参数不合法');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//update-begin-author:taoyan date:2022-5-31 for: VUEN-1156 popup 多数据有分页时,选中其他页,关闭popup 再点开,分页仍然选中上一次点击的分页,但数据是第一页的数据 未刷新
|
||||
watch(
|
||||
() => pagination.current,
|
||||
(current) => {
|
||||
if (current) {
|
||||
tableRef.value.setPagination({
|
||||
current: current,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
//update-end-author:taoyan date:2022-5-31 for: VUEN-1156 popup 多数据有分页时,选中其他页,关闭popup 再点开,分页仍然选中上一次点击的分页,但数据是第一页的数据 未刷新
|
||||
|
||||
function handleToggleSearch() {
|
||||
toggleSearchStatus.value = !unref(toggleSearchStatus);
|
||||
}
|
||||
/**
|
||||
* 取消/关闭
|
||||
*/
|
||||
function handleCancel() {
|
||||
closeModal();
|
||||
checkedKeys.value = [];
|
||||
selectRows.value = [];
|
||||
// update-begin--author:liaozhiyang---date:20230908---for:【issues/742】选择后删除默认仍然存在
|
||||
tableRef.value.clearSelectedRowKeys();
|
||||
// update-end--author:liaozhiyang---date:20230908---for:【issues/742】选择后删除默认仍然存在
|
||||
}
|
||||
|
||||
/**
|
||||
*确认提交
|
||||
*/
|
||||
function handleSubmit() {
|
||||
filterUnuseSelect();
|
||||
if (!props.multi && unref(selectRows) && unref(selectRows).length > 1) {
|
||||
createMessage.warning('只能选择一条记录');
|
||||
return false;
|
||||
}
|
||||
if (!unref(selectRows) || unref(selectRows).length == 0) {
|
||||
createMessage.warning('至少选择一条记录');
|
||||
return false;
|
||||
}
|
||||
//update-begin-author:taoyan date:2022-5-31 for: VUEN-1155 popup 选择数据时,会选择多条重复数据
|
||||
let rows = getOkSelectRows!();
|
||||
emit('ok', rows);
|
||||
//update-end-author:taoyan date:2022-5-31 for: VUEN-1155 popup 选择数据时,会选择多条重复数据
|
||||
handleCancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询
|
||||
*/
|
||||
function searchQuery() {
|
||||
loadData(1);
|
||||
}
|
||||
/**
|
||||
* 重置
|
||||
*/
|
||||
function searchReset() {
|
||||
queryParam.value = {};
|
||||
loadData(1);
|
||||
}
|
||||
return {
|
||||
attrs,
|
||||
register,
|
||||
tableScroll,
|
||||
dataSource,
|
||||
pagination,
|
||||
columns,
|
||||
rowSelection,
|
||||
checkedKeys,
|
||||
loading,
|
||||
title,
|
||||
handleCancel,
|
||||
handleSubmit,
|
||||
clickThenCheck,
|
||||
loadData,
|
||||
combineRowKey,
|
||||
handleChangeInTable,
|
||||
visibleChange,
|
||||
queryInfo,
|
||||
queryParam,
|
||||
tableRef,
|
||||
formRef,
|
||||
labelCol,
|
||||
wrapperCol,
|
||||
dictOptions,
|
||||
showSearchFlag,
|
||||
toggleSearchStatus,
|
||||
handleToggleSearch,
|
||||
searchQuery,
|
||||
searchReset,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.jeecg-basic-table-form-container {
|
||||
padding: 5px;
|
||||
|
||||
.table-page-search-submitButtons {
|
||||
display: block;
|
||||
margin-bottom: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
:deep(.jeecg-basic-table .ant-table-wrapper .ant-table-title){
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,194 @@
|
||||
<!--职务选择框-->
|
||||
<template>
|
||||
<div>
|
||||
<BasicModal
|
||||
v-bind="$attrs"
|
||||
@register="register"
|
||||
:title="modalTitle"
|
||||
width="1100px"
|
||||
wrapClassName="j-user-select-modal"
|
||||
@ok="handleOk"
|
||||
destroyOnClose
|
||||
@visible-change="visibleChange"
|
||||
>
|
||||
<a-row>
|
||||
<a-col :span="showSelected ? 18 : 24">
|
||||
<BasicTable
|
||||
:columns="columns"
|
||||
:bordered="true"
|
||||
:useSearchForm="true"
|
||||
:formConfig="formConfig"
|
||||
:api="getPositionList"
|
||||
:searchInfo="searchInfo"
|
||||
:rowSelection="rowSelection"
|
||||
:indexColumnProps="indexColumnProps"
|
||||
v-bind="getBindValue"
|
||||
></BasicTable>
|
||||
</a-col>
|
||||
<a-col :span="showSelected ? 6 : 0">
|
||||
<BasicTable
|
||||
v-bind="selectedTable"
|
||||
:dataSource="selectRows"
|
||||
:useSearchForm="true"
|
||||
:formConfig="{ showActionButtonGroup: false, baseRowStyle: { minHeight: '40px' } }"
|
||||
>
|
||||
<!--操作栏-->
|
||||
<template #action="{ record }">
|
||||
<a href="javascript:void(0)" @click="handleDeleteSelected(record)"><Icon icon="ant-design:delete-outlined"></Icon></a>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</BasicModal>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, unref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { getPositionList } from '/@/api/common/api';
|
||||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||
import { useSelectBiz } from '/@/components/Form/src/jeecg/hooks/useSelectBiz';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { selectProps } from '/@/components/Form/src/jeecg/props/props';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PositionSelectModal',
|
||||
components: {
|
||||
//此处需要异步加载BasicTable
|
||||
BasicModal,
|
||||
BasicTable: createAsyncComponent(() => import('/@/components/Table/src/BasicTable.vue'), {
|
||||
loading: true,
|
||||
}),
|
||||
},
|
||||
props: {
|
||||
...selectProps,
|
||||
//选择框标题
|
||||
modalTitle: {
|
||||
type: String,
|
||||
default: '职务选择',
|
||||
},
|
||||
},
|
||||
emits: ['register', 'getSelectResult'],
|
||||
setup(props, { emit, refs }) {
|
||||
//注册弹框
|
||||
const [register, { closeModal }] = useModalInner();
|
||||
const attrs = useAttrs();
|
||||
//表格配置
|
||||
const config = {
|
||||
canResize: false,
|
||||
bordered: true,
|
||||
size: 'small',
|
||||
//改成读取rowKey,自定义传递参数
|
||||
rowKey: props.rowKey,
|
||||
};
|
||||
const getBindValue = Object.assign({}, unref(props), unref(attrs), config);
|
||||
const [{ rowSelection, visibleChange, indexColumnProps, getSelectResult, handleDeleteSelected, selectRows }] = useSelectBiz(
|
||||
getPositionList,
|
||||
getBindValue
|
||||
);
|
||||
const searchInfo = ref(props.params);
|
||||
//查询form
|
||||
const formConfig = {
|
||||
labelCol: {
|
||||
span: 4,
|
||||
},
|
||||
baseColProps: {
|
||||
xs: 24,
|
||||
sm: 10,
|
||||
md: 10,
|
||||
lg: 10,
|
||||
xl: 10,
|
||||
xxl: 10,
|
||||
},
|
||||
//update-begin-author:liusq date:2023-10-30 for: [issues/5514]组件页面显示错位
|
||||
actionColOptions: {
|
||||
xs: 24,
|
||||
sm: 8,
|
||||
md: 8,
|
||||
lg: 8,
|
||||
xl: 8,
|
||||
xxl: 8,
|
||||
},
|
||||
//update-end-author:liusq date:2023-10-30 for: [issues/5514]组件页面显示错位
|
||||
schemas: [
|
||||
{
|
||||
label: '职务名称',
|
||||
field: 'name',
|
||||
component: 'JInput',
|
||||
colProps: { span: 10 },
|
||||
},
|
||||
],
|
||||
};
|
||||
//定义表格列
|
||||
const columns = [
|
||||
{
|
||||
title: '职务编码',
|
||||
dataIndex: 'code',
|
||||
width: 180,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: '职务名称',
|
||||
dataIndex: 'name',
|
||||
// width: 180,
|
||||
},
|
||||
{
|
||||
title: '职务等级',
|
||||
dataIndex: 'postRank_dictText',
|
||||
width: 180,
|
||||
},
|
||||
];
|
||||
//已选择的table信息
|
||||
const selectedTable = {
|
||||
pagination: false,
|
||||
showIndexColumn: false,
|
||||
scroll: { y: 390 },
|
||||
size: 'small',
|
||||
canResize: false,
|
||||
bordered: true,
|
||||
rowKey: 'id',
|
||||
columns: [
|
||||
{
|
||||
title: '职务名称',
|
||||
dataIndex: 'name',
|
||||
width: 40,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
align: 'center',
|
||||
width: 40,
|
||||
slots: { customRender: 'action' },
|
||||
},
|
||||
],
|
||||
};
|
||||
/**
|
||||
* 确定选择
|
||||
*/
|
||||
function handleOk() {
|
||||
getSelectResult((options, values) => {
|
||||
//回传选项和已选择的值
|
||||
emit('getSelectResult', options, values);
|
||||
//关闭弹窗
|
||||
closeModal();
|
||||
});
|
||||
}
|
||||
return {
|
||||
handleOk,
|
||||
getPositionList,
|
||||
register,
|
||||
visibleChange,
|
||||
getBindValue,
|
||||
formConfig,
|
||||
indexColumnProps,
|
||||
columns,
|
||||
rowSelection,
|
||||
|
||||
selectedTable,
|
||||
selectRows,
|
||||
handleDeleteSelected,
|
||||
searchInfo,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,130 @@
|
||||
<!--角色选择框-->
|
||||
<template>
|
||||
<div>
|
||||
<BasicModal v-bind="$attrs" @register="register" :title="modalTitle" width="800px" @ok="handleOk" destroyOnClose @visible-change="visibleChange">
|
||||
<BasicTable
|
||||
:columns="columns"
|
||||
v-bind="config"
|
||||
:useSearchForm="true"
|
||||
:formConfig="formConfig"
|
||||
:api="getRoleList"
|
||||
:searchInfo="searchInfo"
|
||||
:rowSelection="rowSelection"
|
||||
:indexColumnProps="indexColumnProps"
|
||||
></BasicTable>
|
||||
</BasicModal>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, unref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { getRoleList } from '/@/api/common/api';
|
||||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||
import { useSelectBiz } from '/@/components/Form/src/jeecg/hooks/useSelectBiz';
|
||||
import { selectProps } from '/@/components/Form/src/jeecg/props/props';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'UserSelectModal',
|
||||
components: {
|
||||
//此处需要异步加载BasicTable
|
||||
BasicModal,
|
||||
BasicTable: createAsyncComponent(() => import('/@/components/Table/src/BasicTable.vue'), {
|
||||
loading: true,
|
||||
}),
|
||||
},
|
||||
props: {
|
||||
...selectProps,
|
||||
//选择框标题
|
||||
modalTitle: {
|
||||
type: String,
|
||||
default: '角色选择',
|
||||
},
|
||||
},
|
||||
emits: ['register', 'getSelectResult'],
|
||||
setup(props, { emit, refs }) {
|
||||
//注册弹框
|
||||
const [register, { closeModal }] = useModalInner();
|
||||
const attrs = useAttrs();
|
||||
//表格配置
|
||||
const config = {
|
||||
canResize: false,
|
||||
bordered: true,
|
||||
size: 'small',
|
||||
rowKey: unref(props).rowKey,
|
||||
};
|
||||
const getBindValue = Object.assign({}, unref(props), unref(attrs), config);
|
||||
const [{ rowSelection, indexColumnProps, visibleChange, getSelectResult }] = useSelectBiz(getRoleList, getBindValue);
|
||||
const searchInfo = ref(props.params);
|
||||
//查询form
|
||||
const formConfig = {
|
||||
//labelWidth: 220,
|
||||
baseColProps: {
|
||||
xs: 24,
|
||||
sm: 24,
|
||||
md: 24,
|
||||
lg: 14,
|
||||
xl: 14,
|
||||
xxl: 14,
|
||||
},
|
||||
//update-begin-author:liusq date:2023-10-30 for: [issues/5514]组件页面显示错位
|
||||
actionColOptions: {
|
||||
xs: 24,
|
||||
sm: 8,
|
||||
md: 8,
|
||||
lg: 8,
|
||||
xl: 8,
|
||||
xxl: 8,
|
||||
},
|
||||
//update-end-author:liusq date:2023-10-30 for: [issues/5514]组件页面显示错位
|
||||
schemas: [
|
||||
{
|
||||
label: '角色名称',
|
||||
field: 'roleName',
|
||||
component: 'Input',
|
||||
},
|
||||
],
|
||||
};
|
||||
//定义表格列
|
||||
const columns = [
|
||||
{
|
||||
title: '角色名称',
|
||||
dataIndex: 'roleName',
|
||||
width: 240,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: '角色编码',
|
||||
dataIndex: 'roleCode',
|
||||
// width: 40,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 确定选择
|
||||
*/
|
||||
function handleOk() {
|
||||
getSelectResult((options, values) => {
|
||||
//回传选项和已选择的值
|
||||
emit('getSelectResult', options, values);
|
||||
//关闭弹窗
|
||||
closeModal();
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
config,
|
||||
handleOk,
|
||||
searchInfo,
|
||||
register,
|
||||
indexColumnProps,
|
||||
visibleChange,
|
||||
getRoleList,
|
||||
formConfig,
|
||||
getBindValue,
|
||||
columns,
|
||||
rowSelection,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,224 @@
|
||||
<!--通过部门选择用户-->
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="register" :title="modalTitle" width="1200px" @ok="handleOk" destroyOnClose @visible-change="visibleChange">
|
||||
<a-row :gutter="10">
|
||||
<a-col :md="7" :sm="24">
|
||||
<a-card :style="{ minHeight: '613px', overflow: 'auto' }">
|
||||
<!--组织机构-->
|
||||
<BasicTree
|
||||
ref="treeRef"
|
||||
:style="{ minWidth: '250px' }"
|
||||
selectable
|
||||
@select="onDepSelect"
|
||||
:load-data="loadChildrenTreeData"
|
||||
:treeData="departTree"
|
||||
:selectedKeys="selectedDepIds"
|
||||
:expandedKeys="expandedKeys"
|
||||
:clickRowToExpand="false"
|
||||
></BasicTree>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :md="17" :sm="24">
|
||||
<a-card :style="{ minHeight: '613px', overflow: 'auto' }">
|
||||
<!--用户列表-->
|
||||
<BasicTable ref="tableRef" v-bind="getBindValue" :searchInfo="searchInfo" :api="getTableList" :rowSelection="rowSelection"></BasicTable>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</BasicModal>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, unref, ref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { BasicTree } from '/@/components/Tree/index';
|
||||
import { queryTreeList, getTableList } from '/@/api/common/api';
|
||||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||
import { useSelectBiz } from '/@/components/Form/src/jeecg/hooks/useSelectBiz';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { queryDepartTreeSync } from '/@/views/system/depart/depart.api';
|
||||
import { selectProps } from '/@/components/Form/src/jeecg/props/props';
|
||||
export default defineComponent({
|
||||
name: 'UserSelectByDepModal',
|
||||
components: {
|
||||
//此处需要异步加载BasicTable
|
||||
BasicModal,
|
||||
BasicTree,
|
||||
BasicTable: createAsyncComponent(() => import('/@/components/Table/src/BasicTable.vue'), {
|
||||
loading: true,
|
||||
}),
|
||||
},
|
||||
props: {
|
||||
...selectProps,
|
||||
//选择框标题
|
||||
modalTitle: {
|
||||
type: String,
|
||||
default: '部门用户选择',
|
||||
},
|
||||
},
|
||||
emits: ['register', 'getSelectResult'],
|
||||
setup(props, { emit, refs }) {
|
||||
const tableRef = ref();
|
||||
const treeRef = ref();
|
||||
//注册弹框
|
||||
const [register, { closeModal }] = useModalInner(async (data) => {
|
||||
await queryDepartTree();
|
||||
});
|
||||
const attrs = useAttrs();
|
||||
const departTree = ref([]);
|
||||
const selectedDepIds = ref([]);
|
||||
const expandedKeys = ref([]);
|
||||
const searchInfo = {};
|
||||
/**
|
||||
*表格配置
|
||||
*/
|
||||
const tableProps = {
|
||||
columns: [
|
||||
{
|
||||
title: '用户账号',
|
||||
dataIndex: 'username',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '用户姓名',
|
||||
dataIndex: 'realname',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '性别',
|
||||
dataIndex: 'sex_dictText',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '手机号码',
|
||||
dataIndex: 'phone',
|
||||
// width: 50,
|
||||
},
|
||||
],
|
||||
useSearchForm: true,
|
||||
canResize: false,
|
||||
showIndexColumn: false,
|
||||
striped: true,
|
||||
bordered: true,
|
||||
size: 'small',
|
||||
formConfig: {
|
||||
//labelWidth: 200,
|
||||
baseColProps: {
|
||||
xs: 24,
|
||||
sm: 8,
|
||||
md: 6,
|
||||
lg: 8,
|
||||
xl: 6,
|
||||
xxl: 10,
|
||||
},
|
||||
//update-begin-author:liusq date:2023-10-30 for: [issues/5514]组件页面显示错位
|
||||
actionColOptions: {
|
||||
xs: 24,
|
||||
sm: 12,
|
||||
md: 12,
|
||||
lg: 12,
|
||||
xl: 8,
|
||||
xxl: 8,
|
||||
},
|
||||
//update-end-author:liusq date:2023-10-30 for: [issues/5514]组件页面显示错位
|
||||
schemas: [
|
||||
{
|
||||
label: '账号',
|
||||
field: 'username',
|
||||
component: 'Input',
|
||||
},
|
||||
],
|
||||
resetFunc: customResetFunc,
|
||||
},
|
||||
};
|
||||
const getBindValue = Object.assign({}, unref(props), unref(attrs), tableProps);
|
||||
const [{ rowSelection, visibleChange, indexColumnProps, getSelectResult, reset }] = useSelectBiz(getTableList, getBindValue);
|
||||
/**
|
||||
* 加载树形数据
|
||||
*/
|
||||
function queryDepartTree() {
|
||||
queryDepartTreeSync().then((res) => {
|
||||
if (res) {
|
||||
departTree.value = res;
|
||||
// 默认展开父节点
|
||||
//expandedKeys.value = unref(departTree).map(item => item.id)
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 加载子级部门
|
||||
*/
|
||||
async function loadChildrenTreeData(treeNode) {
|
||||
try {
|
||||
const result = await queryDepartTreeSync({
|
||||
pid: treeNode.eventKey,
|
||||
});
|
||||
const asyncTreeAction = unref(treeRef);
|
||||
if (asyncTreeAction) {
|
||||
asyncTreeAction.updateNodeByKey(treeNode.eventKey, { children: result });
|
||||
asyncTreeAction.setExpandedKeys([treeNode.eventKey, ...asyncTreeAction.getExpandedKeys()]);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
/**
|
||||
* 点击树节点,筛选出对应的用户
|
||||
*/
|
||||
function onDepSelect(keys) {
|
||||
if (keys[0] != null) {
|
||||
if (unref(selectedDepIds)[0] !== keys[0]) {
|
||||
selectedDepIds.value = [keys[0]];
|
||||
}
|
||||
searchInfo['departId'] = unref(selectedDepIds).join(',');
|
||||
tableRef.value.reload();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 自定义重置方法
|
||||
* */
|
||||
async function customResetFunc() {
|
||||
console.log('自定义查询');
|
||||
//树节点清空
|
||||
selectedDepIds.value = [];
|
||||
//查询条件清空
|
||||
searchInfo['departId'] = '';
|
||||
//选择项清空
|
||||
reset();
|
||||
}
|
||||
/**
|
||||
* 确定选择
|
||||
*/
|
||||
function handleOk() {
|
||||
getSelectResult((options, values) => {
|
||||
//回传选项和已选择的值
|
||||
emit('getSelectResult', options, values);
|
||||
//关闭弹窗
|
||||
closeModal();
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
//config,
|
||||
handleOk,
|
||||
searchInfo,
|
||||
register,
|
||||
indexColumnProps,
|
||||
visibleChange,
|
||||
getBindValue,
|
||||
rowSelection,
|
||||
|
||||
departTree,
|
||||
selectedDepIds,
|
||||
expandedKeys,
|
||||
treeRef,
|
||||
tableRef,
|
||||
getTableList,
|
||||
onDepSelect,
|
||||
loadChildrenTreeData,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
||||
@ -0,0 +1,293 @@
|
||||
<!--用户选择框-->
|
||||
<template>
|
||||
<div>
|
||||
<BasicModal
|
||||
v-bind="$attrs"
|
||||
@register="register"
|
||||
:title="modalTitle"
|
||||
:width="showSelected ? '1200px' : '900px'"
|
||||
wrapClassName="j-user-select-modal"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
:maxHeight="maxHeight"
|
||||
:centered="true"
|
||||
destroyOnClose
|
||||
@visible-change="visibleChange"
|
||||
|
||||
>
|
||||
<a-row>
|
||||
<a-col :span="showSelected ? 18 : 24">
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
:scroll="tableScroll"
|
||||
v-bind="getBindValue"
|
||||
:useSearchForm="true"
|
||||
:formConfig="formConfig"
|
||||
:api="getUserList"
|
||||
:searchInfo="searchInfo"
|
||||
:rowSelection="rowSelection"
|
||||
:indexColumnProps="indexColumnProps"
|
||||
:afterFetch="afterFetch"
|
||||
>
|
||||
<!-- update-begin-author:taoyan date:2022-5-25 for: VUEN-1112一对多 用户选择 未显示选择条数,及清空 -->
|
||||
<template #tableTitle></template>
|
||||
<!-- update-end-author:taoyan date:2022-5-25 for: VUEN-1112一对多 用户选择 未显示选择条数,及清空 -->
|
||||
</BasicTable>
|
||||
</a-col>
|
||||
<a-col :span="showSelected ? 6 : 0">
|
||||
<BasicTable
|
||||
v-bind="selectedTable"
|
||||
:dataSource="selectRows"
|
||||
:useSearchForm="true"
|
||||
:formConfig="{ showActionButtonGroup: false, baseRowStyle: { minHeight: '40px' } }"
|
||||
>
|
||||
<!--操作栏-->
|
||||
<template #action="{ record }">
|
||||
<a href="javascript:void(0)" @click="handleDeleteSelected(record)"><Icon icon="ant-design:delete-outlined"></Icon></a>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</BasicModal>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, unref, ref, watch } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { getUserList } from '/@/api/common/api';
|
||||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||
import { useSelectBiz } from '/@/components/Form/src/jeecg/hooks/useSelectBiz';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { selectProps } from '/@/components/Form/src/jeecg/props/props';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'UserSelectModal',
|
||||
components: {
|
||||
//此处需要异步加载BasicTable
|
||||
BasicModal,
|
||||
BasicTable: createAsyncComponent(() => import('/@/components/Table/src/BasicTable.vue'), {
|
||||
loading: true,
|
||||
}),
|
||||
},
|
||||
props: {
|
||||
...selectProps,
|
||||
//选择框标题
|
||||
modalTitle: {
|
||||
type: String,
|
||||
default: '选择用户',
|
||||
},
|
||||
//update-begin---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
|
||||
//排除用户id的集合
|
||||
excludeUserIdList: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
//update-end---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
|
||||
},
|
||||
emits: ['register', 'getSelectResult', 'close'],
|
||||
setup(props, { emit, refs }) {
|
||||
// update-begin-author:taoyan date:2022-5-24 for: VUEN-1086 【移动端】用户选择 查询按钮 效果不好 列表展示没有滚动条
|
||||
const tableScroll = ref<any>({ x: false });
|
||||
const tableRef = ref();
|
||||
const maxHeight = ref(600);
|
||||
|
||||
//注册弹框
|
||||
const [register, { closeModal }] = useModalInner(() => {
|
||||
if (window.innerWidth < 900) {
|
||||
tableScroll.value = { x: 900 };
|
||||
} else {
|
||||
tableScroll.value = { x: false };
|
||||
}
|
||||
//update-begin-author:taoyan date:2022-6-2 for: VUEN-1112 一对多 用户选择 未显示选择条数,及清空
|
||||
setTimeout(() => {
|
||||
if (tableRef.value) {
|
||||
tableRef.value.setSelectedRowKeys(selectValues['value'] || []);
|
||||
}
|
||||
}, 800);
|
||||
//update-end-author:taoyan date:2022-6-2 for: VUEN-1112 一对多 用户选择 未显示选择条数,及清空
|
||||
});
|
||||
// update-end-author:taoyan date:2022-5-24 for: VUEN-1086 【移动端】用户选择 查询按钮 效果不好 列表展示没有滚动条
|
||||
const attrs = useAttrs();
|
||||
//表格配置
|
||||
const config = {
|
||||
canResize: false,
|
||||
bordered: true,
|
||||
size: 'small',
|
||||
};
|
||||
const getBindValue = Object.assign({}, unref(props), unref(attrs), config);
|
||||
const [{ rowSelection, visibleChange, selectValues, indexColumnProps, getSelectResult, handleDeleteSelected, selectRows }] = useSelectBiz(
|
||||
getUserList,
|
||||
getBindValue,
|
||||
emit
|
||||
);
|
||||
const searchInfo = ref(props.params);
|
||||
// update-begin--author:liaozhiyang---date:20230811---for:【issues/657】右侧选中列表删除无效
|
||||
watch(rowSelection.selectedRowKeys, (newVal) => {
|
||||
//update-begin---author:wangshuai ---date: 20230829 for:null指针异常导致控制台报错页面不显示------------
|
||||
if(tableRef.value){
|
||||
tableRef.value.setSelectedRowKeys(newVal);
|
||||
}
|
||||
//update-end---author:wangshuai ---date: 20230829 for:null指针异常导致控制台报错页面不显示------------
|
||||
});
|
||||
// update-end--author:liaozhiyang---date:20230811---for:【issues/657】右侧选中列表删除无效
|
||||
//查询form
|
||||
const formConfig = {
|
||||
baseColProps: {
|
||||
xs: 24,
|
||||
sm: 8,
|
||||
md: 6,
|
||||
lg: 8,
|
||||
xl: 6,
|
||||
xxl: 6,
|
||||
},
|
||||
//update-begin-author:taoyan date:2022-5-24 for: VUEN-1086 【移动端】用户选择 查询按钮 效果不好 列表展示没有滚动条---查询表单按钮的栅格布局和表单的保持一致
|
||||
actionColOptions: {
|
||||
xs: 24,
|
||||
sm: 8,
|
||||
md: 8,
|
||||
lg: 8,
|
||||
xl: 8,
|
||||
xxl: 8,
|
||||
},
|
||||
//update-end-author:taoyan date:2022-5-24 for: VUEN-1086 【移动端】用户选择 查询按钮 效果不好 列表展示没有滚动条---查询表单按钮的栅格布局和表单的保持一致
|
||||
schemas: [
|
||||
{
|
||||
label: '账号',
|
||||
field: 'username',
|
||||
component: 'JInput',
|
||||
},
|
||||
{
|
||||
label: '姓名',
|
||||
field: 'realname',
|
||||
component: 'JInput',
|
||||
},
|
||||
],
|
||||
};
|
||||
//定义表格列
|
||||
const columns = [
|
||||
{
|
||||
title: '用户账号',
|
||||
dataIndex: 'username',
|
||||
width: 120,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: '用户姓名',
|
||||
dataIndex: 'realname',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '性别',
|
||||
dataIndex: 'sex_dictText',
|
||||
width: 50,
|
||||
},
|
||||
{
|
||||
title: '手机号码',
|
||||
dataIndex: 'phone',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
dataIndex: 'email',
|
||||
// width: 40,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status_dictText',
|
||||
width: 80,
|
||||
},
|
||||
];
|
||||
//已选择的table信息
|
||||
const selectedTable = {
|
||||
pagination: false,
|
||||
showIndexColumn: false,
|
||||
scroll: { y: 390 },
|
||||
size: 'small',
|
||||
canResize: false,
|
||||
bordered: true,
|
||||
rowKey: 'id',
|
||||
columns: [
|
||||
{
|
||||
title: '用户姓名',
|
||||
dataIndex: 'realname',
|
||||
width: 40,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
align: 'center',
|
||||
width: 40,
|
||||
slots: { customRender: 'action' },
|
||||
},
|
||||
],
|
||||
};
|
||||
/**
|
||||
* 确定选择
|
||||
*/
|
||||
function handleOk() {
|
||||
getSelectResult((options, values) => {
|
||||
//回传选项和已选择的值
|
||||
emit('getSelectResult', options, values);
|
||||
//关闭弹窗
|
||||
closeModal();
|
||||
});
|
||||
}
|
||||
|
||||
//update-begin---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
|
||||
/**
|
||||
* 用户返回结果逻辑查询
|
||||
*/
|
||||
function afterFetch(record) {
|
||||
let excludeList = props.excludeUserIdList;
|
||||
if(!excludeList){
|
||||
return record;
|
||||
}
|
||||
let arr:any[] = [];
|
||||
//如果存在过滤用户id集合,并且后台返回的数据不为空
|
||||
if(excludeList.length>0 && record && record.length>0){
|
||||
for(let item of record){
|
||||
if(excludeList.indexOf(item.id)<0){
|
||||
arr.push({...item})
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
return record;
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20240517---for:【QQYUN-9366】用户选择组件取消和关闭会把选择数据带入
|
||||
const handleCancel = () => {
|
||||
emit('close');
|
||||
};
|
||||
// update-end--author:liaozhiyang---date:20240517---for:【QQYUN-9366】用户选择组件取消和关闭会把选择数据带入
|
||||
//update-end---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
|
||||
|
||||
// update-begin--author:liaozhiyang---date:20240607---for:【TV360X-305】小屏幕展示10条
|
||||
const clientHeight = document.documentElement.clientHeight * 200;
|
||||
maxHeight.value = clientHeight > 600 ? 600 : clientHeight;
|
||||
// update-end--author:liaozhiyang---date:20240607---for:【TV360X-305】小屏幕展示10条
|
||||
|
||||
return {
|
||||
//config,
|
||||
handleOk,
|
||||
searchInfo,
|
||||
register,
|
||||
indexColumnProps,
|
||||
visibleChange,
|
||||
getBindValue,
|
||||
getUserList,
|
||||
formConfig,
|
||||
columns,
|
||||
rowSelection,
|
||||
selectRows,
|
||||
selectedTable,
|
||||
handleDeleteSelected,
|
||||
tableScroll,
|
||||
tableRef,
|
||||
afterFetch,
|
||||
handleCancel,
|
||||
maxHeight,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,282 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
@register="register"
|
||||
:getContainer="getContainer"
|
||||
:canFullscreen="false"
|
||||
:title="title"
|
||||
:width="500"
|
||||
destroyOnClose
|
||||
@ok="handleOk"
|
||||
wrapClassName="j-user-select-modal2" >
|
||||
|
||||
<div style="position: relative; min-height: 350px">
|
||||
<div style="width: 100%">
|
||||
<a-input v-model:value="searchText" allowClear style="width: 100%" placeholder="搜索">
|
||||
<template #prefix>
|
||||
<SearchOutlined style="color: #c0c0c0" />
|
||||
</template>
|
||||
</a-input>
|
||||
</div>
|
||||
|
||||
<!-- tabs -->
|
||||
<div class="modal-select-list-container">
|
||||
<div class="scroll">
|
||||
<div class="content" style="right: -10px">
|
||||
|
||||
<label class="item" v-for="item in showDataList" @click="(e)=>onSelect(e, item)">
|
||||
<a-checkbox v-model:checked="item.checked">
|
||||
<span>{{ item.name }}</span>
|
||||
</a-checkbox>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 选中用户 -->
|
||||
<div class="selected-users" style="width: 100%; overflow-x: hidden">
|
||||
<SelectedUserItem v-for="item in selectedList" :info="item" @unSelect="unSelect" />
|
||||
</div>
|
||||
</div>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { SearchOutlined, CloseOutlined } from '@ant-design/icons-vue';
|
||||
import SelectedUserItem from '../userSelect/SelectedUserItem.vue';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { computed, ref, toRaw, watch } from 'vue';
|
||||
|
||||
export default {
|
||||
name: 'PositionSelectModal',
|
||||
components: {
|
||||
BasicModal,
|
||||
SearchOutlined,
|
||||
CloseOutlined,
|
||||
SelectedUserItem,
|
||||
},
|
||||
props: {
|
||||
multi: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
getContainer: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
title:{
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'sys_position',
|
||||
},
|
||||
appId: {
|
||||
type: String,
|
||||
default: '',
|
||||
}
|
||||
},
|
||||
emits: ['selected', 'register'],
|
||||
setup(props, { emit }) {
|
||||
|
||||
const searchText = ref('');
|
||||
const selectedIdList = computed(() => {
|
||||
let arr = selectedList.value;
|
||||
if (!arr || arr.length == 0) {
|
||||
return [];
|
||||
} else {
|
||||
return arr.map((k) => k.id);
|
||||
}
|
||||
});
|
||||
|
||||
watch(()=>props.appId, async (val)=>{
|
||||
if(val){
|
||||
await loadDataList();
|
||||
}
|
||||
}, {immediate: true});
|
||||
|
||||
|
||||
// 弹窗事件
|
||||
const [register] = useModalInner(() => {
|
||||
let list = dataList.value;
|
||||
if(!list || list.length ==0 ){
|
||||
}
|
||||
for(let item of list){
|
||||
item.checked = false
|
||||
}
|
||||
});
|
||||
|
||||
// 确定事件
|
||||
function handleOk() {
|
||||
let arr = toRaw(selectedIdList.value);
|
||||
emit('selected', arr);
|
||||
}
|
||||
|
||||
const dataList = ref<any[]>([]);
|
||||
const showDataList = computed(()=>{
|
||||
let list = dataList.value;
|
||||
if(!list || list.length ==0 ){
|
||||
return []
|
||||
}
|
||||
let text = searchText.value;
|
||||
if(!text){
|
||||
return list
|
||||
}
|
||||
return list.filter(item=>item.name.indexOf(text)>=0)
|
||||
});
|
||||
|
||||
const selectedList = computed(()=>{
|
||||
let list = dataList.value;
|
||||
if(!list || list.length ==0 ){
|
||||
return []
|
||||
}
|
||||
return list.filter(item=>item.checked)
|
||||
});
|
||||
|
||||
function unSelect(id) {
|
||||
let list = dataList.value;
|
||||
if(!list || list.length ==0 ){
|
||||
return;
|
||||
}
|
||||
let arr = list.filter(item=>item.id == id);
|
||||
arr[0].checked = false;
|
||||
}
|
||||
|
||||
async function loadDataList() {
|
||||
let params = {
|
||||
pageNo: 1,
|
||||
pageSize: 200,
|
||||
column: 'createTime',
|
||||
order: 'desc'
|
||||
};
|
||||
const url = '/sys/position/list'
|
||||
const data = await defHttp.get({ url, params }, { isTransformResponse: false });
|
||||
if (data.success) {
|
||||
const { records } = data.result;
|
||||
let arr:any[] = [];
|
||||
if(records && records.length>0){
|
||||
for(let item of records){
|
||||
arr.push({
|
||||
id: item.id,
|
||||
name: item.name || item.roleName,
|
||||
selectType: props.type,
|
||||
checked: false
|
||||
})
|
||||
}
|
||||
}
|
||||
dataList.value = arr;
|
||||
} else {
|
||||
console.error(data.message);
|
||||
}
|
||||
console.log('loadDataList', data);
|
||||
}
|
||||
|
||||
|
||||
function onSelect(e, item) {
|
||||
prevent(e);
|
||||
console.log('onselect');
|
||||
item.checked = !item.checked;
|
||||
}
|
||||
|
||||
function prevent(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
return {
|
||||
register,
|
||||
showDataList,
|
||||
searchText,
|
||||
handleOk,
|
||||
selectedList,
|
||||
selectedIdList,
|
||||
unSelect,
|
||||
onSelect
|
||||
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.modal-select-list-container{
|
||||
height: 352px;
|
||||
margin-top: 12px;
|
||||
overflow: auto;
|
||||
.scroll{
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
.content{
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow: scroll;
|
||||
overflow-x: hidden;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
.item{
|
||||
padding: 7px 5px;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
&:hover{
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.j-user-select-modal2 {
|
||||
.depart-select {
|
||||
.ant-select-selector {
|
||||
color: #fff !important;
|
||||
background-color: #409eff !important;
|
||||
border-radius: 5px !important;
|
||||
}
|
||||
.ant-select-selection-item,
|
||||
.ant-select-arrow {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
.my-search {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
z-index: 1;
|
||||
&.all-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.anticon {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: #0a8fe9 !important;
|
||||
}
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.my-tabs {
|
||||
}
|
||||
|
||||
.selected-users {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<div>
|
||||
<div @click="showModal" :class="disabled ? 'select-input disabled-select' : 'select-input'">
|
||||
<template v-if="selectedList.length > 0">
|
||||
<template v-for="(item, index) in selectedList">
|
||||
<SelectedUserItem v-if="index < maxCount" :info="item" @unSelect="unSelect" query />
|
||||
</template>
|
||||
</template>
|
||||
<span v-else style="height: 30px; line-height: 30px; display: inline-block; margin-left: 7px; color: #bfbfbf">请选择</span>
|
||||
<div v-if="ellipsisInfo.status" class="user-selected-item">
|
||||
<div class="user-select-ellipsis">
|
||||
<span style="color: red">+{{ ellipsisInfo.count }}...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<RoleSelectModal :appId="currentAppId" :multi="multi" :getContainer="getContainer" title="选择组织角色" @register="registerRoleModal" @selected="onSelected" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { computed, ref, watch, watchEffect, defineComponent } from 'vue';
|
||||
import RoleSelectModal from './RoleSelectModal.vue';
|
||||
import SelectedUserItem from '../userSelect/SelectedUserItem.vue';
|
||||
import { Form } from 'ant-design-vue';
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
|
||||
const maxCount = 2;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'RoleSelectInput',
|
||||
components: {
|
||||
RoleSelectModal,
|
||||
SelectedUserItem,
|
||||
},
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
store: {
|
||||
type: String,
|
||||
default: 'id',
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
multi: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
getContainer: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
appId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['update:value', 'change'],
|
||||
setup(props, { emit }) {
|
||||
const formItemContext = Form.useInjectFormItemContext();
|
||||
const selectedList = ref<any[]>([]);
|
||||
const loading = ref(true);
|
||||
|
||||
const [registerRoleModal, { openModal: openRoleModal, closeModal: closeRoleModal }] = useModal();
|
||||
function showModal(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
let list = selectedList.value.map((item) => item.id);
|
||||
openRoleModal(true, {
|
||||
list,
|
||||
});
|
||||
}
|
||||
|
||||
const ellipsisInfo = computed(() => {
|
||||
let max = maxCount;
|
||||
let len = selectedList.value.length;
|
||||
if (len > max) {
|
||||
return { status: true, count: len - max };
|
||||
} else {
|
||||
return { status: false };
|
||||
}
|
||||
});
|
||||
|
||||
function unSelect(id) {
|
||||
console.log('unSelectUser', id);
|
||||
loading.value = false;
|
||||
let arr = selectedList.value;
|
||||
let index = -1;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (arr[i].id == id) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index >= 0) {
|
||||
arr.splice(index, 1);
|
||||
selectedList.value = arr;
|
||||
onSelectedChange();
|
||||
}
|
||||
}
|
||||
|
||||
function onSelectedChange() {
|
||||
let temp: any[] = [];
|
||||
let arr = selectedList.value;
|
||||
if (arr && arr.length > 0) {
|
||||
temp = arr.map((k) => {
|
||||
return k[props.store];
|
||||
});
|
||||
}
|
||||
let str = temp.join(',');
|
||||
emit('update:value', str);
|
||||
emit('change', str);
|
||||
formItemContext.onFieldChange();
|
||||
console.log('选中数据', str);
|
||||
}
|
||||
|
||||
function onSelected(_v, values) {
|
||||
console.log('角色选择完毕:', values);
|
||||
loading.value = false;
|
||||
if (values && values.length > 0) {
|
||||
selectedList.value = values;
|
||||
} else {
|
||||
selectedList.value = [];
|
||||
}
|
||||
onSelectedChange();
|
||||
closeRoleModal();
|
||||
}
|
||||
|
||||
// 目前仅用于数据重新加载的一个状态
|
||||
const currentAppId = ref('');
|
||||
const userStore = useUserStore();
|
||||
watchEffect(() => {
|
||||
let tenantId = userStore.getTenant;
|
||||
let appId = props.appId;
|
||||
if (appId) {
|
||||
currentAppId.value = appId;
|
||||
} else {
|
||||
currentAppId.value = new Date().getTime() + '-' + tenantId;
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
async (val) => {
|
||||
if (val) {
|
||||
if (loading.value === true) {
|
||||
await getRoleList(val);
|
||||
}
|
||||
} else {
|
||||
selectedList.value = [];
|
||||
}
|
||||
loading.value = true;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* 获取角色列表
|
||||
* @param ids
|
||||
*/
|
||||
async function getRoleList(ids) {
|
||||
const url = '/sys/role/listByTenant';
|
||||
let params = {
|
||||
[props.store]: ids,
|
||||
pageSize: 200
|
||||
};
|
||||
// 特殊条件处理(因为后台实体是roleCode,所以折中一下,不能直接改,会出问题)
|
||||
if (props.store === 'code') {
|
||||
params.roleCode = ids;
|
||||
}
|
||||
selectedList.value = [];
|
||||
const data = await defHttp.get({ url, params }, { isTransformResponse: false });
|
||||
console.log('getRoleList>>', data);
|
||||
if (data.success) {
|
||||
const { records } = data.result;
|
||||
let arr: any[] = [];
|
||||
if (records && records.length > 0) {
|
||||
for (let item of records) {
|
||||
arr.push({
|
||||
id: item.id,
|
||||
name: item.name || item.roleName,
|
||||
code: item.roleCode,
|
||||
checked: true,
|
||||
selectType: 'sys_role',
|
||||
});
|
||||
}
|
||||
}
|
||||
selectedList.value = arr;
|
||||
} else {
|
||||
console.error(data.message);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
selectedList,
|
||||
ellipsisInfo,
|
||||
maxCount,
|
||||
registerRoleModal,
|
||||
closeRoleModal,
|
||||
showModal,
|
||||
onSelected,
|
||||
unSelect,
|
||||
currentAppId,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.select-input {
|
||||
padding: 0 5px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
color: #9e9e9e;
|
||||
font-size: 14px;
|
||||
flex-wrap: nowrap;
|
||||
min-height: 32px;
|
||||
overflow-x: hidden;
|
||||
&.disabled-select {
|
||||
cursor: not-allowed;
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,296 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
@register="register"
|
||||
:getContainer="getContainer"
|
||||
:canFullscreen="false"
|
||||
:title="title"
|
||||
:width="500"
|
||||
destroyOnClose
|
||||
@ok="handleOk"
|
||||
wrapClassName="j-user-select-modal2" >
|
||||
|
||||
<div style="position: relative; min-height: 350px">
|
||||
<div style="width: 100%">
|
||||
<a-input v-model:value="searchText" allowClear style="width: 100%" placeholder="搜索">
|
||||
<template #prefix>
|
||||
<SearchOutlined style="color: #c0c0c0" />
|
||||
</template>
|
||||
</a-input>
|
||||
</div>
|
||||
|
||||
<!-- tabs -->
|
||||
<div class="modal-select-list-container">
|
||||
<div class="scroll">
|
||||
<div class="content" style="right: -10px">
|
||||
|
||||
<label class="item" v-for="item in showDataList" @click="(e)=>onSelect(e, item)">
|
||||
<a-checkbox v-model:checked="item.checked">
|
||||
<span class="text">{{ item.name }}</span>
|
||||
</a-checkbox>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 选中用户 -->
|
||||
<div class="selected-users" style="width: 100%; overflow-x: hidden">
|
||||
<SelectedUserItem v-for="item in selectedList" :info="item" @unSelect="unSelect" />
|
||||
</div>
|
||||
</div>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { SearchOutlined, CloseOutlined } from '@ant-design/icons-vue';
|
||||
import SelectedUserItem from '../userSelect/SelectedUserItem.vue';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
|
||||
import { computed, ref, toRaw, watch } from 'vue';
|
||||
export default {
|
||||
name: 'RoleSelectModal',
|
||||
components: {
|
||||
BasicModal,
|
||||
SearchOutlined,
|
||||
CloseOutlined,
|
||||
SelectedUserItem,
|
||||
},
|
||||
props: {
|
||||
multi: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
getContainer: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
title:{
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'sys_role',
|
||||
},
|
||||
appId: {
|
||||
type: String,
|
||||
default: '',
|
||||
}
|
||||
},
|
||||
emits: ['selected', 'register'],
|
||||
setup(props, { emit }) {
|
||||
|
||||
const searchText = ref('');
|
||||
const selectedIdList = computed(() => {
|
||||
let arr = selectedList.value;
|
||||
if (!arr || arr.length == 0) {
|
||||
return [];
|
||||
} else {
|
||||
return arr.map((k) => k.id);
|
||||
}
|
||||
});
|
||||
|
||||
watch(()=>props.appId, async (val)=>{
|
||||
if(val){
|
||||
await loadDataList();
|
||||
}
|
||||
}, {immediate: true});
|
||||
|
||||
|
||||
// 弹窗事件
|
||||
const [register] = useModalInner((data) => {
|
||||
let list = dataList.value;
|
||||
if(!list || list.length ==0 ){
|
||||
}else{
|
||||
let selectedIdList = data.list || [];
|
||||
for(let item of list){
|
||||
if(selectedIdList.indexOf(item.id)>=0){
|
||||
item.checked = true;
|
||||
}else{
|
||||
item.checked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 确定事件
|
||||
function handleOk() {
|
||||
let arr = toRaw(selectedIdList.value);
|
||||
emit('selected', arr, toRaw(selectedList.value));
|
||||
}
|
||||
|
||||
const dataList = ref<any[]>([]);
|
||||
const showDataList = computed(()=>{
|
||||
let list = dataList.value;
|
||||
if(!list || list.length ==0 ){
|
||||
return []
|
||||
}
|
||||
let text = searchText.value;
|
||||
if(!text){
|
||||
return list
|
||||
}
|
||||
return list.filter(item=>item.name.indexOf(text)>=0)
|
||||
});
|
||||
|
||||
const selectedList = computed(()=>{
|
||||
let list = dataList.value;
|
||||
if(!list || list.length ==0 ){
|
||||
return []
|
||||
}
|
||||
return list.filter(item=>item.checked)
|
||||
});
|
||||
|
||||
function unSelect(id) {
|
||||
let list = dataList.value;
|
||||
if(!list || list.length ==0 ){
|
||||
return;
|
||||
}
|
||||
let arr = list.filter(item=>item.id == id);
|
||||
arr[0].checked = false;
|
||||
}
|
||||
|
||||
async function loadDataList() {
|
||||
let params = {
|
||||
pageNo: 1,
|
||||
pageSize: 200,
|
||||
column: 'createTime',
|
||||
order: 'desc'
|
||||
};
|
||||
const url = '/sys/role/listByTenant';
|
||||
const data = await defHttp.get({ url, params }, { isTransformResponse: false });
|
||||
if (data.success) {
|
||||
const { records } = data.result;
|
||||
let arr:any[] = [];
|
||||
if(records && records.length>0){
|
||||
for(let item of records){
|
||||
arr.push({
|
||||
id: item.id,
|
||||
name: item.name || item.roleName,
|
||||
code: item.roleCode,
|
||||
selectType: props.type,
|
||||
checked: false
|
||||
})
|
||||
}
|
||||
}
|
||||
dataList.value = arr;
|
||||
} else {
|
||||
console.error(data.message);
|
||||
}
|
||||
console.log('loadDataList', data);
|
||||
}
|
||||
|
||||
|
||||
function onSelect(e, item) {
|
||||
prevent(e);
|
||||
console.log('onselect');
|
||||
// 单选判断 只能选中一条数据 其余数据置false
|
||||
if(props.multi === false){
|
||||
let list = dataList.value;
|
||||
for(let item of list){
|
||||
item.checked = false;
|
||||
}
|
||||
}
|
||||
item.checked = !item.checked;
|
||||
}
|
||||
|
||||
function prevent(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
return {
|
||||
register,
|
||||
showDataList,
|
||||
searchText,
|
||||
handleOk,
|
||||
selectedList,
|
||||
selectedIdList,
|
||||
unSelect,
|
||||
onSelect
|
||||
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.modal-select-list-container{
|
||||
height: 352px;
|
||||
margin-top: 12px;
|
||||
overflow: auto;
|
||||
.scroll{
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
.content{
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow: scroll;
|
||||
overflow-x: hidden;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
.item{
|
||||
padding: 7px 5px;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
&:hover{
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.j-user-select-modal2 {
|
||||
.depart-select {
|
||||
.ant-select-selector {
|
||||
color: #fff !important;
|
||||
background-color: #409eff !important;
|
||||
border-radius: 5px !important;
|
||||
}
|
||||
.ant-select-selection-item,
|
||||
.ant-select-arrow {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
.my-search {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
z-index: 1;
|
||||
&.all-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.anticon {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: #0a8fe9 !important;
|
||||
}
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.my-tabs {
|
||||
}
|
||||
|
||||
.selected-users {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<div class="user-selected-item">
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 24px;
|
||||
border-radius: 12px;
|
||||
padding-right: 10px;
|
||||
vertical-align: middle;
|
||||
background-color: #f5f5f5;
|
||||
"
|
||||
>
|
||||
<span style="width: 24px; height: 24px; line-height: 20px; margin-right: 3px; display: inline-block">
|
||||
<a-avatar v-if="info.avatar" :src="getFileAccessHttpUrl(info.avatar)" :size="24"></a-avatar>
|
||||
|
||||
<a-avatar v-else-if="info.avatarIcon" class="ant-btn-primary" :size="24" >
|
||||
<template #icon>
|
||||
<Icon :icon=" 'ant-design:'+info.avatarIcon " style="font-size: 16px;margin-top: 4px"/>
|
||||
</template>
|
||||
</a-avatar>
|
||||
|
||||
<a-avatar v-else-if="info.selectType == 'sys_role'" :size="24" style="background-color: rgb(255, 173, 0);">
|
||||
<template #icon>
|
||||
<team-outlined style="font-size: 16px"/>
|
||||
</template>
|
||||
</a-avatar>
|
||||
<a-avatar v-else-if="info.selectType == 'sys_position'" :size="24" style="background-color: rgb(245, 34, 45);">
|
||||
<template #icon>
|
||||
<TagsOutlined style="font-size: 16px"/>
|
||||
</template>
|
||||
</a-avatar>
|
||||
|
||||
<a-avatar :size="24" v-else>
|
||||
<template #icon><UserOutlined /></template>
|
||||
</a-avatar>
|
||||
</span>
|
||||
|
||||
<div style="height: 24px; line-height: 24px" class="ellipsis">
|
||||
{{ info.realname || info.name }}
|
||||
</div>
|
||||
|
||||
<div v-if="showClose" class="icon-close">
|
||||
<CloseOutlined @click="removeSelect"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!showClose" class="icon-remove">
|
||||
<MinusCircleFilled @click="removeSelect" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { UserOutlined, CloseOutlined, MinusCircleFilled, TagsOutlined, TeamOutlined } from '@ant-design/icons-vue';
|
||||
import {computed} from 'vue'
|
||||
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
|
||||
|
||||
export default {
|
||||
name: 'SelectedUserItem',
|
||||
components: {
|
||||
UserOutlined,
|
||||
MinusCircleFilled,
|
||||
CloseOutlined,
|
||||
TagsOutlined,
|
||||
TeamOutlined
|
||||
},
|
||||
props: {
|
||||
info: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
// 是否作为查询条件
|
||||
query:{
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
emits: ['unSelect'],
|
||||
setup(props, { emit }) {
|
||||
function removeSelect(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
emit('unSelect', props.info.id);
|
||||
}
|
||||
|
||||
const showClose = computed(()=>{
|
||||
if(props.query===true){
|
||||
return true;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
showClose,
|
||||
removeSelect,
|
||||
getFileAccessHttpUrl
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.user-selected-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: 8px;
|
||||
height: 30px;
|
||||
border-radius: 12px;
|
||||
line-height: 30px;
|
||||
vertical-align: middle;
|
||||
|
||||
.ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.icon-remove {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: -4px;
|
||||
font-size: 18px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.icon-close{
|
||||
height: 22px;
|
||||
line-height: 24px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
margin-left: 7px;
|
||||
&:hover{
|
||||
color: #0a8fe9;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.icon-remove {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<a-list item-layout="horizontal" :data-source="showDataList">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item style="padding: 3px 0">
|
||||
<div class="user-select-user-info" @click="(e) => onClickUser(e, item)">
|
||||
<div style="margin-left: 10px">
|
||||
<a-checkbox v-model:checked="checkStatus[item.id]" v-if="multi" />
|
||||
<a-radio v-model:checked="checkStatus[item.id]" v-else />
|
||||
</div>
|
||||
<div>
|
||||
<a-avatar v-if="item.avatar" :src="getFileAccessHttpUrl(item.avatar)"></a-avatar>
|
||||
<a-avatar v-else-if="item.avatarIcon" class="ant-btn-primary">
|
||||
<template #icon>
|
||||
<Icon :icon=" 'ant-design:'+item.avatarIcon " style="margin-top: 4px;font-size: 24px;"/>
|
||||
</template>
|
||||
</a-avatar>
|
||||
<a-avatar v-else>
|
||||
<template #icon><UserOutlined /></template>
|
||||
</a-avatar>
|
||||
</div>
|
||||
<div :style="nameStyle">
|
||||
{{ item.realname }}
|
||||
</div>
|
||||
<div :style="departStyle" class="ellipsis" :title="item.orgCodeTxt">
|
||||
{{ item.orgCodeTxt }}
|
||||
</div>
|
||||
<div style="width: 1px"></div>
|
||||
</div>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { UserOutlined } from '@ant-design/icons-vue';
|
||||
import { computed, toRaw, reactive, watchEffect, ref } from 'vue';
|
||||
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
|
||||
|
||||
export default {
|
||||
name: 'UserList',
|
||||
props: {
|
||||
multi: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
dataList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
// 是否显示部门文本
|
||||
depart: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
selectedIdList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
excludeUserIdList:{
|
||||
type: Array,
|
||||
default: () => [],
|
||||
}
|
||||
},
|
||||
components: {
|
||||
UserOutlined,
|
||||
},
|
||||
emits: ['selected', 'unSelect'],
|
||||
setup(props, { emit }) {
|
||||
function onClickUser(e, user) {
|
||||
e && prevent(e);
|
||||
let status = checkStatus[user.id];
|
||||
if (status === true) {
|
||||
emit('unSelect', user.id);
|
||||
} else {
|
||||
emit('selected', toRaw(user));
|
||||
}
|
||||
}
|
||||
|
||||
function getTwoText(text) {
|
||||
if (!text) {
|
||||
return '';
|
||||
} else {
|
||||
return text.substr(0, 2);
|
||||
}
|
||||
}
|
||||
|
||||
const departStyle = computed(() => {
|
||||
if (props.depart === true) {
|
||||
// 如果显示部门信息
|
||||
return {
|
||||
flex: 1,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
display: 'none',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const nameStyle = computed(() => {
|
||||
if (props.depart === true) {
|
||||
// 如果显示部门信息
|
||||
return {
|
||||
width: '200px',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
flex: 1,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function onChangeChecked(e) {
|
||||
console.error('onChangeChecked', e);
|
||||
}
|
||||
|
||||
// const showDataList = ref<any[]>([])
|
||||
const checkStatus = reactive<any>({});
|
||||
watchEffect(() => {
|
||||
let arr1 = props.dataList;
|
||||
if (!arr1 || arr1.length === 0) {
|
||||
return;
|
||||
}
|
||||
let idList = props.selectedIdList;
|
||||
for (let item of arr1) {
|
||||
if (idList.indexOf(item.id) >= 0) {
|
||||
checkStatus[item.id] = true;
|
||||
} else {
|
||||
checkStatus[item.id] = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
function prevent(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
//update-begin---author:wangshuai---date:2024-02-02---for:【QQYUN-8239】用户角色,添加用户 返回2页数据,实际只显示一页---
|
||||
/* function records2DataList() {
|
||||
let arr:any[] = [];
|
||||
let excludeList = props.excludeUserIdList;
|
||||
let records = props.dataList;
|
||||
if(records && records.length>0){
|
||||
for(let item of records){
|
||||
if(excludeList.indexOf(item.id)<0){
|
||||
arr.push({...item})
|
||||
}
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}*/
|
||||
|
||||
const showDataList = computed(()=>{
|
||||
/* let excludeList = props.excludeUserIdList;
|
||||
if(excludeList && excludeList.length>0){
|
||||
return records2DataList();
|
||||
}*/
|
||||
//update-end---author:wangshuai---date:2024-02-02---for:【QQYUN-8239】用户角色,添加用户 返回2页数据,实际只显示一页---
|
||||
return props.dataList;
|
||||
});
|
||||
|
||||
return {
|
||||
onClickUser,
|
||||
getTwoText,
|
||||
departStyle,
|
||||
nameStyle,
|
||||
onChangeChecked,
|
||||
checkStatus,
|
||||
showDataList,
|
||||
getFileAccessHttpUrl
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.user-select-user-info {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
> div {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.ellipsis {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<div :style="containerStyle">
|
||||
<a-tree
|
||||
v-if="treeData.length > 0"
|
||||
:load-data="loadChildren"
|
||||
showIcon
|
||||
autoExpandParent
|
||||
:treeData="treeData"
|
||||
:selectedKeys="selectedKeys"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
@select="onSelect"
|
||||
>
|
||||
<template #title="{ title, key }">
|
||||
<FolderFilled style="color: #9e9e9e"/><span style="margin-left: 5px">{{ title }}</span>
|
||||
</template>
|
||||
</a-tree>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="12" style="padding-left: 10px">
|
||||
<div :style="containerStyle">
|
||||
<user-list :multi="multi" :excludeUserIdList="excludeUserIdList" :dataList="userDataList" :selectedIdList="selectedIdList" @selected="onSelectUser" @unSelect="unSelectUser" />
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import UserList from './UserList.vue';
|
||||
import { FolderFilled } from '@ant-design/icons-vue';
|
||||
|
||||
export default {
|
||||
name: 'DepartUserList',
|
||||
components: {
|
||||
UserList,
|
||||
FolderFilled
|
||||
},
|
||||
props: {
|
||||
searchText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
selectedIdList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
excludeUserIdList:{
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
multi: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
emits: ['loaded', 'selected', 'unSelect'],
|
||||
setup(props, { emit }) {
|
||||
//export const queryById = (params) => defHttp.get({ url: Api.queryById, params }, { isTransformResponse: false });
|
||||
|
||||
async function loadDepartTree(pid?) {
|
||||
const url = '/sys/sysDepart/queryDepartTreeSync';
|
||||
let params = {};
|
||||
if (pid) {
|
||||
params['pid'] = pid;
|
||||
}
|
||||
const data = await defHttp.get({ url, params }, { isTransformResponse: false });
|
||||
console.log('loadDepartTree', data);
|
||||
return data;
|
||||
}
|
||||
|
||||
async function initRoot() {
|
||||
console.log('initRoot');
|
||||
const data = await loadDepartTree();
|
||||
if (data.success) {
|
||||
let arr = data.result;
|
||||
treeData.value = arr;
|
||||
emitDepartOptions(arr);
|
||||
} else {
|
||||
console.error(data.message);
|
||||
}
|
||||
clear();
|
||||
}
|
||||
|
||||
function emitDepartOptions(arr) {
|
||||
let options = [];
|
||||
if (arr && arr.length > 0) {
|
||||
options = arr.map((k) => {
|
||||
return {
|
||||
value: k.id,
|
||||
label: k.departName,
|
||||
};
|
||||
});
|
||||
}
|
||||
emit('loaded', options);
|
||||
}
|
||||
|
||||
initRoot();
|
||||
|
||||
const treeData = ref<any[]>([]);
|
||||
const selectedKeys = ref<string[]>([]);
|
||||
const expandedKeys = ref<string[]>([]);
|
||||
const selectedDepartId = ref('');
|
||||
function onSelect(ids, e) {
|
||||
let record = e.node.dataRef;
|
||||
selectedKeys.value = [record.key];
|
||||
|
||||
let id = ids[0];
|
||||
selectedDepartId.value = id;
|
||||
loadUserList();
|
||||
}
|
||||
|
||||
function clear() {
|
||||
selectedDepartId.value = '';
|
||||
}
|
||||
async function loadChildren(treeNode) {
|
||||
console.log('loadChildren', treeNode);
|
||||
const data = await loadDepartTree(treeNode.eventKey);
|
||||
if (data.success) {
|
||||
let arr = data.result;
|
||||
treeNode.dataRef.children = [...arr];
|
||||
treeData.value = [...treeData.value];
|
||||
} else {
|
||||
console.error(data.message);
|
||||
}
|
||||
}
|
||||
|
||||
const maxHeight = ref(300);
|
||||
maxHeight.value = window.innerHeight - 300;
|
||||
const containerStyle = computed(() => {
|
||||
return {
|
||||
'overflow-y': 'auto',
|
||||
'max-height': maxHeight.value + 'px',
|
||||
};
|
||||
});
|
||||
|
||||
const userDataList = ref<any[]>([]);
|
||||
async function loadUserList() {
|
||||
const url = '/sys/user/selectUserList';
|
||||
let params = {
|
||||
pageNo: 1,
|
||||
pageSize: 99,
|
||||
};
|
||||
if (props.searchText) {
|
||||
params['keyword'] = props.searchText;
|
||||
}
|
||||
if (selectedDepartId.value) {
|
||||
params['departId'] = selectedDepartId.value;
|
||||
}
|
||||
//update-begin---author:wangshuai---date:2024-02-02---for:【QQYUN-8239】用户角色,添加用户 返回2页数据,实际只显示一页---
|
||||
if(props.excludeUserIdList && props.excludeUserIdList.length>0){
|
||||
params['excludeUserIdList'] = props.excludeUserIdList.join(",");
|
||||
}
|
||||
//update-end---author:wangshuai---date:2024-02-02---for:【QQYUN-8239】用户角色,添加用户 返回2页数据,实际只显示一页---
|
||||
const data = await defHttp.get({ url, params }, { isTransformResponse: false });
|
||||
if (data.success) {
|
||||
const { records } = data.result;
|
||||
userDataList.value = records;
|
||||
} else {
|
||||
console.error(data.message);
|
||||
}
|
||||
console.log('depart-loadUserList', data);
|
||||
}
|
||||
watch(
|
||||
() => props.searchText,
|
||||
() => {
|
||||
loadUserList();
|
||||
}
|
||||
);
|
||||
|
||||
function onSelectUser(info) {
|
||||
emit('selected', info);
|
||||
}
|
||||
function unSelectUser(id) {
|
||||
emit('unSelect', id);
|
||||
}
|
||||
|
||||
return {
|
||||
containerStyle,
|
||||
treeData,
|
||||
selectedKeys,
|
||||
expandedKeys,
|
||||
onSelect,
|
||||
loadChildren,
|
||||
onSelectUser,
|
||||
unSelectUser,
|
||||
userDataList,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<div :style="containerStyle">
|
||||
<a-tree v-if="treeData.length > 0" showIcon :treeData="treeData" :selectedKeys="selectedKeys" @select="onSelect">
|
||||
<template #title="{ title, key }">
|
||||
<UserOutlined style="color: #9e9e9e"/><span style="margin-left: 5px">{{ title }}</span>
|
||||
</template>
|
||||
</a-tree>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="12" style="padding-left: 10px">
|
||||
<div :style="containerStyle">
|
||||
<user-list :multi="multi" :excludeUserIdList="excludeUserIdList" :dataList="userDataList" :selectedIdList="selectedIdList" @selected="onSelectUser" @unSelect="unSelectUser" />
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import UserList from './UserList.vue';
|
||||
import { UserOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
export default {
|
||||
name: 'RoleUserList',
|
||||
components: {
|
||||
UserList,
|
||||
UserOutlined
|
||||
},
|
||||
props: {
|
||||
searchText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
selectedIdList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
excludeUserIdList:{
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
multi: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
emits: ['selected', 'unSelect'],
|
||||
setup(props, { emit }) {
|
||||
const treeData = ref<any[]>([]);
|
||||
async function loadRoleList() {
|
||||
const url = '/sys/role/listByTenant';
|
||||
let params = {
|
||||
order: 'desc',
|
||||
column: 'createTime',
|
||||
pageSize: 200,
|
||||
};
|
||||
let arr = [];
|
||||
const data = await defHttp.get({ url, params }, { isTransformResponse: false });
|
||||
if (data.success) {
|
||||
const { records } = data.result;
|
||||
arr = records.map((k) => {
|
||||
return {
|
||||
title: k.roleName,
|
||||
id: k.id,
|
||||
key: k.id,
|
||||
};
|
||||
});
|
||||
}
|
||||
console.log('loadRoleList', data);
|
||||
treeData.value = arr;
|
||||
}
|
||||
loadRoleList();
|
||||
|
||||
const selectedKeys = ref<any[]>([]);
|
||||
const selectedRoleId = ref('');
|
||||
function onSelect(ids, e) {
|
||||
let record = e.node.dataRef;
|
||||
selectedKeys.value = [record.key];
|
||||
|
||||
let id = ids[0];
|
||||
selectedRoleId.value = id;
|
||||
loadUserList();
|
||||
}
|
||||
|
||||
const userDataList = ref<any[]>([]);
|
||||
async function loadUserList() {
|
||||
const url = '/sys/user/selectUserList';
|
||||
let params = {
|
||||
pageNo: 1,
|
||||
pageSize: 99,
|
||||
};
|
||||
if (props.searchText) {
|
||||
params['keyword'] = props.searchText;
|
||||
}
|
||||
if (selectedRoleId.value) {
|
||||
params['roleId'] = selectedRoleId.value;
|
||||
}
|
||||
//update-begin---author:wangshuai---date:2024-02-02---for:【QQYUN-8239】用户角色,添加用户 返回2页数据,实际只显示一页---
|
||||
if(props.excludeUserIdList && props.excludeUserIdList.length>0){
|
||||
params['excludeUserIdList'] = props.excludeUserIdList.join(",");
|
||||
}
|
||||
//update-end---author:wangshuai---date:2024-02-02---for:【QQYUN-8239】用户角色,添加用户 返回2页数据,实际只显示一页---
|
||||
const data = await defHttp.get({ url, params }, { isTransformResponse: false });
|
||||
if (data.success) {
|
||||
const { records } = data.result;
|
||||
userDataList.value = records;
|
||||
} else {
|
||||
console.error(data.message);
|
||||
}
|
||||
console.log('role-loadUserList', data);
|
||||
}
|
||||
watch(
|
||||
() => props.searchText,
|
||||
() => {
|
||||
loadUserList();
|
||||
}
|
||||
);
|
||||
|
||||
function onSelectUser(info) {
|
||||
emit('selected', info);
|
||||
}
|
||||
function unSelectUser(id) {
|
||||
emit('unSelect', id);
|
||||
}
|
||||
|
||||
const maxHeight = ref(300);
|
||||
maxHeight.value = window.innerHeight - 300;
|
||||
const containerStyle = computed(() => {
|
||||
return {
|
||||
'overflow-y': 'auto',
|
||||
'max-height': maxHeight.value + 'px',
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
containerStyle,
|
||||
treeData,
|
||||
selectedKeys,
|
||||
onSelect,
|
||||
onSelectUser,
|
||||
unSelectUser,
|
||||
userDataList,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@ -0,0 +1,376 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
@register="register"
|
||||
:getContainer="getContainer"
|
||||
:canFullscreen="false"
|
||||
title="选择用户"
|
||||
:width="600"
|
||||
wrapClassName="j-user-select-modal2"
|
||||
>
|
||||
<!-- 部门下拉框 -->
|
||||
<a-select v-model:value="selectedDepart" style="width: 100%" class="depart-select" @change="onDepartChange">
|
||||
<a-select-option v-for="item in departOptions" :value="item.value">{{ item.label }}</a-select-option>
|
||||
</a-select>
|
||||
|
||||
<div style="position: relative; min-height: 350px">
|
||||
<!-- 用户搜索框 -->
|
||||
<div :class="searchInputStatus ? 'my-search all-width' : 'my-search'">
|
||||
<span :class="searchInputStatus ? 'hidden' : ''" style="margin-left: 10px"
|
||||
><SearchOutlined style="color: #c0c0c0" @click="showSearchInput"
|
||||
/></span>
|
||||
<div style="width: 100%" :class="searchInputStatus ? '' : 'hidden'">
|
||||
<a-input v-model:value="searchText" @pressEnter="onSearchUser" style="width: 100%" placeholder="请输入用户名按回车搜索">
|
||||
<template #prefix>
|
||||
<SearchOutlined style="color: #c0c0c0" />
|
||||
</template>
|
||||
<template #suffix>
|
||||
<CloseOutlined title="退出搜索" @click="clearSearch" />
|
||||
</template>
|
||||
</a-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- tabs -->
|
||||
<div class="my-tabs">
|
||||
<a-tabs v-model:activeKey="myActiveKey" :centered="true" @change="onChangeTab">
|
||||
<!-- 所有用户 -->
|
||||
<a-tab-pane key="1" tab="全部" forceRender>
|
||||
<user-list :multi="multi" :excludeUserIdList="excludeUserIdList" :dataList="userDataList" :selectedIdList="selectedIdList" depart @selected="onSelectUser" @unSelect="unSelectUser" />
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 部门用户 -->
|
||||
<a-tab-pane key="2" tab="按部门" forceRender>
|
||||
<depart-user-list
|
||||
:searchText="searchText"
|
||||
:selectedIdList="selectedIdList"
|
||||
:excludeUserIdList="excludeUserIdList"
|
||||
@loaded="initDepartOptions"
|
||||
@selected="onSelectUser"
|
||||
@unSelect="unSelectUser"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 角色用户 -->
|
||||
<a-tab-pane key="3" tab="按角色" forceRender>
|
||||
<role-user-list :excludeUserIdList="excludeUserIdList" :searchText="searchText" :selectedIdList="selectedIdList" @selected="onSelectUser" @unSelect="unSelectUser" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 选中用户 -->
|
||||
<div class="selected-users" style="width: 100%; overflow-x: hidden">
|
||||
<SelectedUserItem v-for="item in selectedUserList" :info="item" @unSelect="unSelectUser" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div style="display: flex; justify-content: space-between; width: 100%">
|
||||
<div class="select-user-page-info">
|
||||
<a-pagination
|
||||
v-if="myActiveKey == '1'"
|
||||
v-model:current="pageNo"
|
||||
size="small"
|
||||
:total="totalRecord"
|
||||
show-quick-jumper
|
||||
@change="onPageChange"
|
||||
/>
|
||||
</div>
|
||||
<a-button type="primary" @click="handleOk">确 定</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { SearchOutlined, CloseOutlined } from '@ant-design/icons-vue';
|
||||
import UserList from './UserList.vue';
|
||||
import SelectedUserItem from './SelectedUserItem.vue';
|
||||
import DepartUserList from './UserListAndDepart.vue';
|
||||
import RoleUserList from './UserListAndRole.vue';
|
||||
import { Pagination } from 'ant-design-vue';
|
||||
const APagination = Pagination;
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
|
||||
import {computed, ref, toRaw, unref} from 'vue';
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
import { mySelfData } from './useUserSelect'
|
||||
|
||||
export default {
|
||||
name: 'UserSelectModal',
|
||||
components: {
|
||||
BasicModal,
|
||||
SearchOutlined,
|
||||
CloseOutlined,
|
||||
SelectedUserItem,
|
||||
UserList,
|
||||
DepartUserList,
|
||||
RoleUserList,
|
||||
APagination,
|
||||
},
|
||||
props: {
|
||||
multi: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
getContainer: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
//是否排除我自己
|
||||
izExcludeMy: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
//是否在高级查询中作为条件 可以选择当前用户表达式
|
||||
inSuperQuery:{
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
emits: ['selected', 'register'],
|
||||
setup(props, { emit }) {
|
||||
const myActiveKey = ref('1');
|
||||
const selectedUserList = ref<any[]>([]);
|
||||
const userStore = useUserStore();
|
||||
const selectedIdList = computed(() => {
|
||||
let arr = selectedUserList.value;
|
||||
if (!arr || arr.length == 0) {
|
||||
return [];
|
||||
} else {
|
||||
return arr.map((k) => k.id);
|
||||
}
|
||||
});
|
||||
// QQYUN-4152【应用】已经存在的用户,添加的时候还可以重复选择
|
||||
const excludeUserIdList = ref<any[]>([]);
|
||||
|
||||
// 弹窗事件
|
||||
const [register] = useModalInner((data) => {
|
||||
let list = data.list;
|
||||
if (list && list.length > 0) {
|
||||
selectedUserList.value = [...list];
|
||||
} else {
|
||||
selectedUserList.value = [];
|
||||
}
|
||||
if(data.excludeUserIdList){
|
||||
excludeUserIdList.value = data.excludeUserIdList;
|
||||
}else{
|
||||
excludeUserIdList.value = [];
|
||||
}
|
||||
//如果排除我自己,直接excludeUserIdList.push排除即可
|
||||
if (props.izExcludeMy) {
|
||||
excludeUserIdList.value.push(userStore.getUserInfo.id);
|
||||
}
|
||||
//加载用户列表
|
||||
loadUserList();
|
||||
});
|
||||
|
||||
// 确定事件
|
||||
function handleOk() {
|
||||
let arr = toRaw(selectedUserList.value);
|
||||
emit('selected', arr);
|
||||
}
|
||||
|
||||
/*--------------部门下拉框,用于筛选用户---------------*/
|
||||
const selectedDepart = ref('');
|
||||
const departOptions = ref<any[]>([]);
|
||||
function initDepartOptions(options) {
|
||||
departOptions.value = [{ value: '', label: '全部用户' }, ...options];
|
||||
selectedDepart.value = '';
|
||||
}
|
||||
function onDepartChange() {
|
||||
loadUserList();
|
||||
}
|
||||
/*--------------部门下拉框,用于筛选用户---------------*/
|
||||
|
||||
/*--------------第一页 搜索框---------------*/
|
||||
const searchInputStatus = ref(false);
|
||||
const searchText = ref('');
|
||||
function showSearchInput(e) {
|
||||
e && prevent(e);
|
||||
searchInputStatus.value = true;
|
||||
}
|
||||
|
||||
// 回车事件,触发查询
|
||||
function onSearchUser() {
|
||||
pageNo.value = 1;
|
||||
loadUserList();
|
||||
}
|
||||
|
||||
// 清除按名称筛选
|
||||
function clearSearch(e) {
|
||||
e && prevent(e);
|
||||
pageNo.value = 1;
|
||||
searchText.value = '';
|
||||
searchInputStatus.value = false;
|
||||
loadUserList();
|
||||
}
|
||||
/*--------------第一页 搜索框---------------*/
|
||||
|
||||
/*--------------加载数据---------------*/
|
||||
const pageNo = ref(1);
|
||||
const totalRecord = ref(0);
|
||||
const userDataList = ref<any[]>([]);
|
||||
async function onPageChange() {
|
||||
console.log('onPageChange', pageNo.value);
|
||||
await loadUserList();
|
||||
}
|
||||
async function loadUserList() {
|
||||
const url = '/sys/user/selectUserList';
|
||||
let params = {
|
||||
pageNo: pageNo.value,
|
||||
pageSize: 10,
|
||||
};
|
||||
if (searchText.value) {
|
||||
params['keyword'] = searchText.value;
|
||||
}
|
||||
if (selectedDepart.value) {
|
||||
params['departId'] = selectedDepart.value;
|
||||
}
|
||||
|
||||
//update-begin---author:wangshuai---date:2024-02-02---for:【QQYUN-8239】用户角色,添加用户 返回2页数据,实际只显示一页---
|
||||
if(unref(excludeUserIdList) && unref(excludeUserIdList).length>0){
|
||||
params['excludeUserIdList'] = excludeUserIdList.value.join(",");
|
||||
}
|
||||
//update-end---author:wangshuai---date:2024-02-02---for:【QQYUN-8239】用户角色,添加用户 返回2页数据,实际只显示一页---
|
||||
|
||||
const data = await defHttp.get({ url, params }, { isTransformResponse: false });
|
||||
if (data.success) {
|
||||
let { records, total } = data.result;
|
||||
totalRecord.value = total;
|
||||
initCurrentUserData(records);
|
||||
userDataList.value = records;
|
||||
} else {
|
||||
console.error(data.message);
|
||||
}
|
||||
console.log('loadUserList', data);
|
||||
}
|
||||
|
||||
// 往用户列表中添加一个 当前用户选项
|
||||
function initCurrentUserData(records) {
|
||||
if(pageNo.value==1 && props.inSuperQuery === true){
|
||||
records.unshift({...mySelfData})
|
||||
}
|
||||
}
|
||||
/*--------------加载数据---------------*/
|
||||
|
||||
/*--------------选中/取消选中---------------*/
|
||||
function onSelectUser(info) {
|
||||
if (props.multi === true) {
|
||||
let arr = selectedUserList.value;
|
||||
let idList = selectedIdList.value;
|
||||
if (idList.indexOf(info.id) < 0) {
|
||||
arr.push({ ...info });
|
||||
selectedUserList.value = arr;
|
||||
}
|
||||
} else {
|
||||
selectedUserList.value = [{ ...info }];
|
||||
}
|
||||
}
|
||||
function unSelectUser(id) {
|
||||
let arr = selectedUserList.value;
|
||||
let index = -1;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (arr[i].id === id) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index >= 0) {
|
||||
arr.splice(index, 1);
|
||||
selectedUserList.value = arr;
|
||||
}
|
||||
}
|
||||
/*--------------选中/取消选中---------------*/
|
||||
|
||||
function onChangeTab(tab) {
|
||||
myActiveKey.value = tab;
|
||||
}
|
||||
|
||||
function prevent(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
//加载第一页数据
|
||||
loadUserList();
|
||||
|
||||
return {
|
||||
selectedDepart,
|
||||
departOptions,
|
||||
initDepartOptions,
|
||||
onDepartChange,
|
||||
|
||||
register,
|
||||
handleOk,
|
||||
|
||||
searchText,
|
||||
searchInputStatus,
|
||||
showSearchInput,
|
||||
onSearchUser,
|
||||
clearSearch,
|
||||
|
||||
myActiveKey,
|
||||
onChangeTab,
|
||||
|
||||
pageNo,
|
||||
totalRecord,
|
||||
onPageChange,
|
||||
userDataList,
|
||||
selectedUserList,
|
||||
selectedIdList,
|
||||
onSelectUser,
|
||||
unSelectUser,
|
||||
excludeUserIdList
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.j-user-select-modal2 {
|
||||
.depart-select {
|
||||
.ant-select-selector {
|
||||
color: #fff !important;
|
||||
background-color: #409eff !important;
|
||||
border-radius: 5px !important;
|
||||
}
|
||||
.ant-select-selection-item,
|
||||
.ant-select-arrow {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
.my-search {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
z-index: 1;
|
||||
&.all-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.anticon {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: #0a8fe9 !important;
|
||||
}
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.my-tabs {
|
||||
}
|
||||
|
||||
.selected-users {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,273 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="isSearchFormComp" @click="click2Add" :class="disabled?'disabled-user-select':''" style="padding:0 5px;background-color: #fff;border: 1px solid #ccc;border-radius: 3px;box-sizing: border-box;display:flex;color: #9e9e9e;font-size: 14px;flex-wrap: wrap;min-height: 32px;">
|
||||
<template v-if="selectedUserList.length > 0">
|
||||
<SelectedUserItem v-for="item in showUserList" :info="item" @unSelect="unSelectUser" query />
|
||||
</template>
|
||||
<span v-else style="height: 30px;line-height: 30px;display: inline-block;margin-left: 7px;color: #bfbfbf;">请选择用户</span>
|
||||
<div v-if="ellipsisInfo.status" class="user-selected-item">
|
||||
<div class="user-select-ellipsis">
|
||||
<span style="color: red">+{{ellipsisInfo.count}}...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else style="display: flex; flex-wrap: wrap; flex-direction: row" >
|
||||
<template v-if="selectedUserList.length > 0">
|
||||
<SelectedUserItem v-for="item in selectedUserList" :info="item" @unSelect="unSelectUser" />
|
||||
</template>
|
||||
<a-button v-if="showAddButton" shape="circle" @click="onShowModal"><PlusOutlined /></a-button>
|
||||
</div>
|
||||
|
||||
<user-select-modal :inSuperQuery="inSuperQuery" :multi="multi" :getContainer="getContainer" @register="registerModal" @selected="onSelected" :izExcludeMy="izExcludeMy"></user-select-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, watch, ref, computed, toRaw } from 'vue';
|
||||
import { Form } from 'ant-design-vue';
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import UserSelectModal from './UserSelectModal.vue';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import SelectedUserItem from './SelectedUserItem.vue';
|
||||
import { mySelfExpress, mySelfData } from './useUserSelect'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'UserSelect',
|
||||
components: {
|
||||
PlusOutlined,
|
||||
UserSelectModal,
|
||||
SelectedUserItem,
|
||||
},
|
||||
props: {
|
||||
store: {
|
||||
type: String,
|
||||
default: 'id',
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
multi: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
getContainer: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
// 是否作为查询条件
|
||||
query:{
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
//最多显示几个人员-query为true有效
|
||||
maxCount:{
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
disabled:{
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
//是否排除我自己
|
||||
izExcludeMy:{
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
//是否在高级查询中作为条件 可以选择当前用户
|
||||
inSuperQuery:{
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
emits: ['update:value', 'change'],
|
||||
setup(props, { emit }) {
|
||||
const formItemContext = Form.useInjectFormItemContext();
|
||||
const loading = ref(true);
|
||||
const selectedUserList = ref<any[]>([]);
|
||||
const showUserList = computed(()=>{
|
||||
let list = selectedUserList.value
|
||||
let max = props.maxCount;
|
||||
if(list.length<=max){
|
||||
return list;
|
||||
}
|
||||
return list.filter((_item, index)=>index<max);
|
||||
});
|
||||
const ellipsisInfo = computed(()=>{
|
||||
let max = props.maxCount;
|
||||
let len = selectedUserList.value.length
|
||||
if(len > max){
|
||||
return {status: true, count: len-max};
|
||||
}else{
|
||||
return {status: false}
|
||||
}
|
||||
});
|
||||
|
||||
// 注册弹窗
|
||||
const [registerModal, { openModal, closeModal }] = useModal();
|
||||
function onShowModal() {
|
||||
if(props.disabled===true){
|
||||
return ;
|
||||
}
|
||||
let list = toRaw(selectedUserList.value);
|
||||
openModal(true, {
|
||||
list,
|
||||
});
|
||||
}
|
||||
|
||||
function onSelected(arr) {
|
||||
console.log('onSelected', arr);
|
||||
selectedUserList.value = arr;
|
||||
onSelectedChange();
|
||||
closeModal();
|
||||
}
|
||||
|
||||
function onSelectedChange() {
|
||||
loading.value = false;
|
||||
let temp: any[] = [];
|
||||
let arr = selectedUserList.value;
|
||||
if (arr && arr.length > 0) {
|
||||
temp = arr.map((k) => {
|
||||
return k[props.store];
|
||||
});
|
||||
}
|
||||
let str = temp.join(',');
|
||||
emit('update:value', str);
|
||||
emit('change', str);
|
||||
formItemContext.onFieldChange();
|
||||
console.log('选中数据', str);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
async (val) => {
|
||||
if (val) {
|
||||
if (loading.value === true) {
|
||||
await getUserList(val);
|
||||
}
|
||||
} else {
|
||||
selectedUserList.value = [];
|
||||
}
|
||||
loading.value = true;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
async function getUserList(ids) {
|
||||
let hasUserExpress = false;
|
||||
let paramIds = ids;
|
||||
let idList = [];
|
||||
selectedUserList.value = [];
|
||||
if(ids){
|
||||
// update-begin-author:sunjianlei date:20230330 for: 修复用户选择器逗号分割回显不生效的问题
|
||||
let tempArray = ids.split(',').map(s => s.trim()).filter(s => s != '');
|
||||
if (tempArray.includes(mySelfExpress)) {
|
||||
hasUserExpress = true;
|
||||
idList = tempArray.filter(item => item != mySelfExpress);
|
||||
} else {
|
||||
idList = tempArray;
|
||||
}
|
||||
// update-end-author:sunjianlei date:20230330 for: 修复用户选择器逗号分割回显不生效的问题
|
||||
}
|
||||
|
||||
if(idList.length>0){
|
||||
paramIds = idList.join(',')
|
||||
const url = '/sys/user/list';
|
||||
let params = {
|
||||
[props.store]: paramIds,
|
||||
};
|
||||
const data = await defHttp.get({ url, params }, { isTransformResponse: false });
|
||||
console.log('getUserList', data);
|
||||
if (data.success) {
|
||||
const { records } = data.result;
|
||||
selectedUserList.value = records;
|
||||
} else {
|
||||
console.error(data.message);
|
||||
}
|
||||
}
|
||||
if(hasUserExpress){
|
||||
let temp = selectedUserList.value;
|
||||
temp.push({...mySelfData})
|
||||
}
|
||||
}
|
||||
|
||||
const showAddButton = computed(() => {
|
||||
if(props.disabled === true){
|
||||
return false;
|
||||
}
|
||||
if (props.multi === true) {
|
||||
return true;
|
||||
} else {
|
||||
if (selectedUserList.value.length > 0) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function unSelectUser(id) {
|
||||
console.log('unSelectUser', id);
|
||||
let arr = selectedUserList.value;
|
||||
let index = -1;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (arr[i].id == id) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index >= 0) {
|
||||
arr.splice(index, 1);
|
||||
selectedUserList.value = arr;
|
||||
|
||||
onSelectedChange();
|
||||
}
|
||||
}
|
||||
|
||||
function click2Add(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onShowModal();
|
||||
}
|
||||
|
||||
const isSearchFormComp = computed(()=>{
|
||||
if(props.query===true){
|
||||
return true;
|
||||
}else{
|
||||
return false
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
registerModal,
|
||||
onShowModal,
|
||||
isSearchFormComp,
|
||||
onSelected,
|
||||
showAddButton,
|
||||
unSelectUser,
|
||||
selectedUserList,
|
||||
showUserList,
|
||||
ellipsisInfo,
|
||||
click2Add
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.user-select-ellipsis{
|
||||
width: 40px;
|
||||
height: 24px;
|
||||
text-align: center;
|
||||
line-height: 22px;
|
||||
border-radius: 8px;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
.disabled-user-select{
|
||||
cursor: not-allowed;
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 用户选择组件支持选择 我自己,以表达式的形式传值
|
||||
*/
|
||||
export const mySelfExpress = '#{sys_user_code}';
|
||||
|
||||
/**
|
||||
* 用户列表 我自己的数据
|
||||
*/
|
||||
export const mySelfData = {
|
||||
id: mySelfExpress, username: mySelfExpress, realname: '当前用户', avatarIcon: 'idcard-outlined', avatarColor: 'rgb(75 176 79)'
|
||||
}
|
||||
@ -0,0 +1,133 @@
|
||||
export const useCodeHinting = (CodeMirror, keywords, language) => {
|
||||
const currentKeywords: any = [...keywords];
|
||||
const codeHintingMount = (coder) => {
|
||||
if (keywords.length) {
|
||||
coder.setOption('mode', language);
|
||||
setTimeout(() => {
|
||||
coder!.on('cursorActivity', function () {
|
||||
coder?.showHint({
|
||||
completeSingle: false,
|
||||
// container: containerRef.value
|
||||
});
|
||||
});
|
||||
}, 1e3);
|
||||
}
|
||||
};
|
||||
|
||||
const codeHintingRegistry = () => {
|
||||
// 自定义关键词(.的上一级)
|
||||
const customKeywords: string[] = [];
|
||||
|
||||
currentKeywords.forEach((item) => {
|
||||
if (item.superiors) {
|
||||
customKeywords.push(item.superiors);
|
||||
}
|
||||
});
|
||||
const funcsHint = (cm, callback) => {
|
||||
// 获取光标位置
|
||||
const cur = cm.getCursor();
|
||||
// 获取当前单词的信息
|
||||
const token = cm.getTokenAt(cur);
|
||||
const start = token.start;
|
||||
const end = cur.ch;
|
||||
const str = token.string;
|
||||
let recordKeyword = null;
|
||||
console.log('光标位置:', cur, '单词信息:', token, `start:${start},end:${end},str:${str}`);
|
||||
|
||||
if (str.length) {
|
||||
if (str === '.') {
|
||||
// 查找.前面是否有定义的关键词
|
||||
const curLineCode = cm.getLine(cur.line);
|
||||
for (let i = 0, len = customKeywords.length; i < len; i++) {
|
||||
const k = curLineCode.substring(-1, customKeywords[i].length);
|
||||
if (customKeywords.includes(k)) {
|
||||
recordKeyword = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const findIdx = (a, b) => a.toLowerCase().indexOf(b.toLowerCase());
|
||||
let list = currentKeywords.filter((item) => {
|
||||
if (recordKeyword) {
|
||||
// 查特定对象下的属性or方法
|
||||
return item.superiors === recordKeyword;
|
||||
} else {
|
||||
// 查全局属性或者方法
|
||||
return item.superiors == undefined;
|
||||
}
|
||||
});
|
||||
if (str === '.') {
|
||||
if (recordKeyword == null) {
|
||||
list = [];
|
||||
}
|
||||
} else {
|
||||
list = list
|
||||
.filter((item) => {
|
||||
const { text } = item;
|
||||
const index = findIdx(text, str);
|
||||
let result = text.startsWith('.') ? index === 1 : index === 0;
|
||||
return result;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (findIdx(a.text, str) < findIdx(b.text, str)) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (list.length === 1) {
|
||||
// 只有一个时可能是自己输入,输到最后需要去掉提示。
|
||||
const item = list[0];
|
||||
if (item.text === str || item.text.substring(1) === str) {
|
||||
list = [];
|
||||
}
|
||||
}
|
||||
if (list.length) {
|
||||
// 当str不是点时去掉点
|
||||
if (str != '.') {
|
||||
list = list.map((item) => {
|
||||
if (item.text.indexOf('.') === 0) {
|
||||
return { ...item, text: item.text.substring(1) };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
callback({
|
||||
list: list,
|
||||
from: CodeMirror.Pos(cur.line, start),
|
||||
to: CodeMirror.Pos(cur.line, end),
|
||||
});
|
||||
// update-begin--author:liaozhiyang---date:20240429---for:【QQYUN-8865】js增强加上鼠标移入提示
|
||||
const item = currentKeywords[0];
|
||||
if (item?.desc) {
|
||||
setTimeout(() => {
|
||||
const elem: HTMLUListElement = document.querySelector('.CodeMirror-hints')!;
|
||||
if (elem) {
|
||||
const childElems = elem.children;
|
||||
Array.from(childElems).forEach((item) => {
|
||||
const displayText = item.textContent;
|
||||
const findItem = currentKeywords.find((item) => item.displayText === displayText);
|
||||
if (findItem) {
|
||||
item.setAttribute('title', findItem.desc);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240429---for:【QQYUN-8865】js增强加上鼠标移入提示
|
||||
} else {
|
||||
}
|
||||
}
|
||||
};
|
||||
funcsHint.async = true;
|
||||
funcsHint.supportsSelection = true;
|
||||
// 自动补全
|
||||
keywords.length && CodeMirror.registerHelper('hint', language, funcsHint);
|
||||
};
|
||||
return {
|
||||
codeHintingRegistry,
|
||||
codeHintingMount,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,176 @@
|
||||
import { inject, reactive, ref, watch, unref, Ref } from 'vue';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { isEmpty } from '@/utils/is';
|
||||
|
||||
export function useSelectBiz(getList, props, emit) {
|
||||
//接收下拉框选项
|
||||
const selectOptions = inject('selectOptions', ref<Array<object>>([]));
|
||||
//接收已选择的值
|
||||
const selectValues = <object>inject('selectValues', reactive({ value: [], change: false }));
|
||||
// 是否正在加载回显
|
||||
const loadingEcho = inject<Ref<boolean>>('loadingEcho', ref(false));
|
||||
//数据集
|
||||
const dataSource = ref<Array<object>>([]);
|
||||
//已选择的值
|
||||
const checkedKeys = ref<Array<string | number>>([]);
|
||||
//选则的行记录
|
||||
const selectRows = ref<Array<object>>([]);
|
||||
//提示弹窗
|
||||
const $message = useMessage();
|
||||
// 是否是首次加载回显,只有首次加载,才会显示 loading
|
||||
let isFirstLoadEcho = true;
|
||||
|
||||
/**
|
||||
* 监听selectValues变化
|
||||
*/
|
||||
watch(
|
||||
selectValues,
|
||||
() => {
|
||||
//update-begin-author:liusq---date:2023-10-19--for: [issues/788]判断有设置数值才去加载
|
||||
//if (selectValues['change'] == false && !isEmpty(selectValues['value'])) {
|
||||
if (selectValues['change'] == false && !isEmpty(selectValues['value'])) {
|
||||
//update-end-author:liusq---date:2023-10-19--for: [issues/788]判断有设置数值才去加载
|
||||
//update-begin---author:wangshuai ---date:20220412 for:[VUEN-672]发文草稿箱编辑时拟稿人显示用户名------------
|
||||
let params = { isMultiTranslate: 'true' };
|
||||
params[props.rowKey] = selectValues['value'].join(',');
|
||||
//update-end---author:wangshuai ---date:20220412 for:[VUEN-672]发文草稿箱编辑时拟稿人显示用户名--------------
|
||||
loadingEcho.value = isFirstLoadEcho;
|
||||
isFirstLoadEcho = false;
|
||||
getDataSource(params, true)
|
||||
.then()
|
||||
.finally(() => {
|
||||
loadingEcho.value = isFirstLoadEcho;
|
||||
});
|
||||
}
|
||||
//设置列表默认选中
|
||||
checkedKeys['value'] = selectValues['value'];
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
async function onSelectChange(selectedRowKeys: (string | number)[], selectRow) {
|
||||
checkedKeys.value = selectedRowKeys;
|
||||
//判断全选的问题checkedKeys和selectRows必须一致
|
||||
if (props.showSelected && unref(checkedKeys).length !== unref(selectRow).length) {
|
||||
let { records } = await getList({
|
||||
code: unref(checkedKeys).join(','),
|
||||
pageSize: unref(checkedKeys).length,
|
||||
});
|
||||
selectRows.value = records;
|
||||
} else {
|
||||
selectRows.value = selectRow;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择列配置
|
||||
*/
|
||||
const rowSelection = {
|
||||
//update-begin-author:liusq---date:20220517--for: 动态设置rowSelection的type值,默认是'checkbox' ---
|
||||
type: props.isRadioSelection ? 'radio' : 'checkbox',
|
||||
//update-end-author:liusq---date:20220517--for: 动态设置rowSelection的type值,默认是'checkbox' ---
|
||||
columnWidth: 20,
|
||||
selectedRowKeys: checkedKeys,
|
||||
onChange: onSelectChange,
|
||||
//update-begin-author:wangshuai---date:20221102--for: [VUEN-2562]用户选择,跨页选择后,只有当前页人员 ---
|
||||
//table4.4.0新增属性选中之后是否清空上一页下一页的数据,默认false
|
||||
preserveSelectedRowKeys:true,
|
||||
//update-end-author:wangshuai---date:20221102--for: [VUEN-2562]用户选择,跨页选择后,只有当前页人员 ---
|
||||
};
|
||||
|
||||
/**
|
||||
* 序号列配置
|
||||
*/
|
||||
const indexColumnProps = {
|
||||
dataIndex: 'index',
|
||||
width: 50,
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载列表数据集
|
||||
* @param params
|
||||
* @param flag 是否是默认回显模式加载
|
||||
*/
|
||||
async function getDataSource(params, flag) {
|
||||
let { records } = await getList(params);
|
||||
dataSource.value = records;
|
||||
if (flag) {
|
||||
let options = <any[]>[];
|
||||
records.forEach((item) => {
|
||||
options.push({ label: item[props.labelKey], value: item[props.rowKey] });
|
||||
});
|
||||
selectOptions.value = options;
|
||||
}
|
||||
}
|
||||
async function initSelectRows() {
|
||||
let { records } = await getList({
|
||||
code: selectValues['value'].join(','),
|
||||
pageSize: selectValues['value'].length,
|
||||
});
|
||||
checkedKeys['value'] = selectValues['value'];
|
||||
selectRows['value'] = records;
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹出框显示隐藏触发事件
|
||||
*/
|
||||
async function visibleChange(visible) {
|
||||
if (visible) {
|
||||
//设置列表默认选中
|
||||
props.showSelected && initSelectRows();
|
||||
} else {
|
||||
// update-begin--author:liaozhiyang---date:20240517---for:【QQYUN-9366】用户选择组件取消和关闭会把选择数据带入
|
||||
emit('close');
|
||||
// update-end--author:liaozhiyang---date:20240517---for:【QQYUN-9366】用户选择组件取消和关闭会把选择数据带入
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确定选择
|
||||
*/
|
||||
function getSelectResult(success) {
|
||||
let options = <any[]>[];
|
||||
let values = <any[]>[];
|
||||
selectRows.value.forEach((item) => {
|
||||
options.push({ label: item[props.labelKey], value: item[props.rowKey] });
|
||||
});
|
||||
checkedKeys.value.forEach((item) => {
|
||||
values.push(item);
|
||||
});
|
||||
selectOptions.value = options;
|
||||
if (props.maxSelectCount && values.length > props.maxSelectCount) {
|
||||
$message.createMessage.warning(`最多只能选择${props.maxSelectCount}条数据`);
|
||||
return false;
|
||||
}
|
||||
success && success(options, values);
|
||||
}
|
||||
//删除已选择的信息
|
||||
function handleDeleteSelected(record) {
|
||||
//update-begin---author:wangshuai ---date:20230404 for:【issues/424】开启右侧列表后,在右侧列表中删除用户时,逻辑有问题------------
|
||||
checkedKeys.value = checkedKeys.value.filter((item) => item != record[props.rowKey]);
|
||||
selectRows.value = selectRows.value.filter((item) => item[props.rowKey] !== record[props.rowKey]);
|
||||
//update-end---author:wangshuai ---date:20230404 for:【issues/424】开启右侧列表后,在右侧列表中删除用户时,逻辑有问题------------
|
||||
}
|
||||
//清空选择项
|
||||
function reset() {
|
||||
checkedKeys.value = [];
|
||||
selectRows.value = [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
onSelectChange,
|
||||
getDataSource,
|
||||
visibleChange,
|
||||
selectOptions,
|
||||
selectValues,
|
||||
rowSelection,
|
||||
indexColumnProps,
|
||||
checkedKeys,
|
||||
selectRows,
|
||||
dataSource,
|
||||
getSelectResult,
|
||||
handleDeleteSelected,
|
||||
reset,
|
||||
},
|
||||
];
|
||||
}
|
||||
278
jeecgboot-vue3/src/components/Form/src/jeecg/hooks/useTreeBiz.ts
Normal file
278
jeecgboot-vue3/src/components/Form/src/jeecg/hooks/useTreeBiz.ts
Normal file
@ -0,0 +1,278 @@
|
||||
import type { Ref } from 'vue';
|
||||
import { inject, reactive, ref, computed, unref, watch, nextTick } from 'vue';
|
||||
import { TreeActionType } from '/@/components/Tree';
|
||||
import { listToTree } from '/@/utils/common/compUtils';
|
||||
|
||||
export function useTreeBiz(treeRef, getList, props, realProps, emit) {
|
||||
//接收下拉框选项
|
||||
const selectOptions = inject('selectOptions', ref<Array<object>>([]));
|
||||
//接收已选择的值
|
||||
const selectValues = <object>inject('selectValues', reactive({}));
|
||||
// 是否正在加载回显
|
||||
const loadingEcho = inject<Ref<boolean>>('loadingEcho', ref(false));
|
||||
//数据集
|
||||
const treeData = ref<Array<object>>([]);
|
||||
//已选择的值
|
||||
const checkedKeys = ref<Array<string | number>>([]);
|
||||
//选则的行记录
|
||||
const selectRows = ref<Array<object>>([]);
|
||||
//是否是打开弹框模式
|
||||
const openModal = ref(false);
|
||||
// 是否开启父子关联,如果不可以多选,就始终取消父子关联
|
||||
const getCheckStrictly = computed(() => (realProps.multiple ? props.checkStrictly : true));
|
||||
// 是否是首次加载回显,只有首次加载,才会显示 loading
|
||||
let isFirstLoadEcho = true;
|
||||
|
||||
/**
|
||||
* 监听selectValues变化
|
||||
*/
|
||||
watch(
|
||||
selectValues,
|
||||
({ value: values }: Recordable) => {
|
||||
if(!values){
|
||||
return;
|
||||
}
|
||||
if (openModal.value == false && values.length > 0) {
|
||||
loadingEcho.value = isFirstLoadEcho;
|
||||
isFirstLoadEcho = false;
|
||||
onLoadData(null, values.join(',')).finally(() => {
|
||||
loadingEcho.value = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* 获取树实例
|
||||
*/
|
||||
function getTree() {
|
||||
const tree = unref(treeRef);
|
||||
if (!tree) {
|
||||
throw new Error('tree is null!');
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置树展开级别
|
||||
*/
|
||||
function expandTree() {
|
||||
nextTick(() => {
|
||||
if (props.defaultExpandLevel && props.defaultExpandLevel > 0) {
|
||||
getTree().filterByLevel(props.defaultExpandLevel);
|
||||
}
|
||||
//设置列表默认选中
|
||||
checkedKeys.value = selectValues['value'];
|
||||
}).then();
|
||||
}
|
||||
|
||||
/**
|
||||
* 树节点选择
|
||||
*/
|
||||
function onSelect(keys, info) {
|
||||
if (props.checkable == false) {
|
||||
checkedKeys.value = props.checkStrictly ? keys.checked : keys;
|
||||
const { selectedNodes } = info;
|
||||
let rows = <any[]>[];
|
||||
selectedNodes.forEach((item) => {
|
||||
rows.push(item);
|
||||
});
|
||||
selectRows.value = rows;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 树节点选择
|
||||
*/
|
||||
function onCheck(keys, info) {
|
||||
if (props.checkable == true) {
|
||||
// 如果不能多选,就只保留最后一个选中的
|
||||
if (!realProps.multiple) {
|
||||
if (info.checked) {
|
||||
//update-begin-author:taoyan date:20220408 for: 单选模式下,设定rowKey,无法选中数据-
|
||||
checkedKeys.value = [info.node.eventKey];
|
||||
let rowKey = props.rowKey;
|
||||
let temp = info.checkedNodes.find((n) => n[rowKey] === info.node.eventKey);
|
||||
selectRows.value = [temp];
|
||||
//update-end-author:taoyan date:20220408 for: 单选模式下,设定rowKey,无法选中数据-
|
||||
} else {
|
||||
checkedKeys.value = [];
|
||||
selectRows.value = [];
|
||||
}
|
||||
return;
|
||||
}
|
||||
checkedKeys.value = props.checkStrictly ? keys.checked : keys;
|
||||
const { checkedNodes } = info;
|
||||
let rows = <any[]>[];
|
||||
checkedNodes.forEach((item) => {
|
||||
rows.push(item);
|
||||
});
|
||||
selectRows.value = rows;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 勾选全部
|
||||
*/
|
||||
async function checkALL(checkAll) {
|
||||
getTree().checkAll(checkAll);
|
||||
//update-begin---author:wangshuai ---date:20230403 for:【issues/394】所属部门树操作全部勾选不生效/【issues/4646】部门全部勾选后,点击确认按钮,部门信息丢失------------
|
||||
await nextTick();
|
||||
checkedKeys.value = getTree().getCheckedKeys();
|
||||
if(checkAll){
|
||||
getTreeRow();
|
||||
}else{
|
||||
selectRows.value = [];
|
||||
}
|
||||
//update-end---author:wangshuai ---date:20230403 for:【issues/394】所属部门树操作全部勾选不生效/【issues/4646】部门全部勾选后,点击确认按钮,部门信息丢失------------
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数列表
|
||||
* @param res
|
||||
*/
|
||||
function getTreeRow() {
|
||||
let ids = "";
|
||||
if(unref(checkedKeys).length>0){
|
||||
ids = checkedKeys.value.join(",");
|
||||
}
|
||||
getList({ids:ids}).then((res) =>{
|
||||
selectRows.value = res;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 展开全部
|
||||
*/
|
||||
function expandAll(expandAll) {
|
||||
getTree().expandAll(expandAll);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载树数据
|
||||
*/
|
||||
async function onLoadData(treeNode, ids) {
|
||||
let params = {};
|
||||
let startPid = '';
|
||||
if (treeNode) {
|
||||
startPid = treeNode.eventKey;
|
||||
//update-begin---author:wangshuai ---date:20220407 for:rowkey不设置成id,sync开启异步的时候,点击上级下级不显示------------
|
||||
params['pid'] = treeNode.value;
|
||||
//update-end---author:wangshuai ---date:20220407 for:rowkey不设置成id,sync开启异步的时候,点击上级下级不显示------------
|
||||
}
|
||||
if (ids) {
|
||||
startPid = '';
|
||||
params['ids'] = ids;
|
||||
}
|
||||
let record = await getList(params);
|
||||
let optionData = record;
|
||||
if (!props.serverTreeData) {
|
||||
//前端处理数据为tree结构
|
||||
record = listToTree(record, props, startPid);
|
||||
if (record.length == 0 && treeNode) {
|
||||
checkHasChild(startPid, treeData.value);
|
||||
}
|
||||
}
|
||||
|
||||
if (openModal.value == true) {
|
||||
//弹框模式下加载全部数据
|
||||
if (!treeNode) {
|
||||
treeData.value = record;
|
||||
} else {
|
||||
return new Promise((resolve: (value?: unknown) => void) => {
|
||||
if (!treeNode.children) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const asyncTreeAction: TreeActionType | null = unref(treeRef);
|
||||
if (asyncTreeAction) {
|
||||
asyncTreeAction.updateNodeByKey(treeNode.eventKey, { children: record });
|
||||
asyncTreeAction.setExpandedKeys([treeNode.eventKey, ...asyncTreeAction.getExpandedKeys()]);
|
||||
}
|
||||
resolve();
|
||||
return;
|
||||
});
|
||||
}
|
||||
expandTree();
|
||||
} else {
|
||||
const options = <any[]>[];
|
||||
optionData.forEach((item) => {
|
||||
//update-begin-author:taoyan date:2022-7-4 for: issues/I5F3P4 online配置部门选择后编辑,查看数据应该显示部门名称,不是部门代码
|
||||
options.push({ label: item[props.labelKey], value: item[props.rowKey] });
|
||||
//update-end-author:taoyan date:2022-7-4 for: issues/I5F3P4 online配置部门选择后编辑,查看数据应该显示部门名称,不是部门代码
|
||||
});
|
||||
selectOptions.value = options;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步加载时检测是否含有下级节点
|
||||
* @param pid 父节点
|
||||
* @param treeArray tree数据
|
||||
*/
|
||||
function checkHasChild(pid, treeArray) {
|
||||
if (treeArray && treeArray.length > 0) {
|
||||
for (let item of treeArray) {
|
||||
if (item.key == pid) {
|
||||
if (!item.child) {
|
||||
item.isLeaf = true;
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
checkHasChild(pid, item.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已选择数据
|
||||
*/
|
||||
function getSelectTreeData(success) {
|
||||
const options = <any[]>[];
|
||||
const values = <any[]>[];
|
||||
selectRows.value.forEach((item) => {
|
||||
options.push({ label: item[props.labelKey], value: item[props.rowKey] });
|
||||
});
|
||||
checkedKeys.value.forEach((item) => {
|
||||
values.push(item);
|
||||
});
|
||||
selectOptions.value = options;
|
||||
success && success(options, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹出框显示隐藏触发事件
|
||||
*/
|
||||
async function visibleChange(visible) {
|
||||
if (visible) {
|
||||
//弹出框打开时加载全部数据
|
||||
openModal.value = true;
|
||||
await onLoadData(null, null);
|
||||
} else {
|
||||
openModal.value = false;
|
||||
// update-begin--author:liaozhiyang---date:20240527---for:【TV360X-414】部门设置了默认值,查询重置变成空了(同步JSelectUser组件改法)
|
||||
emit?.('close');
|
||||
// update-end--author:liaozhiyang---date:20240527---for:【TV360X-414】部门设置了默认值,查询重置变成空了(同步JSelectUser组件改法)
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
visibleChange,
|
||||
selectOptions,
|
||||
selectValues,
|
||||
onLoadData,
|
||||
onCheck,
|
||||
onSelect,
|
||||
checkALL,
|
||||
expandAll,
|
||||
checkedKeys,
|
||||
selectRows,
|
||||
treeData,
|
||||
getCheckStrictly,
|
||||
getSelectTreeData,
|
||||
},
|
||||
];
|
||||
}
|
||||
87
jeecgboot-vue3/src/components/Form/src/jeecg/props/props.ts
Normal file
87
jeecgboot-vue3/src/components/Form/src/jeecg/props/props.ts
Normal file
@ -0,0 +1,87 @@
|
||||
//下拉选择框组件公共props
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
|
||||
export const selectProps = {
|
||||
//是否多选
|
||||
isRadioSelection: {
|
||||
type: Boolean,
|
||||
//update-begin---author:wangshuai ---date:20220527 for:部门用户组件默认应该单选,否则其他地方有问题------------
|
||||
default: false,
|
||||
//update-end---author:wangshuai ---date:20220527 for:部门用户组件默认应该单选,否则其他地方有问题--------------
|
||||
},
|
||||
//回传value字段名
|
||||
rowKey: {
|
||||
type: String,
|
||||
default: 'id',
|
||||
},
|
||||
//回传文本字段名
|
||||
labelKey: {
|
||||
type: String,
|
||||
default: 'name',
|
||||
},
|
||||
//查询参数
|
||||
params: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
//是否显示选择按钮
|
||||
showButton: propTypes.bool.def(true),
|
||||
//是否显示右侧选中列表
|
||||
showSelected: propTypes.bool.def(false),
|
||||
//最大选择数量
|
||||
maxSelectCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
};
|
||||
|
||||
//树形选择组件公共props
|
||||
export const treeProps = {
|
||||
//回传value字段名
|
||||
rowKey: {
|
||||
type: String,
|
||||
default: 'key',
|
||||
},
|
||||
//回传文本字段名
|
||||
labelKey: {
|
||||
type: String,
|
||||
default: 'title',
|
||||
},
|
||||
//初始展开的层级
|
||||
defaultExpandLevel: {
|
||||
type: [Number],
|
||||
default: 0,
|
||||
},
|
||||
//根pid值
|
||||
startPid: {
|
||||
type: [Number, String],
|
||||
default: '',
|
||||
},
|
||||
//主键字段
|
||||
primaryKey: {
|
||||
type: [String],
|
||||
default: 'id',
|
||||
},
|
||||
//父ID字段
|
||||
parentKey: {
|
||||
type: [String],
|
||||
default: 'parentId',
|
||||
},
|
||||
//title字段
|
||||
titleKey: {
|
||||
type: [String],
|
||||
default: 'title',
|
||||
},
|
||||
//是否开启服务端转换tree数据结构
|
||||
serverTreeData: propTypes.bool.def(true),
|
||||
//是否开启异步加载数据
|
||||
sync: propTypes.bool.def(true),
|
||||
//是否显示选择按钮
|
||||
showButton: propTypes.bool.def(true),
|
||||
//是否显示复选框
|
||||
checkable: propTypes.bool.def(true),
|
||||
//checkable 状态下节点选择完全受控(父子节点选中状态不再关联)
|
||||
checkStrictly: propTypes.bool.def(false),
|
||||
// 是否允许多选,默认 true
|
||||
multiple: propTypes.bool.def(true),
|
||||
};
|
||||
121
jeecgboot-vue3/src/components/Form/src/props.ts
Normal file
121
jeecgboot-vue3/src/components/Form/src/props.ts
Normal file
@ -0,0 +1,121 @@
|
||||
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 dayjs from "dayjs";
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import componentSetting from '/@/settings/componentSetting';
|
||||
|
||||
const { form } = componentSetting;
|
||||
export const basicProps = {
|
||||
model: {
|
||||
type: Object as PropType<Recordable>,
|
||||
default: {},
|
||||
},
|
||||
// 标签宽度 固定宽度
|
||||
labelWidth: {
|
||||
type: [Number, String] as PropType<number | string>,
|
||||
default: 0,
|
||||
},
|
||||
fieldMapToTime: {
|
||||
type: Array as PropType<FieldMapToTime>,
|
||||
default: () => [],
|
||||
},
|
||||
fieldMapToNumber: {
|
||||
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>>,
|
||||
},
|
||||
autoSetPlaceHolder: propTypes.bool.def(true),
|
||||
// 在INPUT组件上单击回车时,是否自动提交
|
||||
autoSubmitOnEnter: propTypes.bool.def(false),
|
||||
submitOnReset: propTypes.bool,
|
||||
size: propTypes.oneOf(['default', 'small', 'large']).def('default'),
|
||||
// 禁用表单
|
||||
disabled: propTypes.bool,
|
||||
emptySpan: {
|
||||
type: [Number, Object] as PropType<number>,
|
||||
default: 0,
|
||||
},
|
||||
// 是否显示收起展开按钮
|
||||
showAdvancedButton: propTypes.bool,
|
||||
// 转化时间
|
||||
transformDateFunc: {
|
||||
type: Function as PropType<Fn>,
|
||||
default: (date: any) => {
|
||||
// 判断是否是dayjs实例
|
||||
return dayjs.isDayjs(date) ? date?.format('YYYY-MM-DD HH:mm:ss') : date;
|
||||
},
|
||||
},
|
||||
rulesMessageJoinLabel: propTypes.bool.def(true),
|
||||
// 【jeecg】超过3列自动折叠
|
||||
autoAdvancedCol: propTypes.number.def(3),
|
||||
// 超过3行自动折叠
|
||||
autoAdvancedLine: propTypes.number.def(3),
|
||||
// 不受折叠影响的行数
|
||||
alwaysShowLines: propTypes.number.def(1),
|
||||
|
||||
// 是否显示操作按钮
|
||||
showActionButtonGroup: propTypes.bool.def(true),
|
||||
// 操作列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: {
|
||||
type: Object as PropType<Partial<ColEx>>,
|
||||
default: form.labelCol,
|
||||
},
|
||||
|
||||
layout: propTypes.oneOf(['horizontal', 'vertical', 'inline']).def('horizontal'),
|
||||
tableAction: {
|
||||
type: Object as PropType<TableActionType>,
|
||||
},
|
||||
|
||||
wrapperCol: {
|
||||
type: Object as PropType<Partial<ColEx>>,
|
||||
default: form.wrapperCol,
|
||||
},
|
||||
|
||||
colon: propTypes.bool.def(form.colon),
|
||||
|
||||
labelAlign: propTypes.string,
|
||||
|
||||
rowProps: Object as PropType<RowProps>,
|
||||
|
||||
// 当表单是查询条件的时候 当表单改变后自动查询,不需要点击查询按钮
|
||||
autoSearch: propTypes.bool.def(false),
|
||||
};
|
||||
224
jeecgboot-vue3/src/components/Form/src/types/form.ts
Normal file
224
jeecgboot-vue3/src/components/Form/src/types/form.ts
Normal file
@ -0,0 +1,224 @@
|
||||
import type { NamePath, RuleObject, ValidateOptions } from 'ant-design-vue/lib/form/interface';
|
||||
import type { VNode, ComputedRef } 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?][];
|
||||
export type FieldMapToNumber = [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>(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>;
|
||||
getProps: ComputedRef<Partial<FormProps>>;
|
||||
removeSchemaByFiled: (field: string | string[]) => Promise<void>;
|
||||
appendSchemaByField: (schema: FormSchema, prefixField: string | undefined, first?: boolean | undefined) => Promise<void>;
|
||||
validateFields: (nameList?: NamePath[], options?: ValidateOptions) => 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 {
|
||||
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;
|
||||
// Col configuration for the entire form
|
||||
labelCol?: Partial<ColEx> | null;
|
||||
// Col configuration for the entire form
|
||||
wrapperCol?: Partial<ColEx> | null;
|
||||
|
||||
// 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;
|
||||
// number interval fields are mapped into multiple
|
||||
fieldMapToNumber?: FieldMapToNumber;
|
||||
// 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;
|
||||
// 是否显示展开收起按钮
|
||||
showAdvancedButton?: boolean;
|
||||
// Whether to focus on the first input box, only works when the first form item is input
|
||||
autoFocusFirstItem?: boolean;
|
||||
// 【jeecg】如果 showAdvancedButton 为 true,超过指定列数默认折叠,默认为3
|
||||
autoAdvancedCol?: number;
|
||||
// 如果 showAdvancedButton 为 true,超过指定行数行默认折叠
|
||||
autoAdvancedLine?: number;
|
||||
// 折叠时始终保持显示的行数
|
||||
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;
|
||||
// Label name
|
||||
label: string | VNode;
|
||||
// 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[];
|
||||
// update-begin--author:liaozhiyang---date:20240308---for:【QQYUN-8377】formSchema props支持动态修改
|
||||
// 设置组件props的key
|
||||
dynamicPropskey?: string;
|
||||
dynamicPropsVal?: ((renderCallbackParams: RenderCallbackParams) => any);
|
||||
// update-end--author:liaozhiyang---date:20240308---for:【QQYUN-8377】formSchema props支持动态修改
|
||||
|
||||
// 这个属性自定义的 用于自定义的业务 比如在表单打开的时候修改表单的禁用状态,但是又不能重写componentProps,因为他的内容太多了,所以使用dynamicDisabled和buss实现
|
||||
buss?: any;
|
||||
|
||||
//label字数控制(label宽度)
|
||||
labelLength?: number;
|
||||
// update-begin--author:liaozhiyang---date:20240529---for【TV360X-460】basicForm支持v-auth指令(权限控制显隐)
|
||||
auth?: string;
|
||||
// update-end--author:liaozhiyang---date:20240529---for【TV360X-460】basicForm支持v-auth指令(权限控制显隐)
|
||||
}
|
||||
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;
|
||||
}
|
||||
91
jeecgboot-vue3/src/components/Form/src/types/formItem.ts
Normal file
91
jeecgboot-vue3/src/components/Form/src/types/formItem.ts
Normal 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;
|
||||
}
|
||||
6
jeecgboot-vue3/src/components/Form/src/types/hooks.ts
Normal file
6
jeecgboot-vue3/src/components/Form/src/types/hooks.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface AdvanceState {
|
||||
isAdvanced: boolean;
|
||||
hideAdvanceBtn: boolean;
|
||||
isLoad: boolean;
|
||||
actionSpan: number;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user