前端和后端源码,合并到一个git仓库中,方便用户下载,避免前后端不匹配的问题

This commit is contained in:
JEECG
2024-06-23 10:39:52 +08:00
parent bb918b742e
commit 0325e34dcb
1439 changed files with 171106 additions and 0 deletions

View 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 };

View 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>

View 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 };

View File

@ -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>

View 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>

View File

@ -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>

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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();

View 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 };
}

View 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();
});
}

View File

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

View File

@ -0,0 +1,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];
}

View File

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

View File

@ -0,0 +1,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,
};
}

View File

@ -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 };
}

View File

@ -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,
},
};
});
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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.

View File

@ -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,
};

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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';

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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,
};
}

View File

@ -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;

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 forJTreeSelect组件pidValue还原成空否则会影响自定义组件树示例------------
pidValue: propTypes.string.def(''),
//update-end---author:wangshuai ---date:20220620 forJTreeSelect组件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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,3 @@
export { UploadTypeEnum } from './upload.data';
export { default as JUpload } from './JUpload.vue';
export { default as JUploadModal } from './JUploadModal.vue';

View File

@ -0,0 +1,5 @@
export enum UploadTypeEnum {
all = 'all',
image = 'image',
file = 'file',
}

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 fornull指针异常导致控制台报错页面不显示------------
if(tableRef.value){
tableRef.value.setSelectedRowKeys(newVal);
}
//update-end---author:wangshuai ---date: 20230829 fornull指针异常导致控制台报错页面不显示------------
});
// 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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)'
}

View File

@ -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,
};
};

View File

@ -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,
},
];
}

View 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 forrowkey不设置成idsync开启异步的时候点击上级下级不显示------------
params['pid'] = treeNode.value;
//update-end---author:wangshuai ---date:20220407 forrowkey不设置成idsync开启异步的时候点击上级下级不显示------------
}
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,
},
];
}

View 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),
};

View 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),
};

View 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;
}

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More