first commit

This commit is contained in:
ye1023
2026-04-09 15:09:34 +08:00
commit d8e3a63df1
2246 changed files with 352109 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
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 JSelectUserByDepartment } from './src/jeecg/components/JSelectUserByDepartment.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 };
+423
View File
@@ -0,0 +1,423 @@
<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"
:formName="getBindValue.name"
: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,
getSchemaByField,
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,
getSchemaByField,
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>
+192
View File
@@ -0,0 +1,192 @@
/**
* 目前实现了异步加载的组件清单 :
* 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 JSelectUserByDepartment from './jeecg/components/JSelectUserByDepartment.vue';
import JLinkTableCard from './jeecg/components/JLinkTableCard/JLinkTableCard.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 LinkTableForQuery from '/@/components/jeecg/super/desform/LinkTableForQuery.vue';
import CascaderPcaForQuery from '/@/components/jeecg/super/desform/CascaderPcaForQuery.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('JLinkTableCard', JLinkTableCard);
// 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('JSelectUserByDepartment', JSelectUserByDepartment);
componentMap.set('JUpload', JUpload);
componentMap.set('JSearchSelect', JSearchSelect);
componentMap.set('JAddInput', JAddInput);
componentMap.set('JRangeNumber', JRangeNumber);
componentMap.set('CascaderPcaForQuery', CascaderPcaForQuery);
componentMap.set('LinkTableForQuery', LinkTableForQuery);
componentMap.set('CascaderPcaInFilter', CascaderPcaInFilter);
componentMap.set('UserSelect', UserSelect);
componentMap.set('RangeDate', JRangeDate);
componentMap.set('RangeTime', JRangeTime);
componentMap.set('RoleSelect', RoleSelectInput);
componentMap.set('JInputSelect', JInputSelect);
export function add(compName: ComponentType, component: Component) {
componentMap.set(compName, component);
}
export function del(compName: ComponentType) {
componentMap.delete(compName);
}
export { componentMap };
@@ -0,0 +1,130 @@
<!--
* @Description:It is troublesome to implement radio button group in the form. So it is extracted independently as a separate component
-->
<template>
<RadioGroup v-bind="attrs" v-model:value="state" button-style="solid" @change="handleChange">
<template v-for="item in getOptions" :key="`${item.value}`">
<RadioButton v-if="props.isBtn" :value="item.value" :disabled="item.disabled">
{{ item.label }}
</RadioButton>
<Radio v-else :value="item.value" :disabled="item.disabled">
{{ item.label }}
</Radio>
</template>
</RadioGroup>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, watchEffect, computed, unref, watch } from 'vue';
import { Radio } from 'ant-design-vue';
import { isFunction } from '/@/utils/is';
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { propTypes } from '/@/utils/propTypes';
import { get, omit } from 'lodash-es';
import { useI18n } from '/@/hooks/web/useI18n';
type OptionsItem = { label: string; value: string | number | boolean; disabled?: boolean };
export default defineComponent({
name: 'ApiRadioGroup',
components: {
RadioGroup: Radio.Group,
RadioButton: Radio.Button,
Radio,
},
props: {
api: {
type: Function as PropType<(arg?: Recordable | string) => Promise<OptionsItem[]>>,
default: null,
},
params: {
type: [Object, String] as PropType<Recordable | string>,
default: () => ({}),
},
value: {
type: [String, Number, Boolean] as PropType<string | number | boolean>,
},
isBtn: {
type: [Boolean] as PropType<boolean>,
default: false,
},
numberToString: propTypes.bool,
resultField: propTypes.string.def(''),
labelField: propTypes.string.def('label'),
valueField: propTypes.string.def('value'),
immediate: propTypes.bool.def(true),
},
emits: ['options-change', 'change'],
setup(props, { emit }) {
const options = ref<OptionsItem[]>([]);
const loading = ref(false);
const isFirstLoad = ref(true);
const emitData = ref<any[]>([]);
const attrs = useAttrs();
const { t } = useI18n();
// Embedded in the form, just use the hook binding to perform form verification
const [state] = useRuleFormItem(props);
// Processing options value
const getOptions = computed(() => {
const { labelField, valueField, numberToString } = props;
return unref(options).reduce((prev, next: Recordable) => {
if (next) {
const value = next[valueField];
prev.push({
label: next[labelField],
value: numberToString ? `${value}` : value,
...omit(next, [labelField, valueField]),
});
}
return prev;
}, [] as OptionsItem[]);
});
watchEffect(() => {
props.immediate && fetch();
});
watch(
() => props.params,
() => {
!unref(isFirstLoad) && fetch();
},
{ deep: true }
);
async function fetch() {
const api = props.api;
if (!api || !isFunction(api)) return;
options.value = [];
try {
loading.value = true;
const res = await api(props.params);
if (Array.isArray(res)) {
options.value = res;
emitChange();
return;
}
if (props.resultField) {
options.value = get(res, props.resultField) || [];
}
emitChange();
} catch (error) {
console.warn(error);
} finally {
loading.value = false;
}
}
function emitChange() {
emit('options-change', unref(getOptions));
}
function handleChange(_, ...args) {
emitData.value = args;
}
return { state, getOptions, attrs, loading, t, handleChange, props };
},
});
</script>
@@ -0,0 +1,233 @@
<template>
<Select
v-bind="attrs_"
v-model:value="state"
:options="getOptions"
@change="handleChange"
@dropdownVisibleChange="handleFetch"
@popupScroll="handlePopupScroll"
>
<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';
import { isNumber } from '/@/utils/is';
type OptionsItem = { label: string; value: string; disabled?: boolean };
//文档 https://help.jeecg.com/ui/apiSelect#pageconfig%E5%8F%82%E6%95%B0%E9%85%8D%E7%BD%AE
export default defineComponent({
name: 'ApiSelect',
components: {
Select,
LoadingOutlined,
},
inheritAttrs: false,
props: {
value: [Array, 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: () => ({}),
},
//分页配置
pageConfig: {
type: Object as PropType<Recordable>,
default: () => ({ isPage: false }),
},
// 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();
// update-begin--author:liusq---date:20250407---for:【QQYUN-11831】ApiSelect 分页下拉方案 #7883
const hasMore = ref(true);
const pagination = ref({
pageNo: 1,
pageSize: 10,
total: 0,
});
const defPageConfig = { isPage: false, pageField: 'pageNo', pageSizeField: 'pageSize', totalField: 'total', listField: 'records' };
// update-end--author:liusq---date:20250407---for:【QQYUN-11831】ApiSelect 分页下拉方案 #7883
// 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:20240823---for:【issues/6999】ApiSelect联动更新字段不生效(代码还原)
watchEffect(() => {
props.immediate && fetch();
});
// update-end--author:liaozhiyang---date:20240823---for:【issues/6999】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;
// update-begin--author:liusq---date:20250407---for:【QQYUN-11831】ApiSelect 分页下拉方案 #7883
if (!props.pageConfig.isPage || pagination.value.pageNo == 1) {
options.value = [];
}
try {
loading.value = true;
let { isPage, pageField, pageSizeField, totalField, listField } = { ...defPageConfig, ...props.pageConfig };
let params = isPage
? { ...props.params, [pageField]: pagination.value.pageNo, [pageSizeField]: pagination.value.pageSize }
: { ...props.params };
// update-end--author:liusq---date:20250407---for:【QQYUN-11831】ApiSelect 分页下拉方案 #7883
const res = await api(params);
if (isPage) {
// update-begin--author:liusq---date:20250407---for:【QQYUN-11831】ApiSelect 分页下拉方案 #7883
options.value = [...options.value, ...res[listField]];
pagination.value.total = res[totalField] || 0;
hasMore.value = res[totalField] ? options.value.length < res[totalField] : res[listField] < pagination.value.pageSize;
// update-end--author:liusq---date:20250407---for:【QQYUN-11831】ApiSelect 分页下拉方案 #7883
} else {
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;
// update-begin--author:liaozhiyang---date:20250407---for:【issues/8037】初始化值单选的值被错误地写入数组值
if (unref(attrs).mode == 'multiple') {
if (value && typeof value === 'string' && value != 'null' && value != 'undefined') {
state.value = value.split(',');
} else if (isNumber(value)) {
state.value = [value];
}
} else {
state.value = value;
}
// update-end--author:liaozhiyang---date:20250407---for:【issues/8037】初始化值单选的值被错误地写入数组值
}
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;
}
// update-begin--author:liusq---date:20250407---for:【QQYUN-11831】ApiSelect 分页下拉方案 #7883
// 滚动加载更多
function handlePopupScroll(e) {
const { scrollTop, scrollHeight, clientHeight } = e.target;
const isNearBottom = scrollHeight - scrollTop <= clientHeight + 20;
if (props.pageConfig.isPage && isNearBottom && hasMore.value && !loading.value) {
pagination.value.pageNo += 1;
fetch();
}
}
// update-end--author:liusq---date:20250407---for:【QQYUN-11831】ApiSelect 分页下拉方案 #7883
return { state, attrs_, attrs, getOptions, loading, t, handleFetch, handleChange, handlePopupScroll };
},
});
</script>
@@ -0,0 +1,88 @@
<template>
<a-tree-select v-bind="getAttrs" @change="handleChange">
<template #[item]="data" v-for="item in Object.keys($slots)">
<slot :name="item" v-bind="data || {}"></slot>
</template>
<template #suffixIcon v-if="loading">
<LoadingOutlined spin />
</template>
</a-tree-select>
</template>
<script lang="ts">
import { computed, defineComponent, watch, ref, onMounted, unref } from 'vue';
import { TreeSelect } from 'ant-design-vue';
import { isArray, isFunction } from '/@/utils/is';
import { get } from 'lodash-es';
import { propTypes } from '/@/utils/propTypes';
import { LoadingOutlined } from '@ant-design/icons-vue';
export default defineComponent({
name: 'ApiTreeSelect',
components: { ATreeSelect: TreeSelect, LoadingOutlined },
props: {
api: { type: Function as PropType<(arg?: Recordable) => Promise<Recordable>> },
params: { type: Object },
immediate: { type: Boolean, default: true },
resultField: propTypes.string.def(''),
},
emits: ['options-change', 'change'],
setup(props, { attrs, emit }) {
const treeData = ref<Recordable[]>([]);
const isFirstLoaded = ref<Boolean>(false);
const loading = ref(false);
const getAttrs = computed(() => {
return {
...(props.api ? { treeData: unref(treeData) } : {}),
...attrs,
};
});
function handleChange(...args) {
emit('change', ...args);
}
watch(
() => props.params,
() => {
//update-begin---author:wangshuai---date:2024-02-28---for:【QQYUN-8346】 ApiTreeSelect组件入参变化时,不及时刷新数据 #1054---
unref(isFirstLoaded) && fetch();
//update-end---author:wangshuai---date:2024-02-28---for:【QQYUN-8346】 ApiTreeSelect组件入参变化时,不及时刷新数据 #1054---
},
{ deep: true }
);
watch(
() => props.immediate,
(v) => {
v && !isFirstLoaded.value && fetch();
}
);
onMounted(() => {
props.immediate && fetch();
});
async function fetch() {
const { api } = props;
if (!api || !isFunction(api)) return;
loading.value = true;
treeData.value = [];
let result;
try {
result = await api(props.params);
} catch (e) {
console.error(e);
}
loading.value = false;
if (!result) return;
if (!isArray(result)) {
result = get(result, props.resultField);
}
treeData.value = (result as Recordable[]) || [];
isFirstLoaded.value = true;
emit('options-change', treeData.value);
}
return { getAttrs, loading, handleChange };
},
});
</script>
@@ -0,0 +1,141 @@
<template>
<a-col v-bind="actionColOpt" v-if="showActionButtonGroup">
<div class="btnArea" 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>
<style lang="less" scoped>
// update-begin--author:liaozhiyang---date:20240617---for:【TV360X-999】在1753px宽度下 流程设计页面查询的展开换行了
.btnArea {
:deep(.ant-form-item-control-input-content) {
display: flex;
align-items: center;
.ant-btn-link {
padding-left: 0;
}
}
}
// update-end--author:liaozhiyang---date:20240617---for:【TV360X-999】在1753px宽度下 流程设计页面查询的展开换行了
</style>
@@ -0,0 +1,543 @@
<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';
import { useLocaleStoreWithOut } from '/@/store/modules/locale';
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】解决禁用状态下触发校验
// update-begin--author:liaozhiyang---date:20240625---for:【TV360X-1511】blur不生效
formName: {
type: String,
default: '',
},
// update-end--author:liaozhiyang---date:20240625---for:【TV360X-1511】blur不生效
},
setup(props, { slots }) {
const { t } = useI18n();
const localeStore = useLocaleStoreWithOut();
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, rules:defRules = [] } = 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)}`;
const getRules = (): ValidationRule[] => {
const dyRules = isFunction(dynamicRules) ? dynamicRules(unref(getValues)) : [];
return [...dyRules, ...defRules];
};
// 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:20240625---for:【TV360X-1511】blur不生效
const findItem = getRules().find((item) => item?.trigger === 'blur');
if (!findItem) {
// update-begin--author:liaozhiyang---date:20240522---for:【TV360X-341】有值之后必填校验不消失
props.validateFields([field]).catch((_) => {});
// update-end--author:liaozhiyang---date:20240625---for:【TV360X-341】有值之后必填校验不消失
}
// update-end--author:liaozhiyang---date:20240625---for:【TV360X-1511】blur不生效
},
// 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支持动态修改
// update-begin--author:sunjianlei---date:20240725---for:【TV360X-972】控件禁用时统一占位内容
// const isCreatePlaceholder = !propsData.disabled && autoSetPlaceHolder;
const isCreatePlaceholder = !!autoSetPlaceHolder;
// update-end----author:sunjianlei---date:20240725---for:【TV360X-972】控件禁用时统一占位内容
// RangePicker place是一个数组
if (isCreatePlaceholder && component !== 'RangePicker' && component) {
//自动设置placeholder
// update-begin--author:liaozhiyang---date:20240724---for:【issues/6908】多语言无刷新切换时,BasicColumn和FormSchema里面的值不能正常切换
let label = isFunction(props.schema.label) ? props.schema.label() : props.schema.label;
if (localeStore.getLocale === 'en' && !(/^\s/.test(label))) {
label = ' ' + label;
}
// update-end--author:liaozhiyang---date:20240724---for:【issues/6908】多语言无刷新切换时,BasicColumn和FormSchema里面的值不能正常切换
propsData.placeholder = unref(getComponentsProps)?.placeholder || createPlaceholderMessage(component) + 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: itemLabel, helpMessage, helpComponentProps, subLabel, labelLength } = props.schema;
// update-begin--author:liaozhiyang---date:20240724---for:【issues/6908】多语言无刷新切换时,BasicColumn和FormSchema里面的值不能正常切换
const label = isFunction(itemLabel) ? itemLabel() : itemLabel;
// update-end--author:liaozhiyang---date:20240724---for:【issues/6908】多语言无刷新切换时,BasicColumn和FormSchema里面的值不能正常切换
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---forissues/1244标识了必填但是必填标识没显示
validateFirst = { true }
// update-end--author:liaozhiyang---date:20240514---forissues/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 formName={props.formName} fieldName={field}>{getContent()}</Middleware>
{/* update-end--author:liaozhiyang---date:20240510---for:【TV360X-719】表单校验不通过项滚动到可视区内 */}
{showSuffix && <span class="suffix">{getSuffix}</span>}
</div>
</Form.Item>
);
}
}
return () => {
const { colProps = {}, colSlot, renderColContent, component } = props.schema;
if (!componentMap.has(component)) {
return null;
}
const { baseColProps = {} } = props.formProps;
// update-begin--author:liaozhiyang---date:20230803---for:【issues-641】调整表格搜索表单的span配置无效
const { getIsMobile } = useAppInject();
let realColProps;
realColProps = { ...baseColProps, ...colProps };
if (colProps['span'] && !unref(getIsMobile)) {
['xs', 'sm', 'md', 'lg', 'xl', 'xxl'].forEach((name) => delete realColProps[name]);
}
// update-end--author:liaozhiyang---date:20230803---for:【issues-641】调整表格搜索表单的span配置无效
const { isIfShow, isShow } = getShow();
const values = unref(getValues);
const getContent = () => {
return colSlot ? getSlot(slots, colSlot, values) : renderColContent ? renderColContent(values) : renderItem();
};
return (
isIfShow && (
<Col {...realColProps} v-show={isShow}>
{getContent()}
</Col>
)
);
};
},
});
</script>
@@ -0,0 +1,24 @@
<template>
<div :id="formItemId" style="flex: 1; width: 100%">
<slot></slot>
</div>
</template>
<script setup>
import { ref } from 'vue';
// update-begin--author:liaozhiyang---date:20240625---for:【TV360X-1511】blur不生效
const formItemId = ref(null);
const props = defineProps(['formName', 'fieldName']);
if (props.formName && props.fieldName) {
formItemId.value = `${props.formName}_${props.fieldName}`;
}
// update-end--author:liaozhiyang---date:20240625---for:【TV360X-1511】blur不生效
</script>
<style lang="less" scoped>
// update-begin--author:liaozhiyang---date:20240617---for:【TV360X-1253】代码生成查询区域和新增组件没撑满
div > :deep(.ant-picker) {
width: 100%;
}
// update-end--author:liaozhiyang---date:20240617---for:【TV360X-1253】代码生成查询区域和新增组件没撑满
</style>
@@ -0,0 +1,57 @@
<!--
* @Description:It is troublesome to implement radio button group in the form. So it is extracted independently as a separate component
-->
<template>
<RadioGroup v-bind="attrs" v-model:value="state" button-style="solid">
<template v-for="item in getOptions" :key="`${item.value}`">
<RadioButton :value="item.value" :disabled="item.disabled">
{{ item.label }}
</RadioButton>
</template>
</RadioGroup>
</template>
<script lang="ts">
import { defineComponent, PropType, computed } from 'vue';
import { Radio } from 'ant-design-vue';
import { isString } from '/@/utils/is';
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
import { useAttrs } from '/@/hooks/core/useAttrs';
type OptionsItem = { label: string; value: string | number | boolean; disabled?: boolean };
type RadioItem = string | OptionsItem;
export default defineComponent({
name: 'RadioButtonGroup',
components: {
RadioGroup: Radio.Group,
RadioButton: Radio.Button,
},
props: {
value: {
type: [String, Number, Boolean] as PropType<string | number | boolean>,
},
options: {
type: Array as PropType<RadioItem[]>,
default: () => [],
},
},
setup(props) {
const attrs = useAttrs();
// Embedded in the form, just use the hook binding to perform form verification
const [state] = useRuleFormItem(props);
// Processing options value
const getOptions = computed((): OptionsItem[] => {
const { options } = props;
if (!options || options?.length === 0) return [];
const isStringArr = options.some((item) => isString(item));
if (!isStringArr) return options as OptionsItem[];
return options.map((item) => ({ label: item, value: item })) as OptionsItem[];
});
return { state, getOptions, attrs };
},
});
</script>
@@ -0,0 +1,221 @@
<template>
<div :class="formDisabled ? 'jeecg-form-container-disabled jeecg-form-detail-effect' : 'jeecg-and-modal-form'">
<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">
// update-begin--author:liaozhiyang---date:20240719---for:【TV360X-1090】表单label超长省略显示
.jeecg-and-modal-form {
:deep(.ant-form-item-label) {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 6px;
> label {
line-height: 32px;
display: inline;
&::after {
margin-right: 0;
}
}
}
}
// update-end--author:liaozhiyang---date:20240719---for:【TV360X-1090】表单label超长省略显示
.jeecg-form-container-disabled {
cursor: not-allowed;
}
.jeecg-form-container-disabled fieldset[disabled] {
-ms-pointer-events: none;
pointer-events: none;
}
.jeecg-form-container-disabled :deep(.ant-select) {
-ms-pointer-events: none;
pointer-events: none;
}
// update-begin--author:liaozhiyang---date:20240605---for:【TV360X-857】online代码生成详情样式调整
// begin antdv 禁用样式
// .jeecg-form-container-disabled :deep(.ant-input-number),
// .jeecg-form-container-disabled :deep(.ant-input),
// .jeecg-form-container-disabled :deep(.ant-input-password),
// .jeecg-form-container-disabled :deep(.ant-select-single .ant-select-selector),
// .jeecg-form-container-disabled :deep(.ant-radio-wrapper .ant-radio-checked .ant-radio-inner),
// .jeecg-form-container-disabled :deep(.ant-switch),
// .jeecg-form-container-disabled :deep(.ant-picker),
// .jeecg-form-container-disabled :deep(.ant-select:not(.ant-select-customize-input) .ant-select-selector),
// .jeecg-form-container-disabled :deep(.ant-input-affix-wrapper),
// .jeecg-form-container-disabled :deep(.tox .tox-toolbar__group),
// .jeecg-form-container-disabled :deep(.tox .tox-edit-area__iframe),
// .jeecg-form-container-disabled :deep(.vditor-toolbar),
// .jeecg-form-container-disabled :deep(.vditor-preview),
// .jeecg-form-container-disabled :deep(.jeecg-tinymce-img-upload) {
// background: rgba(51, 51, 51, 0.04);
// }
// .jeecg-form-container-disabled :deep(.ant-radio-wrapper),
// .jeecg-form-container-disabled :deep(.ant-checkbox-wrapper),
// .jeecg-form-container-disabled :deep(.ant-btn) {
// color: rgba(0, 0, 0, 0.65);
// }
// .jeecg-form-container-disabled :deep(.ant-radio-wrapper .ant-radio-inner:after),
// .jeecg-form-container-disabled :deep(.ant-checkbox-checked .ant-checkbox-inner) {
// background-color: rgba(51, 51, 51, 0.25);
// }
// .jeecg-form-container-disabled :deep(.ant-radio-inner),
// .jeecg-form-container-disabled :deep(.ant-checkbox-inner) {
// border-color: rgba(51, 51, 51, 0.25) !important;
// }
// .jeecg-form-container-disabled :deep(.ant-input-password > .ant-input),
// .jeecg-form-container-disabled :deep(.ant-input-affix-wrapper .ant-input) {
// background: none;
// }
html[data-theme='light'] {
.jeecg-form-detail-effect {
:deep(.ant-select-selector),
:deep(.ant-btn),
:deep(.ant-input),
:deep(.ant-input-affix-wrapper),
:deep(.ant-picker),
:deep(.ant-input-number) {
color: #606266 !important;
}
:deep(.ant-select) {
color: #606266 !important;
}
:deep(.ant-select-selection-item-content),:deep(.ant-select-selection-item),:deep(input) {
color: #606266 !important;
}
:deep(.ant-radio-wrapper),
:deep(.ant-checkbox-wrapper),
:deep(.ant-btn) {
color: rgba(0, 0, 0, 0.65);
}
:deep(.ant-radio-wrapper .ant-radio-inner:after),
:deep(.ant-checkbox-checked .ant-checkbox-inner) {
color: #606266 !important;
}
:deep(.ant-radio-inner),
:deep(.ant-checkbox-inner) {
border-color: rgba(51, 51, 51, 0.25) !important;
background-color: rgba(51, 51, 51, 0.04) !important;
}
:deep(.ant-checkbox-checked .ant-checkbox-inner::after), :deep(.ant-tree-checkbox-checked .ant-tree-checkbox-inner::after){
border-color: rgba(51, 51, 51, 0.25) !important;
}
:deep(.ant-switch) {
background-color: rgba(51, 51, 51, 0.25);
}
:deep(.tox .tox-toolbar__group),
:deep(.tox .tox-edit-area__iframe),
:deep(.vditor-toolbar),
:deep(.vditor-preview),
:deep(.jeecg-tinymce-img-upload) {
background: rgba(51, 51, 51, 0.04);
}
}
}
html[data-theme='dark'] {
.jeecg-form-detail-effect {
:deep(.ant-select-selector),
:deep(.ant-btn),
:deep(.ant-input),
:deep(.ant-input-affix-wrapper),
:deep(.ant-picker),
:deep(.ant-input-number) {
color: rgba(255, 255, 255, 0.25) !important;
//background-color: rgba(255, 255, 255, 0.08) !important;
}
:deep(.ant-select) {
color: rgba(255, 255, 255, 0.25) !important;
}
:deep(.ant-select-selection-item-content),:deep(.ant-select-selection-item),:deep(input) {
color: rgba(255, 255, 255, 0.25) !important;
}
:deep(.ant-radio-wrapper),
:deep(.ant-checkbox-wrapper){
color: rgba(255, 255, 255, 0.25);
}
:deep(.ant-radio-wrapper .ant-radio-inner:after),
:deep(.ant-checkbox-checked .ant-checkbox-inner) {
background-color: rgba(255, 255, 255, 0.08);
}
:deep(.ant-radio-inner),
:deep(.ant-checkbox-inner) {
border-color: #424242 !important;
background-color: rgba(255, 255, 255, 0.08);
}
:deep(.ant-switch) {
background-color: rgba(51, 51, 51, 0.25);
opacity: 0.65;
}
:deep(.tox .tox-toolbar__group),
:deep(.tox .tox-edit-area__iframe),
:deep(.vditor-toolbar),
:deep(.vditor-preview),
:deep(.jeecg-tinymce-img-upload) {
background: rgba(51, 51, 51, 0.04);
}
}
}
// end antdv 禁用样式
// update-begin--author:liaozhiyang---date:20240605---for:【TV360X-857】online代码生成详情样式调整
.jeecg-form-container-disabled :deep(.ant-upload-select) {
cursor: grabbing;
}
.jeecg-form-container-disabled :deep(.ant-upload-list) {
cursor: grabbing;
}
.jeecg-form-container-disabled fieldset[disabled] :deep(.ant-upload-list){
// -ms-pointer-events: auto !important;
// pointer-events: auto !important;
}
.jeecg-form-container-disabled fieldset[disabled] iframe {
-ms-pointer-events: auto !important;
pointer-events: auto !important;
}
.jeecg-form-container-disabled :deep(.ant-upload-list-item-actions .anticon-delete),
.jeecg-form-container-disabled :deep(.ant-upload-list-item .anticon-close) {
display: none;
}
.jeecg-form-container-disabled :deep(.vditor-sv) {
display: none !important;
background: rgba(51, 51, 51, 0.04);
}
</style>
+88
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();
@@ -0,0 +1,167 @@
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行时,也显示展开收起按钮
// update-begin--author:liaozhiyang---date:202401009---for:【issues/7261】表格上方查询项autoAdvancedLine配置没有效果(删除autoAdvancedLine
/*if (itemColSum > BASIC_COL_LEN * 2 && itemColSum <= BASIC_COL_LEN * (unref(getProps).autoAdvancedLine || 3)) {
advanceState.hideAdvanceBtn = false;
// 默认超过 3 行折叠
} else*/
// update-end--author:liaozhiyang---date:202401009---for:【issues/7261】表格上方查询项autoAdvancedLine配置没有效果(删除autoAdvancedLine
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 };
}
@@ -0,0 +1,35 @@
import type { ComputedRef, Ref } from 'vue';
import type { FormSchema, FormActionType, FormProps } from '../types/form';
import { unref, nextTick, watchEffect } from 'vue';
interface UseAutoFocusContext {
getSchema: ComputedRef<FormSchema[]>;
getProps: ComputedRef<FormProps>;
isInitedDefault: Ref<boolean>;
formElRef: Ref<FormActionType>;
}
export async function useAutoFocus({ getSchema, getProps, formElRef, isInitedDefault }: UseAutoFocusContext) {
watchEffect(async () => {
if (unref(isInitedDefault) || !unref(getProps).autoFocusFirstItem) {
return;
}
await nextTick();
const schemas = unref(getSchema);
const formEl = unref(formElRef);
const el = (formEl as any)?.$el as HTMLElement;
if (!formEl || !el || !schemas || schemas.length === 0) {
return;
}
const firstItem = schemas[0];
// Only open when the first form item is input type
if (!firstItem.component.includes('Input')) {
return;
}
const inputEl = el.querySelector('.ant-row:first-child input') as Nullable<HTMLInputElement>;
if (!inputEl) return;
inputEl?.focus();
});
}
@@ -0,0 +1,11 @@
import type { ComponentType } from '../types/index';
import { tryOnUnmounted } from '@vueuse/core';
import { add, del } from '../componentMap';
import type { Component } from 'vue';
export function useComponentRegister(compName: ComponentType, comp: Component) {
add(compName, comp);
tryOnUnmounted(() => {
del(compName);
});
}
+151
View File
@@ -0,0 +1,151 @@
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, getValueTypeBySchema } from '/@/utils';
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);
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 = getValueTypeBySchema(form.getSchemaByField(key)!);
if (valueType === 'string') {
values[key] = values[key].join(',');
}
}
}
//--@updateBy-begin----author:liusq---date:20210916------for:处理区域事件字典信息------
return handleRangeValue(getProps, values);
//--@updateBy-end----author:liusq---date:20210916------for:处理区域事件字典信息------
});
return values;
},
validateFields: async (nameList?: NamePath[], options?: ValidateOptions): Promise<Recordable> => {
const form = await getForm();
return form.validateFields(nameList, options);
},
};
return [register, methods];
}
@@ -0,0 +1,17 @@
import type { InjectionKey } from 'vue';
import { createContext, useContext } from '/@/hooks/core/useContext';
export interface FormContextProps {
resetAction: () => Promise<void>;
submitAction: () => Promise<void>;
}
const key: InjectionKey<FormContextProps> = Symbol();
export function createFormContext(context: FormContextProps) {
return createContext<FormContextProps>(context, key);
}
export function useFormContext() {
return useContext<FormContextProps>(key);
}
@@ -0,0 +1,297 @@
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((_) => {});
}
/**
* 根据字段名获取schema
* @param field
*/
function getSchemaByField(field: string): Nullable<FormSchema> {
if (!isString(field)) {
return null
}
const schemaList: FormSchema[] = unref(getSchema);
const index = schemaList.findIndex((schema) => schema.field === field);
if (index !== -1) {
return cloneDeep(schemaList[index]);
}
return null
}
/**
* @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,
getSchemaByField,
appendSchemaByField,
removeSchemaByFiled,
resetFields,
setFieldsValue,
scrollToField,
};
}
@@ -0,0 +1,59 @@
import { isArray, isFunction, isObject, isString, isNullOrUnDef } from '/@/utils/is';
import { unref } from 'vue';
import type { Ref, ComputedRef } from 'vue';
import type { FormProps, FormSchema } from '../types/form';
import dayjs from "dayjs";
import { set } from 'lodash-es';
import { handleRangeValue } from '/@/components/Form/src/utils/formUtils';
interface UseFormValuesContext {
defaultValueRef: Ref<any>;
getSchema: ComputedRef<FormSchema[]>;
getProps: ComputedRef<FormProps>;
formModel: Recordable;
}
export function useFormValues({ defaultValueRef, getSchema, formModel, getProps }: UseFormValuesContext) {
// Processing form values
function handleFormValues(values: Recordable) {
if (!isObject(values)) {
return {};
}
const res: Recordable = {};
for (const item of Object.entries(values)) {
let [, value] = item;
const [key] = item;
if (!key || (isArray(value) && value.length === 0) || isFunction(value)) {
continue;
}
const transformDateFunc = unref(getProps).transformDateFunc;
if (isObject(value)) {
value = transformDateFunc?.(value);
}
// 判断是否是dayjs实例
if (isArray(value) && dayjs.isDayjs(value[0]) && dayjs.isDayjs(value[1])) {
value = value.map((item) => transformDateFunc?.(item));
}
// Remove spaces
if (isString(value)) {
value = value.trim();
}
set(res, key, value);
}
return handleRangeValue(getProps, res);
}
function initDefault() {
const schemas = unref(getSchema);
const obj: Recordable = {};
schemas.forEach((item) => {
const { defaultValue } = item;
if (!isNullOrUnDef(defaultValue)) {
obj[item.field] = defaultValue;
formModel[item.field] = defaultValue;
}
});
defaultValueRef.value = obj;
}
return { handleFormValues, initDefault };
}
@@ -0,0 +1,47 @@
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;
let col = { ...globalLabelCol, ...labelCol };
const wrapCol = { ...globWrapperCol, ...wrapperCol };
if (width) {
width = isNumber(width) ? `${width}px` : width;
// update-begin--author:liaozhiyang---date:20240717---for:【issues/6865】配置单个的labelWidth不生效
col = {};
// update-end--author:liaozhiyang---date:20240717---for:【issues/6865】配置单个的labelWidth不生效
}
return {
labelCol: { style: { width: width ? width : '100%' }, ...col },
wrapperCol: {
style: { width: layout === 'vertical' ? '100%' : `calc(100% - ${width})` },
...wrapCol,
},
};
});
}
@@ -0,0 +1,123 @@
<template>
<div v-for="(param, index) in dynamicInput.params" :key="index" style="display: flex">
<a-input placeholder="请输入参数key" v-model:value="param.label" style="width: 30%; margin-bottom: 5px" @input="emitChange" />
<a-input placeholder="请输入参数value" v-model:value="param.value" style="width: 30%; margin: 0 0 5px 5px" @input="emitChange" />
<MinusCircleOutlined
v-if="dynamicInput.params.length > min"
class="dynamic-delete-button"
@click="remove(param)"
style="width: 50px"
></MinusCircleOutlined>
</div>
<div>
<a-button type="dashed" style="width: 60%" @click="add">
<PlusOutlined />
新增
</a-button>
</div>
</template>
<script lang="ts">
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons-vue';
import { defineComponent, reactive, ref, UnwrapRef, watchEffect } from 'vue';
import { propTypes } from '/@/utils/propTypes';
import { isEmpty } from '/@/utils/is';
import { tryOnMounted, tryOnUnmounted } from '@vueuse/core';
interface Params {
label: string;
value: string;
}
export default defineComponent({
name: 'JAddInput',
props: {
value: propTypes.string.def(''),
//update-begin---author:wangshuai ---date:20220516 for[VUEN-1043]系统编码规则,最后一个输入框不能删除------------
//自定义删除按钮多少才会显示
min: propTypes.integer.def(1),
//update-end---author:wangshuai ---date:20220516 for[VUEN-1043]系统编码规则,最后一个输入框不能删除--------------
},
emits: ['change', 'update:value'],
setup(props, { emit }) {
//input动态数据
const dynamicInput: UnwrapRef<{ params: Params[] }> = reactive({ params: [] });
//删除Input
const remove = (item: Params) => {
let index = dynamicInput.params.indexOf(item);
if (index !== -1) {
dynamicInput.params.splice(index, 1);
}
emitChange();
};
//新增Input
const add = () => {
dynamicInput.params.push({
label: '',
value: '',
});
emitChange();
};
//监听传入数据value
watchEffect(() => {
initVal();
});
/**
* 初始化数值
*/
function initVal() {
console.log('props.value', props.value);
dynamicInput.params = [];
if (props.value && props.value.indexOf('{') == 0) {
let jsonObj = JSON.parse(props.value);
Object.keys(jsonObj).forEach((key) => {
dynamicInput.params.push({ label: key, value: jsonObj[key] });
});
}
}
/**
* 数值改变
*/
function emitChange() {
let obj = {};
if (dynamicInput.params.length > 0) {
dynamicInput.params.forEach((item) => {
obj[item['label']] = item['value'];
});
}
emit('change', isEmpty(obj) ? '' : JSON.stringify(obj));
emit('update:value', isEmpty(obj) ? '' : JSON.stringify(obj));
}
return {
dynamicInput,
emitChange,
remove,
add,
};
},
components: {
MinusCircleOutlined,
PlusOutlined,
},
});
</script>
<style scoped>
.dynamic-delete-button {
cursor: pointer;
position: relative;
top: 4px;
font-size: 24px;
color: #999;
transition: all 0.3s;
}
.dynamic-delete-button:hover {
color: #777;
}
.dynamic-delete-button[disabled] {
cursor: not-allowed;
opacity: 0.5;
}
</style>
@@ -0,0 +1,149 @@
<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),
// 存储数据 (all时:传递到外面的是数组;province, city, region传递外面的是字符串)
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') {
const regionCode = arr[0];
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;
}
/**
* liaozhiyang
* 2024-06-17
* 【TV360X-1224】省市区组件默认传到外面的值是字符串逗号分隔
* */
const send = (data) => {
let result = data;
if (result) {
if (props.saveCode === 'all') {
// 传递的是数组
} else {
// 传递的是字符串
result = data.join(',');
}
}
emit('change', result);
emit('update:value', 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;
}
send(result);
} else {
send(arr);
}
// update-end--author:liaozhiyang---date:20240607---for:【TV360X-501】省市区换新组件
// emitData.value = args;
//update-begin-author:taoyan date:2022-6-27 for: VUEN-1424【vue3】树表、单表、jvxe、erp 、内嵌子表省市县 选择不上
// 上面改的v-model:value导致选中数据没有显示
// state.value = result;
//update-end-author:taoyan date:2022-6-27 for: VUEN-1424【vue3】树表、单表、jvxe、erp 、内嵌子表省市县 选择不上
}
return {
cascaderValue,
attrs,
regionData,
getOptions,
handleChange,
};
},
});
</script>
@@ -0,0 +1,168 @@
<template>
<a-form-item-rest>
<div class="area-select">
<!--省份-->
<a-select v-model:value="province" @change="proChange" allowClear :disabled="disabled">
<template v-for="item in provinceOptions" :key="`${item.value}`">
<a-select-option :value="item.value">{{ item.label }}</a-select-option>
</template>
</a-select>
<!--城市-->
<a-select v-if="level >= 2" v-model:value="city" @change="cityChange" :disabled="disabled">
<template v-for="item in cityOptions" :key="`${item.value}`">
<a-select-option :value="item.value">{{ item.label }}</a-select-option>
</template>
</a-select>
<!--地区-->
<a-select v-if="level >= 3" v-model:value="area" @change="areaChange" :disabled="disabled">
<template v-for="item in areaOptions" :key="`${item.value}`">
<a-select-option :value="item.value">{{ item.label }}</a-select-option>
</template>
</a-select>
</div>
</a-form-item-rest>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, reactive, watchEffect, computed, unref, watch, onMounted, onUnmounted, toRefs } from 'vue';
import { propTypes } from '/@/utils/propTypes';
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
import { provinceOptions, getDataByCode, getRealCode } from '../../utils/areaDataUtil';
export default defineComponent({
name: 'JAreaSelect',
props: {
value: [Array, String],
province: [String],
city: [String],
area: [String],
level: propTypes.number.def(3),
disabled: propTypes.bool.def(false),
codeField: propTypes.string,
size: propTypes.string,
placeholder: propTypes.string,
formValues: propTypes.any,
allowClear: propTypes.bool.def(false),
getPopupContainer: {
type: Function,
default: (node) => node?.parentNode,
},
},
emits: ['change', 'update:value','update:area','update:city','update:province'],
setup(props, { emit, refs }) {
const emitData = ref<any[]>([]);
//下拉框的选择值
const pca = reactive({
province: '',
city: '',
area: '',
});
//表单值
const [state] = useRuleFormItem(props, 'value', 'change', emitData);
//城市下拉框的选项
const cityOptions = computed(() => {
return pca.province ? getDataByCode(pca.province) : [];
});
//地区下拉框的选项
const areaOptions = computed(() => {
return pca.city ? getDataByCode(pca.city) : [];
});
/**
* 监听props值
*/
watchEffect(() => {
props && initValue();
});
/**
* 监听组件值变化
*/
watch(pca, (newVal) => {
if (!props.value) {
emit('update:province', pca.province);
emit('update:city', pca.city);
emit('update:area', pca.area);
}
});
/**
* 数据初始化
*/
function initValue() {
if (props.value) {
//传参是数组的情况下的处理
if (Array.isArray(props.value)) {
pca.province = props.value[0];
pca.city = props.value[1] ? props.value[1] : '';
pca.area = props.value[2] ? props.value[2] : '';
} else {
//传参是数值
let valueArr = getRealCode(props.value, props.level);
if (valueArr) {
pca.province = valueArr[0];
pca.city = props.level >= 2 && valueArr[1] ? valueArr[1] : '';
pca.area = props.level >= 3 && valueArr[2] ? valueArr[2] : '';
}
}
} else {
//绑定三个数据的情况
pca.province = props.province ? props.province : '';
pca.city = props.city ? props.city : '';
pca.area = props.area ? props.area : '';
}
}
/**
* 省份change事件
*/
function proChange(val) {
pca.city = val && getDataByCode(val)[0]?.value;
pca.area = pca.city && getDataByCode(pca.city)[0]?.value;
state.value = props.level <= 1 ? val : props.level <= 2 ? pca.city : pca.area;
emit('update:value', unref(state));
}
/**
* 城市change事件
*/
function cityChange(val) {
pca.area = val && getDataByCode(val)[0]?.value;
state.value = props.level <= 2 ? val : pca.area;
emit('update:value', unref(state));
}
/**
* 区域change事件
*/
function areaChange(val) {
state.value = val;
emit('update:value', unref(state));
}
return {
...toRefs(pca),
provinceOptions,
cityOptions,
areaOptions,
proChange,
cityChange,
areaChange,
};
},
});
</script>
<style lang="less" scoped>
.area-select {
width: 100%;
/* update-begin-author:taoyan date:2023-2-18 for: QQYUN-4292【online表单】高级查询 2.省市县样式问题 */
/* display: flex;*/
.ant-select {
width: calc(33.3% - 7px)
}
/* update-end-author:taoyan date:2023-2-18 for: QQYUN-4292【online表单】高级查询 2.省市县样式问题 */
.ant-select:not(:first-child) {
margin-left: 10px;
}
}
</style>
@@ -0,0 +1,268 @@
<!--下拉树-->
<template>
<a-tree-select
allowClear
labelInValue
style="width: 100%"
:disabled="disabled"
:dropdownStyle="{ maxHeight: '400px', overflow: 'auto' }"
showCheckedStrategy="SHOW_ALL"
: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();
},
//update-begin---author:wangshuai---date:2024-06-17---for:【TV360X-480】封装表单和原生表单,默认值生成有问题的字段:分类字典树附默认值不生效---
{ deep: true, immediate: true }
//update-end---author:wangshuai---date:2024-06-17---for:【TV360X-480】封装表单和原生表单,默认值生成有问题的字段:分类字典树附默认值不生效---
);
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 {
console.log("props.value:::",props.value)
loadDictItem({ ids: props.value }).then((res) => {
let values = props.value.split(',');
treeValue.value = res.map((item, index) => ({
key: values[index],
value: values[index],
label: item,
}));
if(!props.multiple){
treeValue.value = treeValue.value[0];
}
onLoadTriggleChange(res[0]);
});
}
}
function onLoadTriggleChange(text) {
//只有单选才会触发
if (!props.multiple && props.loadTriggleChange) {
backValue(props.value, text);
}
}
function backValue(value, label) {
let obj = {};
if (props.back) {
obj[props.back] = label;
}
emit('change', value, obj);
emit("update:value",value)
}
function asyncLoadTreeData(treeNode) {
let dataRef = treeNode.dataRef;
return new Promise<void>((resolve) => {
if (treeNode.children && treeNode.children.length > 0) {
resolve();
return;
}
let pid = dataRef.key;
let param = {
pid: pid,
condition: props.condition,
};
loadTreeData(param).then((res) => {
if (res) {
for (let i of res) {
i.value = i.key;
if (i.leaf == false) {
i.isLeaf = false;
} else if (i.leaf == true) {
i.isLeaf = true;
}
}
addChildren(pid, res, treeData.value);
resolve();
}
});
});
}
function addChildren(pid, children, treeArray) {
if (treeArray && treeArray.length > 0) {
for (let item of treeArray) {
if (item.key == pid) {
if (!children || children.length == 0) {
item.isLeaf = true;
} else {
item.children = children;
}
break;
} else {
addChildren(pid, children, item.children);
}
}
}
}
function onChange(value) {
if (!value) {
emit('change', '');
treeValue.value = '';
emit("update:value",'')
} else if (Array.isArray(value)) {
let labels = [];
let values = value.map((item) => {
labels.push(item.label);
return item.value;
});
backValue(values.join(','), labels.join(','));
treeValue.value = value;
} else {
backValue(value.value, value.label);
treeValue.value = value;
}
// update-begin--author:liaozhiyang---date:20240429---for:【QQYUN-9110】组件有值校验没消失
nextTick(() => {
formItemContext?.onFieldChange();
});
// update-end--author:liaozhiyang---date:20240429---for:【QQYUN-9110】组件有值校验没消失
}
function getCurrTreeData() {
return treeData;
}
function validateProp() {
let mycondition = props.condition;
return new Promise((resolve, reject) => {
if (!mycondition) {
resolve();
} else {
try {
let test = JSON.parse(mycondition);
if (typeof test == 'object' && test) {
resolve();
} else {
createMessage.error('组件JTreeSelect-condition传值有误,需要一个json字符串!');
reject();
}
} catch (e) {
createMessage.error('组件JTreeSelect-condition传值有误,需要一个json字符串!');
reject();
}
}
});
}
return {
state,
attrs,
onChange,
treeData,
treeValue,
asyncLoadTreeData,
};
},
});
</script>
@@ -0,0 +1,128 @@
<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 {getDictItems} from "@/api/common/api";
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) {
loadDictOptions()
}
}
// 根据字典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) {
checkOptions.value = res.map((item) => ({value: item.value, label: item.text, color: item.color}));
//console.info('res', dictOptions.value);
} else {
console.error('getDictItems error: : ', res);
checkOptions.value = [];
}
});
}
/**
* change事件
* @param $event
*/
function handleChange($event) {
emit('update:value', $event.join(','));
emit('change', $event.join(','));
}
const getDicColor = (value) => {
if (props.useDicColor) {
const findItem = checkOptions.value.find((item) => item.value == value);
if (findItem) {
return findItem.color;
}
}
return null;
};
return { checkboxArray, checkOptions, attrs, handleChange, getDicColor };
},
});
</script>
<style lang="less" scoped>
// update-begin--author:liaozhiyang---date:20230110---for:【QQYUN-7799】字典组件(原生组件除外)加上颜色配置
.colorText {
display: inline-block;
height: 20px;
line-height: 20px;
padding: 0 6px;
border-radius: 8px;
background-color: red;
color: #fff;
font-size: 12px;
}
// update-begin--author:liaozhiyang---date:20230110---for:【QQYUN-7799】字典组件(原生组件除外)加上颜色配置
</style>
@@ -0,0 +1,382 @@
<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 'codemirror/addon/display/placeholder.js';
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 pre.CodeMirror-placeholder {
color: #cacaca;
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Helvetica Neue,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;
}
}
.CodeMirror-hints.idea,
.CodeMirror-hints.monokai {
z-index: 1001;
max-width: 600px;
max-height: 300px;
}
// update-begin--author:liaozhiyang---date:20240327---for:【QQYUN-8639】暗黑主题适配
html[data-theme='dark'] {
.@{prefix-cls} {
.CodeMirror {
border: 1px solid #3a3a3a;
}
}
}
// update-end--author:liaozhiyang---date:20240327---for:【QQYUN-8639】暗黑主题适配
</style>
@@ -0,0 +1,237 @@
<template>
<a-radio-group v-if="compType === CompTypeEnum.Radio" v-bind="attrs" v-model:value="state" @change="handleChangeRadio">
<template v-for="item in dictOptions" :key="`${item.value}`">
<a-radio :value="item.value">
<span :class="[useDicColor && item.color ? 'colorText' : '']" :style="{ backgroundColor: `${useDicColor && item.color}` }">
{{ item.label }}
</span>
</a-radio>
</template>
</a-radio-group>
<a-radio-group
v-else-if="compType === CompTypeEnum.RadioButton"
v-bind="attrs"
v-model:value="state"
buttonStyle="solid"
@change="handleChangeRadio"
>
<template v-for="item in dictOptions" :key="`${item.value}`">
<a-radio-button :value="item.value">
{{ item.label }}
</a-radio-button>
</template>
</a-radio-group>
<template v-else-if="compType === CompTypeEnum.Select">
<!-- 显示加载效果 -->
<a-input v-if="loadingEcho" readOnly placeholder="加载中…">
<template #prefix>
<LoadingOutlined />
</template>
</a-input>
<a-select
v-else
:placeholder="placeholder"
v-bind="attrs"
v-model:value="state"
:filterOption="handleFilterOption"
:getPopupContainer="getPopupContainer"
:style="style"
@change="handleChange"
>
<a-select-option v-if="showChooseOption" :value="null">请选择</a-select-option>
<template v-for="item in dictOptions" :key="`${item.value}`">
<a-select-option :value="item.value">
<span
:class="[useDicColor && item.color ? 'colorText' : '']"
:style="{ backgroundColor: `${useDicColor && item.color}` }"
:title="item.label"
>
{{ item.label }}
</span>
</a-select-option>
</template>
</a-select>
</template>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, reactive, watchEffect, computed, unref, watch, onMounted, nextTick } from 'vue';
import { propTypes } from '/@/utils/propTypes';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { initDictOptions } from '/@/utils/dict';
import { get, omit } from 'lodash-es';
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
import { CompTypeEnum } from '/@/enums/CompTypeEnum';
import { LoadingOutlined } from '@ant-design/icons-vue';
export default defineComponent({
name: 'JDictSelectTag',
inheritAttrs: false,
components: { LoadingOutlined },
props: {
value: propTypes.oneOfType([propTypes.string, propTypes.number, propTypes.array]),
dictCode: propTypes.string,
type: propTypes.string,
placeholder: propTypes.string,
stringToNumber: propTypes.bool,
useDicColor: propTypes.bool.def(false),
getPopupContainer: {
type: Function,
default: (node) => node?.parentNode,
},
// 是否显示【请选择】选项
showChooseOption: propTypes.bool.def(true),
// 下拉项-online使用
options: {
type: Array,
default: [],
required: false,
},
style: propTypes.any,
},
emits: ['options-change', 'change','update:value'],
setup(props, { emit, refs }) {
const dictOptions = ref<any[]>([]);
const attrs = useAttrs();
const [state, , , formItemContext] = useRuleFormItem(props, 'value', 'change');
const getBindValue = Object.assign({}, unref(props), unref(attrs));
// 是否正在加载回显数据
const loadingEcho = ref<boolean>(false);
// 是否是首次加载回显,只有首次加载,才会显示 loading
let isFirstLoadEcho = true;
//组件类型
const compType = computed(() => {
return !props.type || props.type === 'list' ? 'select' : props.type;
});
/**
* 监听字典code
*/
watchEffect(() => {
if (props.dictCode) {
loadingEcho.value = isFirstLoadEcho;
isFirstLoadEcho = false;
initDictData().finally(() => {
loadingEcho.value = isFirstLoadEcho;
});
}
//update-begin-author:taoyan date: 如果没有提供dictCode 可以走options的配置--
if (!props.dictCode) {
dictOptions.value = props.options;
}
//update-end-author:taoyan date: 如果没有提供dictCode 可以走options的配置--
});
//update-begin-author:taoyan date:20220404 for: 使用useRuleFormItem定义的value,会有一个问题,如果不是操作设置的值而是代码设置的控件值而不能触发change事件
// 此处添加空值的change事件,即当组件调用地代码设置value为''也能触发change事件
watch(
() => props.value,
() => {
if (props.value === '') {
emit('change', '');
nextTick(() => formItemContext.onFieldChange());
}
}
);
//update-end-author:taoyan date:20220404 for: 使用useRuleFormItem定义的value,会有一个问题,如果不是操作设置的值而是代码设置的控件值而不能触发change事件
async function initDictData() {
let { dictCode, stringToNumber } = props;
//根据字典Code, 初始化字典数组
const dictData = await initDictOptions(dictCode);
dictOptions.value = dictData.reduce((prev, next) => {
if (next) {
const value = next['value'];
prev.push({
label: next['text'] || next['label'],
value: stringToNumber ? +value : value,
color: next['color'],
...omit(next, ['text', 'value', 'color']),
});
}
return prev;
}, []);
}
function handleChange(e) {
const { mode } = unref<Recordable>(getBindValue);
let changeValue:any;
// 兼容多选模式
//update-begin---author:wangshuai ---date:20230216 for[QQYUN-4290]公文发文:选择机关代字报错,是因为值改变触发了change事件三次,导致数据发生改变------------
//采用一个值,不然的话state值变换触发多个change
if (mode === 'multiple') {
changeValue = e?.target?.value ?? e;
// 过滤掉空值
if (changeValue == null || changeValue === '') {
changeValue = [];
}
if (Array.isArray(changeValue)) {
changeValue = changeValue.filter((item) => item != null && item !== '');
}
} else {
changeValue = e?.target?.value ?? e;
}
state.value = changeValue;
//update-begin---author:wangshuai ---date:20230403 for:【issues/4507】JDictSelectTag组件使用时,浏览器给出警告提示:Expected Function, got Array------------
emit('update:value',changeValue)
//update-end---author:wangshuai ---date:20230403 for:【issues/4507】JDictSelectTag组件使用时,浏览器给出警告提示:Expected Function, got Array述------------
//update-end---author:wangshuai ---date:20230216 for[QQYUN-4290]公文发文:选择机关代字报错,是因为值改变触发了change事件三次,导致数据发生改变------------
// nextTick(() => formItemContext.onFieldChange());
}
/** 单选radio的值变化事件 */
function handleChangeRadio(e) {
state.value = e?.target?.value ?? e;
//update-begin---author:wangshuai ---date:20230504 for:【issues/506】JDictSelectTag 组件 type="radio" 没有返回值------------
emit('update:value',e?.target?.value ?? e)
//update-end---author:wangshuai ---date:20230504 for:【issues/506】JDictSelectTag 组件 type="radio" 没有返回值------------
}
/** 用于搜索下拉框中的内容 */
function handleFilterOption(input, option) {
// update-begin--author:liaozhiyang---date:20230914---for:【QQYUN-6514】 配置的时候,Y轴不能输入多个字段了,控制台报错
if (typeof option.children === 'function') {
// 在 label 中搜索
let labelIf = option.children()[0]?.children.toLowerCase().indexOf(input.toLowerCase()) >= 0;
if (labelIf) {
return true;
}
}
// update-end--author:liaozhiyang---date:20230914---for:【QQYUN-6514】 配置的时候,Y轴不能输入多个字段了,控制台报错
// 在 value 中搜索
return (option.value || '').toString().toLowerCase().indexOf(input.toLowerCase()) >= 0;
}
return {
state,
compType,
attrs,
loadingEcho,
getBindValue,
dictOptions,
CompTypeEnum,
handleChange,
handleChangeRadio,
handleFilterOption,
};
},
});
</script>
<style scoped lang="less">
// update-begin--author:liaozhiyang---date:20230110---for:【QQYUN-7799】字典组件(原生组件除外)加上颜色配置
.colorText {
display: inline-block;
height: 20px;
line-height: 20px;
padding: 0 6px;
border-radius: 8px;
background-color: red;
color: #fff;
font-size: 12px;
}
// update-begin--author:liaozhiyang---date:20230110---for:【QQYUN-7799】字典组件(原生组件除外)加上颜色配置
</style>
@@ -0,0 +1,321 @@
<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 />
<!-- 执行时间预览 -->
<div style="overflow: hidden">
<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>
</div>
</template>
<script lang="ts" setup>
import { computed, reactive, ref, watch, provide } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign';
import CronParser from 'cron-parser';
import SecondUI from './tabs/SecondUI.vue';
import MinuteUI from './tabs/MinuteUI.vue';
import HourUI from './tabs/HourUI.vue';
import DayUI from './tabs/DayUI.vue';
import MonthUI from './tabs/MonthUI.vue';
import WeekUI from './tabs/WeekUI.vue';
import YearUI from './tabs/YearUI.vue';
import { cronEmits, cronProps } from './easy.cron.data';
import { dateFormat, simpleDebounce } from '/@/utils/common/compUtils';
const { prefixCls } = useDesign('easy-cron-inner');
provide('prefixCls', prefixCls);
const emit = defineEmits([...cronEmits]);
const props = defineProps({ ...cronProps });
const activeKey = ref(props.hideSecond ? 'minute' : 'second');
const second = ref('*');
const minute = ref('*');
const hour = ref('*');
const day = ref('*');
const month = ref('*');
const week = ref('?');
const year = ref('*');
const inputValues = reactive({
second: '',
minute: '',
hour: '',
day: '',
month: '',
week: '',
year: '',
cron: '',
});
const preTimeList = ref('执行预览,会忽略年份参数。');
// cron表达式
const cronValueInner = computed(() => {
let result: string[] = [];
if (!props.hideSecond) {
result.push(second.value ? second.value : '*');
}
result.push(minute.value ? minute.value : '*');
result.push(hour.value ? hour.value : '*');
result.push(day.value ? day.value : '*');
result.push(month.value ? month.value : '*');
result.push(week.value ? week.value : '?');
if (!props.hideYear && !props.hideSecond) result.push(year.value ? year.value : '*');
return result.join(' ');
});
// 不含年
const cronValueNoYear = computed(() => {
const v = cronValueInner.value;
if (props.hideYear || props.hideSecond) return v;
const vs = v.split(' ');
if (vs.length >= 6) {
// 转成 Quartz 的规则
vs[5] = convertWeekToQuartz(vs[5]);
}
return vs.slice(0, vs.length - 1).join(' ');
});
const calTriggerList = simpleDebounce(calTriggerListInner, 500);
watch(
() => props.value,
(newVal) => {
if (newVal === cronValueInner.value) {
return;
}
formatValue();
}
);
watch(cronValueInner, (newValue) => {
calTriggerList();
emitValue(newValue);
assignInput();
});
// watch(minute, () => {
// if (second.value === '*') {
// second.value = '0'
// }
// })
// watch(hour, () => {
// if (minute.value === '*') {
// minute.value = '0'
// }
// })
// watch(day, () => {
// if (day.value !== '?' && hour.value === '*') {
// hour.value = '0'
// }
// })
// watch(week, () => {
// if (week.value !== '?' && hour.value === '*') {
// hour.value = '0'
// }
// })
// watch(month, () => {
// if (day.value === '?' && week.value === '*') {
// week.value = '1'
// } else if (week.value === '?' && day.value === '*') {
// day.value = '1'
// }
// })
// watch(year, () => {
// if (month.value === '*') {
// month.value = '1'
// }
// })
assignInput();
formatValue();
calTriggerListInner();
function assignInput() {
inputValues.second = second.value;
inputValues.minute = minute.value;
inputValues.hour = hour.value;
inputValues.day = day.value;
inputValues.month = month.value;
inputValues.week = week.value;
inputValues.year = year.value;
inputValues.cron = cronValueInner.value;
}
function formatValue() {
if (!props.value) return;
const values = props.value.split(' ').filter((item) => !!item);
if (!values || values.length <= 0) return;
let i = 0;
if (!props.hideSecond) second.value = values[i++];
if (values.length > i) minute.value = values[i++];
if (values.length > i) hour.value = values[i++];
if (values.length > i) day.value = values[i++];
if (values.length > i) month.value = values[i++];
if (values.length > i) week.value = values[i++];
if (values.length > i) year.value = values[i];
assignInput();
}
// Quartz 的规则:
// 1 = 周日,2 = 周一,3 = 周二,4 = 周三,5 = 周四,6 = 周五,7 = 周六
function convertWeekToQuartz(week: string) {
let convert = (v: string) => {
if (v === '0') {
return '1';
}
if (v === '1') {
return '0';
}
return (Number.parseInt(v) - 1).toString();
};
// 匹配示例 1-7 or 1/7
let patten1 = /^([0-7])([-/])([0-7])$/;
// 匹配示例 1,4,7
let patten2 = /^([0-7])(,[0-7])+$/;
if (/^[0-7]$/.test(week)) {
return convert(week);
} else if (patten1.test(week)) {
return week.replace(patten1, ($0, before, separator, after) => {
if (separator === '/') {
return convert(before) + separator + after;
} else {
return convert(before) + separator + convert(after);
}
});
} else if (patten2.test(week)) {
return week
.split(',')
.map((v) => convert(v))
.join(',');
}
return week;
}
function calTriggerListInner() {
// 设置了回调函数
if (props.remote) {
props.remote(cronValueInner.value, +new Date(), (v) => {
preTimeList.value = v;
});
return;
}
const format = 'yyyy-MM-dd hh:mm:ss';
const options = {
currentDate: dateFormat(new Date(), format),
};
const iter = CronParser.parseExpression(cronValueNoYear.value, options);
const result: string[] = [];
for (let i = 1; i <= 10; i++) {
result.push(dateFormat(new Date(iter.next() as any), format));
}
preTimeList.value = result.length > 0 ? result.join('\n') : '无执行时间';
}
function onInputBlur() {
second.value = inputValues.second;
minute.value = inputValues.minute;
hour.value = inputValues.hour;
day.value = inputValues.day;
month.value = inputValues.month;
week.value = inputValues.week;
year.value = inputValues.year;
}
function onInputCronBlur(event) {
emitValue(event.target.value);
}
function emitValue(value) {
emit('change', value);
emit('update:value', value);
}
</script>
<style lang="less">
@import 'easy.cron.inner';
</style>
@@ -0,0 +1,63 @@
<template>
<div :class="`${prefixCls}`">
<a-input :placeholder="placeholder" v-model:value="editCronValue" :disabled="disabled">
<template #addonAfter>
<a class="open-btn" :disabled="disabled ? 'disabled' : null" @click="showConfigModal">
<Icon icon="ant-design:setting-outlined" />
<span>选择</span>
</a>
</template>
</a-input>
<EasyCronModal
@register="registerModal"
v-model:value="editCronValue"
:exeStartTime="exeStartTime"
:hideYear="hideYear"
:remote="remote"
:hideSecond="hideSecond"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { useModal } from '/@/components/Modal';
import { propTypes } from '/@/utils/propTypes';
import Icon from '/@/components/Icon/src/Icon.vue';
import EasyCronModal from './EasyCronModal.vue';
import { cronEmits, cronProps } from './easy.cron.data';
const { prefixCls } = useDesign('easy-cron-input');
const emit = defineEmits([...cronEmits]);
const props = defineProps({
...cronProps,
placeholder: propTypes.string.def('请输入cron表达式'),
exeStartTime: propTypes.oneOfType([propTypes.number, propTypes.string, propTypes.object]).def(0),
});
const [registerModal, { openModal }] = useModal();
const editCronValue = ref(props.value);
watch(
() => props.value,
(newVal) => {
if (newVal !== editCronValue.value) {
editCronValue.value = newVal;
}
}
);
watch(editCronValue, (newVal) => {
emit('change', newVal);
emit('update:value', newVal);
});
function showConfigModal() {
if (!props.disabled) {
openModal();
}
}
</script>
<style lang="less">
@import 'easy.cron.input';
</style>
@@ -0,0 +1,28 @@
<template>
<BasicModal @register="registerModal" title="Cron表达式" width="1000px" @ok="onOk">
<EasyCron v-bind="attrs" />
</BasicModal>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { BasicModal, useModalInner } from '/@/components/Modal';
import EasyCron from './EasyCronInner.vue';
export default defineComponent({
name: 'EasyCronModal',
inheritAttrs: false,
components: { BasicModal, EasyCron },
setup() {
const attrs = useAttrs();
const [registerModal, { closeModal }] = useModalInner();
function onOk() {
closeModal();
}
return { attrs, registerModal, onOk };
},
});
</script>
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 知行合一
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,10 @@
import { propTypes } from '/@/utils/propTypes';
export const cronEmits = ['change', 'update:value'];
export const cronProps = {
value: propTypes.string.def(''),
disabled: propTypes.bool.def(false),
hideSecond: propTypes.bool.def(false),
hideYear: propTypes.bool.def(false),
remote: propTypes.func,
};
@@ -0,0 +1,59 @@
//noinspection LessUnresolvedVariable
@prefix-cls: ~'@{namespace}-easy-cron-inner';
.@{prefix-cls} {
.content {
.ant-checkbox-wrapper + .ant-checkbox-wrapper {
margin-left: 0;
}
}
&-config-list {
text-align: left;
margin: 0 10px 10px 10px;
.item {
margin-top: 5px;
font-size: 14px;
span {
padding: 0 2px;
}
}
.choice {
padding: 5px 8px;
}
.w60 {
width: 60px;
min-width: 60px;
}
.w80 {
width: 80px;
min-width: 80px;
}
.list {
margin: 0 20px;
}
.list-check-item {
padding: 1px 3px;
width: 4em;
}
.list-cn .list-check-item {
width: 5em;
}
.tip-info {
color: #999;
}
}
.allow-click {
cursor: pointer;
}
}
@@ -0,0 +1,14 @@
//noinspection LessUnresolvedVariable
@prefix-cls: ~'@{namespace}-easy-cron-input';
.@{prefix-cls} {
a.open-btn {
cursor: pointer;
.app-iconify {
position: relative;
top: 1px;
right: 2px;
}
}
}
@@ -0,0 +1,6 @@
// 原开源项目地址:https://gitee.com/toktok/easy-cron
export { default as JEasyCron } from './EasyCronInput.vue';
export { default as JEasyCronInner } from './EasyCronInner.vue';
export { default as JEasyCronModal } from './EasyCronModal.vue';
export { default as JCronValidator } from './validator';
@@ -0,0 +1,94 @@
<template>
<div :class="`${prefixCls}-config-list`">
<a-radio-group v-model:value="type">
<div class="item">
<a-radio :value="TypeEnum.unset" v-bind="beforeRadioAttrs">不设置</a-radio>
<span class="tip-info">日和周只能设置其中之一</span>
</div>
<div class="item">
<a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每日</a-radio>
</div>
<div class="item">
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
<span> </span>
<InputNumber v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
<span> </span>
<InputNumber v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
<span> </span>
</div>
<div class="item">
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
<span> </span>
<InputNumber v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
<span> 日开始间隔 </span>
<InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
<span> </span>
</div>
<!-- 工作日暂不支持会报错先隐藏了 -->
<!-- <div class="item">-->
<!-- <a-radio :value="TypeEnum.work" v-bind="beforeRadioAttrs">工作日</a-radio>-->
<!-- <span> 本月 </span>-->
<!-- <InputNumber v-model:value="valueWork" v-bind="typeWorkAttrs" />-->
<!-- <span> 最近的工作日 </span>-->
<!-- </div>-->
<div class="item">
<a-radio :value="TypeEnum.last" v-bind="beforeRadioAttrs">最后一日</a-radio>
</div>
<div class="item">
<a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
<div class="list">
<a-checkbox-group v-model:value="valueList">
<template v-for="i in specifyRange" :key="i">
<a-checkbox :value="i" v-bind="typeSpecifyAttrs">{{ i }}</a-checkbox>
</template>
</a-checkbox-group>
</div>
</div>
</a-radio-group>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, watch } from 'vue';
import { InputNumber } from 'ant-design-vue';
import { TypeEnum, useTabEmits, useTabProps, useTabSetup } from './useTabMixin';
export default defineComponent({
name: 'DayUI',
components: { InputNumber },
props: useTabProps({
defaultValue: '*',
props: {
week: { type: String, default: '?' },
},
}),
emits: useTabEmits(),
setup(props, context) {
const disabledChoice = computed(() => {
return (props.week && props.week !== '?') || props.disabled;
});
const setup = useTabSetup(props, context, {
defaultValue: '*',
valueWork: 1,
minValue: 1,
maxValue: 31,
valueRange: { start: 1, end: 31 },
valueLoop: { start: 1, interval: 1 },
disabled: disabledChoice,
});
const typeWorkAttrs = computed(() => ({
disabled: setup.type.value !== TypeEnum.work || props.disabled || disabledChoice.value,
...setup.inputNumberAttrs.value,
}));
watch(
() => props.week,
() => {
setup.updateValue(disabledChoice.value ? '?' : setup.computeValue.value);
}
);
return { ...setup, typeWorkAttrs };
},
});
</script>
@@ -0,0 +1,59 @@
<template>
<div :class="`${prefixCls}-config-list`">
<a-radio-group v-model:value="type">
<div class="item">
<a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每时</a-radio>
</div>
<div class="item">
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
<span> </span>
<InputNumber v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
<span> </span>
<InputNumber v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
<span> </span>
</div>
<div class="item">
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
<span> </span>
<InputNumber v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
<span> 时开始间隔 </span>
<InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
<span> </span>
</div>
<div class="item">
<a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
<div class="list">
<a-checkbox-group v-model:value="valueList">
<template v-for="i in specifyRange" :key="i">
<a-checkbox :value="i" v-bind="typeSpecifyAttrs">{{ i }}</a-checkbox>
</template>
</a-checkbox-group>
</div>
</div>
</a-radio-group>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { InputNumber } from 'ant-design-vue';
import { useTabProps, useTabEmits, useTabSetup } from './useTabMixin';
export default defineComponent({
name: 'HourUI',
components: { InputNumber },
props: useTabProps({
defaultValue: '*',
}),
emits: useTabEmits(),
setup(props, context) {
return useTabSetup(props, context, {
defaultValue: '*',
minValue: 0,
maxValue: 23,
valueRange: { start: 0, end: 23 },
valueLoop: { start: 0, interval: 1 },
});
},
});
</script>
@@ -0,0 +1,59 @@
<template>
<div :class="`${prefixCls}-config-list`">
<a-radio-group v-model:value="type">
<div class="item">
<a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每分</a-radio>
</div>
<div class="item">
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
<span> </span>
<InputNumber v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
<span> </span>
<InputNumber v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
<span> </span>
</div>
<div class="item">
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
<span> </span>
<InputNumber v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
<span> 分开始间隔 </span>
<InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
<span> </span>
</div>
<div class="item">
<a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
<div class="list">
<a-checkbox-group v-model:value="valueList">
<template v-for="i in specifyRange" :key="i">
<a-checkbox :value="i" v-bind="typeSpecifyAttrs">{{ i }}</a-checkbox>
</template>
</a-checkbox-group>
</div>
</div>
</a-radio-group>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { InputNumber } from 'ant-design-vue';
import { useTabProps, useTabEmits, useTabSetup } from './useTabMixin';
export default defineComponent({
name: 'MinuteUI',
components: { InputNumber },
props: useTabProps({
defaultValue: '*',
}),
emits: useTabEmits(),
setup(props, context) {
return useTabSetup(props, context, {
defaultValue: '*',
minValue: 0,
maxValue: 59,
valueRange: { start: 0, end: 59 },
valueLoop: { start: 0, interval: 1 },
});
},
});
</script>
@@ -0,0 +1,59 @@
<template>
<div :class="`${prefixCls}-config-list`">
<a-radio-group v-model:value="type">
<div class="item">
<a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每月</a-radio>
</div>
<div class="item">
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
<span> </span>
<InputNumber v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
<span> </span>
<InputNumber v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
<span> </span>
</div>
<div class="item">
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
<span> </span>
<InputNumber v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
<span> 月开始间隔 </span>
<InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
<span> </span>
</div>
<div class="item">
<a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
<div class="list">
<a-checkbox-group v-model:value="valueList">
<template v-for="i in specifyRange" :key="i">
<a-checkbox :value="i" v-bind="typeSpecifyAttrs">{{ i }}</a-checkbox>
</template>
</a-checkbox-group>
</div>
</div>
</a-radio-group>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { InputNumber } from 'ant-design-vue';
import { useTabProps, useTabEmits, useTabSetup } from './useTabMixin';
export default defineComponent({
name: 'MonthUI',
components: { InputNumber },
props: useTabProps({
defaultValue: '*',
}),
emits: useTabEmits(),
setup(props, context) {
return useTabSetup(props, context, {
defaultValue: '*',
minValue: 1,
maxValue: 12,
valueRange: { start: 1, end: 12 },
valueLoop: { start: 1, interval: 1 },
});
},
});
</script>
@@ -0,0 +1,59 @@
<template>
<div :class="`${prefixCls}-config-list`">
<a-radio-group v-model:value="type">
<div class="item">
<a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每秒</a-radio>
</div>
<div class="item">
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
<span> </span>
<InputNumber v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
<span> </span>
<InputNumber v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
<span> </span>
</div>
<div class="item">
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
<span> </span>
<InputNumber v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
<span> 秒开始间隔 </span>
<InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
<span> </span>
</div>
<div class="item">
<a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
<div class="list">
<a-checkbox-group v-model:value="valueList">
<template v-for="i in specifyRange" :key="i">
<a-checkbox :value="i" v-bind="typeSpecifyAttrs">{{ i }}</a-checkbox>
</template>
</a-checkbox-group>
</div>
</div>
</a-radio-group>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { InputNumber } from 'ant-design-vue';
import { useTabProps, useTabEmits, useTabSetup } from './useTabMixin';
export default defineComponent({
name: 'SecondUI',
components: { InputNumber },
props: useTabProps({
defaultValue: '*',
}),
emits: useTabEmits(),
setup(props, context) {
return useTabSetup(props, context, {
defaultValue: '*',
minValue: 0,
maxValue: 59,
valueRange: { start: 0, end: 59 },
valueLoop: { start: 0, interval: 1 },
});
},
});
</script>
@@ -0,0 +1,125 @@
<template>
<div :class="`${prefixCls}-config-list`">
<a-radio-group v-model:value="type">
<div class="item">
<a-radio :value="TypeEnum.unset" v-bind="beforeRadioAttrs">不设置</a-radio>
<span class="tip-info">日和周只能设置其中之一</span>
</div>
<div class="item">
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
<span> </span>
<a-select v-model:value="valueRange.start" :options="weekOptions" v-bind="typeRangeSelectAttrs" />
<span> </span>
<a-select v-model:value="valueRange.end" :options="weekOptions" v-bind="typeRangeSelectAttrs" />
</div>
<div class="item">
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
<span> </span>
<a-select v-model:value="valueLoop.start" :options="weekOptions" v-bind="typeLoopSelectAttrs" />
<span> 开始间隔 </span>
<InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
<span> </span>
</div>
<div class="item">
<a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
<div class="list list-cn">
<a-checkbox-group v-model:value="valueList">
<template v-for="opt in weekOptions" :key="i">
<a-checkbox :value="opt.value" v-bind="typeSpecifyAttrs">{{ opt.label }}</a-checkbox>
</template>
</a-checkbox-group>
</div>
</div>
</a-radio-group>
</div>
</template>
<script lang="ts">
import { computed, watch, defineComponent } from 'vue';
import { InputNumber } from 'ant-design-vue';
import { useTabProps, useTabEmits, useTabSetup, TypeEnum } from './useTabMixin';
const WEEK_MAP_EN = {
'1': 'SUN',
'2': 'MON',
'3': 'TUE',
'4': 'WED',
'5': 'THU',
'6': 'FRI',
'7': 'SAT',
};
const WEEK_MAP_CN = {
'1': '周日',
'2': '周一',
'3': '周二',
'4': '周三',
'5': '周四',
'6': '周五',
'7': '周六',
};
export default defineComponent({
name: 'WeekUI',
components: { InputNumber },
props: useTabProps({
defaultValue: '?',
props: {
day: { type: String, default: '*' },
},
}),
emits: useTabEmits(),
setup(props, context) {
const disabledChoice = computed(() => {
return (props.day && props.day !== '?') || props.disabled;
});
const setup = useTabSetup(props, context, {
defaultType: TypeEnum.unset,
defaultValue: '?',
minValue: 1,
maxValue: 7,
// 0,7表示周日 1表示周一
valueRange: { start: 1, end: 7 },
valueLoop: { start: 2, interval: 1 },
disabled: disabledChoice,
});
const weekOptions = computed(() => {
let options: { label: string; value: number }[] = [];
for (let weekKey of Object.keys(WEEK_MAP_CN)) {
let weekName: string = WEEK_MAP_CN[weekKey];
options.push({
value: Number.parseInt(weekKey),
label: weekName,
});
}
return options;
});
const typeRangeSelectAttrs = computed(() => ({
class: ['w80'],
disabled: setup.typeRangeAttrs.value.disabled,
}));
const typeLoopSelectAttrs = computed(() => ({
class: ['w80'],
disabled: setup.typeLoopAttrs.value.disabled,
}));
watch(
() => props.day,
() => {
setup.updateValue(disabledChoice.value ? '?' : setup.computeValue.value);
}
);
return {
...setup,
weekOptions,
typeLoopSelectAttrs,
typeRangeSelectAttrs,
WEEK_MAP_CN,
WEEK_MAP_EN,
};
},
});
</script>
@@ -0,0 +1,49 @@
<template>
<div :class="`${prefixCls}-config-list`">
<a-radio-group v-model:value="type">
<div class="item">
<a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每年</a-radio>
</div>
<div class="item">
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
<span> </span>
<InputNumber class="w80" v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
<span> </span>
<InputNumber class="w80" v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
<span> </span>
</div>
<div class="item">
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
<span> </span>
<InputNumber class="w80" v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
<span> 年开始间隔 </span>
<InputNumber class="w80" v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
<span> </span>
</div>
</a-radio-group>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { InputNumber } from 'ant-design-vue';
import { useTabProps, useTabEmits, useTabSetup } from './useTabMixin';
export default defineComponent({
name: 'YearUI',
components: { InputNumber },
props: useTabProps({
defaultValue: '*',
}),
emits: useTabEmits(),
setup(props, context) {
const nowYear = new Date().getFullYear();
return useTabSetup(props, context, {
defaultValue: '*',
minValue: 0,
valueRange: { start: nowYear, end: nowYear + 100 },
valueLoop: { start: nowYear, interval: 1 },
});
},
});
</script>
@@ -0,0 +1,199 @@
// 主要用于日和星期的互斥使用
import { computed, inject, reactive, ref, unref, watch } from 'vue';
import { propTypes } from '/@/utils/propTypes';
export enum TypeEnum {
unset = 'UNSET',
every = 'EVERY',
range = 'RANGE',
loop = 'LOOP',
work = 'WORK',
last = 'LAST',
specify = 'SPECIFY',
}
// use 公共 props
export function useTabProps(options) {
const defaultValue = options?.defaultValue ?? '?';
return {
value: propTypes.string.def(defaultValue),
disabled: propTypes.bool.def(false),
...options?.props,
};
}
// use 公共 emits
export function useTabEmits() {
return ['change', 'update:value'];
}
// use 公共 setup
export function useTabSetup(props, context, options) {
const { emit } = context;
const prefixCls = inject('prefixCls');
const defaultValue = ref(options?.defaultValue ?? '?');
// 类型
const type = ref(options.defaultType ?? TypeEnum.every);
const valueList = ref<any[]>([]);
// 对于不同的类型,所定义的值也有所不同
const valueRange = reactive(options.valueRange);
const valueLoop = reactive(options.valueLoop);
const valueWeek = reactive(options.valueWeek);
const valueWork = ref(options.valueWork);
const maxValue = ref(options.maxValue);
const minValue = ref(options.minValue);
// 根据不同的类型计算出的value
const computeValue = computed(() => {
let valueArray: any[] = [];
switch (type.value) {
case TypeEnum.unset:
valueArray.push('?');
break;
case TypeEnum.every:
valueArray.push('*');
break;
case TypeEnum.range:
valueArray.push(`${valueRange.start}-${valueRange.end}`);
break;
case TypeEnum.loop:
valueArray.push(`${valueLoop.start}/${valueLoop.interval}`);
break;
case TypeEnum.work:
valueArray.push(`${valueWork.value}W`);
break;
case TypeEnum.last:
valueArray.push('L');
break;
case TypeEnum.specify:
if (valueList.value.length === 0) {
valueList.value.push(minValue.value);
}
valueArray.push(valueList.value.join(','));
break;
default:
valueArray.push(defaultValue.value);
break;
}
return valueArray.length > 0 ? valueArray.join('') : defaultValue.value;
});
// 指定值范围区间,介于最小值和最大值之间
const specifyRange = computed(() => {
let range: number[] = [];
if (maxValue.value != null) {
for (let i = minValue.value; i <= maxValue.value; i++) {
range.push(i);
}
}
return range;
});
watch(
() => props.value,
(val) => {
if (val !== computeValue.value) {
parseValue(val);
}
},
{ immediate: true }
);
watch(computeValue, (v) => updateValue(v));
function updateValue(value) {
emit('change', value);
emit('update:value', value);
}
/**
* parseValue
* @param value
*/
function parseValue(value) {
if (value === computeValue.value) {
return;
}
try {
if (!value || value === defaultValue.value) {
type.value = TypeEnum.every;
} else if (value.indexOf('?') >= 0) {
type.value = TypeEnum.unset;
} else if (value.indexOf('-') >= 0) {
type.value = TypeEnum.range;
const values = value.split('-');
if (values.length >= 2) {
valueRange.start = parseInt(values[0]);
valueRange.end = parseInt(values[1]);
}
} else if (value.indexOf('/') >= 0) {
type.value = TypeEnum.loop;
const values = value.split('/');
if (values.length >= 2) {
valueLoop.start = value[0] === '*' ? 0 : parseInt(values[0]);
valueLoop.interval = parseInt(values[1]);
}
} else if (value.indexOf('W') >= 0) {
type.value = TypeEnum.work;
const values = value.split('W');
if (!values[0] && !isNaN(values[0])) {
valueWork.value = parseInt(values[0]);
}
} else if (value.indexOf('L') >= 0) {
type.value = TypeEnum.last;
} else if (value.indexOf(',') >= 0 || !isNaN(value)) {
type.value = TypeEnum.specify;
valueList.value = value.split(',').map((item) => parseInt(item));
} else {
type.value = TypeEnum.every;
}
} catch (e) {
type.value = TypeEnum.every;
}
}
const beforeRadioAttrs = computed(() => ({
class: ['choice'],
disabled: props.disabled || unref(options.disabled),
}));
const inputNumberAttrs = computed(() => ({
class: ['w60'],
max: maxValue.value,
min: minValue.value,
precision: 0,
}));
const typeRangeAttrs = computed(() => ({
disabled: type.value !== TypeEnum.range || props.disabled || unref(options.disabled),
...inputNumberAttrs.value,
}));
const typeLoopAttrs = computed(() => ({
disabled: type.value !== TypeEnum.loop || props.disabled || unref(options.disabled),
...inputNumberAttrs.value,
}));
const typeSpecifyAttrs = computed(() => ({
disabled: type.value !== TypeEnum.specify || props.disabled || unref(options.disabled),
class: ['list-check-item'],
}));
return {
type,
TypeEnum,
prefixCls,
defaultValue,
valueRange,
valueLoop,
valueWeek,
valueList,
valueWork,
maxValue,
minValue,
computeValue,
specifyRange,
updateValue,
parseValue,
beforeRadioAttrs,
inputNumberAttrs,
typeRangeAttrs,
typeLoopAttrs,
typeSpecifyAttrs,
};
}
@@ -0,0 +1,48 @@
import CronParser from 'cron-parser';
import type { ValidatorRule } from 'ant-design-vue/lib/form/interface';
const cronRule: ValidatorRule = {
validator({}, value) {
// 没填写就不校验
if (!value) {
return Promise.resolve();
}
const values: string[] = value.split(' ').filter((item) => !!item);
if (values.length > 7) {
return Promise.reject('Cron表达式最多7项!');
}
// 检查第7项
let val: string = value;
if (values.length === 7) {
const year = values[6];
if (year !== '*' && year !== '?') {
let yearValues: string[] = [];
if (year.indexOf('-') >= 0) {
yearValues = year.split('-');
} else if (year.indexOf('/')) {
yearValues = year.split('/');
} else {
yearValues = [year];
}
// 判断是否都是数字
const checkYear = yearValues.some((item) => isNaN(Number(item)));
if (checkYear) {
return Promise.reject('Cron表达式参数[年]错误:' + year);
}
}
// 取其中的前六项
val = values.slice(0, 6).join(' ');
}
// 6位 没有年
// 5位没有秒、年
try {
const iter = CronParser.parseExpression(val);
iter.next();
return Promise.resolve();
} catch (e) {
return Promise.reject('Cron表达式错误:' + e);
}
},
};
export default cronRule.validator;
@@ -0,0 +1,47 @@
<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),
//是否聚焦
autoFocus: propTypes.bool.def(true),
},
emits: ['change', 'update:value'],
setup(props, { emit, attrs }) {
// 合并 props 和 attrs
const bindProps = computed(() => Object.assign({}, props, attrs));
const formItemContext = Form.useInjectFormItemContext();
// value change 事件
function onChange(value) {
emit('change', value);
emit('update:value', value);
// update-begin--author:liaozhiyang---date:20240429---for:【QQYUN-9110】组件有值校验没消失
nextTick(() => {
formItemContext?.onFieldChange();
});
// update-end--author:liaozhiyang---date:20240429---for:【QQYUN-9110】组件有值校验没消失
}
return {
bindProps,
onChange,
};
},
});
</script>
<style lang="less" scoped></style>
@@ -0,0 +1,21 @@
<template>
<a-tooltip placement="topLeft">
<template #title>
<span>{{ value }}</span>
</template>
{{ showText }}
</a-tooltip>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { propTypes } from '/@/utils/propTypes';
const props = defineProps({
value: propTypes.oneOfType([propTypes.string, propTypes.number, propTypes.array]),
length: propTypes.number.def(25),
});
//显示的文本
const showText = computed(() =>
props.value ? (props.value.length > props.length ? props.value.slice(0, props.length) + '...' : props.value) : props.value
);
</script>
@@ -0,0 +1,62 @@
<template>
<div :class="disabled ? 'jeecg-form-container-disabled' : ''">
<fieldset :disabled="disabled">
<slot></slot>
</fieldset>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'JFormContainer',
props: {
disabled: {
type: Boolean,
default: false,
required: false,
},
},
});
</script>
<style lang="less">
.jeecg-form-container-disabled {
cursor: not-allowed;
fieldset[disabled] {
-ms-pointer-events: none;
pointer-events: none;
}
.ant-select {
-ms-pointer-events: none;
pointer-events: none;
}
.ant-upload-select {
display: none;
}
.ant-upload-list {
cursor: grabbing;
}
fieldset[disabled]{
.anticon-delete{
display: none !important;
}
}
}
.jeecg-form-container-disabled fieldset[disabled] .ant-upload-list {
-ms-pointer-events: auto !important;
pointer-events: auto !important;
}
.jeecg-form-container-disabled .ant-upload-list-item-actions .anticon-delete,
.jeecg-form-container-disabled .ant-upload-list-item .anticon-close {
display: none;
}
</style>
@@ -0,0 +1,282 @@
<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 as systemUploadUrl } 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,
},
uploadUrl: {
type: String,
default: systemUploadUrl,
},
},
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} 上传失败.`);
}
// update-begin--author:liaozhiyang---date:20240704---for:【TV360X-1640】上传图片大小超出限制显示优化
if (file.status === 'done' && file.response.success === false) {
const failIndex = uploadFileList.value.findIndex((item) => item.uid === file.uid);
if (failIndex != -1) {
uploadFileList.value.splice(failIndex, 1);
}
createMessage.warning(file.response.message);
return;
}
// update-end--author:liaozhiyang---date:20240704---for:【TV360X-1640】上传图片大小超出限制显示优化
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,
beforeUpload,
uploadVisible,
handlePreview,
handleCancel,
handleChange,
};
},
});
</script>
<style scoped>
.ant-upload-select-picture-card i {
font-size: 32px;
color: #999;
}
.ant-upload-select-picture-card .ant-upload-text {
margin-top: 8px;
color: #666;
}
</style>
@@ -0,0 +1,192 @@
<template>
<div>
<BasicModal v-bind="$attrs" @register="register" title="导入EXCEL" :width="600" @cancel="handleClose" :confirmLoading="uploading" destroyOnClose>
<!--是否校验-->
<div style="margin: 0 5px 5px" v-if="online">
<span style="display: inline-block; height: 32px; line-height: 32px; vertical-align: middle">是否开启校验:</span>
<span style="margin-left: 6px">
<a-switch :checked="validateStatus == 1" @change="handleChangeValidateStatus" checked-children="" un-checked-children="" />
</span>
</div>
<!--上传-->
<a-upload name="file" accept=".xls,.xlsx" :multiple="true" :fileList="fileList" @remove="handleRemove" :beforeUpload="beforeUpload">
<a-button preIcon="ant-design:upload-outlined">选择导入文件</a-button>
</a-upload>
<!--页脚-->
<template #footer>
<a-button @click="handleClose">关闭</a-button>
<a-button type="primary" @click="handleImport" :disabled="uploadDisabled" :loading="uploading">{{
uploading ? '上传中...' : '开始上传'
}}</a-button>
</template>
</BasicModal>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, unref, watchEffect, computed } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { defHttp } from '/@/utils/http/axios';
import { useGlobSetting } from '/@/hooks/setting';
import { useMessage } from '/@/hooks/web/useMessage';
import { isObject } from '/@/utils/is';
export default defineComponent({
name: 'JImportModal',
components: {
BasicModal,
},
props: {
url: {
type: String,
default: '',
required: false,
},
biz: {
type: String,
default: '',
required: false,
},
//是否online导入
online: {
type: Boolean,
default: false,
required: false,
},
},
emits: ['ok', 'register'],
setup(props, { emit, refs }) {
const { createMessage, createWarningModal } = useMessage();
//注册弹框
const [register, { closeModal }] = useModalInner((data) => {
reset(data);
});
const glob = useGlobSetting();
const attrs = useAttrs();
const uploading = ref(false);
//文件集合
const fileList = ref([]);
//上传url
const uploadAction = ref('');
const foreignKeys = ref('');
//校验状态
const validateStatus = ref(0);
const getBindValue = Object.assign({}, unref(props), unref(attrs));
//监听url
watchEffect(() => {
props.url && (uploadAction.value = `${glob.uploadUrl}${props.url}`);
});
//按钮disabled状态
const uploadDisabled = computed(() => !(unref(fileList).length > 0));
//关闭方法
function handleClose() {
// update-begin--author:liaozhiyang---date:20231226---for:【QQYUN-7477】关闭弹窗清空内容(之前上传失败关闭后不会清除)
closeModal();
reset();
// update-end--author:liaozhiyang---date:20231226---for:【QQYUN-7477】关闭弹窗清空内容(之前上传失败关闭后不会清除)
}
//校验状态切换
function handleChangeValidateStatus(checked) {
validateStatus.value = !!checked ? 1 : 0;
}
//移除上传文件
function handleRemove(file) {
const index = unref(fileList).indexOf(file);
const newFileList = unref(fileList).slice();
newFileList.splice(index, 1);
fileList.value = newFileList;
}
//上传前处理
function beforeUpload(file) {
fileList.value = [...unref(fileList), file];
return false;
}
//文件上传
function handleImport() {
let { biz, online } = props;
const formData = new FormData();
if (biz) {
formData.append('isSingleTableImport', biz);
}
if (unref(foreignKeys) && unref(foreignKeys).length > 0) {
formData.append('foreignKeys', unref(foreignKeys));
}
// update-begin--author:liaozhiyang---date:20240429---for:【issues/6124】当用户没有【Online表单开发】页面的权限时用户无权导入从表数据
if (isObject(foreignKeys.value)) {
formData.append('foreignKeys', JSON.stringify(foreignKeys.value));
}
// update-end--author:liaozhiyang---date:20240429---for:【issues/6124】当用户没有【Online表单开发】页面的权限时用户无权导入从表数据
if (!!online) {
formData.append('validateStatus', unref(validateStatus));
}
unref(fileList).forEach((file) => {
formData.append('files[]', file);
});
uploading.value = true;
//TODO 请求怎样处理的问题
let headers = {
'Content-Type': 'multipart/form-data;boundary = ' + new Date().getTime(),
};
defHttp.post({ url: props.url, params: formData, headers }, { isTransformResponse: false }).then((res) => {
uploading.value = false;
if (res.success) {
if (res.code == 201) {
errorTip(res.message, res.result);
} else {
createMessage.success(res.message);
}
handleClose();
reset();
emit('ok');
} else {
createMessage.warning(res.message);
}
}).catch(() => {
uploading.value = false;
});
}
//错误信息提示
function errorTip(tipMessage, fileUrl) {
let href = glob.uploadUrl + fileUrl;
createWarningModal({
title: '导入成功,但是有错误数据!',
centered: false,
content: `<div>
<span>${tipMessage}</span><br/>
<span>具体详情请<a href = ${href} target="_blank"> 点击下载 </a> </span>
</div>`,
});
}
//重置
function reset(arg?) {
fileList.value = [];
uploading.value = false;
foreignKeys.value = arg;
validateStatus.value = 0;
}
return {
register,
getBindValue,
uploadDisabled,
fileList,
uploading,
validateStatus,
handleClose,
handleChangeValidateStatus,
handleRemove,
beforeUpload,
handleImport,
};
},
});
</script>
@@ -0,0 +1,110 @@
<template>
<a-input v-bind="getBindValue" v-model:value="showText" @input="backValue"></a-input>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, watchEffect, unref, watch, computed } from 'vue';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { propTypes } from '/@/utils/propTypes';
import { JInputTypeEnum } from '/@/enums/jeecgEnum.ts';
import { omit } from 'lodash-es';
export default defineComponent({
name: 'JInput',
inheritAttrs: false,
props: {
value: propTypes.string.def(''),
type: propTypes.string.def(JInputTypeEnum.JINPUT_QUERY_LIKE),
placeholder: propTypes.string.def(''),
trim: propTypes.bool.def(false),
},
emits: ['change', 'update:value'],
setup(props, { emit }) {
const attrs = useAttrs();
//表单值
const showText = ref('');
// update-begin--author:liaozhiyang---date:20231026---for:【issues/803】JIput updateSchema不生效
//绑定属性
const getBindValue = computed(() => {
return omit(Object.assign({}, unref(props), unref(attrs)), ['value']);
});
// update-end--author:liaozhiyang---date:20231026---for:【issues/803】JIput updateSchema不生效
//监听类型变化
watch(
() => props.type,
(val) => {
val && backValue({ target: { value: unref(showText) } });
}
);
//监听value变化
watch(
() => props.value,
() => {
initVal();
},
{ immediate: true }
);
/**
* 初始化数值
*/
function initVal() {
if (!props.value) {
showText.value = '';
} else {
let text = props.value;
switch (props.type) {
case JInputTypeEnum.JINPUT_QUERY_LIKE:
//修复路由传参的值传送到jinput框被前后各截取了一位 #1336
if (text.indexOf('*') != -1) {
text = text.substring(1, text.length - 1);
}
break;
case JInputTypeEnum.JINPUT_QUERY_NE:
text = text.substring(1);
break;
case JInputTypeEnum.JINPUT_QUERY_GE:
text = text.substring(2);
break;
case JInputTypeEnum.JINPUT_QUERY_LE:
text = text.substring(2);
break;
default:
}
showText.value = text;
}
}
/**
* 返回值
*/
function backValue(e) {
let text = e?.target?.value ?? '';
if (text && !!props.trim) {
text = text.trim();
}
switch (props.type) {
case JInputTypeEnum.JINPUT_QUERY_LIKE:
text = '*' + text + '*';
break;
case JInputTypeEnum.JINPUT_QUERY_NE:
text = '!' + text;
break;
case JInputTypeEnum.JINPUT_QUERY_GE:
text = '>=' + text;
break;
case JInputTypeEnum.JINPUT_QUERY_LE:
text = '<=' + text;
break;
default:
}
emit('change', text);
emit('update:value', text);
}
return { showText, attrs, getBindValue, backValue };
},
});
</script>
<style scoped></style>
@@ -0,0 +1,126 @@
<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" @blur="onInputBlur" />
</template>
<a-input :class="`${prefixCls}-input`" :value="innerValue" :disabled="disabled" v-bind="attrs" @change="onInputChange" @blur="onInputBlur">
<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', 'blur']);
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);
}
const onInputBlur = (event) => {
emit('blur', event);
}
</script>
<style lang="less">
//noinspection LessUnresolvedVariable
@prefix-cls: ~'@{namespace}-j-input-popup';
.@{prefix-cls} {
&-popover {
// update-begin--author:liaozhiyang---date:20240520---for:【TV360X-144】jVxetable中的多行文本组件当title没有时去掉多余的线
.ant-popover-title:has(.emptyTitle) {
border-bottom: none;
}
// update-end--author:liaozhiyang---date:20240520---for:【TV360X-144】jVxetable中的多行文本组件当title没有时去掉多余的线
}
&-input {
.app-iconify {
cursor: pointer;
color: #666666;
transition: color 0.3s;
&:hover {
color: black;
}
}
}
}
</style>
@@ -0,0 +1,77 @@
<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"
:style="{width:props.selectWidth}"
>
<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(''),
selectWidth:propTypes.string.def('auto'),
});
const emit = defineEmits(['update:value', 'change']);
const selectVal = ref<string>();
const inputVal = ref<string>();
const handleInputChange = (e) => {
const val = e.target.value;
setSelectValByInputVal(val);
emits(val);
};
const handleSelectChange = (val) => {
inputVal.value = val;
emits(val);
};
const setSelectValByInputVal = (val) => {
const findItem = props.options.find((item) => item.value === val);
if (findItem) {
selectVal.value = val;
} else {
selectVal.value = undefined;
}
}
watchEffect(() => {
inputVal.value = props.value;
setSelectValByInputVal(props.value);
});
const emits = (val) => {
emit('update:value', val);
emit('change', val);
};
</script>
<style lang="less" scoped>
.JInputSelect {
.ant-input-group {
display: flex;
}
}
</style>
@@ -0,0 +1,379 @@
<template>
<div ref="tableLinkCardRef">
<div class="table-link-card">
<div style="width: 100%; height: 100%">
<div class="card-button" v-if="showButton">
<a-button @click="handleAddRecord"><PlusOutlined /> </a-button>
</div>
<a-row>
<a-col :span="fixedSpan ? fixedSpan : itemSpan" v-for="(record, index) in selectRecords" :key="index">
<div class="card-item" :class="{ 'disabled-chunk': detail == true }">
<!-- -->
<div class="card-item-left" :class="{ 'show-right-image': getImageSrc(record) }">
<span class="card-delete" v-if="disabled == false">
<minus-circle-filled @click="(e) => handleDeleteRecord(e, index)" />
</span>
<div class="card-inner">
<div class="card-main-content">{{ getMainContent(record) }}</div>
<div class="other-content">
<a-row>
<a-col :span="columnSpan" v-for="(col, cIndex) in realShowColumns" :key="cIndex">
<span class="label ellipsis">{{ col.title }}</span>
<span class="text ellipsis">{{ record[col.dataIndex] }}</span>
</a-col>
</a-row>
</div>
</div>
</div>
<div class="card-item-image" v-if="getImageSrc(record)">
<img v-if="getImageSrc(record)" :src="getImageSrc(record)" @error="handleImageError" />
</div>
</div>
</a-col>
</a-row>
</div>
</div>
<LinkTableListModal @register="registerListModal" :multi="multi" :id="popTableName" @success="addCard" />
</div>
</template>
<script>
import { propTypes } from '/@/utils/propTypes';
import { PlusOutlined, MinusCircleFilled } from '@ant-design/icons-vue';
import { computed, ref, watch, onMounted } from 'vue';
import { useLinkTable } from './hooks/useLinkTable';
import { useModal } from '/@/components/Modal';
import placeholderImage from '/@/assets/images/placeholderImage.png';
import { createAsyncComponent } from '@/utils/factory/createAsyncComponent';
export default {
name: 'JLinkTableCard',
inheritAttrs: false,
props: {
// value字段
valueField: propTypes.string.def(''),
// 文本字段
textField: propTypes.string.def(''),
// 关联表名
tableName: propTypes.string.def(''),
// 是否多选
multi: propTypes.bool.def(false),
value: propTypes.oneOfType([propTypes.string, propTypes.number]),
// ["表单字段,表字典字段","表单字段,表字典字段"]
linkFields: propTypes.array.def([]),
//是否是禁用页面
disabled: propTypes.bool.def(false),
// 是否是detail页面
detail: propTypes.bool.def(false),
// 图片字段
imageField: propTypes.string.def(''),
},
components: {
PlusOutlined,
MinusCircleFilled,
LinkTableListModal: createAsyncComponent(() => import('./components/LinkTableListModal.vue'), { loading: true }),
},
emits: ['change', 'update:value'],
setup(props, { emit }) {
const popTableName = computed(() => {
return props.tableName;
});
//注册model
const [registerListModal, { openModal: openListModal }] = useModal();
const selectValue = ref([]);
const selectRecords = ref([]);
const tableLinkCardRef = ref(null);
const fixedSpan = ref(0);
const showButton = computed(() => {
if (props.disabled == true) {
return false;
}
if (props.multi === false) {
//单选且有值
if (selectRecords.value.length > 0) {
return false;
}
}
return true;
});
const {
auths,
otherColumns,
realShowColumns,
tableColumns,
textFieldArray,
transData,
loadOne,
compareData,
formatData,
initFormData,
getImageSrc,
showImage,
} = useLinkTable(props);
const itemSpan = computed(() => {
if (props.multi === true) {
return 12;
}
return 24;
});
const columnSpan = computed(() => {
if (props.multi === true) {
return 24;
}
return 12;
});
function getMainContent(record) {
if (record) {
if (textFieldArray.value.length > 0) {
let field = textFieldArray.value[0];
return record[field];
}
}
}
function prevent(e) {
e?.stopPropagation();
e?.preventDefault();
}
function handleAddRecord(e) {
prevent(e);
openListModal(true, {
// update-begin--author:liaozhiyang---date:20240517---for:【TV360X-43】修复关联记录可以添加重复数据
selectedRowKeys: selectRecords.value.map((item) => item.id),
selectedRows: [...selectRecords.value],
// update-end--author:liaozhiyang---date:20240517---for:【TV360X-43】修复关联记录可以添加重复数据
});
}
function addCard(data) {
// update-begin--author:liaozhiyang---date:20240517---for:【TV360X-43】修复关联记录可以添加重复数据
let arr = [];
// update-end--author:liaozhiyang---date:20240517---for:【TV360X-43】修复关联记录可以添加重复数据
for (let item of data) {
let temp = { ...item };
transData(temp);
arr.push(temp);
}
selectRecords.value = arr;
emitValue();
}
function updateCardData(formData) {
let arr = selectRecords.value;
for (let i = 0; i < arr.length; i++) {
if (arr[i].id === formData.id) {
let temp = { ...formData };
transData(temp);
arr.splice(i, 1, temp);
}
}
selectRecords.value = arr;
emitValue();
}
function handleDeleteRecord(e, index) {
prevent(e);
let temp = selectRecords.value;
if (temp && temp.length > index) {
temp.splice(index, 1);
selectRecords.value = temp;
}
emitValue();
}
function emitValue() {
let arr = selectRecords.value;
let values = [];
let formData = {};
let linkFieldArray = props.linkFields;
if (arr.length > 0) {
for (let i = 0; i < arr.length; i++) {
values.push(arr[i][props.valueField]);
initFormData(formData, linkFieldArray, arr[i]);
}
} else {
initFormData(formData, linkFieldArray);
}
let text = values.join(',');
formatData(formData);
emit('change', text, formData);
emit('update:value', text);
}
watch(
() => props.value,
async (val) => {
if (val) {
let flag = compareData(selectRecords.value, val);
if (flag === false) {
let arr = await loadOne(val);
selectRecords.value = arr;
}
//保证表单其他值回显成功
if (props.linkFields && props.linkFields.length > 0) {
emitValue();
}
} else {
selectRecords.value = [];
}
},
{ immediate: true }
);
onMounted(() => {
// update-begin--author:liaozhiyang---date:20240522---for:【TV360X-281】分辨率小时关联记录文字被图片挤没了
if (tableLinkCardRef.value.offsetWidth < 250) {
fixedSpan.value = 24;
}
// update-end--author:liaozhiyang---date:20240522---for:【TV360X-281】分辨率小时关联记录文字被图片挤没了
});
// update-begin--author:liaozhiyang---date:20240529---for:【TV360X-389】下拉和卡片关联记录图裂开给个默认图片
const handleImageError = (event) => {
event.target.src = placeholderImage;
};
// update-end--author:liaozhiyang---date:20240529---for:【TV360X-389】下拉和卡片关联记录图裂开给个默认图片
return {
popTableName,
selectRecords,
otherColumns,
realShowColumns,
showButton,
selectValue,
handleAddRecord,
handleDeleteRecord,
getMainContent,
itemSpan,
columnSpan,
tableColumns,
addCard,
registerListModal,
updateCardData,
getImageSrc,
showImage,
auths,
tableLinkCardRef,
fixedSpan,
placeholderImage,
handleImageError,
};
},
};
</script>
<style scoped lang="less">
.table-link-card {
box-sizing: border-box;
position: relative;
width: 100%;
.card-button {
margin-bottom: 10px !important;
}
.card-item {
width: calc(100% - 10px);
display: inline-flex;
flex-direction: row;
margin: 0px 10px 10px 0px;
position: relative;
border-radius: 3px;
background-color: rgb(255, 255, 255);
box-shadow:
rgb(0 0 0 / 12%) 0px 1px 4px 0px,
rgb(0 0 0 / 12%) 0px 0px 2px 0px;
cursor: pointer;
&:hover {
/* box-shadow: rgb(0 0 0 / 12%) 0px 4px 12px 0px, rgb(0 0 0 / 12%) 0px 0px 2px 0px;*/
.card-item-left {
.card-delete {
display: inline-block;
}
}
}
&.disabled-chunk {
background: none;
box-shadow: none;
}
.card-item-image {
width: 100px;
padding-right: 8px;
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
img {
width: 100%;
}
}
.card-item-left {
width: 100%;
display: inline-block;
&.show-right-image {
width: calc(100% - 100px);
}
.card-delete {
position: absolute;
top: -8px;
right: -8px;
font-size: 16px;
color: #757575;
line-height: 1em;
overflow: hidden;
display: none;
}
.card-inner {
flex: 1 1 0%;
padding: 12px 16px;
overflow: hidden;
padding-bottom: 10px;
.card-main-content {
overflow: hidden;
text-overflow: ellipsis;
vertical-align: top;
white-space: nowrap;
margin-bottom: 8px;
font-weight: 500;
font-size: 14px;
line-height: 20px;
color: rgb(51, 51, 51);
}
.other-content {
.text {
font-size: 12px !important;
display: inline-block;
width: 80%;
}
.label {
max-width: 160px;
color: rgb(158, 158, 158);
padding-right: 0.7em;
display: inline-block;
}
.ellipsis {
overflow: hidden;
height: 22px;
line-height: 22px;
text-overflow: ellipsis;
/* vertical-align: top;*/
white-space: nowrap;
}
}
}
}
}
}
</style>
@@ -0,0 +1,320 @@
<template>
<BasicModal
@register="registerModal"
:width="popModalFixedWidth"
:dialogStyle="{ top: '70px' }"
:bodyStyle="popBodyStyle"
:title="modalTitle"
wrapClassName="jeecg-online-pop-list-modal"
>
<template #footer>
<a-button key="back" @click="handleCancel">关闭</a-button>
<a-button :disabled="submitDisabled" key="submit" type="primary" @click="handleSubmit" :loading="submitLoading">确定</a-button>
</template>
<BasicTable ref="tableRef" @register="registerTable" :rowSelection="rowSelection">
<!-- update-begin-author:taoyan date:2023-7-11 for: issues/4992 online表单开发 字段控件类型是关联记录 新增的时候选择列表可以添加查询么 -->
<template #tableTitle>
<a-input-search v-model:value="searchText" @search="onSearch" placeholder="请输入关键词,按回车搜索" style="width: 240px" />
</template>
<!-- update-end-author:taoyan date:2023-7-11 for: issues/4992 online表单开发 字段控件类型是关联记录 新增的时候选择列表可以添加查询么 -->
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)"> </TableAction>
</template>
<template #fileSlot="{ text }">
<span v-if="!text" style="font-size: 12px; font-style: italic">无文件</span>
<a-button v-else :ghost="true" type="primary" preIcon="ant-design:download" size="small" @click="downloadRowFile(text)"> 下载 </a-button>
</template>
<template #imgSlot="{ text }">
<span v-if="!text" style="font-size: 12px; font-style: italic">无图片</span>
<img v-else :src="getImgView(text)" alt="图片不存在" class="online-cell-image" @click="viewOnlineCellImage(text)" />
</template>
<template #htmlSlot="{ text }">
<div v-html="text"></div>
</template>
<template #pcaSlot="{ text }">
<div :title="getPcaText(text)">{{ getPcaText(text) }}</div>
</template>
<template #dateSlot="{ text, column }">
<span>{{ getFormatDate(text, column) }}</span>
</template>
</BasicTable>
</BasicModal>
</template>
<script lang="ts">
import { defineComponent, watch, ref, toRaw, computed, nextTick } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { useListPage } from '/@/hooks/system/useListPage';
import { useMessage } from '/@/hooks/web/useMessage';
import { defHttp } from '/@/utils/http/axios';
import { useTableColumns } from '@/views/super/online/cgform/hooks/auto/useTableColumns';
import { createAsyncComponent } from '@/utils/factory/createAsyncComponent';
import { useFixedHeightModal } from '../hooks/useLinkTable';
export default defineComponent({
name: 'LinkTableListModal',
props: {
/**可以是表名 可以是ID*/
id: {
type: String,
default: '',
},
multi: {
type: Boolean,
default: false,
},
addAuth: {
type: Boolean,
default: true,
},
},
components: {
BasicModal,
BasicTable: createAsyncComponent(() => import('/@/components/Table/src/BasicTable.vue'), { loading: true, delay: 1000 }),
TableAction: createAsyncComponent(() => import('/@/components/Table/src/components/TableAction.vue'), { loading: true, delay: 1000 }),
},
emits: ['success', 'register'],
setup(props, { emit }) {
const { createMessage: $message } = useMessage();
const tableRef = ref(null);
// 弹窗高度控制
const { popModalFixedWidth, resetBodyStyle, popBodyStyle } = useFixedHeightModal();
const searchText = ref('');
const modalWidth = ref(800);
//useModalInner
const [registerModal, { closeModal }] = useModalInner((data) => {
searchText.value = '';
selectedRowKeys.value = data.selectedRowKeys;
selectedRows.value = data.selectedRows;
setTimeout(async () => {
await setPagination({ current: 1 });
await reload(); // 等待表格加载
resetBodyStyle();
}, 100);
});
function handleCancel() {
closeModal();
}
const submitDisabled = computed(() => {
const arr = selectedRowKeys.value;
if (arr && arr.length > 0) {
return false;
}
return true;
});
const submitLoading = ref(false);
function handleSubmit() {
submitLoading.value = true;
let arr = toRaw(selectedRows.value);
if (arr && arr.length > 0) {
emit('success', arr);
closeModal();
}
setTimeout(() => {
submitLoading.value = false;
}, 200);
}
//---------------------列表------------------------
function queryTableData(params) {
const url = '/online/cgform/api/getData/' + props.id;
return defHttp.get({ url, params });
}
function list(params) {
params['column'] = 'id';
return new Promise(async (resolve, _reject) => {
const aa = await queryTableData(params);
resolve(aa);
});
}
const onlineTableContext = {
isPopList: true,
reloadTable() {
console.log('reloadTable');
},
isTree() {
return false;
},
};
const extConfigJson = ref<any>({});
// 处理 BasicTable 的配置
const { columns, downloadRowFile, getImgView, getPcaText, getFormatDate, handleColumnResult, hrefComponent, viewOnlineCellImage } =
useTableColumns(onlineTableContext, extConfigJson);
/**
* 查询table列信息 及其他配置
*/
function getColumnList() {
const url = '/online/cgform/api/getColumns/' + props.id;
return new Promise((resolve, reject) => {
defHttp.get({ url }, { isTransformResponse: false }).then((res) => {
if (res.success) {
resolve(res.result);
} else {
$message.warning(res.message);
reject();
}
});
});
}
const modalTitle = ref('');
watch(
() => props.id,
async () => {
let columnResult: any = await getColumnList();
handleColumnResult(columnResult);
modalTitle.value = columnResult.description;
},
{ immediate: true }
);
const { tableContext } = useListPage({
designScope: 'process-design',
pagination: true,
tableProps: {
title: '',
api: list,
clickToRowSelect: true,
columns: columns,
showTableSetting: false,
immediate: false,
//showIndexColumn: true,
canResize: false,
showActionColumn: false,
actionColumn: {
dataIndex: 'action',
slots: { customRender: 'action' },
},
useSearchForm: false,
beforeFetch: (params) => {
return addQueryParams(params);
},
},
});
const [registerTable, { reload, setPagination }, { rowSelection, selectedRowKeys, selectedRows }] = tableContext;
watch(
() => props.multi,
(val) => {
if (val == true) {
rowSelection.type = 'checkbox';
} else {
rowSelection.type = 'radio';
}
},
{ immediate: true }
);
/**
* 操作栏
*/
function getTableAction(record) {
return [
{
label: '编辑',
onClick: handleUpdate.bind(null, record),
},
];
}
function handleUpdate(record) {
console.log('handleUpdate', record);
}
function onSearch() {
reload();
}
const eqConditonTypes = ['int', 'double', 'Date', 'Datetime', 'BigDecimal'];
function addQueryParams(params) {
let text = searchText.value;
if (!text) {
params['superQueryMatchType'] = 'or';
params['superQueryParams'] = '';
return params;
}
let arr = columns.value;
let conditions: any[] = [];
if (arr && arr.length > 0) {
for (let item of arr) {
if (item.dbType) {
if (item.dbType == 'string') {
conditions.push({ field: item.dataIndex, type: item.dbType.toLowerCase(), rule: 'like', val: text });
} else if (item.dbType == 'Date') {
if (text.length == '2020-10-10'.length) {
conditions.push({ field: item.dataIndex, type: item.dbType.toLowerCase(), rule: 'eq', val: text });
}
} else if (item.dbType == 'Datetime') {
if (text.length == '2020-10-10 10:10:10'.length) {
conditions.push({ field: item.dataIndex, type: item.dbType.toLowerCase(), rule: 'eq', val: text });
}
} else if (eqConditonTypes.indexOf(item.dbType)) {
conditions.push({ field: item.dataIndex, type: item.dbType.toLowerCase(), rule: 'eq', val: text });
} else {
//text blob不做处理
}
}
}
}
params['superQueryMatchType'] = 'or';
params['superQueryParams'] = encodeURI(JSON.stringify(conditions));
return params;
}
// modal数据新增完成 直接关闭list,将新增的数据带回表单
function handleDataSave(data) {
console.log('handleDateSave', data);
// update-begin--author:liaozhiyang---date:20250429---for:【issues/8163】关联记录新增丢失
let arr = [data, ...selectedRows.value];
// update-end--author:liaozhiyang---date:20250429---for:【issues/8163】关联记录新增丢失
emit('success', arr);
closeModal();
//reload();
}
return {
registerModal,
modalWidth,
handleCancel,
submitDisabled,
submitLoading,
handleSubmit,
registerTable,
getTableAction,
searchText,
onSearch,
downloadRowFile,
getImgView,
getPcaText,
getFormatDate,
hrefComponent,
viewOnlineCellImage,
rowSelection,
modalTitle,
reload,
popModalFixedWidth,
popBodyStyle,
handleDataSave,
tableRef,
};
},
});
</script>
<style scoped></style>
@@ -0,0 +1,358 @@
import { defHttp } from '/@/utils/http/axios';
import { ref, watchEffect, computed, reactive } from 'vue';
import { pick } from 'lodash-es';
import { filterMultiDictText } from '/@/utils/dict/JDictSelectUtil';
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
function queryTableData(tableName, params) {
const url = '/online/cgform/api/getData/' + tableName;
return defHttp.get({ url, params });
}
function queryTableColumns(tableName, params) {
const url = '/online/cgform/api/getColumns/' + tableName;
return defHttp.get({ url, params });
}
export function useLinkTable(props) {
//TODO 目前只支持查询第一页的数据,可以输入关键字搜索
const pageNo = ref('1');
// 查询列
const baseParam = ref<any>({});
// 搜素条件
const searchParam = ref<any>({});
// 第一个文本列
const mainContentField = ref('');
//权限数据
const auths = reactive({
add: true,
update: true,
});
//显示列
const textFieldArray = computed(() => {
if (props.textField) {
return props.textField.split(',');
}
return [];
});
const otherColumns = ref<any[]>([]);
// 展示的列 配置的很多列,但是只展示三行
const realShowColumns = computed(() => {
const columns = otherColumns.value;
if (props.multi == true) {
return columns.slice(0, 3);
} else {
return columns.slice(0, 6);
}
});
watchEffect(async () => {
const table = props.tableName;
if (table) {
const valueField = props.valueField || '';
const textField = props.textField || '';
const arr: any[] = [];
if (valueField) {
arr.push(valueField);
}
if (textField) {
const temp = textField.split(',');
mainContentField.value = temp[0];
for (const field of temp) {
arr.push(field);
}
}
const imageField = props.imageField || '';
if (imageField) {
arr.push(imageField);
}
baseParam.value = {
linkTableSelectFields: arr.join(','),
};
await resetTableColumns();
await reloadTableLinkOptions();
}
});
const otherFields = computed(() => {
const textField = props.textField || '';
const others: any[] = [];
let labelField = '';
if (textField) {
const temp = textField.split(',');
labelField = temp[0];
for (let i = 0; i < temp.length; i++) {
if (i > 0) {
others.push(temp[i]);
}
}
}
return {
others,
labelField,
};
});
// 选项
const selectOptions = ref<any[]>([]);
const tableColumns = ref<any[]>([]);
const dictOptions = ref<any>({});
async function resetTableColumns() {
const params = baseParam.value;
const data = await queryTableColumns(props.tableName, params);
tableColumns.value = data.columns;
if (data.columns) {
const imageField = props.imageField;
const arr = data.columns.filter((c) => c.dataIndex != mainContentField.value && c.dataIndex != imageField);
otherColumns.value = arr;
}
dictOptions.value = data.dictOptions;
// 权限数据
console.log('隐藏的按钮', data.hideColumns);
if (data.hideColumns) {
const hideCols = data.hideColumns;
if (hideCols.indexOf('add') >= 0) {
auths.add = false;
} else {
auths.add = true;
}
if (hideCols.indexOf('update') >= 0) {
auths.update = false;
} else {
auths.update = true;
}
}
}
async function reloadTableLinkOptions() {
const params = getLoadDataParams();
const data = await queryTableData(props.tableName, params);
const records = data.records;
//tableTitle.value = data.head.tableTxt;
const dataList: any[] = [];
const { others, labelField } = otherFields.value;
const imageField = props.imageField;
if (records && records.length > 0) {
for (const rd of records) {
const temp = { ...rd };
transData(temp);
const result = Object.assign({}, pick(temp, others), { id: temp.id, label: temp[labelField], value: temp[props.valueField] });
if (imageField) {
result[imageField] = temp[imageField];
}
dataList.push(result);
}
}
//添加一个空对象 为add操作占位
// update-begin--author:liaozhiyang---date:20240607---for:【TV360X-1095】高级查询关联记录去掉编辑按钮及去掉记录按钮
props.editBtnShow && dataList.push({});
// update-end--author:liaozhiyang---date:20240607---for:【TV360X-1095】高级查询关联记录去掉编辑按钮及去掉记录按钮
selectOptions.value = dataList;
}
/**
* 数据简单翻译-字典
* @param data
*/
function transData(data) {
const columns = tableColumns.value;
const dictInfo = dictOptions.value;
for (const c of columns) {
const { dataIndex, customRender } = c;
if (data[dataIndex] || data[dataIndex] === 0) {
if (customRender && customRender == dataIndex) {
//这样的就是 字典数据了 可以直接翻译
if (dictInfo[customRender]) {
data[dataIndex] = filterMultiDictText(dictInfo[customRender], data[dataIndex]);
continue;
}
}
}
// 兼容后台翻译字段
const dictText = data[dataIndex + '_dictText'];
if (dictText) {
data[dataIndex] = dictText;
}
}
}
//获取加载数据的查询条件
function getLoadDataParams() {
const params = Object.assign({ pageSize: 100, pageNo: pageNo.value }, baseParam.value, searchParam.value);
return params;
}
//设置查询条件
function addQueryParams(text) {
if (!text) {
searchParam.value = {};
} else {
const arr = textFieldArray.value;
const params: any[] = [];
const fields: any[] = [];
for (let i = 0; i < arr.length; i++) {
if (i <= 1) {
fields.push(arr[i]);
params.push({ field: arr[i], rule: 'like', val: text });
}
}
// params[arr[i]] = `*${text}*`
// params['selectConditionFields'] = fields.join(',')
// searchParam.value = params;
params['superQueryMatchType'] = 'or';
params['superQueryParams'] = encodeURI(JSON.stringify(params));
searchParam.value = params;
}
}
async function loadOne(value) {
if (!value) {
return [];
}
let valueFieldName = props.valueField;
let params = {
...baseParam.value,
pageSize: 100,
pageNo: pageNo.value,
};
params['superQueryMatchType'] = 'and';
let valueCondition = [{ field: valueFieldName, rule: 'in', val: value }];
params['superQueryParams'] = encodeURI(JSON.stringify(valueCondition));
const data = await queryTableData(props.tableName, params);
let records = data.records;
//tableTitle.value = data.head.tableTxt;
let dataList: any[] = [];
if (records && records.length > 0) {
for (let item of records) {
let temp = { ...item };
transData(temp);
dataList.push(temp);
}
}
return dataList;
}
/**
* true:数据一致;false:数据不一致
* @param arr
* @param value
*/
function compareData(arr, value) {
if (!arr || arr.length == 0) {
return false;
}
const valueArray = value.split(',');
if (valueArray.length != arr.length) {
return false;
}
let flag = true;
for (const item of arr) {
const temp = item[props.valueField];
if (valueArray.indexOf(temp) < 0) {
flag = false;
}
}
return flag;
}
function formatData(formData) {
Object.keys(formData).map((k) => {
if (formData[k] instanceof Array) {
formData[k] = formData[k].join(',');
}
});
}
function initFormData(formData, linkFieldArray, record) {
if (!record) {
record = {};
}
if (linkFieldArray && linkFieldArray.length > 0) {
for (const str of linkFieldArray) {
const arr = str.split(',');
//["表单字段,表字典字段"]
const field = arr[0];
const dictField = arr[1];
if (!formData[field]) {
const value = record[dictField] || '';
formData[field] = [value];
} else {
formData[field].push(record[dictField]);
}
}
}
}
// 获取图片地址
function getImageSrc(item) {
if (props.imageField) {
let url = item[props.imageField];
// update-begin--author:liaozhiyang---date:20250517---for:【TV360X-38】关联记录空间,被关联数据优多个图片时,封面图片不展示
if (typeof url === 'string') {
// 有多张图时默认取第一张
url = url.split(',')[0];
}
// update-end--author:liaozhiyang---date:20250517---for:【TV360X-38】关联记录空间,被关联数据优多个图片时,封面图片不展示
return getFileAccessHttpUrl(url);
}
return '';
}
const showImage = computed(() => {
if (props.imageField) {
return true;
} else {
return false;
}
});
return {
pageNo,
otherColumns,
realShowColumns,
selectOptions,
reloadTableLinkOptions,
textFieldArray,
addQueryParams,
tableColumns,
transData,
mainContentField,
loadOne,
compareData,
formatData,
initFormData,
getImageSrc,
showImage,
auths,
};
}
/**
* 使用固定高度的modal
*/
export function useFixedHeightModal() {
const minWidth = 800;
const popModalFixedWidth = ref(800);
let tempWidth = window.innerWidth - 300;
if (tempWidth < minWidth) {
tempWidth = minWidth;
}
popModalFixedWidth.value = tempWidth;
// 弹窗高度控制
const popBodyStyle = ref({});
function resetBodyStyle() {
const height = window.innerHeight - 210;
popBodyStyle.value = {
height: height + 'px',
overflowY: 'auto',
};
}
return {
popModalFixedWidth,
popBodyStyle,
resetBodyStyle,
};
}
@@ -0,0 +1,62 @@
<template>
<MarkDown v-bind="bindProps" @change="onChange" @get="onGetVditor" />
</template>
<script lang="ts">
import { computed, defineComponent, watch, nextTick } from 'vue';
import { MarkDown } from '/@/components/Markdown';
import { propTypes } from '/@/utils/propTypes';
import { Form } from 'ant-design-vue';
export default defineComponent({
name: 'JMarkdownEditor',
// 不将 attrs 的属性绑定到 html 标签上
inheritAttrs: false,
components: { MarkDown },
props: {
value: propTypes.string.def(''),
disabled: propTypes.bool.def(false),
},
emits: ['change', 'update:value'],
setup(props, { emit, attrs }) {
// markdown 组件实例
let mdRef: any = null;
// vditor 组件实例
let vditorRef: any = null;
// 合并 props 和 attrs
const bindProps = computed(() => Object.assign({}, props, attrs));
const formItemContext = Form.useInjectFormItemContext();
// 相当于 onMounted
function onGetVditor(instance) {
mdRef = instance;
vditorRef = mdRef.getVditor();
// 监听禁用,切换编辑器禁用状态
watch(
() => props.disabled,
(disabled) => (disabled ? vditorRef.disabled() : vditorRef.enable()),
{ immediate: true }
);
}
// value change 事件
function onChange(value) {
emit('change', value);
emit('update:value', value);
// update-begin--author:liaozhiyang---date:20240429---for:【QQYUN-9110】组件有值校验没消失
nextTick(() => {
formItemContext?.onFieldChange();
});
// update-end--author:liaozhiyang---date:20240429---for:【QQYUN-9110】组件有值校验没消失
}
return {
bindProps,
onChange,
onGetVditor,
};
},
});
</script>
<style lang="less" scoped></style>
@@ -0,0 +1,200 @@
<!--popup组件-->
<template>
<div class="JPopup components-input-demo-presuffix" v-if="avalid">
<!--输入框-->
<a-input @click="handleOpen" :value="innerShowText || 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---forQQYUN-9260必填模式下会影响到弹窗内antd组件的样式 -->
<a-form-item>
<!--popup弹窗-->
<JPopupOnlReportModal
@register="regModal"
:code="code"
:multi="multi"
:sorter="sorter"
:groupId="uniqGroupId"
:param="param"
:showAdvancedButton="showAdvancedButton"
:getContainer="getContainer"
:getFormValues="getFormValues"
@ok="callBack"
></JPopupOnlReportModal>
</a-form-item>
<!-- update-end--author:liaozhiyang---date:20240515---forQQYUN-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,
getFormValues: propTypes.func,
getContainer: propTypes.func,
fieldConfig: {
type: Array,
default: () => [],
},
showAdvancedButton: propTypes.bool.def(true),
// 是否是在 筛选(search) 中使用
inSearch: propTypes.bool.def(false),
},
emits: ['update:value', 'register', 'popUpChange', 'focus'],
setup(props, { emit, refs }) {
const { createMessage } = useMessage();
const attrs = useAttrs();
//pop是否展示
const avalid = ref(true);
const showText = ref('');
const innerShowText = 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 = {};
let labels = []
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;
});
if (props.inSearch) {
// 处理显示值
if (item.label) {
let txt = rows.map((row) => row[item.label]);
txt = txt.length == 1 ? txt[0] : txt.join(',');
labels.push(txt);
} else {
labels.push(val);
}
}
}
innerShowText.value = labels.join(',');
//传入表单示例方式赋值
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,
innerShowText,
avalid,
uniqGroupId,
attrs,
regModal,
handleOpen,
handleEmpty,
callBack,
};
},
});
</script>
<style lang="less" scoped>
// update-begin--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
.JPopup {
> .ant-form-item {
display: none;
}
}
// update-end--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
.components-input-demo-presuffix .anticon-close-circle {
cursor: pointer;
color: #ccc;
transition: color 0.3s;
font-size: 12px;
}
.components-input-demo-presuffix .anticon-close-circle:hover {
color: #f5222d;
}
.components-input-demo-presuffix .anticon-close-circle:active {
color: #666;
}
</style>
@@ -0,0 +1,240 @@
<!--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---forQQYUN-9260必填模式下会影响到弹窗内antd组件的样式 -->
<a-form-item>
<!--popup弹窗-->
<JPopupOnlReportModal
@register="regModal"
:code="code"
:multi="multi"
:selected="selected"
:rowkey="valueFiled"
:sorter="sorter"
:groupId="''"
:param="param"
:getFormValues="getFormValues"
:getContainer="getContainer"
:showAdvancedButton="showAdvancedButton"
@ok="callBack"
/>
</a-form-item>
<!-- update-end--author:liaozhiyang---date:20240515---forQQYUN-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(','),
getFormValues: propTypes.func,
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];
const selected = ref([]);
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] };
});
selected.value = data.records;
}
})
.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,
selected,
valueFiled,
};
},
});
</script>
<style lang="less" scoped>
// update-begin--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
.JPopupDict {
> .ant-form-item {
display: none;
}
}
// update-end--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
.components-input-demo-presuffix {
:deep(.ant-select-dropdown) {
display: none !important;
}
}
.components-input-demo-presuffix .anticon-close-circle {
cursor: pointer;
color: #ccc;
transition: color 0.3s;
font-size: 12px;
}
.components-input-demo-presuffix .anticon-close-circle:hover {
color: #f5222d;
}
.components-input-demo-presuffix .anticon-close-circle:active {
color: #666;
}
</style>
@@ -0,0 +1,65 @@
<template>
<a-range-picker v-model:value="rangeValue" @change="handleChange" :show-time="datetime" :placeholder="placeholder" :valueFormat="valueFormat"/>
</template>
<script>
import { defineComponent, ref, watch, computed } from 'vue';
import { propTypes } from '/@/utils/propTypes';
import { Form } from 'ant-design-vue';
const placeholder = ['开始日期', '结束日期']
/**
* 用于范围查询
*/
export default defineComponent({
name: "JRangeDate",
props:{
value: propTypes.string.def(''),
datetime: propTypes.bool.def(false),
placeholder: propTypes.string.def(''),
},
emits:['change', 'update:value'],
setup(props, {emit}){
const rangeValue = ref([])
const formItemContext = Form.useInjectFormItemContext();
watch(()=>props.value, (val)=>{
if(val){
rangeValue.value = val.split(',')
}else{
rangeValue.value = []
}
}, {immediate: true});
const valueFormat = computed(()=>{
if(props.datetime === true){
return 'YYYY-MM-DD HH:mm:ss'
}else{
return 'YYYY-MM-DD'
}
});
function handleChange(arr){
let str = ''
if(arr && arr.length>0){
// update-begin--author:liaozhiyang---date:20240710---for[issues/6368] rangeDate去掉判断允许起始项或结束项为空兼容allowEmpty
str = arr.join(',')
// update-end--author:liaozhiyang---date:20240710---for[issues/6368] rangeDate去掉判断允许起始项或结束项为空兼容allowEmpty
}
emit('change', str);
emit('update:value', str);
formItemContext.onFieldChange();
}
return {
rangeValue,
placeholder,
valueFormat,
handleChange
}
}
});
</script>
<style scoped>
</style>
@@ -0,0 +1,93 @@
<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 = [];
// update-begin--author:liaozhiyang---date:20240704---for:【TV360X-1749】数量0输入不了,输入清空了
let begin = beginValue.value ?? '';
let end = endValue.value ?? '';
// update-end--author:liaozhiyang---date:20240704---for:【TV360X-1749】数量0输入不了,输入清空了
arr.push(begin);
arr.push(end);
emit('change', arr);
emit('update:value', arr);
formItemContext.onFieldChange();
}
watch(
() => props.value,
(val) => {
if (val && val.length == 2) {
beginValue.value = val[0];
endValue.value = val[1];
} else {
beginValue.value = '';
endValue.value = '';
}
}, {immediate: true}
);
return {
beginValue,
endValue,
handleChangeBegin,
handleChangeEnd,
attrs,
};
},
};
</script>
<style lang="less" scoped>
// update-begin--author:liaozhiyang---date:20240607---for:【TV360X-214】范围查询控件没有根据配置格式化
.ant-input-group {
display: flex;
.ant-input-number {
&:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
&:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
}
// update-end--author:liaozhiyang---date:20240607---for:【TV360X-214】范围查询控件没有根据配置格式化
</style>
@@ -0,0 +1,53 @@
<template>
<a-time-range-picker v-model:value="rangeValue" @change="handleChange" :placeholder="placeholder" :valueFormat="format" :format="format"/>
</template>
<script>
import { defineComponent, ref, watch } from 'vue';
import { propTypes } from '/@/utils/propTypes';
import { Form } from 'ant-design-vue';
const placeholder = ['开始时间', '结束时间']
/**
* 用于时间-time组件的范围查询
*/
export default defineComponent({
name: "JRangeTime",
props:{
value: propTypes.string.def(''),
format: propTypes.string.def('HH:mm:ss'),
placeholder: propTypes.string.def(''),
},
emits:['change', 'update:value'],
setup(props, {emit}){
const rangeValue = ref([])
const formItemContext = Form.useInjectFormItemContext();
watch(()=>props.value, (val)=>{
if(val){
rangeValue.value = val.split(',')
}else{
rangeValue.value = []
}
}, {immediate: true});
function handleChange(arr){
let str = ''
if(arr && arr.length>0){
if(arr[1] && arr[0]){
str = arr.join(',')
}
}
emit('change', str);
emit('update:value', str);
formItemContext.onFieldChange();
}
return {
rangeValue,
placeholder,
handleChange
}
}
});
</script>
@@ -0,0 +1,576 @@
<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"
@popupScroll="handlePopupScroll"
:mode="multiple?'multiple':''"
@select="handleSelect"
@deselect="handleDeSelect"
>
<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"
:mode="multiple?'multiple':''"
@select="handleSelect"
@deselect="handleDeSelect"
>
<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';
import { isObject } from '/@/utils/is';
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
//update-begin---author:wangshuai---date:2025-04-17---for:【issues/8101】前端dict组件导致内存溢出问题:搜索组件支持多选---
//是否为多选
multiple:{
type: Boolean,
default: false
},
//update-end---author:wangshuai---date:2025-04-17---for:【issues/8101】前端dict组件导致内存溢出问题:搜索组件支持多选---
},
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);
// 异步(字典表) - 滚动加载时会用到
let isHasData = true;
let scrollLoading = false;
let pageNo = 1;
let searchKeyword = '';
// 是否是字典表
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;
}
// update-begin--author:liaozhiyang---date:20240731---for:【TV360X-1898】JsearchSelect组件传入字典表格式则支持滚动加载
pageNo = 1;
isHasData = true;
searchKeyword = value;
// update-end--author:liaozhiyang---date:20240731---for:【TV360X-1898】JsearchSelect组件传入字典表格式则支持滚动加载
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, pageNo },
})
.then((res) => {
loading.value = false;
if (res && res.length > 0) {
if (currentLoad != unref(lastLoad)) {
return;
}
options.value = res;
// update-begin--author:liaozhiyang---date:20240731---for:【TV360X-1898】JsearchSelect组件传入字典表格式则支持滚动加载
pageNo++;
// update-end--author:liaozhiyang---date:20240731---for:【TV360X-1898】JsearchSelect组件传入字典表格式则支持滚动加载
} else {
// update-begin--author:liaozhiyang---date:20240731---for:【TV360X-1898】JsearchSelect组件传入字典表格式则支持滚动加载
pageNo == 1 && (isHasData = false);
// update-end--author:liaozhiyang---date:20240731---for:【TV360X-1898】JsearchSelect组件传入字典表格式则支持滚动加载
}
});
}, 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) {
//update-begin---author:wangshuai---date:2025-04-17---for:【issues/8101】前端dict组件导致内存溢出问题:搜索组件支持多选---
//判断组件是否为多选
if(props.multiple){
if(value){
let arr: any = [];
//多选返回的是以逗号拼接的方式
let values = value.toString().split(',');
for (let i = 0; i < res.length; i++) {
let obj = {
key: values[i],
label: res[i],
};
arr.push(obj);
selectedValue.value.push(obj.key);
}
selectedAsyncValue.value = arr;
}
} else {
let obj = {
key: value,
label: res,
};
if (props.value == value) {
selectedAsyncValue.value = { ...obj };
}
//update-begin-author:taoyan date:2022-8-11 for: 值改变触发change事件--用于online关联记录配置页面
if(props.immediateChange == true){
emit('change', props.value);
}
//update-end-author:taoyan date:2022-8-11 for: 值改变触发change事件--用于online关联记录配置页面
//update-end---author:wangshuai---date:2025-04-17---for:【issues/8101】前端dict组件导致内存溢出问题:搜索组件支持多选---
}
}
});
}
} else {
//update-begin---author:wangshuai---date:2025-04-17---for:【issues/8101】前端dict组件导致内存溢出问题:搜索组件支持多选---
if(!props.multiple){
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他表字段配置界面
}else{
//多选的情况下需要转成数组
selectedValue.value = value.toString().split(',');
}
//update-begin---author:wangshuai---date:2025-04-17---for:【issues/8101】前端dict组件导致内存溢出问题:搜索组件支持多选---
}
}
/**
* 初始化字典下拉数据
*/
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 {
// update-begin--author:liaozhiyang---date:20240731---for:【TV360X-1898】JsearchSelect组件传入字典表格式则支持滚动加载
pageNo = 1;
isHasData = true;
searchKeyword = '';
// update-end--author:liaozhiyang---date:20240731---for:【TV360X-1898】JsearchSelect组件传入字典表格式则支持滚动加载
//异步一开始也加载一点数据
loading.value = true;
let keywordInfo = getKeywordParam('');
defHttp
.get({
url: `/sys/dict/loadDict/${dict}`,
params: { pageSize: pageSize, keyword: keywordInfo, pageNo },
})
.then((res) => {
loading.value = false;
if (res && res.length > 0) {
options.value = res;
// update-begin--author:liaozhiyang---date:20240731---for:【TV360X-1898】JsearchSelect组件传入字典表格式则支持滚动加载
pageNo++;
// update-end--author:liaozhiyang---date:20240731---for:【TV360X-1898】JsearchSelect组件传入字典表格式则支持滚动加载
} else {
// update-begin--author:liaozhiyang---date:20240731---for:【TV360X-1898】JsearchSelect组件传入字典表格式则支持滚动加载
pageNo == 1 && (isHasData = false);
// update-end--author:liaozhiyang---date:20240731---for:【TV360X-1898】JsearchSelect组件传入字典表格式则支持滚动加载
}
});
}
}
}
/**
* 查询数据字典
*/
async function initDictCodeData() {
options.value = await initDictOptions(props.dict);
}
/**
* 同步改变事件
* */
function handleChange(value) {
//update-begin---author:wangshuai---date:2025-04-17---for:【issues/8101】前端dict组件导致内存溢出问题:搜索组件支持多选---
//多选也会触发change事件,需要判断如果时多选不需要赋值
if(!props.multiple){
selectedValue.value = value;
callback();
}
//update-end---author:wangshuai---date:2025-04-17---for:【issues/8101】前端dict组件导致内存溢出问题:搜索组件支持多选---
}
/**
* 异步改变事件
* */
function handleAsyncChange(selectedObj) {
//update-begin---author:wangshuai---date:2025-04-17---for:【issues/8101】前端dict组件导致内存溢出问题:搜索组件支持多选---
// 单选情况下使用change事件
if(!props.multiple){
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】下拉搜索设置了默认值,把查询条件删掉,再点击重置,没附上值
}
//update-end---author:wangshuai---date:2025-04-17---for:【issues/8101】前端dict组件导致内存溢出问题:搜索组件支持多选---
}
//update-begin---author:wangshuai---date:2025-04-17---for:【issues/8101】前端dict组件导致内存溢出问题:搜索组件支持多选---
/**
* 异步值选中事件
* @param selectedObj
*/
function handleSelect(selectedObj){
let key = selectedObj;
if(props.async){
key = selectedObj.key;
}
//多选情况下使用select事件
if(props.multiple && key){
//异步的时候才需要在selectedValue数组中添加值操作,同步的情况下直接走更新值操作
if(props.async){
selectedValue.value.push(key);
}
selectedObj ?? (loadSelectText.value = true);
callback();
}
}
/**
* 异步值取消选中事件
* @param selectedObj
*/
function handleDeSelect(selectedObj){
let key = selectedObj;
if(props.async){
key = selectedObj.key;
}
//多选情况下使用select事件
if(props.multiple){
//异步的时候才需要在selectedValue数组中删除值操作,同步的情况下直接走更新值操作
if(props.async){
let findIndex = selectedValue.value.findIndex(item => item === key);
if(findIndex != -1){
selectedValue.value.splice(findIndex,1);
}
}
selectedObj ?? (loadSelectText.value = true);
callback();
}
}
//update-end---author:wangshuai---date:2025-04-17---for:【issues/8101】前端dict组件导致内存溢出问题:搜索组件支持多选---
/**
*回调方法
* */
function callback() {
loadSelectText.value = false;
//update-begin---author:wangshuai---date:2025-04-17---for:【issues/8101】前端dict组件导致内存溢出问题:搜索组件支持多选---
//单选直接走更新值操作
if(!props.multiple){
emit('change', unref(selectedValue));
emit('update:value', unref(selectedValue));
} else {
//多选需要把数组转成字符串
emit('change', unref(selectedValue).join(","));
emit('update:value', unref(selectedValue).join(","));
}
//update-end---author:wangshuai---date:2025-04-17---for:【issues/8101】前端dict组件导致内存溢出问题:搜索组件支持多选---
}
/**
* 过滤选中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 = () => {
// update-begin--author:liaozhiyang---date:20240709---for:【issues/6681】异步查询不生效
if ((isObject(selectedAsyncValue.value) || selectedAsyncValue.value?.length) && isDictTable.value && props.async) {
// update-begin--author:liaozhiyang---date:20240809---for:【TV360X-2062】下拉搜索选择第二页数据后,第一次点击时(得到焦点)滚动条没复原到初始位置且数据会加载第二页数据(应该只加载第一页数据)
options.value = [];
// update-end--author:liaozhiyang---date:20240809---for:【TV360X-2062】下拉搜索选择第二页数据后,第一次点击时(得到焦点)滚动条没复原到初始位置且数据会加载第二页数据(应该只加载第一页数据)
initDictTableData();
}
// update-begin--author:liaozhiyang---date:20240919---for:【TV360X-2348】得到焦点时options选项显示第一页内容(解决新增时显示非第一页内容)
if (Array.isArray(selectedAsyncValue.value) && selectedAsyncValue.value.length === 0 && isDictTable.value && props.async) {
if (pageNo > 2) {
options.value = [];
initDictTableData();
}
}
// update-end--author:liaozhiyang---date:20240919---for:【TV360X-2348】得到焦点时options选项显示第一页内容(解决新增时显示非第一页内容)
attrs.onFocus?.();
};
// update-end--author:liaozhiyang---date:20240523---for:【TV360X-26】下拉搜索控件选中选项后再次点击下拉应该显示初始的下拉选项,而不是只展示选中结果
/**
* 2024-07-30
* liaozhiyang
* 【TV360X-1898】JsearchSelect组件传入字典表格式则支持滚动加载
* */
const handlePopupScroll = async (e) => {
// 字典表才才支持滚动加载
if (isDictTable.value) {
const { target } = e;
const { scrollTop, scrollHeight, clientHeight } = target;
if (!scrollLoading && isHasData && scrollTop + clientHeight >= scrollHeight - 10) {
scrollLoading = true;
let keywordInfo = getKeywordParam(searchKeyword);
defHttp
.get({ url: `/sys/dict/loadDict/${props.dict}`, params: { pageSize: props.pageSize, keyword: keywordInfo, pageNo } })
.then((res) => {
loading.value = false;
if (res?.length > 0) {
// 防止开源只更新了前端代码没更新后端代码(第一页和第二页面的第一条数据相同则是后端代码没更新,没分页)
if (JSON.stringify(res[0]) === JSON.stringify(options.value[0])) {
isHasData = false;
return;
}
options.value.push(...res);
pageNo++;
} else {
isHasData = false;
}
})
.finally(() => {
scrollLoading = false;
})
.catch(() => {
pageNo != 1 && pageNo--;
});
}
}
};
return {
attrs,
options,
loading,
isDictTable,
selectedValue,
selectedAsyncValue,
loadData: useDebounceFn(loadData, 800),
getParentContainer,
filterOption,
handleChange,
handleAsyncChange,
handleAsyncFocus,
handlePopupScroll,
handleSelect,
handleDeSelect,
};
},
});
</script>
<style scoped></style>
@@ -0,0 +1,207 @@
<!--部门选择组件-->
<template>
<div class="JSelectDept">
<JSelectBiz @change="handleSelectChange" @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs"/>
<!-- update-begin--author:liaozhiyang---date:20240515---forQQYUN-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---forQQYUN-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-begin--author:liaozhiyang---date:20240627---for:【TV360X-1648】用户编辑界面“所属部门”与“负责部门”联动出错(同步之前丢的代码)
if (!values || values.length == 0) {
emit('select', null, null);
}
// update-end--author:liaozhiyang---date:20240627---for:【TV360X-1648】用户编辑界面“所属部门”与“负责部门”联动出错(同步之前丢的代码)
};
// update-end--author:liaozhiyang---date:20240527---for:【TV360X-414】部门设置了默认值,查询重置变成空了(同步JSelectUser组件改法)
return {
// state,
attrs,
selectOptions,
selectValues,
loadingEcho,
getBindValue,
tag,
regModal,
setValue,
handleOpen,
handleClose,
handleSelectChange,
};
},
});
</script>
<style lang="less" scoped>
// update-begin--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
.JSelectDept {
> .ant-form-item {
display: none;
}
}
// update-end--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
.j-select-row {
@width: 82px;
.left {
width: calc(100% - @width - 8px);
}
.right {
width: @width;
}
.full {
width: 100%;
}
:deep(.ant-select-search__field) {
display: none !important;
}
}
</style>
@@ -0,0 +1,89 @@
<template>
<a-select v-bind="bindProps" @change="onChange" @search="onSearch" />
</template>
<script lang="ts">
import { propTypes } from '/@/utils/propTypes';
import { defineComponent, ref, watch, computed } from 'vue';
// 可以输入的下拉框(此组件暂时没有人用)
export default defineComponent({
name: 'JSelectInput',
props: {
options: propTypes.array.def(() => []),
},
emits: ['change', 'update:value'],
setup(props, { emit, attrs }) {
// 内部 options 选项
const options = ref<any[]>([]);
// 监听外部 options 变化,并覆盖内部 options
watch(
() => props.options,
() => {
options.value = [...props.options];
},
{ deep: true, immediate: true }
);
// 合并 props 和 attrs
const bindProps: any = computed(() =>
Object.assign(
{
showSearch: true,
},
props,
attrs,
{
options: options.value,
}
)
);
function onChange(...args: any[]) {
deleteSearchAdd(args[0]);
emit('change', ...args);
emit('update:value', args[0]);
}
function onSearch(value) {
// 是否找到了对应的项,找不到则添加这一项
let foundIt =
options.value.findIndex((option) => {
return option.value.toString() === value.toString();
}) !== -1;
// !!value :不添加空值
if (!foundIt && !!value) {
deleteSearchAdd(value);
// searchAdd 是否是通过搜索添加的
options.value.push({ value: value, searchAdd: true });
//onChange(value,{ value })
} else if (foundIt) {
onChange(value);
}
}
// 删除无用的因搜索(用户输入)而创建的项
function deleteSearchAdd(value = '') {
let indexes: any[] = [];
options.value.forEach((option, index) => {
if (option.searchAdd) {
if ((option.value ?? '').toString() !== value.toString()) {
indexes.push(index);
}
}
});
// 翻转删除数组中的项
for (let index of indexes.reverse()) {
options.value.splice(index, 1);
}
}
return {
bindProps,
onChange,
onSearch,
};
},
});
</script>
<style scoped></style>
@@ -0,0 +1,199 @@
<!--字典下拉多选-->
<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.dictCode,
() => {
if (props.dictCode) {
loadDictOptions();
} else {
dictOptions.value = props.options;
}
}
);
watch(
() => props.value,
(val) => {
if (!val) {
arrayValue.value = [];
} else {
arrayValue.value = props.value.split(props.spliter);
}
}
);
//适用于 动态改变下拉选项的操作
watch(()=>props.options, ()=>{
if (props.dictCode) {
// nothing to do
} else {
dictOptions.value = props.options;
}
});
function onChange(selectedValue) {
if (props.triggerChange) {
emit('change', selectedValue.join(props.spliter));
emit('update:value', selectedValue.join(props.spliter));
} else {
emit('input', selectedValue.join(props.spliter));
emit('update:value', selectedValue.join(props.spliter));
}
// update-begin--author:liaozhiyang---date:20240429---for:【QQYUN-9110】组件有值校验没消失
nextTick(() => {
formItemContext?.onFieldChange();
});
// update-end--author:liaozhiyang---date:20240429---for:【QQYUN-9110】组件有值校验没消失
}
function getParentContainer(node) {
if (!props.popContainer) {
return node?.parentNode;
} else {
// update-begin--author:liaozhiyang---date:20240517---for:【QQYUN-9339】有多个modal弹窗内都有下拉字典多选和下拉搜索组件时,打开另一个modal时组件的options不展示
return setPopContainer(node, props.popContainer);
// update-end--author:liaozhiyang---date:20240517---for:【QQYUN-9339】有多个modal弹窗内都有下拉字典多选和下拉搜索组件时,打开另一个modal时组件的options不展示
}
}
// 根据字典code查询字典项
function loadDictOptions() {
//update-begin-author:taoyan date:2022-6-21 for: 字典数据请求前将参数编码处理,但是不能直接编码,因为可能之前已经编码过了
let temp = props.dictCode || '';
if (temp.indexOf(',') > 0 && temp.indexOf(' ') > 0) {
// 编码后 是不包含空格的
temp = encodeURI(temp);
}
//update-end-author:taoyan date:2022-6-21 for: 字典数据请求前将参数编码处理,但是不能直接编码,因为可能之前已经编码过了
getDictItems(temp).then((res) => {
if (res) {
dictOptions.value = res.map((item) => ({ value: item.value, label: item.text, color:item.color }));
//console.info('res', dictOptions.value);
} else {
console.error('getDictItems error: : ', res);
dictOptions.value = [];
}
});
}
//update-begin-author:taoyan date:2022-5-31 for: VUEN-1145 下拉多选,搜索时,查不到数据
function filterOption(input, option) {
return option.children()[0].children.toLowerCase().indexOf(input.toLowerCase()) >= 0;
}
//update-end-author:taoyan date:2022-5-31 for: VUEN-1145 下拉多选,搜索时,查不到数据
return {
state,
attrs,
dictOptions,
onChange,
arrayValue,
getParentContainer,
filterOption,
};
},
});
</script>
<style scoped lang='less'>
.colorText{
display: inline-block;
height: 20px;
line-height: 20px;
padding: 0 6px;
border-radius: 8px;
background-color: red;
color: #fff;
font-size: 12px;
}
</style>
@@ -0,0 +1,178 @@
<!--职务选择组件-->
<template>
<div class="JSelectPosition">
<JSelectBiz @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs" @change="(changeValue) => $emit('update:value', changeValue)"></JSelectBiz>
<!-- update-begin--author:liaozhiyang---date:20240515---forQQYUN-9260必填模式下会影响到弹窗内antd组件的样式 -->
<a-form-item>
<PositionSelectModal @register="regModal" @getSelectResult="setValue" v-bind="getBindValue"></PositionSelectModal>
</a-form-item>
<!-- update-end--author:liaozhiyang---date:20240515---forQQYUN-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<any>({
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();
/**
* 监听组件值
*/
// update-begin--author:liaozhiyang---date:20250423---for:【pull/8014】插槽方式弹窗中取消该数据checkbox的选中状态,需要点击第二次才生效。
watch(
() => props.value,
() => {
if (props.value) {
initValue();
} else {
// update-begin--author:liaozhiyang---date:20250604---for:【issues/8233】resetFields时无法重置
if (selectValues.value?.length) {
selectValues.value = [];
}
// update-end--author:liaozhiyang---date:20250604---for:【issues/8233】resetFields时无法重置
}
},
{ deep: true, immediate: true }
);
// update-end--author:liaozhiyang---date:20250423---for:【pull/8014】插槽方式弹窗中取消该数据checkbox的选中状态,需要点击第二次才生效。
/**
* 监听selectValues变化
*/
watch(selectValues, () => {
if (selectValues) {
state.value = selectValues.value;
}
});
/**
* 打卡弹出框
*/
function handleOpen() {
tag.value = true;
openModal(true, {
isUpdate: false,
});
}
/**
* 将字符串值转化为数组
*/
function initValue() {
let value = props.value ? props.value : [];
if (value && typeof value === 'string' && value != 'null' && value != 'undefined') {
state.value = value.split(',');
selectValues.value = value.split(',');
}
}
/**
* 设置下拉框的值
*/
function setValue(options, values) {
selectOptions.value = options;
//emitData.value = values.join(",");
state.value = values;
selectValues.value = values;
//update-begin-author:liusq date:20230517 for:选择职务组件v-model方式绑定值不生效
emit('update:value', values.join(','));
//update-begin-author:liusq date:20230517 for:选择职务组件v-model方式绑定值不生效
}
const getBindValue = Object.assign({}, unref(props), unref(attrs));
return {
state,
getBindValue,
attrs,
selectOptions,
selectValues,
loadingEcho,
tag,
regModal,
setValue,
handleOpen,
};
},
});
</script>
<style lang="less" scoped>
// update-begin--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
.JSelectPosition {
> .ant-form-item {
display: none;
}
}
// update-end--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
.j-select-row {
@width: 82px;
.left {
width: calc(100% - @width - 8px);
}
.right {
width: @width;
}
.full {
width: 100%;
}
:deep(.ant-select-search__field) {
display: none !important;
}
}
</style>
@@ -0,0 +1,168 @@
<!--角色选择组件-->
<template>
<div class="JSelectRole">
<JSelectBiz @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs"></JSelectBiz>
<!-- update-begin--author:liaozhiyang---date:20240515---forQQYUN-9260必填模式下会影响到弹窗内antd组件的样式 -->
<a-form-item>
<RoleSelectModal @register="regModal" @getSelectResult="setValue" v-bind="getBindValue"></RoleSelectModal>
</a-form-item>
<!-- update-end--author:liaozhiyang---date:20240515---forQQYUN-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', '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: [],
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;
// update-begin--author:liaozhiyang---date:20250318---for:【issues/7948】修复JselectRole组件不支持双向绑定
emit('update:value', values);
// update-end--author:liaozhiyang---date:20250318---for:【issues/7948】修复JselectRole组件不支持双向绑定
}
const getBindValue = Object.assign({}, unref(props), unref(attrs));
return {
state,
attrs,
getBindValue,
selectOptions,
selectValues,
loadingEcho,
tag,
regModal,
setValue,
handleOpen,
};
},
});
</script>
<style lang="less" scoped>
// update-begin--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
.JSelectRole {
> .ant-form-item {
display: none;
}
}
// update-end--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
.j-select-row {
@width: 82px;
.left {
width: calc(100% - @width - 8px);
}
.right {
width: @width;
}
.full {
width: 100%;
}
:deep(.ant-select-search__field) {
display: none !important;
}
}
</style>
@@ -0,0 +1,222 @@
<!--用户选择组件-->
<template>
<div class="JselectUser">
<JSelectBiz @change="handleSelectChange" @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs"></JSelectBiz>
<!-- update-begin--author:liaozhiyang---date:20240515---forQQYUN-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---forQQYUN-9260必填模式下会影响到弹窗内antd组件的样式 -->
</div>
</template>
<script lang="ts">
import { unref } from 'vue';
import UserSelectModal from './modal/UserSelectModal.vue';
import JSelectBiz from './base/JSelectBiz.vue';
import { defineComponent, ref, reactive, watchEffect, watch, provide } from 'vue';
import { useModal } from '/@/components/Modal';
import { propTypes } from '/@/utils/propTypes';
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { SelectValue } from 'ant-design-vue/es/select';
import { cloneDeep } from 'lodash-es';
export default defineComponent({
name: 'JSelectUser',
components: {
UserSelectModal,
JSelectBiz,
},
inheritAttrs: false,
props: {
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
labelKey: {
type: String,
default: 'realname',
},
rowKey: {
type: String,
default: 'username',
},
params: {
type: Object,
default: () => {},
},
//update-begin---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
//排除用户id的集合
excludeUserIdList:{
type: Array,
default: () => [],
}
//update-end---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
},
emits: ['options-change', 'change', 'update:value'],
setup(props, { emit }) {
const emitData = ref<any[]>();
//注册model
const [regModal, { openModal }] = useModal();
//表单值
// const [state] = useRuleFormItem(props, 'value', 'change', emitData);
//下拉框选项值
const selectOptions = ref<SelectValue>([]);
//下拉框选中值
let selectValues = reactive<Recordable>({
value: [],
change: false,
});
let tempSave: any = [];
// 是否正在加载回显数据
const loadingEcho = ref<boolean>(false);
//下发 selectOptions,xxxBiz组件接收
provide('selectOptions', selectOptions);
//下发 selectValues,xxxBiz组件接收
provide('selectValues', selectValues);
//下发 loadingEcho,xxxBiz组件接收
provide('loadingEcho', loadingEcho);
const tag = ref(false);
const attrs = useAttrs();
/**
* 监听组件值
*/
watchEffect(() => {
// update-begin--author:liaozhiyang---date:20240611---for:【TV360X-576】已选中了数据,再次选择打开弹窗点击取消,数据清空了
//update-begin-author:liusq---date:2024-06-03--for: [TV360X-840]用户授权,没有选择,点取消,也会回显一个选过的用户
tempSave = [];
//update-end-author:liusq---date:2024-06-03--for:[TV360X-840]用户授权,没有选择,点取消,也会回显一个选过的用户
// update-end--author:liaozhiyang---date:20240611---for:【TV360X-576】已选中了数据,再次选择打开弹窗点击取消,数据清空了
props.value && initValue();
// 查询条件重置的时候 界面显示未清空
if (!props.value) {
selectValues.value = [];
}
});
/**
* 监听selectValues变化
*/
// watch(selectValues, () => {
// if (selectValues) {
// state.value = selectValues.value;
// }
// });
//update-begin---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
const excludeUserIdList = ref<any>([]);
/**
* 需要监听一下excludeUserIdList,否则modal获取不到
*/
watch(()=>props.excludeUserIdList,(data)=>{
excludeUserIdList.value = data;
},{ immediate: true })
//update-end---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
/**
* 打卡弹出框
*/
function handleOpen() {
tag.value = true;
openModal(true, {
isUpdate: false,
});
}
/**
* 将字符串值转化为数组
*/
function initValue() {
let value = props.value ? props.value : [];
if (value && typeof value === 'string' && value != 'null' && value != 'undefined') {
// state.value = value.split(',');
selectValues.value = value.split(',');
tempSave = value.split(',');
} else {
// 【VUEN-857】兼容数组(行编辑的用法问题)
selectValues.value = value;
tempSave = cloneDeep(value);
}
}
/**
* 设置下拉框的值
*/
function setValue(options, values) {
selectOptions.value = options;
//emitData.value = values.join(",");
// state.value = values;
selectValues.value = values;
send(values);
}
const getBindValue = Object.assign({}, unref(props), unref(attrs));
// update-begin--author:liaozhiyang---date:20240517---for:【QQYUN-9366】用户选择组件取消和关闭会把选择数据带入
const handleClose = () => {
if (tempSave.length) {
selectValues.value = cloneDeep(tempSave);
} else {
send(tempSave);
}
};
const handleSelectChange = (values) => {
tempSave = cloneDeep(values);
send(tempSave);
};
const send = (values) => {
let result = typeof props.value == "string" ? values.join(',') : values;
emit('update:value', result);
emit('change', result);
};
// update-end--author:liaozhiyang---date:20240517---for:【QQYUN-9366】用户选择组件取消和关闭会把选择数据带入
return {
// state,
attrs,
selectOptions,
getBindValue,
selectValues,
loadingEcho,
tag,
regModal,
setValue,
handleOpen,
excludeUserIdList,
handleClose,
handleSelectChange,
};
},
});
</script>
<style lang="less" scoped>
// update-begin--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
.JselectUser {
> .ant-form-item {
display: none;
}
}
// update-end--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
.j-select-row {
@width: 82px;
.left {
width: calc(100% - @width - 8px);
}
.right {
width: @width;
}
.full {
width: 100%;
}
:deep(.ant-select-search__field) {
display: none !important;
}
}
</style>
@@ -0,0 +1,176 @@
<!--用户选择组件-->
<template>
<div>
<JSelectBiz @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs" @change="handleSelectChange"></JSelectBiz>
<JSelectUserByDepartmentModal
v-if="modalShow"
:selectedUser="selectOptions"
:modalTitle="modalTitle"
:rowKey="rowKey"
:labelKey="labelKey"
:isRadioSelection="isRadioSelection"
:params="params"
@register="regModal"
@change="setValue"
@close="() => (modalShow = false)"
v-bind="attrs"
></JSelectUserByDepartmentModal>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, watch, provide } from 'vue';
import JSelectUserByDepartmentModal from './modal/JSelectUserByDepartmentModal.vue';
import JSelectBiz from './base/JSelectBiz.vue';
import { useModal } from '/@/components/Modal';
import { propTypes } from '/@/utils/propTypes';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { SelectValue } from 'ant-design-vue/es/select';
import { isArray, isString, isObject } from '/@/utils/is';
import { getTableList as getTableListOrigin } from '/@/api/common/api';
import { useMessage } from '/@/hooks/web/useMessage';
defineOptions({ name: 'JSelectUserByDepartment' });
const props = defineProps({
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
modalTitle: {
type: String,
default: '部门用户选择',
},
rowKey: {
type: String,
default: 'username',
},
labelKey: {
type: String,
default: 'realname',
},
//查询参数
params: {
type: Object,
default: () => {},
},
isRadioSelection: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['options-change', 'change', 'update:value']);
const { createMessage } = useMessage();
//注册model
const [regModal, { openModal }] = useModal();
// 是否显示弹窗
const modalShow = ref(false);
//下拉框选项值
const selectOptions: any = ref<SelectValue>([]);
//下拉框选中值
let selectValues: any = 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 attrs: any = useAttrs();
// 打开弹窗
function handleOpen() {
modalShow.value = true;
setTimeout(() => {
openModal(true, {
isUpdate: false,
});
}, 0);
}
const handleSelectChange = (data) => {
selectOptions.value = selectOptions.value.filter((item) => data.includes(item[props.rowKey]));
setValue(selectOptions.value);
};
// 设置下拉框的值
const setValue = (data) => {
selectOptions.value = data.map((item) => {
return {
...item,
label: item[props.labelKey],
value: item[props.rowKey],
};
});
selectValues.value = data.map((item) => item[props.rowKey]);
// 更新value
emit('update:value', selectValues.value);
// 触发change事件(不转是因为basicForm提交时会自动将字符串转化为数组)
emit('change', selectValues.value);
// 触发options-change事件
emit('options-change', selectOptions.value);
};
// 翻译
const transform = () => {
let value = props.value;
let len;
if (isArray(value) || isString(value)) {
if (isArray(value)) {
len = value.length;
value = value.join(',');
} else {
len = value.split(',').length;
}
value = value.trim();
if (value) {
// 如果value的值在selectedUser中存在,则不请求翻译
let isNotRequestTransform = false;
isNotRequestTransform = value.split(',').every((value) => !!selectOptions.value.find((item) => item[props.rowKey] === value));
if (isNotRequestTransform) {
selectValues.value = value.split(',')
return;
}
const params = { isMultiTranslate: true, pageSize: len, [props.rowKey]: value };
if (isObject(attrs.params)) {
Object.assign(params, attrs.params);
}
getTableListOrigin(params).then((result: any) => {
const records = result.records ?? [];
selectValues.value = records.map((item) => item[props.rowKey]);
selectOptions.value = records.map((item) => {
return {
...item,
label: item[props.labelKey],
value: item[props.rowKey],
};
});
});
}
} else {
selectValues.value = [];
}
};
// 监听value变化
watch(
() => props.value,
() => {
transform();
},
{ deep: true, immediate: true }
);
</script>
<style lang="less" scoped>
.j-select-row {
@width: 82px;
.left {
width: calc(100% - @width - 8px);
}
.right {
width: @width;
}
.full {
width: 100%;
}
:deep(.ant-select-search__field) {
display: none !important;
}
}
</style>
@@ -0,0 +1,153 @@
<!--用户选择组件-->
<template>
<div>
<JSelectBiz @change="handleChange" @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs"></JSelectBiz>
<UserSelectByDepModal :rowKey="rowKey" @register="regModal" @getSelectResult="setValue" v-bind="getBindValue"></UserSelectByDepModal>
</div>
</template>
<script lang="ts">
import { unref } from 'vue';
import UserSelectByDepModal from './modal/UserSelectByDepModal.vue';
import JSelectBiz from './base/JSelectBiz.vue';
import { defineComponent, ref, reactive, watchEffect, watch, provide } from 'vue';
import { useModal } from '/@/components/Modal';
import { propTypes } from '/@/utils/propTypes';
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { SelectValue } from 'ant-design-vue/es/select';
export default defineComponent({
name: 'JSelectUserByDept',
components: {
UserSelectByDepModal,
JSelectBiz,
},
inheritAttrs: false,
props: {
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
rowKey: {
type: String,
default: 'username',
},
labelKey: {
type: String,
default: 'realname',
},
},
emits: ['options-change', 'change', 'update:value'],
setup(props, { emit, refs }) {
const emitData = ref<any[]>();
//注册model
const [regModal, { openModal }] = useModal();
//表单值
const [state] = useRuleFormItem(props, 'value', 'change', emitData);
//下拉框选项值
const selectOptions = ref<SelectValue>([]);
//下拉框选中值
let selectValues = reactive<object>({
value: [],
change: false,
});
// 是否正在加载回显数据
const loadingEcho = ref<boolean>(false);
//下发 selectOptions,xxxBiz组件接收
provide('selectOptions', selectOptions);
//下发 selectValues,xxxBiz组件接收
provide('selectValues', selectValues);
//下发 loadingEcho,xxxBiz组件接收
provide('loadingEcho', loadingEcho);
const tag = ref(false);
const attrs = useAttrs();
/**
* 监听组件值
*/
watchEffect(() => {
initValue();
});
/**
* 监听selectValues变化
*/
watch(selectValues, () => {
if (selectValues) {
state.value = selectValues.value;
}
});
/**
* 打卡弹出框
*/
function handleOpen() {
tag.value = true;
openModal(true, {
isUpdate: false,
});
}
/**
* 将字符串值转化为数组
*/
function initValue() {
let value = props.value ? props.value : [];
if (value && typeof value === 'string' && value != 'null' && value != 'undefined') {
state.value = value.split(',');
selectValues.value = value.split(',');
} else {
selectValues.value = value;
}
}
/**
* 设置下拉框的值
*/
function setValue(options, values) {
selectOptions.value = options;
//emitData.value = values.join(",");
state.value = values;
selectValues.value = values;
emit('update:value', values);
emit('options-change', options);
}
function handleChange(values) {
emit('update:value', values);
}
const getBindValue = Object.assign({}, unref(props), unref(attrs));
return {
state,
attrs,
selectOptions,
getBindValue,
selectValues,
loadingEcho,
tag,
regModal,
setValue,
handleOpen,
handleChange,
};
},
});
</script>
<style lang="less" scoped>
.j-select-row {
@width: 82px;
.left {
width: calc(100% - @width - 8px);
}
.right {
width: @width;
}
.full {
width: 100%;
}
:deep(.ant-select-search__field) {
display: none !important;
}
}
</style>
@@ -0,0 +1,85 @@
<template>
<div :class="prefixCls">
<a-select
v-if="query"
v-model:value="state"
:options="selectOptions"
:disabled="disabled"
style="width: 100%"
v-bind="attrs"
@change="onSelectChange"
/>
<a-switch v-else v-model:checked="checked" :disabled="disabled" v-bind="attrs" @change="onSwitchChange" />
</div>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { propTypes } from '/@/utils/propTypes';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { useDesign } from '/@/hooks/web/useDesign';
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
const { prefixCls } = useDesign('j-switch');
const props = defineProps({
// v-model:value
value: propTypes.oneOfType([propTypes.string, propTypes.number]),
// 取值 options
options: propTypes.array.def(() => ['Y', 'N']),
// 文本 options
labelOptions: propTypes.array.def(() => ['是', '否']),
// 是否使用下拉
query: propTypes.bool.def(false),
// 是否禁用
disabled: propTypes.bool.def(false),
});
const attrs = useAttrs();
const emit = defineEmits(['change', 'update:value']);
const checked = ref<boolean>(false);
const [state] = useRuleFormItem(props, 'value', 'change');
watch(
() => props.value,
(val) => {
if (!props.query) {
// update-begin--author:liaozhiyang---date:20231226---for:【QQYUN-7473】options使用[0,1],导致开关无法切换
if (!val && !props.options.includes(val)) {
checked.value = false;
emitValue(props.options[1]);
} else {
checked.value = props.options[0] == val;
}
// update-end--author:liaozhiyang---date:20231226---for:【QQYUN-7473】options使用[0,1],导致开关无法切换
}
},
{ immediate: true }
);
const selectOptions = computed(() => {
let options: any[] = [];
options.push({ value: props.options[0], label: props.labelOptions[0] });
options.push({ value: props.options[1], label: props.labelOptions[1] });
return options;
});
function onSwitchChange(checked) {
let flag = checked === false ? props.options[1] : props.options[0];
emitValue(flag);
}
function onSelectChange(value) {
emitValue(value);
}
function emitValue(value) {
emit('change', value);
emit('update:value', value);
}
</script>
<style lang="less">
//noinspection LessUnresolvedVariable
@prefix-cls: ~'@{namespace}-j-switch';
.@{prefix-cls} {
}
</style>
@@ -0,0 +1,141 @@
<template>
<TreeSelect
:class="prefixCls"
:value="treeValue"
:treeData="treeData"
:loadData="asyncLoadTreeData"
allowClear
labelInValue
:dropdownStyle="{ maxHeight: '400px', overflow: 'auto' }"
style="width: 100%"
v-bind="attrs"
@change="onChange"
>
</TreeSelect>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { defHttp } from '/@/utils/http/axios';
import { propTypes } from '/@/utils/propTypes';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { useDesign } from '/@/hooks/web/useDesign';
import { TreeSelect } from 'ant-design-vue';
enum Api {
view = '/sys/category/loadOne',
root = '/sys/category/loadTreeRoot',
children = '/sys/category/loadTreeChildren',
}
const { prefixCls } = useDesign('j-tree-dict');
const props = defineProps({
// v-model:value
value: propTypes.string.def(''),
field: propTypes.string.def('id'),
parentCode: propTypes.string.def(''),
async: propTypes.bool.def(false),
});
const attrs = useAttrs();
const emit = defineEmits(['change', 'update:value']);
const treeData = ref<any[]>([]);
const treeValue = ref<any>(null);
watch(
() => props.value,
() => loadViewInfo(),
{ deep: true, immediate: true }
);
watch(
() => props.parentCode,
() => loadRoot(),
{ deep: true, immediate: true }
);
async function loadViewInfo() {
if (!props.value || props.value == '0') {
treeValue.value = { value: null, label: null };
} else {
let params = { field: props.field, val: props.value };
let result = await defHttp.get({ url: Api.view, params });
treeValue.value = {
value: props.value,
label: result.name,
};
}
}
async function loadRoot() {
let params = {
async: props.async,
pcode: props.parentCode,
};
let result = await defHttp.get({ url: Api.root, params });
treeData.value = [...result];
handleTreeNodeValue(result);
}
async function asyncLoadTreeData(treeNode) {
if (!props.async || treeNode.dataRef.children) {
return Promise.resolve();
}
let pid = treeNode.dataRef.key;
let params = { pid: pid };
let result = await defHttp.get({ url: Api.children, params });
handleTreeNodeValue(result);
addChildren(pid, result, treeData.value);
treeData.value = [...treeData.value];
return Promise.resolve();
}
function addChildren(pid, children, treeArray) {
if (treeArray && treeArray.length > 0) {
for (let item of treeArray) {
if (item.key == pid) {
if (!children || children.length == 0) {
item.leaf = true;
} else {
item.children = children;
}
break;
} else {
addChildren(pid, children, item.children);
}
}
}
}
function handleTreeNodeValue(result) {
let storeField = props.field == 'code' ? 'code' : 'key';
for (let i of result) {
i.value = i[storeField];
i.isLeaf = i.leaf;
if (i.children && i.children.length > 0) {
handleTreeNodeValue(i.children);
}
}
}
function onChange(value) {
if (!value) {
emitValue('');
} else {
emitValue(value.value);
}
treeValue.value = value;
}
function emitValue(value) {
emit('change', value);
emit('update:value', value);
}
</script>
<style lang="less">
//noinspection LessUnresolvedVariable
@prefix-cls: ~'@{namespace}-j-tree-dict';
.@{prefix-cls} {
}
</style>
@@ -0,0 +1,471 @@
<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"
>
<template #[name]="data" v-for="name in slotNamesGroup" :key="name">
<slot :name="name" v-bind="data"></slot>
</template>
</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, computed } 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';
import { isObject, isArray } from '/@/utils/is';
import { useI18n } from '/@/hooks/web/useI18n';
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 { t } = useI18n();
const emit = defineEmits(['change', 'update:value']);
const slots = defineSlots();
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变化
*/
// update-begin--author:liaozhiyang---date:20240612---for:【issues/1283】JtreeSelect组件初始调用了两次接口
watch(
() => props.dict,
() => {
initDictInfo();
loadRoot();
}
);
// update-end--author:liaozhiyang---date:20240612---for:【issues/1283】JtreeSelect组件初始调用了两次接口
// 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{
// update-begin--author:liaozhiyang---date:20250423---for:【issues/8093】选择节点后会先变成编码再显示label文字
if (props.value) {
if (isArray(treeValue.value)) {
let isNotRequestTransform = false;
const value = isArray(props.value) ? props.value : props.value.split(',');
isNotRequestTransform = value.every((value) => !!treeValue.value.find((item) => item.value === value));
if (isNotRequestTransform) {
return;
}
} else if (isObject(treeValue.value) && unref(treeValue).label != null) {
if (props.value == unref(treeValue).value) {
// 不需要再去请求翻译
return;
}
}
}
// update-end--author:liaozhiyang---date:20250423---for:【issues/8093】选择节点后会先变成编码再显示label文字
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: translateTitle(item),
}));
}else{
treeValue.value = { key: props.value, value: props.value, label: translateTitle(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.title = translateTitle(i.title);
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);
}
}
/**
* 翻译
* @param text
*/
function translateTitle(text) {
if (text.includes("t('") && t) {
return new Function('t', `return ${text}`)(t);
}
return text;
}
/**
* 异步加载数据
*/
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.title = translateTitle(i.title);
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);
}
// update-begin--author:liaozhiyang---date:20250423---for:【issues/8093】删除后会先变成编码再显示label文字
if (isArray(value)) {
// 编辑删除时有选中的值是异步(第二级以上的)会不显示label
value.forEach((item) => {
if (item.label === undefined && item.value != null) {
const findItem = treeValue.value.find((o) => o.value === item.value);
if (findItem) {
item.label = findItem.label;
}
}
});
treeValue.value = value;
} else {
treeValue.value = value;
}
// update-end--author:liaozhiyang---date:20250423---for:【issues/8093】删除后会先变成编码再显示label文字
}
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.title = translateTitle(i.title);
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;
}
}
}
}
/**
* 2024-07-30
* liaozhiyang
* 【issues/6953】JTreeSelect 组件能支持antdv 对应的a-tree-select 组件的插槽
*/
const slotNamesGroup = computed(() => {
const native: string[] = [];
if (isObject(slots)) {
for (const name of Object.keys(slots)) {
native.push(name);
}
}
return native;
});
// onCreated
validateProp().then(() => {
initDictInfo();
loadRoot();
loadItemByCode();
});
</script>
<style lang="less"></style>
@@ -0,0 +1,468 @@
<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';
import { split } from '/@/utils/index';
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[] = [];
// update-begin--author:liaozhiyang---date:20250325---for:【issues/7990】图片参数中包含逗号会错误的识别成多张图
const result = split(paths);
// update-end--author:liaozhiyang---date:20250325---for:【issues/7990】图片参数中包含逗号会错误的识别成多张图
for (const item of result) {
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} 上传失败.`);
}
// update-begin--author:liaozhiyang---date:20240628---for:【issues/1273】上传组件JUpload配置beforeUpload阻止了上传,前端页面中还是显示缩略图
// beforeUpload 返回false,则没有status
info.file.status && (fileList.value = fileListTemp);
// update-end--author:liaozhiyang---date:20240628---for:【issues/1273】上传组件JUpload配置beforeUpload阻止了上传,前端页面中还是显示缩略图
if (info.file.status === 'done' || info.file.status === 'removed') {
//returnUrl为true时仅返回文件路径
if (props.returnUrl) {
handlePathChange();
} else {
//returnUrl为false时返回文件名称、文件路径及文件大小
let newFileList: any[] = [];
for (const item of fileListTemp) {
if (item.status === 'done') {
let fileJson = {
fileName: item.name,
filePath: item.response.message,
fileSize: item.size,
};
newFileList.push(fileJson);
}else{
return;
}
}
//update-begin---author:liusq ---date:20230914 for[issues/5327]Upload组件returnUrl为false时上传的字段值返回了一个'[object Object]' ------------
emitValue(JSON.stringify(newFileList));
//update-end---author:liusq ---date:20230914 for[issues/5327]Upload组件returnUrl为false时上传的字段值返回了一个'[object Object]' ------------
}
}
}
function handlePathChange() {
let uploadFiles = fileList.value;
let path = '';
if (!uploadFiles || uploadFiles.length == 0) {
path = '';
}
let pathList: string[] = [];
for (const item of uploadFiles) {
if (item.status === 'done') {
pathList.push(item.response.message);
} else {
return;
}
}
if (pathList.length > 0) {
path = pathList.join(',');
}
emitValue(path);
}
// 预览文件、图片
function onFilePreview(file) {
if (isImageMode.value) {
createImgPreview({ imageList: [file.url], maskClosable: true });
} else {
window.open(file.url);
}
}
function emitValue(value) {
emit('change', value);
emit('update:value', value);
}
function uidGenerator() {
return '-' + parseInt(Math.random() * 10000 + 1, 10);
}
function getFileName(path) {
if (path.lastIndexOf('\\') >= 0) {
let reg = new RegExp('\\\\', 'g');
path = path.replace(reg, '/');
}
return path.substring(path.lastIndexOf('/') + 1);
}
defineExpose({
addActionsListener,
});
</script>
<style lang="less">
//noinspection LessUnresolvedVariable
@prefix-cls: ~'@{namespace}-j-upload';
.@{prefix-cls} {
&-container {
position: relative;
.upload-disabled {
.ant-upload-list-item {
.anticon-close {
display: none;
}
.anticon-delete {
display: none;
}
}
/* update-begin-author:taoyan date:2022-5-24 for:VUEN-1093详情界面 图片下载按钮显示不全*/
.upload-download-handler {
right: 6px !important;
}
/* update-end-author:taoyan date:2022-5-24 for:VUEN-1093详情界面 图片下载按钮显示不全*/
}
.ant-upload-text-icon {
color: @primary-color;
}
.ant-upload-list-item {
.upload-actions-container {
position: absolute;
top: -31px;
left: -18px;
z-index: 11;
width: 84px;
height: 84px;
line-height: 28px;
text-align: center;
pointer-events: none;
a {
opacity: 0.9;
margin: 0 5px;
cursor: pointer;
transition: opacity 0.3s;
.anticon {
color: #fff;
font-size: 16px;
}
&:hover {
opacity: 1;
}
}
.upload-mover-handler,
.upload-download-handler {
position: absolute;
pointer-events: auto;
}
.upload-mover-handler {
width: 100%;
bottom: 0;
}
.upload-download-handler {
top: -4px;
right: -4px;
}
}
}
}
}
</style>
@@ -0,0 +1,45 @@
<template>
<BasicModal @register="registerModal" :title="modalTitle" :width="width" @ok="handleOk" v-bind="$attrs">
<JUpload ref="uploadRef" :value="value" v-bind="uploadBinds.props" @change="emitValue" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, unref, reactive, computed, nextTick } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import JUpload from './JUpload.vue';
import { UploadTypeEnum } from './upload.data';
import { propTypes } from '/@/utils/propTypes';
const { createMessage } = useMessage();
const emit = defineEmits(['change', 'update:value', 'register']);
const props = defineProps({
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
width: propTypes.number.def(520),
});
const uploadRef = ref();
const uploadBinds = reactive({ props: {} as any });
const modalTitle = computed(() => (uploadBinds.props?.fileType === UploadTypeEnum.image ? '图片上传' : '文件上传'));
// 注册弹窗
const [registerModal, { closeModal }] = useModalInner(async (data) => {
uploadBinds.props = unref(data) || {};
if ([UploadTypeEnum.image, 'img', 'picture'].includes(uploadBinds.props?.fileType)) {
uploadBinds.props.fileType = UploadTypeEnum.image;
} else {
uploadBinds.props.fileType = UploadTypeEnum.file;
}
nextTick(() => uploadRef.value.addActionsListener());
});
function handleOk() {
closeModal();
}
function emitValue(value) {
emit('change', value);
emit('update:value', value);
}
</script>
@@ -0,0 +1,90 @@
<template>
<div v-show="download" class="upload-download-handler">
<a class="download" title="下载" @click="onDownload">
<Icon icon="ant-design:download" />
</a>
</div>
<div v-show="mover && list.length > 1" class="upload-mover-handler">
<a title="向前移动" @click="onMoveForward">
<Icon icon="ant-design:arrow-left" />
</a>
<a title="向后移动" @click="onMoveBack">
<Icon icon="ant-design:arrow-right" />
</a>
</div>
</template>
<script lang="ts" setup>
import { unref, computed } from 'vue';
import { Icon } from '/@/components/Icon';
import { useMessage } from '/@/hooks/web/useMessage';
const { createMessage } = useMessage();
const props = defineProps({
element: { type: HTMLElement, required: true },
fileList: { type: Object, required: true },
mover: { type: Boolean, required: true },
download: { type: Boolean, required: true },
emitValue: { type: Function, required: true },
});
const list = computed(() => unref(props.fileList));
// 向前移动图片
function onMoveForward() {
let index = getIndexByUrl();
if (index === -1) {
createMessage.warn('移动失败:' + index);
return;
}
if (index === 0) {
doSwap(index, unref(list).length - 1);
return;
}
doSwap(index, index - 1);
}
// 向后移动图片
function onMoveBack() {
let index = getIndexByUrl();
if (index === -1) {
createMessage.warn('移动失败:' + index);
return;
}
if (index == unref(list).length - 1) {
doSwap(index, 0);
return;
}
doSwap(index, index + 1);
}
function doSwap(oldIndex, newIndex) {
if (oldIndex !== newIndex) {
let array: any[] = [...(unref(list) as Array<any>)];
let temp = array[oldIndex];
array[oldIndex] = array[newIndex];
array[newIndex] = temp;
props.emitValue(array.map((i) => i.url).join(','));
}
}
function getIndexByUrl() {
const url = props.element?.getElementsByTagName('img')[0]?.src;
if (url) {
const fileList: any = unref(list);
for (let i = 0; i < fileList.length; i++) {
let current = fileList[i].url;
const replace = url.replace(window.location.origin, '');
if (current === replace || encodeURI(current) === replace) {
return i;
}
}
}
return -1;
}
function onDownload() {
const url = props.element?.getElementsByTagName('img')[0]?.src;
window.open(url);
}
</script>
@@ -0,0 +1,3 @@
export { UploadTypeEnum } from './upload.data';
export { default as JUpload } from './JUpload.vue';
export { default as JUploadModal } from './JUploadModal.vue';
@@ -0,0 +1,5 @@
export enum UploadTypeEnum {
all = 'all',
image = 'image',
file = 'file',
}
@@ -0,0 +1,149 @@
<template>
<div>
<div v-if="isDetailsMode">
<p class="detailStr" :title="detailStr">{{ detailStr }}</p>
</div>
<a-row v-else 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, watch } 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(''),
// 【TV360X-1002】是否是详情模式
isDetailsMode: propTypes.bool.def(false),
},
emits: ['handleOpen', 'change'],
setup(props, { emit, refs }) {
//接收下拉框选项
const options = inject('selectOptions') || ref([]);
//接收选择的值
const selectValues = inject('selectValues') || ref({});
const attrs = useAttrs();
const detailStr = ref('');
/**
* 打开弹出框
*/
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);
}
// -update-begin--author:liaozhiyang---date:20240617---for:【TV360X-1002】详情页面行编辑用户组件和部门组件显示方式优化
watch(
[selectValues, options],
() => {
if (props.isDetailsMode) {
if (Array.isArray(selectValues.value) && Array.isArray(options.value)) {
const result = options.value.map((item) => item.label);
detailStr.value = result.join(',');
}
}
},
{ immediate: true }
);
// -update-end--author:liaozhiyang---date:20240617---for:【TV360X-1002】详情页面行编辑用户组件和部门组件显示方式优化
return {
attrs,
selectValues,
options,
handleChange,
openModal,
detailStr,
};
},
});
</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;
}
}
.detailStr {
margin: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
</style>
@@ -0,0 +1,91 @@
<template>
<div>
<a-row class="j-select-row" type="flex" :gutter="8">
<a-col class="left" :class="{ full: !showButton }">
<a-select
ref="select"
v-model:value="selectValues.value"
:mode="multiple"
:open="false"
:options="options"
@change="handleChange"
style="width: 100%"
/>
</a-col>
<a-col v-if="showButton" class="right">
<a-button type="primary" @click="openModal">选择</a-button>
</a-col>
</a-row>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, inject, reactive } from 'vue';
import { propTypes } from '/@/utils/propTypes';
import { useAttrs } from '/@/hooks/core/useAttrs';
export default defineComponent({
name: 'JSelectBiz',
components: {},
inheritAttrs: false,
props: {
showButton: propTypes.bool.def(true),
// 是否支持多选,默认 true
multiple: {
type: Boolean,
default: 'multiple',
},
},
emits: ['btnOk'],
setup(props, { emit, refs }) {
//接收下拉框选项
const options = inject('selectOptions') || ref([]);
//接收选择的值
const selectValues = inject('selectValues') || ref({});
const attrs = useAttrs();
/**
* 打开弹出框
*/
function openModal() {
emit('btnOk');
}
/**
* 下拉框值改变事件
*/
function handleChange(value) {
selectValues.value = value;
selectValues.change = true;
}
return {
attrs,
selectValues,
options,
handleChange,
openModal,
};
},
});
</script>
<style lang="less" scoped>
.j-select-row {
@width: 82px;
.left {
width: calc(100% - @width - 8px);
}
.right {
width: @width;
}
.full {
width: 100%;
}
:deep(.ant-select-search__field) {
display: none !important;
}
}
</style>
@@ -0,0 +1,131 @@
<!--部门选择框-->
<template>
<div>
<BasicModal v-bind="$attrs" @register="register" :title="modalTitle" width="500px" :maxHeight="maxHeight" @ok="handleOk" destroyOnClose @visible-change="visibleChange">
<BasicTree
ref="treeRef"
:treeData="treeData"
:load-data="sync == false ? null : onLoadData"
v-bind="getBindValue"
@select="onSelect"
@check="onCheck"
:fieldNames="fieldNames"
:checkedKeys="checkedKeys"
:multiple="multiple"
:checkStrictly="getCheckStrictly"
/>
<!--树操作部分-->
<template #insertFooter>
<a-dropdown placement="top">
<template #overlay>
<a-menu>
<a-menu-item v-if="multiple" key="1" @click="checkALL(true)">全部勾选</a-menu-item>
<a-menu-item v-if="multiple" key="2" @click="checkALL(false)">取消全选</a-menu-item>
<a-menu-item key="3" @click="expandAll(true)">展开全部</a-menu-item>
<a-menu-item key="4" @click="expandAll(false)">折叠全部</a-menu-item>
</a-menu>
</template>
<a-button style="float: left"> 树操作 <Icon icon="ant-design:up-outlined" /> </a-button>
</a-dropdown>
</template>
</BasicModal>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { queryDepartTreeSync, queryTreeList } from '/@/api/common/api';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { treeProps } from '/@/components/Form/src/jeecg/props/props';
import { BasicTree, TreeActionType } from '/@/components/Tree';
import { useTreeBiz } from '/@/components/Form/src/jeecg/hooks/useTreeBiz';
import {propTypes} from "/@/utils/propTypes";
import { omit } from 'lodash-es';
export default defineComponent({
name: 'DeptSelectModal',
components: {
BasicModal,
BasicTree,
},
props: {
...treeProps,
//选择框标题
modalTitle: {
type: String,
default: '部门选择',
},
// update-begin--author:liaozhiyang---date:20231220---for:【QQYUN-7678】部门组件内容过多没有滚动条(给一个默认最大高)
maxHeight: {
type: Number,
default: 500,
},
// update-end--author:liaozhiyang---date:20231220---for:【QQYUN-7678】部门组件内容过多没有滚动条(给一个默认最大高)
value: propTypes.oneOfType([propTypes.string, propTypes.array])
},
emits: ['register', 'getSelectResult', 'close'],
setup(props, { emit, refs }) {
//注册弹框
const [register, { closeModal }] = useModalInner();
const attrs = useAttrs();
const treeRef = ref<Nullable<TreeActionType>>(null);
//update-begin-author:taoyan date:2022-10-28 for: 部门选择警告类型不匹配
let propValue = props.value === ''?[]:props.value;
//update-begin-author:liusq date:2023-05-26 for: [issues/538]JSelectDept组件受 dynamicDisabled 影响
let temp = Object.assign({}, unref(props), unref(attrs), {value: propValue},{disabled: false});
const getBindValue = omit(temp, 'multiple');
//update-end-author:liusq date:2023-05-26 for: [issues/538]JSelectDept组件受 dynamicDisabled 影响
//update-end-author:taoyan date:2022-10-28 for: 部门选择警告类型不匹配
const queryUrl = getQueryUrl();
const [{ visibleChange, checkedKeys, getCheckStrictly, getSelectTreeData, onCheck, onLoadData, treeData, checkALL, expandAll, onSelect }] =
useTreeBiz(treeRef, queryUrl, getBindValue, props, emit);
const searchInfo = ref(props.params);
const tree = ref([]);
//替换treeNode中key字段为treeData中对应的字段
const fieldNames = {
key: props.rowKey,
};
// {children:'children', title:'title', key:'key' }
/**
* 确定选择
*/
function handleOk() {
getSelectTreeData((options, values) => {
//回传选项和已选择的值
emit('getSelectResult', options, values);
//关闭弹窗
closeModal();
});
}
/** 获取查询数据方法 */
function getQueryUrl() {
let queryFn = props.sync ? queryDepartTreeSync : queryTreeList;
//update-begin-author:taoyan date:2022-7-4 for: issues/I5F3P4 online配置部门选择后编辑,查看数据应该显示部门名称,不是部门代码
return (params) => queryFn(Object.assign({}, params, { primaryKey: props.rowKey }));
//update-end-author:taoyan date:2022-7-4 for: issues/I5F3P4 online配置部门选择后编辑,查看数据应该显示部门名称,不是部门代码
}
return {
tree,
handleOk,
searchInfo,
treeRef,
treeData,
onCheck,
onSelect,
checkALL,
expandAll,
fieldNames,
checkedKeys,
register,
getBindValue,
getCheckStrictly,
visibleChange,
onLoadData,
};
},
});
</script>
@@ -0,0 +1,354 @@
<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">
<a-form ref="formRef" v-if="showSearchFlag" :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:search-outlined" @click="searchQuery">查询</a-button>
<a-button preIcon="ant-design:reload-outlined" @click="searchReset" 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="rowkey ? rowkey : combineRowKey"
:columns="columns"
:showIndexColumn="false"
:dataSource="dataSource"
:pagination="pagination"
:rowSelection="rowSelection"
@row-click="clickThenCheck"
@change="handleChangeInTable"
>
<template #tableTitle></template>
<template #bodyCell="{text, column}">
<template v-if="column.fieldType === 'Image'">
<span v-if="!text" style="font-size: 12px; font-style: italic">无图片</span>
<img v-else :src="getImgView(text)" alt="图片不存在" class="cellIamge" @click="viewOnlineCellImage($event, text)" />
</template>
</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';
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
import { createImgPreview } from '/@/components/Preview/index';
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', 'getFormValues', 'selected', 'rowkey'],
emits: ['ok', 'register'],
setup(props, { emit }) {
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);
}
/**
* 2024-07-24
* liaozhiyang
* 【TV360X-1756】报表添加图片类型
* 图片
* @param text
*/
function getImgView(text) {
if (text && text.indexOf(',') > 0) {
text = text.substring(0, text.indexOf(','));
}
return getFileAccessHttpUrl(text);
}
/**
* 2024-07-24
* liaozhiyang
* 【TV360X-1756】报表添加图片类型
* 预览列表 cell 图片
* @param text
*/
function viewOnlineCellImage(e, text) {
e.stopPropagation();
if (text) {
let imgList: any = [];
let arr = text.split(',');
for (let str of arr) {
if (str) {
imgList.push(getFileAccessHttpUrl(str));
}
}
createImgPreview({ imageList: imgList });
}
}
// update-begin--author:liaozhiyang---date:20250415--for:【issues/3656】popupdict回显
watchEffect(() => {
if (props.selected && props.rowkey) {
const selected = props.multi ? props.selected : [props.selected];
checkedKeys!.value = selected.map((item) => item[props.rowkey]);
selectRows!.value = selected;
}
});
// update-end--author:liaozhiyang---date:20250415--for:【issues/3656】popupdict回显
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,
getImgView,
viewOnlineCellImage,
};
},
});
</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;
}
.cellIamge {
height: 25px !important;
margin: 0 auto;
max-width: 80px;
font-size: 12px;
font-style: italic;
cursor: pointer;
}
</style>
@@ -0,0 +1,833 @@
<template>
<BasicModal
wrapClassName="JSelectUserByDepartmentModal"
v-bind="$attrs"
@register="register"
:title="modalTitle"
width="800px"
@ok="handleOk"
destroyOnClose
@visible-change="visibleChange"
>
<div class="j-select-user-by-dept">
<div class="modal-content">
<!-- 左侧搜索和组织列表 -->
<div class="left-content">
<!-- 搜索框 -->
<div class="search-box">
<a-input v-model:value.trim="searchText" placeholder="搜索" @change="handleSearch" @pressEnter="handleSearch" allowClear />
</div>
<!-- 组织架构 -->
<div class="tree-box">
<template v-if="searchText.length">
<template v-if="searchResult.depart.length || searchResult.user.length">
<div class="search-result">
<template v-if="searchResult.user.length">
<div class="search-user">
<p class="search-user-title">人员</p>
<template v-for="item in searchResult.user" :key="item.id">
<div class="search-user-item" @click="handleSearchUserCheck(item)">
<a-checkbox v-model:checked="item.checked" />
<div class="right">
<div class="search-user-item-circle">
<img v-if="item.avatar" :src="getFileAccessHttpUrl(item.avatar)" alt="avatar" />
</div>
<div class="search-user-item-info">
<div class="search-user-item-name">{{ item.realname }}</div>
<div class="search-user-item-org">{{ item.orgCodeTxt }}</div>
</div>
</div>
</div>
</template>
</div>
</template>
<template v-if="searchResult.depart.length">
<div class="search-depart">
<p class="search-depart-title">部门</p>
<template v-for="item in searchResult.depart" :key="item.id">
<div class="search-depart-item" @click="handleSearchDepartClick(item)">
<a-checkbox v-model:checked="item.checked" @click.stop @change="($event) => handleSearchDepartCheck($event, item)" />
<div class="search-depart-item-name">{{ item.departName }}</div>
<RightOutlined />
</div>
</template>
</div>
</template>
</div>
</template>
<template v-else>
<div class="no-data">
<a-empty description="暂无数据" />
</div>
</template>
</template>
<template v-else>
<a-breadcrumb v-if="breadcrumb.length">
<a-breadcrumb-item @click="handleBreadcrumbClick()">
<HomeOutlined />
</a-breadcrumb-item>
<template v-for="item in breadcrumb" :key="item?.id">
<a-breadcrumb-item @click="handleBreadcrumbClick(item)">
<span>{{ item.departName }}</span>
</a-breadcrumb-item>
</template>
</a-breadcrumb>
<div v-if="currentDepartUsers.length">
<!-- 当前部门用户树 -->
<div class="depart-users-tree">
<div v-if="!currentDepartTree.length" class="allChecked">
<a-checkbox v-model:checked="currentDepartAllUsers" @change="handleAllUsers">全选</a-checkbox>
</div>
<template v-for="item in currentDepartUsers" :key="item.id">
<div class="depart-users-tree-item" @click="handleDepartUsersTreeCheck(item)">
<a-checkbox v-model:checked="item.checked" />
<div class="right">
<div class="depart-users-tree-item-circle">
<img v-if="item.avatar" :src="getFileAccessHttpUrl(item.avatar)" alt="avatar" />
</div>
<div class="depart-users-tree-item-name">{{ item.realname }}</div>
</div>
</div>
</template>
</div>
</div>
<!-- 部门树 -->
<div v-if="currentDepartTree.length" class="depart-tree">
<template v-for="item in currentDepartTree" :key="item.id">
<div class="depart-tree-item" @click="handleDepartTreeClick(item)">
<a-checkbox v-model:checked="item.checked" @click.stop @change="($event) => handleDepartTreeCheck($event, item)" />
<div class="depart-tree-item-name">{{ item.departName }}</div>
<RightOutlined />
</div>
</template>
</div>
<div v-if="currentDepartTree.length === 0 && currentDepartUsers.length === 0" class="no-data">
<a-empty description="暂无数据" />
</div>
</template>
</div>
</div>
<!-- 右侧已选人员展示 -->
<div class="right-content">
<div class="selected-header"> 已选人员{{ selectedUsers.length }} </div>
<div class="selected-users">
<div class="content">
<div v-for="user in selectedUsers" :key="user.id" class="user-avatar" @click="handleDelUser(user)">
<div class="avatar-circle">
<img v-if="user.avatar" :src="getFileAccessHttpUrl(user.avatar)" alt="avatar" />
<div class="mask">
<CloseOutlined></CloseOutlined>
</div>
</div>
<div class="user-name">{{ user.realname }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</BasicModal>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue';
import { RightOutlined, HomeOutlined, CloseOutlined } from '@ant-design/icons-vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { queryTreeList, getTableList as getTableListOrigin } from '/@/api/common/api';
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
import { isArray } from '/@/utils/is';
import { defHttp } from '/@/utils/http/axios';
defineOptions({ name: 'JSelectUserByDepartmentModal' });
const props = defineProps({
// ...selectProps,
//回传value字段名
rowKey: {
type: String,
default: 'id',
},
//回传文本字段名
labelKey: {
type: String,
default: 'name',
},
modalTitle: {
type: String,
default: '部门用户选择',
},
selectedUser: {
type: Array,
default: () => [],
},
//查询参数
params: {
type: Object,
default: () => {},
},
//最大选择数量
maxSelectCount: {
type: Number,
default: 0,
},
// 是否单选
isRadioSelection: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['close', 'register', 'change']);
import { useMessage } from '/@/hooks/web/useMessage';
const { createMessage } = useMessage();
// 搜索文本
const searchText = ref('');
const breadcrumb = ref<any[]>([]);
// 部门树(整颗树)
const departTree = ref([]);
// 当前部门树
const currentDepartTree = ref<any[]>([]);
// 选中的部门节点
const checkedDepartIds = ref<string[]>([]);
// 当前部门用户
const currentDepartUsers = ref([]);
// 已选用户
const selectedUsers = ref<any[]>([]);
// 全选
const currentDepartAllUsers = ref(false);
// 搜索结构
const searchResult: any = reactive({
depart: [],
user: [],
});
// 映射部门和人员的关系
const cacheDepartUser = {};
//注册弹框
const [register, { closeModal }] = useModalInner(async (data) => {
// 初始化
if (props.selectedUser.length) {
// 编辑时,传进来已选中的数据
selectedUsers.value = props.selectedUser;
}
getQueryTreeList();
});
const visibleChange = (visible) => {
if (visible === false) {
setTimeout(() => {
emit('close');
}, 300);
}
};
const handleOk = () => {
if (selectedUsers.value.length == 0) {
createMessage.warning('请选择人员');
return;
}
if (props.isRadioSelection && selectedUsers.value.length > 1) {
createMessage.warning('只允许选择一个用户');
return;
}
if (props.maxSelectCount && selectedUsers.value.length > props.maxSelectCount) {
createMessage.warning(`最多只能选择${props.maxSelectCount}个用户`);
return;
}
emit('change', selectedUsers.value);
closeModal();
};
// 搜索人员/部门
const handleSearch = () => {
if (searchText.value) {
defHttp
.get({
url: `/sys/user/listAll`,
params: {
column: 'createTime',
order: 'desc',
pageNo: 1,
pageSize: 100,
realname: `*${searchText.value}*`,
},
})
.then((result: any) => {
result.records?.forEach((item) => {
const findItem = selectedUsers.value.find((user) => user.id == item.id);
if (findItem) {
// 能在右侧找到说明选中了,左侧同样需要选中。
item.checked = true;
} else {
item.checked = false;
}
});
searchResult.user = result.records ?? [];
});
searchResult.depart = getDepartByName(searchText.value) ?? [];
} else {
searchResult.user = [];
searchResult.depart = [];
}
};
// 面包屑
const handleBreadcrumbClick = (item?) => {
// 先清空
currentDepartUsers.value = [];
if (item) {
const findIndex = breadcrumb.value.findIndex((o) => o.id === item.id);
if (findIndex != -1) {
breadcrumb.value = breadcrumb.value.filter((item, index) => {
console.log(item);
return index <= findIndex;
});
}
const data = getDepartTreeNodeById(item.id, departTree.value);
currentDepartTree.value = data.children;
} else {
// 根节点
currentDepartTree.value = departTree.value;
breadcrumb.value = [];
}
};
// 点击部门树复选框触发
const handleDepartTreeCheck = (e, item) => {
const { target } = e;
if (target.checked) {
// 选中
getUsersByDeptId(item['id']).then((users) => {
addUsers(users);
});
checkedDepartIds.value.push((item as any).id);
// 检查父节点下所有子节点是否选中
const parentItem = getDepartTreeParentById(item.id);
if (parentItem?.children) {
const isChildAllChecked = parentItem.children.every((item) => item.checked);
if (isChildAllChecked) {
parentItem.checked = true;
} else {
parentItem.checked = false;
}
}
} else {
// 取消选中
const findIndex = checkedDepartIds.value.findIndex((o: any) => o.id === item.id);
if (findIndex != -1) {
checkedDepartIds.value.splice(findIndex, 1);
}
// 如果父节点是选中,则需要取消
const parentItem = getDepartTreeParentById(item.id);
if (parentItem) {
parentItem.checked = false;
}
getUsersByDeptId(item['id']).then((users) => {
users.forEach((item) => {
const findIndex = selectedUsers.value.findIndex((user) => user.id === item.id);
if (findIndex != -1) {
selectedUsers.value.splice(findIndex, 1);
}
});
});
}
};
// 点击部门树节点触发
const handleDepartTreeClick = (item) => {
breadcrumb.value = [...breadcrumb.value, item];
if (item.children) {
// 有子节点,则显示部门
if (item.checked) {
// 父节点勾选,则子节点全部勾选
item.children.forEach((item) => {
item.checked = true;
});
}
currentDepartTree.value = item.children;
defHttp
.get({
url: '/sys/sysDepart/getUsersByDepartId',
params: {
id: item['id'],
},
})
.then((res: any) => {
const result = res ?? [];
if (item.checked) {
// 父节点勾选,则默认勾选
result.forEach((item) => {
item.checked = true;
});
}
// 右侧勾选了,则默认勾选(用户存在多部门,在别的部门被选中了)
if (selectedUsers.value.length) {
result.forEach((item) => {
const findItem = selectedUsers.value.find((user) => user.id === item.id);
if (findItem) {
// 说明在selectedUsers中被找到
item.checked = true;
}
});
}
currentDepartUsers.value = result;
});
} else {
// 没有子节点,则显示用户
currentDepartTree.value = [];
getTableList({
departId: item['id'],
}).then((res: any) => {
if (res?.records) {
let checked = true;
res.records.forEach((item) => {
const findItem = selectedUsers.value.find((user) => user.id == item.id);
if (findItem) {
// 能在右侧找到说明选中了,左侧同样需要选中。
item.checked = true;
} else {
item.checked = false;
checked = false;
}
});
currentDepartAllUsers.value = checked;
currentDepartUsers.value = res.records;
}
});
}
};
// 点击部门用户树复选框触发
const handleDepartUsersTreeCheck = (item) => {
item.checked = !item.checked;
if (item.checked) {
addUsers(item);
} else {
selectedUsers.value = selectedUsers.value.filter((user) => user.id !== item.id);
}
if (item.checked == false) {
// 有一个是false,则全选false
currentDepartAllUsers.value = false;
}
};
// 全选
const handleAllUsers = ({ target }) => {
const { checked } = target;
if (checked) {
currentDepartUsers.value.forEach((item: any) => (item.checked = true));
addUsers(currentDepartUsers.value);
} else {
currentDepartUsers.value.forEach((item: any) => (item.checked = false));
selectedUsers.value = selectedUsers.value.filter((user) => {
const userId = user.id;
const findItem = currentDepartUsers.value.find((item: any) => item.id === userId);
if (findItem) {
return false;
} else {
return true;
}
});
}
};
// 删除人员
const handleDelUser = (item) => {
const findIndex = selectedUsers.value.findIndex((user) => user.id === item.id);
if (findIndex != -1) {
selectedUsers.value.splice(findIndex, 1);
}
const findItem: any = currentDepartUsers.value.find((user: any) => user.id === item.id);
if (findItem) {
findItem.checked = false;
currentDepartAllUsers.value = false;
}
};
// 点击搜索用户复选框
const handleSearchUserCheck = (item) => {
item.checked = !item.checked;
if (item.checked) {
addUsers(item);
} else {
selectedUsers.value = selectedUsers.value.filter((user) => user.id !== item.id);
}
};
// 点击搜索部门复选框
const handleSearchDepartCheck = (e, item) => {
handleDepartTreeCheck(e, item);
};
// 点击搜索部门
const handleSearchDepartClick = (item) => {
searchResult.depart = [];
searchResult.user = [];
breadcrumb.value = getPathToNodeById(item.id);
handleDepartTreeClick(item);
};
// 添加人员到右侧
const addUsers = (users) => {
let newUsers: any = [];
if (isArray(users)) {
// selectedUsers里面没有才添加(防止重复)
newUsers = users.filter((user: any) => !selectedUsers.value.find((item) => item.id === user.id));
} else {
if (!selectedUsers.value.find((user) => user.id === users.id)) {
// selectedUsers里面没有才添加(防止重复)
newUsers = [users];
}
}
selectedUsers.value = [...selectedUsers.value, ...newUsers];
const result = currentDepartUsers.value.every((item: any) => !!item.checked);
currentDepartAllUsers.value = result;
};
// 解析参数
const parseParams = (params) => {
if (props?.params) {
return {
...params,
...props.params,
};
}
return params;
};
const getQueryTreeList = (params?) => {
params = parseParams(params);
queryTreeList({ ...params }).then((res) => {
if (res) {
departTree.value = res;
currentDepartTree.value = res;
}
});
};
// 根据部门id获取用户
const getTableList = (params) => {
params = parseParams(params);
return getTableListOrigin({ ...params });
};
const getUsersByDeptId = (id) => {
return new Promise<any[]>((resolve) => {
if (cacheDepartUser[id]) {
resolve(cacheDepartUser[id]);
} else {
getTableList({
departId: id,
}).then((res: any) => {
cacheDepartUser[id] = res.records ?? [];
if (res?.records?.length) {
resolve(res.records ?? []);
}
});
}
});
};
// 根据id获取根节点到当前节点路径
const getPathToNodeById = (id: string, tree = departTree.value, path = []): any[] => {
for (const node of tree) {
if ((node as any).id === id) {
return [...path];
}
if ((node as any).children) {
const foundPath = getPathToNodeById(id, (node as any).children, [...path, node]);
if (foundPath.length) {
return foundPath;
}
}
}
return [];
};
// 根据id获取部门树父节点数据
const getDepartTreeParentById = (id: string, tree = departTree.value, parent = null): any => {
for (const node of tree) {
if ((node as any).id === id) {
return parent;
}
if ((node as any).children) {
const found = getDepartTreeParentById(id, (node as any).children, node);
if (found) {
return found;
}
}
}
return null;
};
// 通过名称搜索部门支持模糊
const getDepartByName = (name: string, tree = departTree.value): any[] => {
const result: any[] = [];
const search = (nodes: any[]) => {
for (const node of nodes) {
if (node.departName?.toLowerCase().includes(name.toLowerCase())) {
result.push(node);
}
if (node.children?.length) {
search(node.children);
}
}
};
search(tree);
return result;
};
// 根据id获取部门树当前节点数据
const getDepartTreeNodeById = (id: string, tree = departTree.value): any => {
for (const node of tree) {
if ((node as any).id === id) {
return node;
}
if ((node as any).children) {
const found = getDepartTreeNodeById(id, (node as any).children);
if (found) {
return found;
}
}
}
return null;
};
</script>
<style lang="less">
.JSelectUserByDepartmentModal {
.scroll-container {
padding: 0;
}
}
</style>
<style lang="less" scoped>
.j-select-user-by-dept {
background: #fff;
border-radius: 4px;
}
.modal-content {
display: flex;
padding: 20px;
padding-bottom: 0;
padding-left: 0;
height: 400px;
font-size: 12px;
}
.left-content {
display: flex;
flex-direction: column;
flex: 1;
border-right: 1px solid #e8e8e8;
.search-box {
margin: 0 16px 16px 16px;
}
:deep(.ant-breadcrumb) {
font-size: 12px;
margin-left: 16px;
color: inherit;
cursor: pointer;
li {
.ant-breadcrumb-link {
cursor: pointer;
&:hover {
color: @primary-color;
}
}
&:last-child {
.ant-breadcrumb-link {
pointer-events: none;
}
}
}
}
.tree-box {
display: flex;
flex-direction: column;
flex: 1;
overflow-y: auto;
.no-data {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
}
.depart-tree {
.depart-tree-item {
padding: 0 16px;
line-height: 40px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
&:hover {
background-color: #f4f6fa;
}
}
.depart-tree-item-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0 8px;
}
}
}
.depart-users-tree {
.allChecked {
padding: 0 16px;
margin-bottom: 16px;
padding-top: 12px;
:deep(.ant-checkbox-wrapper) {
font-size: 12px;
}
}
.depart-users-tree-item {
line-height: 50px;
padding: 0 16px;
display: flex;
cursor: pointer;
&:hover {
background-color: #f4f6fa;
}
.right {
flex: 1;
display: flex;
align-items: center;
margin: 0 8px;
}
.depart-users-tree-item-circle {
width: 36px;
height: 36px;
border-radius: 50%;
background-color: #aaa;
overflow: hidden;
img {
display: block;
width: 100%;
height: 100%;
}
}
.depart-users-tree-item-name {
margin-left: 8px;
}
}
}
.search-depart {
margin-bottom: 8px;
.search-depart-title {
padding-left: 16px;
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
}
.search-depart-item {
display: flex;
align-items: center;
padding: 8px 16px;
cursor: pointer;
&:hover {
background-color: #f4f6fa;
}
.search-depart-item-name {
margin-left: 8px;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.search-user {
margin-bottom: 8px;
.search-user-title {
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
padding-left: 16px;
}
.search-user-item {
display: flex;
align-items: center;
padding: 8px 16px;
cursor: pointer;
&:hover {
background-color: #f4f6fa;
}
.right {
flex: 1;
display: flex;
align-items: center;
margin: 0 8px;
}
.search-user-item-info {
display: flex;
flex-direction: column;
justify-content: center;
margin-left: 8px;
}
.search-user-item-circle {
width: 36px;
height: 36px;
border-radius: 50%;
overflow: hidden;
background-color: #aaa;
}
.search-user-item-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.search-user-item-org {
color: #999;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.right-content {
width: 400px;
display: flex;
flex-direction: column;
padding-left: 16px;
.selected-header {
margin-bottom: 16px;
}
.selected-users {
flex: 1;
overflow-y: auto;
}
.content {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
}
.user-avatar {
text-align: center;
width: 70px;
cursor: pointer;
}
.avatar-circle {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
color: white;
margin: 0 auto 8px;
position: relative;
background-color: rgba(0, 0, 0, 0.5);
img {
width: 100%;
height: 100%;
}
&:hover {
.mask {
opacity: 1;
}
}
.mask {
opacity: 0;
transition: opacity;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.5);
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
}
}
.user-name {
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
}
</style>
@@ -0,0 +1,194 @@
<!--职务选择框-->
<template>
<div>
<BasicModal
v-bind="$attrs"
@register="register"
:title="modalTitle"
width="1100px"
wrapClassName="j-user-select-modal"
@ok="handleOk"
destroyOnClose
@visible-change="visibleChange"
>
<a-row>
<a-col :span="showSelected ? 18 : 24">
<BasicTable
:columns="columns"
:bordered="true"
:useSearchForm="true"
:formConfig="formConfig"
:api="getPositionList"
:searchInfo="searchInfo"
:rowSelection="rowSelection"
:indexColumnProps="indexColumnProps"
v-bind="getBindValue"
></BasicTable>
</a-col>
<a-col :span="showSelected ? 6 : 0">
<BasicTable
v-bind="selectedTable"
:dataSource="selectRows"
:useSearchForm="true"
:formConfig="{ showActionButtonGroup: false, baseRowStyle: { minHeight: '40px' } }"
>
<!--操作栏-->
<template #action="{ record }">
<a href="javascript:void(0)" @click="handleDeleteSelected(record)"><Icon icon="ant-design:delete-outlined"></Icon></a>
</template>
</BasicTable>
</a-col>
</a-row>
</BasicModal>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { getPositionList } from '/@/api/common/api';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
import { useSelectBiz } from '/@/components/Form/src/jeecg/hooks/useSelectBiz';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { selectProps } from '/@/components/Form/src/jeecg/props/props';
export default defineComponent({
name: 'PositionSelectModal',
components: {
//此处需要异步加载BasicTable
BasicModal,
BasicTable: createAsyncComponent(() => import('/@/components/Table/src/BasicTable.vue'), {
loading: true,
}),
},
props: {
...selectProps,
//选择框标题
modalTitle: {
type: String,
default: '职务选择',
},
},
emits: ['register', 'getSelectResult'],
setup(props, { emit, refs }) {
//注册弹框
const [register, { closeModal }] = useModalInner();
const attrs = useAttrs();
//表格配置
const config = {
canResize: false,
bordered: true,
size: 'small',
//改成读取rowKey,自定义传递参数
rowKey: props.rowKey,
};
const getBindValue = Object.assign({}, unref(props), unref(attrs), config);
const [{ rowSelection, visibleChange, indexColumnProps, getSelectResult, handleDeleteSelected, selectRows }] = useSelectBiz(
getPositionList,
getBindValue
);
const searchInfo = ref(props.params);
//查询form
const formConfig = {
labelCol: {
span: 4,
},
baseColProps: {
xs: 24,
sm: 10,
md: 10,
lg: 10,
xl: 10,
xxl: 10,
},
//update-begin-author:liusq date:2023-10-30 for: [issues/5514]组件页面显示错位
actionColOptions: {
xs: 24,
sm: 8,
md: 8,
lg: 8,
xl: 8,
xxl: 8,
},
//update-end-author:liusq date:2023-10-30 for: [issues/5514]组件页面显示错位
schemas: [
{
label: '职务名称',
field: 'name',
component: 'JInput',
colProps: { span: 10 },
},
],
};
//定义表格列
const columns = [
{
title: '职务编码',
dataIndex: 'code',
width: 180,
align: 'left',
},
{
title: '职务名称',
dataIndex: 'name',
// width: 180,
},
{
title: '职务等级',
dataIndex: 'postRank_dictText',
width: 180,
},
];
//已选择的table信息
const selectedTable = {
pagination: false,
showIndexColumn: false,
scroll: { y: 390 },
size: 'small',
canResize: false,
bordered: true,
rowKey: 'id',
columns: [
{
title: '职务名称',
dataIndex: 'name',
width: 40,
},
{
title: '操作',
dataIndex: 'action',
align: 'center',
width: 40,
slots: { customRender: 'action' },
},
],
};
/**
* 确定选择
*/
function handleOk() {
getSelectResult((options, values) => {
//回传选项和已选择的值
emit('getSelectResult', options, values);
//关闭弹窗
closeModal();
});
}
return {
handleOk,
getPositionList,
register,
visibleChange,
getBindValue,
formConfig,
indexColumnProps,
columns,
rowSelection,
selectedTable,
selectRows,
handleDeleteSelected,
searchInfo,
};
},
});
</script>
@@ -0,0 +1,130 @@
<!--角色选择框-->
<template>
<div>
<BasicModal v-bind="$attrs" @register="register" :title="modalTitle" width="800px" @ok="handleOk" destroyOnClose @visible-change="visibleChange">
<BasicTable
:columns="columns"
v-bind="config"
:useSearchForm="true"
:formConfig="formConfig"
:api="getRoleList"
:searchInfo="searchInfo"
:rowSelection="rowSelection"
:indexColumnProps="indexColumnProps"
></BasicTable>
</BasicModal>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { getRoleList } from '/@/api/common/api';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
import { useSelectBiz } from '/@/components/Form/src/jeecg/hooks/useSelectBiz';
import { selectProps } from '/@/components/Form/src/jeecg/props/props';
import { useAttrs } from '/@/hooks/core/useAttrs';
export default defineComponent({
name: 'UserSelectModal',
components: {
//此处需要异步加载BasicTable
BasicModal,
BasicTable: createAsyncComponent(() => import('/@/components/Table/src/BasicTable.vue'), {
loading: true,
}),
},
props: {
...selectProps,
//选择框标题
modalTitle: {
type: String,
default: '角色选择',
},
},
emits: ['register', 'getSelectResult'],
setup(props, { emit, refs }) {
//注册弹框
const [register, { closeModal }] = useModalInner();
const attrs = useAttrs();
//表格配置
const config = {
canResize: false,
bordered: true,
size: 'small',
rowKey: unref(props).rowKey,
};
const getBindValue = Object.assign({}, unref(props), unref(attrs), config);
const [{ rowSelection, indexColumnProps, visibleChange, getSelectResult }] = useSelectBiz(getRoleList, getBindValue);
const searchInfo = ref(props.params);
//查询form
const formConfig = {
//labelWidth: 220,
baseColProps: {
xs: 24,
sm: 24,
md: 24,
lg: 14,
xl: 14,
xxl: 14,
},
//update-begin-author:liusq date:2023-10-30 for: [issues/5514]组件页面显示错位
actionColOptions: {
xs: 24,
sm: 8,
md: 8,
lg: 8,
xl: 8,
xxl: 8,
},
//update-end-author:liusq date:2023-10-30 for: [issues/5514]组件页面显示错位
schemas: [
{
label: '角色名称',
field: 'roleName',
component: 'Input',
},
],
};
//定义表格列
const columns = [
{
title: '角色名称',
dataIndex: 'roleName',
width: 240,
align: 'left',
},
{
title: '角色编码',
dataIndex: 'roleCode',
// width: 40,
},
];
/**
* 确定选择
*/
function handleOk() {
getSelectResult((options, values) => {
//回传选项和已选择的值
emit('getSelectResult', options, values);
//关闭弹窗
closeModal();
});
}
return {
config,
handleOk,
searchInfo,
register,
indexColumnProps,
visibleChange,
getRoleList,
formConfig,
getBindValue,
columns,
rowSelection,
};
},
});
</script>
@@ -0,0 +1,249 @@
<!--通过部门选择用户-->
<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 as getTableListOrigin } 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 as queryDepartTreeSyncOrigin } 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 getTableList(params) {
params = parseParams(params);
return getTableListOrigin({...params});
}
function queryDepartTreeSync(params) {
params = parseParams(params);
return queryDepartTreeSyncOrigin({...params});
}
/**
* 解析参数
* @param params
*/
function parseParams(params) {
if (props?.params) {
return {
...params,
...props.params,
}
}
return params;
}
/**
* 加载树形数据
*/
function queryDepartTree() {
queryDepartTreeSync().then((res) => {
if (res) {
departTree.value = res;
// 默认展开父节点
//expandedKeys.value = unref(departTree).map(item => item.id)
}
});
}
/**
* 加载子级部门
*/
async function loadChildrenTreeData(treeNode) {
try {
const result = await queryDepartTreeSync({
pid: treeNode.eventKey,
});
const asyncTreeAction = unref(treeRef);
if (asyncTreeAction) {
asyncTreeAction.updateNodeByKey(treeNode.eventKey, { children: result });
asyncTreeAction.setExpandedKeys([treeNode.eventKey, ...asyncTreeAction.getExpandedKeys()]);
}
} catch (e) {
console.error(e);
}
return Promise.resolve();
}
/**
* 点击树节点,筛选出对应的用户
*/
function onDepSelect(keys) {
if (keys[0] != null) {
if (unref(selectedDepIds)[0] !== keys[0]) {
selectedDepIds.value = [keys[0]];
}
searchInfo['departId'] = unref(selectedDepIds).join(',');
tableRef.value.reload();
}
}
/**
* 自定义重置方法
* */
async function customResetFunc() {
console.log('自定义查询');
//树节点清空
selectedDepIds.value = [];
//查询条件清空
searchInfo['departId'] = '';
//选择项清空
reset();
}
/**
* 确定选择
*/
function handleOk() {
getSelectResult((options, values) => {
//回传选项和已选择的值
emit('getSelectResult', options, values);
//关闭弹窗
closeModal();
});
}
return {
//config,
handleOk,
searchInfo,
register,
indexColumnProps,
visibleChange,
getBindValue,
rowSelection,
departTree,
selectedDepIds,
expandedKeys,
treeRef,
tableRef,
getTableList,
onDepSelect,
loadChildrenTreeData,
};
},
});
</script>
<style scoped lang="less"></style>
@@ -0,0 +1,306 @@
<!--用户选择框-->
<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"
:beforeFetch="beforeFetch"
>
<!-- 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---forQQYUN-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条
//update-begin---author:wangshuai---date:2024-07-03---for:【TV360X-1629】用户选择组件不是根据创建时间正序排序的---
/**
* 请求之前根据创建时间排序
*
* @param params
*/
function beforeFetch(params) {
return Object.assign({ column: 'createTime', order: 'desc' }, params);
}
//update-end---author:wangshuai---date:2024-07-03---for:【TV360X-1629】用户选择组件不是根据创建时间正序排序的---
return {
//config,
handleOk,
searchInfo,
register,
indexColumnProps,
visibleChange,
getBindValue,
getUserList,
formConfig,
columns,
rowSelection,
selectRows,
selectedTable,
handleDeleteSelected,
tableScroll,
tableRef,
afterFetch,
handleCancel,
maxHeight,
beforeFetch,
};
},
});
</script>
@@ -0,0 +1,282 @@
<template>
<BasicModal
@register="register"
:getContainer="getContainer"
:canFullscreen="false"
:title="title"
:width="500"
destroyOnClose
@ok="handleOk"
wrapClassName="j-user-select-modal2" >
<div style="position: relative; min-height: 350px">
<div style="width: 100%">
<a-input v-model:value="searchText" allowClear style="width: 100%" placeholder="搜索">
<template #prefix>
<SearchOutlined style="color: #c0c0c0" />
</template>
</a-input>
</div>
<!-- tabs -->
<div class="modal-select-list-container">
<div class="scroll">
<div class="content" style="right: -10px">
<label class="item" v-for="item in showDataList" @click="(e)=>onSelect(e, item)">
<a-checkbox v-model:checked="item.checked">
<span>{{ item.name }}</span>
</a-checkbox>
</label>
</div>
</div>
</div>
<!-- 选中用户 -->
<div class="selected-users" style="width: 100%; overflow-x: hidden">
<SelectedUserItem v-for="item in selectedList" :info="item" @unSelect="unSelect" />
</div>
</div>
</BasicModal>
</template>
<script lang="ts">
import { BasicModal, useModalInner } from '/@/components/Modal';
import { SearchOutlined, CloseOutlined } from '@ant-design/icons-vue';
import SelectedUserItem from '../userSelect/SelectedUserItem.vue';
import { defHttp } from '/@/utils/http/axios';
import { computed, ref, toRaw, watch } from 'vue';
export default {
name: 'PositionSelectModal',
components: {
BasicModal,
SearchOutlined,
CloseOutlined,
SelectedUserItem,
},
props: {
multi: {
type: Boolean,
default: true,
},
getContainer: {
type: Function,
default: null,
},
title:{
type: String,
default: '',
},
type: {
type: String,
default: 'sys_position',
},
appId: {
type: String,
default: '',
}
},
emits: ['selected', 'register'],
setup(props, { emit }) {
const searchText = ref('');
const selectedIdList = computed(() => {
let arr = selectedList.value;
if (!arr || arr.length == 0) {
return [];
} else {
return arr.map((k) => k.id);
}
});
watch(()=>props.appId, async (val)=>{
if(val){
await loadDataList();
}
}, {immediate: true});
// 弹窗事件
const [register] = useModalInner(() => {
let list = dataList.value;
if(!list || list.length ==0 ){
}
for(let item of list){
item.checked = false
}
});
// 确定事件
function handleOk() {
let arr = toRaw(selectedIdList.value);
emit('selected', arr);
}
const dataList = ref<any[]>([]);
const showDataList = computed(()=>{
let list = dataList.value;
if(!list || list.length ==0 ){
return []
}
let text = searchText.value;
if(!text){
return list
}
return list.filter(item=>item.name.indexOf(text)>=0)
});
const selectedList = computed(()=>{
let list = dataList.value;
if(!list || list.length ==0 ){
return []
}
return list.filter(item=>item.checked)
});
function unSelect(id) {
let list = dataList.value;
if(!list || list.length ==0 ){
return;
}
let arr = list.filter(item=>item.id == id);
arr[0].checked = false;
}
async function loadDataList() {
let params = {
pageNo: 1,
pageSize: 200,
column: 'createTime',
order: 'desc'
};
const url = '/sys/position/list'
const data = await defHttp.get({ url, params }, { isTransformResponse: false });
if (data.success) {
const { records } = data.result;
let arr:any[] = [];
if(records && records.length>0){
for(let item of records){
arr.push({
id: item.id,
name: item.name || item.roleName,
selectType: props.type,
checked: false
})
}
}
dataList.value = arr;
} else {
console.error(data.message);
}
console.log('loadDataList', data);
}
function onSelect(e, item) {
prevent(e);
console.log('onselect');
item.checked = !item.checked;
}
function prevent(e) {
e.preventDefault();
e.stopPropagation();
}
return {
register,
showDataList,
searchText,
handleOk,
selectedList,
selectedIdList,
unSelect,
onSelect
};
},
};
</script>
<style scoped lang="less">
.modal-select-list-container{
height: 352px;
margin-top: 12px;
overflow: auto;
.scroll{
height: 100%;
position: relative;
width: 100%;
overflow: hidden;
.content{
bottom: 0;
left: 0;
overflow: scroll;
overflow-x: hidden;
position: absolute;
right: 0;
top: 0;
.item{
padding: 7px 5px;
cursor: pointer;
display: block;
&:hover{
background-color: #f5f5f5;
}
}
}
}
}
</style>
<style lang="less">
.j-user-select-modal2 {
.depart-select {
.ant-select-selector {
color: #fff !important;
background-color: #409eff !important;
border-radius: 5px !important;
}
.ant-select-selection-item,
.ant-select-arrow {
color: #fff !important;
}
}
.my-search {
position: absolute;
top: 14px;
z-index: 1;
&.all-width {
width: 100%;
}
.anticon {
cursor: pointer;
&:hover {
color: #0a8fe9 !important;
}
}
.hidden {
display: none;
}
}
.my-tabs {
}
.selected-users {
display: flex;
flex-wrap: wrap;
flex-direction: row;
padding-top: 15px;
}
.scroll-container {
padding-bottom: 0 !important;
}
}
</style>
@@ -0,0 +1,243 @@
<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 < maxSelectCount" :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 = 3;
export default defineComponent({
name: 'RoleSelectInput',
components: {
RoleSelectModal,
SelectedUserItem,
},
props: {
disabled: {
type: Boolean,
default: false,
},
// update-begin--author:liaozhiyang---date:20250414--for:【issues/8078】角色选择组件点击文字部分会一直选中
maxSelectCount: {
type: Number,
default: 2,
},
// update-end--author:liaozhiyang---date:20250414--for:【issues/8078】角色选择组件点击文字部分会一直选中
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 = props.maxSelectCount;
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',
});
}
// 根据 ids 的顺序对 arr 进行排序
if (props.store === 'code') {
arr.sort((a, b) => ids.indexOf(a.code) - ids.indexOf(b.code));
} else {
arr.sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id));
}
}
selectedList.value = arr;
} else {
console.error(data.message);
}
}
return {
selectedList,
ellipsisInfo,
registerRoleModal,
closeRoleModal,
showModal,
onSelected,
unSelect,
currentAppId,
};
},
});
</script>
<style scoped lang="less">
.select-input {
padding: 0 5px;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 3px;
box-sizing: border-box;
display: flex;
color: #9e9e9e;
font-size: 14px;
flex-wrap: nowrap;
min-height: 32px;
overflow-x: hidden;
&.disabled-select {
cursor: not-allowed;
background-color: #f5f5f5 !important;
}
}
</style>
@@ -0,0 +1,321 @@
<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 selectedKeys = ref<string[]>([]);
const selectedList = computed(()=>{
let list = dataList.value;
if(!list || list.length ==0 ){
return []
}
list = list.filter(item=>item.checked)
// 根据 selectedKeys 的顺序排序
let arr: any[] = [];
for (let key of selectedKeys.value) {
let item = list.find(item => item.id == key);
if (item) {
arr.push(item);
}
}
return arr;
});
function unSelect(id) {
let list = dataList.value;
if(!list || list.length ==0 ){
return;
}
// update-begin--author:liaozhiyang---date:20250414--for:【issues/8078】角色选择组件点击文字部分会一直选中
let findItem = list.find((item) => item.id == id);
findItem.checked = false;
selectedKeys.value = selectedKeys.value.filter((key) => key != id);
// update-end--author:liaozhiyang---date:20250414--for:【issues/8078】角色选择组件点击文字部分会一直选中
}
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);
// update-begin--author:liaozhiyang---date:20250414--for:【issues/8078】角色选择组件点击文字部分会一直选中
// 单选模式下,先清除所有选中状态
if (!props.multi) {
dataList.value.forEach(dataItem => {
if (dataItem.id != item.id) {
dataItem.checked = false;
}
});
// 清空已选择的keys
selectedKeys.value = [];
}
// 切换当前项的选中状态
item.checked = !item.checked;
// 更新selectedKeys数组
if (item.checked) {
selectedKeys.value.push(item.id);
} else {
selectedKeys.value = selectedKeys.value.filter(key => key !== item.id);
}
// update-end--author:liaozhiyang---date:20250414--for:【issues/8078】角色选择组件点击文字部分会一直选中
}
function prevent(e) {
e.preventDefault();
e.stopPropagation();
}
return {
register,
showDataList,
searchText,
handleOk,
selectedList,
selectedIdList,
unSelect,
onSelect
};
},
};
</script>
<style scoped lang="less">
.modal-select-list-container{
height: 352px;
margin-top: 12px;
overflow: auto;
.scroll{
height: 100%;
position: relative;
width: 100%;
overflow: hidden;
.content{
bottom: 0;
left: 0;
overflow: scroll;
overflow-x: hidden;
position: absolute;
right: 0;
top: 0;
.item{
padding: 7px 5px;
cursor: pointer;
display: block;
&:hover{
background-color: #f5f5f5;
}
}
}
}
}
</style>
<style lang="less">
.j-user-select-modal2 {
.depart-select {
.ant-select-selector {
color: #fff !important;
background-color: #409eff !important;
border-radius: 5px !important;
}
.ant-select-selection-item,
.ant-select-arrow {
color: #fff !important;
}
}
.my-search {
position: absolute;
top: 14px;
z-index: 1;
&.all-width {
width: 100%;
}
.anticon {
cursor: pointer;
&:hover {
color: #0a8fe9 !important;
}
}
.hidden {
display: none;
}
}
.my-tabs {
}
.selected-users {
display: flex;
flex-wrap: wrap;
flex-direction: row;
padding-top: 15px;
}
.scroll-container {
padding-bottom: 0 !important;
}
}
</style>
@@ -0,0 +1,150 @@
<template>
<div class="user-selected-item">
<div
style="
display: flex;
flex-direction: row;
height: 24px;
border-radius: 12px;
padding-right: 10px;
vertical-align: middle;
background-color: #f5f5f5;
"
>
<span style="width: 24px; height: 24px; line-height: 20px; margin-right: 3px; display: inline-block">
<a-avatar v-if="info.avatar" :src="getFileAccessHttpUrl(info.avatar)" :size="24"></a-avatar>
<a-avatar v-else-if="info.avatarIcon" class="ant-btn-primary" :size="24" >
<template #icon>
<Icon :icon=" 'ant-design:'+info.avatarIcon " style="font-size: 16px;margin-top: 4px"/>
</template>
</a-avatar>
<a-avatar v-else-if="info.selectType == 'sys_role'" :size="24" style="background-color: rgb(255, 173, 0);">
<template #icon>
<team-outlined style="font-size: 16px"/>
</template>
</a-avatar>
<a-avatar v-else-if="info.selectType == 'sys_position'" :size="24" style="background-color: rgb(245, 34, 45);">
<template #icon>
<TagsOutlined style="font-size: 16px"/>
</template>
</a-avatar>
<a-avatar :size="24" v-else>
<template #icon><UserOutlined /></template>
</a-avatar>
</span>
<div style="height: 24px; line-height: 24px" class="ellipsis">
{{ info.realname || info.name }}
</div>
<div v-if="showClose" class="icon-close">
<CloseOutlined @click="removeSelect"/>
</div>
</div>
<div v-if="!showClose" class="icon-remove">
<MinusCircleFilled @click="removeSelect" />
</div>
</div>
</template>
<script>
import { UserOutlined, CloseOutlined, MinusCircleFilled, TagsOutlined, TeamOutlined } from '@ant-design/icons-vue';
import {computed} from 'vue'
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
export default {
name: 'SelectedUserItem',
components: {
UserOutlined,
MinusCircleFilled,
CloseOutlined,
TagsOutlined,
TeamOutlined
},
props: {
info: {
type: Object,
default: () => {},
},
// 是否作为查询条件
query:{
type: Boolean,
default: false,
}
},
emits: ['unSelect'],
setup(props, { emit }) {
function removeSelect(e) {
e.preventDefault();
e.stopPropagation();
emit('unSelect', props.info.id);
}
const showClose = computed(()=>{
if(props.query===true){
return true;
}else{
return false;
}
});
return {
showClose,
removeSelect,
getFileAccessHttpUrl
};
},
};
</script>
<style lang="less">
.user-selected-item {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-right: 8px;
height: 30px;
border-radius: 12px;
line-height: 30px;
vertical-align: middle;
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.icon-remove {
position: absolute;
top: -10px;
right: -4px;
font-size: 18px;
width: 15px;
height: 15px;
cursor: pointer;
display: none;
}
.icon-close{
height: 22px;
line-height: 24px;
font-size: 10px;
font-weight: bold;
margin-left: 7px;
&:hover{
color: #0a8fe9;
}
}
&:hover {
.icon-remove {
display: block;
}
}
}
</style>
@@ -0,0 +1,193 @@
<template>
<a-list item-layout="horizontal" :data-source="showDataList">
<template #renderItem="{ item }">
<a-list-item style="padding: 3px 0">
<div class="user-select-user-info" @click="(e) => onClickUser(e, item)">
<div style="margin-left: 10px">
<a-checkbox v-model:checked="checkStatus[item.id]" v-if="multi" />
<a-radio v-model:checked="checkStatus[item.id]" v-else />
</div>
<div>
<a-avatar v-if="item.avatar" :src="getFileAccessHttpUrl(item.avatar)"></a-avatar>
<a-avatar v-else-if="item.avatarIcon" class="ant-btn-primary">
<template #icon>
<Icon :icon=" 'ant-design:'+item.avatarIcon " style="margin-top: 4px;font-size: 24px;"/>
</template>
</a-avatar>
<a-avatar v-else>
<template #icon><UserOutlined /></template>
</a-avatar>
</div>
<div :style="nameStyle">
{{ item.realname }}
</div>
<div :style="departStyle" class="ellipsis" :title="item.orgCodeTxt">
{{ item.orgCodeTxt }}
</div>
<div style="width: 1px"></div>
</div>
</a-list-item>
</template>
</a-list>
</template>
<script lang="ts">
import { UserOutlined } from '@ant-design/icons-vue';
import { computed, toRaw, reactive, watchEffect, ref } from 'vue';
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
export default {
name: 'UserList',
props: {
multi: {
type: Boolean,
default: false,
},
dataList: {
type: Array,
default: () => [],
},
// 是否显示部门文本
depart: {
type: Boolean,
default: false,
},
selectedIdList: {
type: Array,
default: () => [],
},
excludeUserIdList:{
type: Array,
default: () => [],
}
},
components: {
UserOutlined,
},
emits: ['selected', 'unSelect'],
setup(props, { emit }) {
function onClickUser(e, user) {
e && prevent(e);
let status = checkStatus[user.id];
if (status === true) {
emit('unSelect', user.id);
} else {
emit('selected', toRaw(user));
}
}
function getTwoText(text) {
if (!text) {
return '';
} else {
return text.substr(0, 2);
}
}
const departStyle = computed(() => {
if (props.depart === true) {
// 如果显示部门信息
return {
flex: 1,
};
} else {
return {
display: 'none',
};
}
});
const nameStyle = computed(() => {
if (props.depart === true) {
// 如果显示部门信息
return {
width: '200px',
};
} else {
return {
flex: 1,
};
}
});
function onChangeChecked(e) {
console.error('onChangeChecked', e);
}
// const showDataList = ref<any[]>([])
const checkStatus = reactive<any>({});
watchEffect(() => {
let arr1 = props.dataList;
if (!arr1 || arr1.length === 0) {
return;
}
let idList = props.selectedIdList;
for (let item of arr1) {
if (idList.indexOf(item.id) >= 0) {
checkStatus[item.id] = true;
} else {
checkStatus[item.id] = false;
}
}
});
function prevent(e) {
e.preventDefault();
e.stopPropagation();
}
//update-begin---author:wangshuai---date:2024-02-02---for:【QQYUN-8239】用户角色,添加用户 返回2页数据,实际只显示一页---
/* function records2DataList() {
let arr:any[] = [];
let excludeList = props.excludeUserIdList;
let records = props.dataList;
if(records && records.length>0){
for(let item of records){
if(excludeList.indexOf(item.id)<0){
arr.push({...item})
}
}
}
return arr;
}*/
const showDataList = computed(()=>{
/* let excludeList = props.excludeUserIdList;
if(excludeList && excludeList.length>0){
return records2DataList();
}*/
//update-end---author:wangshuai---date:2024-02-02---for:【QQYUN-8239】用户角色,添加用户 返回2页数据,实际只显示一页---
return props.dataList;
});
return {
onClickUser,
getTwoText,
departStyle,
nameStyle,
onChangeChecked,
checkStatus,
showDataList,
getFileAccessHttpUrl
};
},
};
</script>
<style lang="less">
.user-select-user-info {
display: flex;
width: 100%;
> div {
height: 36px;
line-height: 36px;
margin-right: 10px;
}
.ellipsis {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
</style>
@@ -0,0 +1,195 @@
<template>
<a-row>
<a-col :span="12">
<div :style="containerStyle">
<a-tree
v-if="treeData.length > 0"
:load-data="loadChildren"
showIcon
autoExpandParent
:treeData="treeData"
:selectedKeys="selectedKeys"
v-model:expandedKeys="expandedKeys"
@select="onSelect"
>
<template #title="{ title, key }">
<FolderFilled style="color: #9e9e9e"/><span style="margin-left: 5px">{{ title }}</span>
</template>
</a-tree>
</div>
</a-col>
<a-col :span="12" style="padding-left: 10px">
<div :style="containerStyle">
<user-list :multi="multi" :excludeUserIdList="excludeUserIdList" :dataList="userDataList" :selectedIdList="selectedIdList" @selected="onSelectUser" @unSelect="unSelectUser" />
</div>
</a-col>
</a-row>
</template>
<script lang="ts">
import { defHttp } from '/@/utils/http/axios';
import { computed, ref, watch } from 'vue';
import UserList from './UserList.vue';
import { FolderFilled } from '@ant-design/icons-vue';
export default {
name: 'DepartUserList',
components: {
UserList,
FolderFilled
},
props: {
searchText: {
type: String,
default: '',
},
selectedIdList: {
type: Array,
default: () => [],
},
excludeUserIdList:{
type: Array,
default: () => [],
},
multi: {
type: Boolean,
default: false,
}
},
emits: ['loaded', 'selected', 'unSelect'],
setup(props, { emit }) {
//export const queryById = (params) => defHttp.get({ url: Api.queryById, params }, { isTransformResponse: false });
async function loadDepartTree(pid?) {
const url = '/sys/sysDepart/queryDepartTreeSync';
let params = {};
if (pid) {
params['pid'] = pid;
}
const data = await defHttp.get({ url, params }, { isTransformResponse: false });
console.log('loadDepartTree', data);
return data;
}
async function initRoot() {
console.log('initRoot');
const data = await loadDepartTree();
if (data.success) {
let arr = data.result;
treeData.value = arr;
emitDepartOptions(arr);
} else {
console.error(data.message);
}
clear();
}
function emitDepartOptions(arr) {
let options = [];
if (arr && arr.length > 0) {
options = arr.map((k) => {
return {
value: k.id,
label: k.departName,
};
});
}
emit('loaded', options);
}
initRoot();
const treeData = ref<any[]>([]);
const selectedKeys = ref<string[]>([]);
const expandedKeys = ref<string[]>([]);
const selectedDepartId = ref('');
function onSelect(ids, e) {
let record = e.node.dataRef;
selectedKeys.value = [record.key];
let id = ids[0];
selectedDepartId.value = id;
loadUserList();
}
function clear() {
selectedDepartId.value = '';
}
async function loadChildren(treeNode) {
console.log('loadChildren', treeNode);
const data = await loadDepartTree(treeNode.eventKey);
if (data.success) {
let arr = data.result;
treeNode.dataRef.children = [...arr];
treeData.value = [...treeData.value];
} else {
console.error(data.message);
}
}
const maxHeight = ref(300);
maxHeight.value = window.innerHeight - 300;
const containerStyle = computed(() => {
return {
'overflow-y': 'auto',
'max-height': maxHeight.value + 'px',
};
});
const userDataList = ref<any[]>([]);
async function loadUserList() {
const url = '/sys/user/selectUserList';
let params = {
pageNo: 1,
pageSize: 99,
};
if (props.searchText) {
params['keyword'] = props.searchText;
}
if (selectedDepartId.value) {
params['departId'] = selectedDepartId.value;
}
//update-begin---author:wangshuai---date:2024-02-02---for:【QQYUN-8239】用户角色,添加用户 返回2页数据,实际只显示一页---
if(props.excludeUserIdList && props.excludeUserIdList.length>0){
params['excludeUserIdList'] = props.excludeUserIdList.join(",");
}
//update-end---author:wangshuai---date:2024-02-02---for:【QQYUN-8239】用户角色,添加用户 返回2页数据,实际只显示一页---
const data = await defHttp.get({ url, params }, { isTransformResponse: false });
if (data.success) {
const { records } = data.result;
userDataList.value = records;
} else {
console.error(data.message);
}
console.log('depart-loadUserList', data);
}
watch(
() => props.searchText,
() => {
loadUserList();
}
);
function onSelectUser(info) {
emit('selected', info);
}
function unSelectUser(id) {
emit('unSelect', id);
}
return {
containerStyle,
treeData,
selectedKeys,
expandedKeys,
onSelect,
loadChildren,
onSelectUser,
unSelectUser,
userDataList,
};
},
};
</script>
<style scoped></style>
@@ -0,0 +1,151 @@
<template>
<a-row>
<a-col :span="12">
<div :style="containerStyle">
<a-tree v-if="treeData.length > 0" showIcon :treeData="treeData" :selectedKeys="selectedKeys" @select="onSelect">
<template #title="{ title, key }">
<UserOutlined style="color: #9e9e9e"/><span style="margin-left: 5px">{{ title }}</span>
</template>
</a-tree>
</div>
</a-col>
<a-col :span="12" style="padding-left: 10px">
<div :style="containerStyle">
<user-list :multi="multi" :excludeUserIdList="excludeUserIdList" :dataList="userDataList" :selectedIdList="selectedIdList" @selected="onSelectUser" @unSelect="unSelectUser" />
</div>
</a-col>
</a-row>
</template>
<script lang="ts">
import { computed, ref, watch } from 'vue';
import { defHttp } from '/@/utils/http/axios';
import UserList from './UserList.vue';
import { UserOutlined } from '@ant-design/icons-vue';
export default {
name: 'RoleUserList',
components: {
UserList,
UserOutlined
},
props: {
searchText: {
type: String,
default: '',
},
selectedIdList: {
type: Array,
default: () => [],
},
excludeUserIdList:{
type: Array,
default: () => [],
},
multi: {
type: Boolean,
default: false,
}
},
emits: ['selected', 'unSelect'],
setup(props, { emit }) {
const treeData = ref<any[]>([]);
async function loadRoleList() {
const url = '/sys/role/listByTenant';
let params = {
order: 'desc',
column: 'createTime',
pageSize: 200,
};
let arr = [];
const data = await defHttp.get({ url, params }, { isTransformResponse: false });
if (data.success) {
const { records } = data.result;
arr = records.map((k) => {
return {
title: k.roleName,
id: k.id,
key: k.id,
};
});
}
console.log('loadRoleList', data);
treeData.value = arr;
}
loadRoleList();
const selectedKeys = ref<any[]>([]);
const selectedRoleId = ref('');
function onSelect(ids, e) {
let record = e.node.dataRef;
selectedKeys.value = [record.key];
let id = ids[0];
selectedRoleId.value = id;
loadUserList();
}
const userDataList = ref<any[]>([]);
async function loadUserList() {
const url = '/sys/user/selectUserList';
let params = {
pageNo: 1,
pageSize: 99,
};
if (props.searchText) {
params['keyword'] = props.searchText;
}
if (selectedRoleId.value) {
params['roleId'] = selectedRoleId.value;
}
//update-begin---author:wangshuai---date:2024-02-02---for:【QQYUN-8239】用户角色,添加用户 返回2页数据,实际只显示一页---
if(props.excludeUserIdList && props.excludeUserIdList.length>0){
params['excludeUserIdList'] = props.excludeUserIdList.join(",");
}
//update-end---author:wangshuai---date:2024-02-02---for:【QQYUN-8239】用户角色,添加用户 返回2页数据,实际只显示一页---
const data = await defHttp.get({ url, params }, { isTransformResponse: false });
if (data.success) {
const { records } = data.result;
userDataList.value = records;
} else {
console.error(data.message);
}
console.log('role-loadUserList', data);
}
watch(
() => props.searchText,
() => {
loadUserList();
}
);
function onSelectUser(info) {
emit('selected', info);
}
function unSelectUser(id) {
emit('unSelect', id);
}
const maxHeight = ref(300);
maxHeight.value = window.innerHeight - 300;
const containerStyle = computed(() => {
return {
'overflow-y': 'auto',
'max-height': maxHeight.value + 'px',
};
});
return {
containerStyle,
treeData,
selectedKeys,
onSelect,
onSelectUser,
unSelectUser,
userDataList,
};
},
};
</script>
<style scoped></style>
@@ -0,0 +1,376 @@
<template>
<BasicModal
@register="register"
:getContainer="getContainer"
:canFullscreen="false"
title="选择用户"
:width="600"
wrapClassName="j-user-select-modal2"
>
<!-- 部门下拉框 -->
<a-select v-model:value="selectedDepart" style="width: 100%" class="depart-select" @change="onDepartChange">
<a-select-option v-for="item in departOptions" :value="item.value">{{ item.label }}</a-select-option>
</a-select>
<div style="position: relative; min-height: 350px">
<!-- 用户搜索框 -->
<div :class="searchInputStatus ? 'my-search all-width' : 'my-search'">
<span :class="searchInputStatus ? 'hidden' : ''" style="margin-left: 10px"
><SearchOutlined style="color: #c0c0c0" @click="showSearchInput"
/></span>
<div style="width: 100%" :class="searchInputStatus ? '' : 'hidden'">
<a-input v-model:value="searchText" @pressEnter="onSearchUser" style="width: 100%" placeholder="请输入用户名按回车搜索">
<template #prefix>
<SearchOutlined style="color: #c0c0c0" />
</template>
<template #suffix>
<CloseOutlined title="退出搜索" @click="clearSearch" />
</template>
</a-input>
</div>
</div>
<!-- tabs -->
<div class="my-tabs">
<a-tabs v-model:activeKey="myActiveKey" :centered="true" @change="onChangeTab">
<!-- 所有用户 -->
<a-tab-pane key="1" tab="全部" forceRender>
<user-list :multi="multi" :excludeUserIdList="excludeUserIdList" :dataList="userDataList" :selectedIdList="selectedIdList" depart @selected="onSelectUser" @unSelect="unSelectUser" />
</a-tab-pane>
<!-- 部门用户 -->
<a-tab-pane key="2" tab="按部门" forceRender>
<depart-user-list
:searchText="searchText"
:selectedIdList="selectedIdList"
:excludeUserIdList="excludeUserIdList"
@loaded="initDepartOptions"
@selected="onSelectUser"
@unSelect="unSelectUser"
/>
</a-tab-pane>
<!-- 角色用户 -->
<a-tab-pane key="3" tab="按角色" forceRender>
<role-user-list :excludeUserIdList="excludeUserIdList" :searchText="searchText" :selectedIdList="selectedIdList" @selected="onSelectUser" @unSelect="unSelectUser" />
</a-tab-pane>
</a-tabs>
</div>
<!-- 选中用户 -->
<div class="selected-users" style="width: 100%; overflow-x: hidden">
<SelectedUserItem v-for="item in selectedUserList" :info="item" @unSelect="unSelectUser" />
</div>
</div>
<template #footer>
<div style="display: flex; justify-content: space-between; width: 100%">
<div class="select-user-page-info">
<a-pagination
v-if="myActiveKey == '1'"
v-model:current="pageNo"
size="small"
:total="totalRecord"
show-quick-jumper
@change="onPageChange"
/>
</div>
<a-button type="primary" @click="handleOk"> </a-button>
</div>
</template>
</BasicModal>
</template>
<script lang="ts">
import { BasicModal, useModalInner } from '/@/components/Modal';
import { SearchOutlined, CloseOutlined } from '@ant-design/icons-vue';
import UserList from './UserList.vue';
import SelectedUserItem from './SelectedUserItem.vue';
import DepartUserList from './UserListAndDepart.vue';
import RoleUserList from './UserListAndRole.vue';
import { Pagination } from 'ant-design-vue';
const APagination = Pagination;
import { defHttp } from '/@/utils/http/axios';
import {computed, ref, toRaw, unref} from 'vue';
import { useUserStore } from '/@/store/modules/user';
import { mySelfData } from './useUserSelect'
export default {
name: 'UserSelectModal',
components: {
BasicModal,
SearchOutlined,
CloseOutlined,
SelectedUserItem,
UserList,
DepartUserList,
RoleUserList,
APagination,
},
props: {
multi: {
type: Boolean,
default: false,
},
getContainer: {
type: Function,
default: null,
},
//是否排除我自己
izExcludeMy: {
type: Boolean,
default: false,
},
//是否在高级查询中作为条件 可以选择当前用户表达式
inSuperQuery:{
type: Boolean,
default: false,
}
},
emits: ['selected', 'register'],
setup(props, { emit }) {
const myActiveKey = ref('1');
const selectedUserList = ref<any[]>([]);
const userStore = useUserStore();
const selectedIdList = computed(() => {
let arr = selectedUserList.value;
if (!arr || arr.length == 0) {
return [];
} else {
return arr.map((k) => k.id);
}
});
// QQYUN-4152【应用】已经存在的用户,添加的时候还可以重复选择
const excludeUserIdList = ref<any[]>([]);
// 弹窗事件
const [register] = useModalInner((data) => {
let list = data.list;
if (list && list.length > 0) {
selectedUserList.value = [...list];
} else {
selectedUserList.value = [];
}
if(data.excludeUserIdList){
excludeUserIdList.value = data.excludeUserIdList;
}else{
excludeUserIdList.value = [];
}
//如果排除我自己,直接excludeUserIdList.push排除即可
if (props.izExcludeMy) {
excludeUserIdList.value.push(userStore.getUserInfo.id);
}
//加载用户列表
loadUserList();
});
// 确定事件
function handleOk() {
let arr = toRaw(selectedUserList.value);
emit('selected', arr);
}
/*--------------部门下拉框,用于筛选用户---------------*/
const selectedDepart = ref('');
const departOptions = ref<any[]>([]);
function initDepartOptions(options) {
departOptions.value = [{ value: '', label: '全部用户' }, ...options];
selectedDepart.value = '';
}
function onDepartChange() {
loadUserList();
}
/*--------------部门下拉框,用于筛选用户---------------*/
/*--------------第一页 搜索框---------------*/
const searchInputStatus = ref(false);
const searchText = ref('');
function showSearchInput(e) {
e && prevent(e);
searchInputStatus.value = true;
}
// 回车事件,触发查询
function onSearchUser() {
pageNo.value = 1;
loadUserList();
}
// 清除按名称筛选
function clearSearch(e) {
e && prevent(e);
pageNo.value = 1;
searchText.value = '';
searchInputStatus.value = false;
loadUserList();
}
/*--------------第一页 搜索框---------------*/
/*--------------加载数据---------------*/
const pageNo = ref(1);
const totalRecord = ref(0);
const userDataList = ref<any[]>([]);
async function onPageChange() {
console.log('onPageChange', pageNo.value);
await loadUserList();
}
async function loadUserList() {
const url = '/sys/user/selectUserList';
let params = {
pageNo: pageNo.value,
pageSize: 10,
};
if (searchText.value) {
params['keyword'] = searchText.value;
}
if (selectedDepart.value) {
params['departId'] = selectedDepart.value;
}
//update-begin---author:wangshuai---date:2024-02-02---for:【QQYUN-8239】用户角色,添加用户 返回2页数据,实际只显示一页---
if(unref(excludeUserIdList) && unref(excludeUserIdList).length>0){
params['excludeUserIdList'] = excludeUserIdList.value.join(",");
}
//update-end---author:wangshuai---date:2024-02-02---for:【QQYUN-8239】用户角色,添加用户 返回2页数据,实际只显示一页---
const data = await defHttp.get({ url, params }, { isTransformResponse: false });
if (data.success) {
let { records, total } = data.result;
totalRecord.value = total;
initCurrentUserData(records);
userDataList.value = records;
} else {
console.error(data.message);
}
console.log('loadUserList', data);
}
// 往用户列表中添加一个 当前用户选项
function initCurrentUserData(records) {
if(pageNo.value==1 && props.inSuperQuery === true){
records.unshift({...mySelfData})
}
}
/*--------------加载数据---------------*/
/*--------------选中/取消选中---------------*/
function onSelectUser(info) {
if (props.multi === true) {
let arr = selectedUserList.value;
let idList = selectedIdList.value;
if (idList.indexOf(info.id) < 0) {
arr.push({ ...info });
selectedUserList.value = arr;
}
} else {
selectedUserList.value = [{ ...info }];
}
}
function unSelectUser(id) {
let arr = selectedUserList.value;
let index = -1;
for (let i = 0; i < arr.length; i++) {
if (arr[i].id === id) {
index = i;
break;
}
}
if (index >= 0) {
arr.splice(index, 1);
selectedUserList.value = arr;
}
}
/*--------------选中/取消选中---------------*/
function onChangeTab(tab) {
myActiveKey.value = tab;
}
function prevent(e) {
e.preventDefault();
e.stopPropagation();
}
//加载第一页数据
loadUserList();
return {
selectedDepart,
departOptions,
initDepartOptions,
onDepartChange,
register,
handleOk,
searchText,
searchInputStatus,
showSearchInput,
onSearchUser,
clearSearch,
myActiveKey,
onChangeTab,
pageNo,
totalRecord,
onPageChange,
userDataList,
selectedUserList,
selectedIdList,
onSelectUser,
unSelectUser,
excludeUserIdList
};
},
};
</script>
<style lang="less">
.j-user-select-modal2 {
.depart-select {
.ant-select-selector {
color: #fff !important;
background-color: #409eff !important;
border-radius: 5px !important;
}
.ant-select-selection-item,
.ant-select-arrow {
color: #fff !important;
}
}
.my-search {
position: absolute;
top: 14px;
z-index: 1;
&.all-width {
width: 100%;
}
.anticon {
cursor: pointer;
&:hover {
color: #0a8fe9 !important;
}
}
.hidden {
display: none;
}
}
.my-tabs {
}
.selected-users {
display: flex;
flex-wrap: wrap;
flex-direction: row;
padding-top: 15px;
}
.scroll-container {
padding-bottom: 0 !important;
}
}
</style>
@@ -0,0 +1,273 @@
<template>
<div>
<div v-if="isSearchFormComp" @click="click2Add" :class="disabled?'disabled-user-select':''" style="padding:0 5px;background-color: #fff;border: 1px solid #ccc;border-radius: 3px;box-sizing: border-box;display:flex;color: #9e9e9e;font-size: 14px;flex-wrap: wrap;min-height: 32px;">
<template v-if="selectedUserList.length > 0">
<SelectedUserItem v-for="item in showUserList" :info="item" @unSelect="unSelectUser" query />
</template>
<span v-else style="height: 30px;line-height: 30px;display: inline-block;margin-left: 7px;color: #bfbfbf;">请选择用户</span>
<div v-if="ellipsisInfo.status" class="user-selected-item">
<div class="user-select-ellipsis">
<span style="color: red">+{{ellipsisInfo.count}}...</span>
</div>
</div>
</div>
<div v-else style="display: flex; flex-wrap: wrap; flex-direction: row" >
<template v-if="selectedUserList.length > 0">
<SelectedUserItem v-for="item in selectedUserList" :info="item" @unSelect="unSelectUser" />
</template>
<a-button v-if="showAddButton" shape="circle" @click="onShowModal"><PlusOutlined /></a-button>
</div>
<user-select-modal :inSuperQuery="inSuperQuery" :multi="multi" :getContainer="getContainer" @register="registerModal" @selected="onSelected" :izExcludeMy="izExcludeMy"></user-select-modal>
</div>
</template>
<script lang="ts">
import { defineComponent, watch, ref, computed, toRaw } from 'vue';
import { Form } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import { useModal } from '/@/components/Modal';
import UserSelectModal from './UserSelectModal.vue';
import { defHttp } from '/@/utils/http/axios';
import SelectedUserItem from './SelectedUserItem.vue';
import { mySelfExpress, mySelfData } from './useUserSelect'
export default defineComponent({
name: 'UserSelect',
components: {
PlusOutlined,
UserSelectModal,
SelectedUserItem,
},
props: {
store: {
type: String,
default: 'id',
},
value: {
type: String,
default: '',
},
multi: {
type: Boolean,
default: false,
},
getContainer: {
type: Function,
default: null,
},
// 是否作为查询条件
query:{
type: Boolean,
default: false,
},
//最多显示几个人员-query为true有效
maxCount:{
type: Number,
default: 2
},
disabled:{
type: Boolean,
default: false,
},
//是否排除我自己
izExcludeMy:{
type: Boolean,
default: false,
},
//是否在高级查询中作为条件 可以选择当前用户
inSuperQuery:{
type: Boolean,
default: false,
}
},
emits: ['update:value', 'change'],
setup(props, { emit }) {
const formItemContext = Form.useInjectFormItemContext();
const loading = ref(true);
const selectedUserList = ref<any[]>([]);
const showUserList = computed(()=>{
let list = selectedUserList.value
let max = props.maxCount;
if(list.length<=max){
return list;
}
return list.filter((_item, index)=>index<max);
});
const ellipsisInfo = computed(()=>{
let max = props.maxCount;
let len = selectedUserList.value.length
if(len > max){
return {status: true, count: len-max};
}else{
return {status: false}
}
});
// 注册弹窗
const [registerModal, { openModal, closeModal }] = useModal();
function onShowModal() {
if(props.disabled===true){
return ;
}
let list = toRaw(selectedUserList.value);
openModal(true, {
list,
});
}
function onSelected(arr) {
console.log('onSelected', arr);
selectedUserList.value = arr;
onSelectedChange();
closeModal();
}
function onSelectedChange() {
loading.value = false;
let temp: any[] = [];
let arr = selectedUserList.value;
if (arr && arr.length > 0) {
temp = arr.map((k) => {
return k[props.store];
});
}
let str = temp.join(',');
emit('update:value', str);
emit('change', str);
formItemContext.onFieldChange();
console.log('选中数据', str);
}
watch(
() => props.value,
async (val) => {
if (val) {
if (loading.value === true) {
await getUserList(val);
}
} else {
selectedUserList.value = [];
}
loading.value = true;
},
{ immediate: true }
);
async function getUserList(ids) {
let hasUserExpress = false;
let paramIds = ids;
let idList = [];
selectedUserList.value = [];
if(ids){
// update-begin-author:sunjianlei date:20230330 for: 修复用户选择器逗号分割回显不生效的问题
let tempArray = ids.split(',').map(s => s.trim()).filter(s => s != '');
if (tempArray.includes(mySelfExpress)) {
hasUserExpress = true;
idList = tempArray.filter(item => item != mySelfExpress);
} else {
idList = tempArray;
}
// update-end-author:sunjianlei date:20230330 for: 修复用户选择器逗号分割回显不生效的问题
}
if(idList.length>0){
paramIds = idList.join(',')
const url = '/sys/user/list';
let params = {
[props.store]: paramIds,
};
const data = await defHttp.get({ url, params }, { isTransformResponse: false });
console.log('getUserList', data);
if (data.success) {
const { records } = data.result;
selectedUserList.value = records;
} else {
console.error(data.message);
}
}
if(hasUserExpress){
let temp = selectedUserList.value;
temp.push({...mySelfData})
}
}
const showAddButton = computed(() => {
if(props.disabled === true){
return false;
}
if (props.multi === true) {
return true;
} else {
if (selectedUserList.value.length > 0) {
return false;
} else {
return true;
}
}
});
function unSelectUser(id) {
console.log('unSelectUser', id);
let arr = selectedUserList.value;
let index = -1;
for (let i = 0; i < arr.length; i++) {
if (arr[i].id == id) {
index = i;
break;
}
}
if (index >= 0) {
arr.splice(index, 1);
selectedUserList.value = arr;
onSelectedChange();
}
}
function click2Add(e) {
e.preventDefault();
e.stopPropagation();
onShowModal();
}
const isSearchFormComp = computed(()=>{
if(props.query===true){
return true;
}else{
return false
}
});
return {
registerModal,
onShowModal,
isSearchFormComp,
onSelected,
showAddButton,
unSelectUser,
selectedUserList,
showUserList,
ellipsisInfo,
click2Add
};
},
});
</script>
<style lang="less" scoped>
.user-select-ellipsis{
width: 40px;
height: 24px;
text-align: center;
line-height: 22px;
border-radius: 8px;
background: #f5f5f5;
border: 1px solid #f0f0f0;
}
.disabled-user-select{
cursor: not-allowed;
background-color: #f5f5f5 !important;
}
</style>
@@ -0,0 +1,11 @@
/**
* 用户选择组件支持选择 我自己,以表达式的形式传值
*/
export const mySelfExpress = '#{sys_user_code}';
/**
* 用户列表 我自己的数据
*/
export const mySelfData = {
id: mySelfExpress, username: mySelfExpress, realname: '当前用户', avatarIcon: 'idcard-outlined', avatarColor: 'rgb(75 176 79)'
}
@@ -0,0 +1,143 @@
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.slice(-(customKeywords[i].length + 1), -1);
if (customKeywords.includes(k)) {
recordKeyword = k;
break;
}
}
} else {
// 查找单词前面是否有.this(.关键词)
const curLineCode = cm.getLine(cur.line);
for (let i = 0, len = customKeywords.length; i < len; i++) {
const k = curLineCode.slice(start - (customKeywords[i].length + 1), start);
if (k.substr(-1) === '.' && customKeywords.includes(k.replace('.', ''))) {
recordKeyword = k.replace('.', '');
break;
}
}
}
const findIdx = (a, b) => a.toLowerCase().indexOf(b.toLowerCase());
let list = currentKeywords.filter((item) => {
if (recordKeyword) {
// 查特定对象下的属性or方法
return item.superiors === recordKeyword;
} else {
// 查全局属性或者方法
return item.superiors == undefined;
}
});
if (str === '.') {
if (recordKeyword == null) {
list = [];
}
} else {
list = list
.filter((item) => {
const { text } = item;
const index = findIdx(text, str);
let result = text.startsWith('.') ? index === 1 : index === 0;
return result;
})
.sort((a, b) => {
if (findIdx(a.text, str) < findIdx(b.text, str)) {
return -1;
} else {
return 1;
}
});
}
if (list.length === 1) {
// 只有一个时可能是自己输入,输到最后需要去掉提示。
const item = list[0];
if (item.text === str || item.text.substring(1) === str) {
list = [];
}
}
if (list.length) {
// 当str不是点时去掉点
if (str != '.') {
list = list.map((item) => {
if (item.text.indexOf('.') === 0) {
return { ...item, text: item.text.substring(1) };
}
return item;
});
}
callback({
list: list,
from: CodeMirror.Pos(cur.line, start),
to: CodeMirror.Pos(cur.line, end),
});
// update-begin--author:liaozhiyang---date:20240429---for:【QQYUN-8865】js增强加上鼠标移入提示
const item = currentKeywords[0];
if (item?.desc) {
setTimeout(() => {
const elem: HTMLUListElement = document.querySelector('.CodeMirror-hints')!;
if (elem) {
const childElems = elem.children;
Array.from(childElems).forEach((item) => {
const displayText = item.textContent;
const findItem = currentKeywords.find((item) => item.displayText === displayText);
if (findItem) {
item.setAttribute('title', findItem.desc);
}
});
}
}, 0);
}
// update-end--author:liaozhiyang---date:20240429---for:【QQYUN-8865】js增强加上鼠标移入提示
} else {
}
}
};
funcsHint.async = true;
funcsHint.supportsSelection = true;
// 自动补全
keywords.length && CodeMirror.registerHelper('hint', language, funcsHint);
};
return {
codeHintingRegistry,
codeHintingMount,
};
};
@@ -0,0 +1,185 @@
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]发文草稿箱编辑时拟稿人显示用户名------------
// update-begin-author:liaozhiyang---date:2024-11-11--for:【issues/7405】部门选择用户同时全部选择两页用户,回显到父页面。第二页用户显示的不是真是姓名
let params = { isMultiTranslate: 'true', pageSize: selectValues.value?.length };
// update-end-author:liaozhiyang---date:2024-10-11--for:【issues/7405】部门选择用户同时全部选择两页用户,回显到父页面。第二页用户显示的不是真是姓名
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;
});
}
//设置列表默认选中
// update-begin--author:liaozhiyang---date:20250423---for:【QQYUN-12155】弹窗中勾选,再点取消,值被选中了
checkedKeys['value'] = [...selectValues['value']];
// update-end--author:liaozhiyang---date:20250423---for:【QQYUN-12155】弹窗中勾选,再点取消,值被选中了
},
{ 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,
});
// update-begin--author:liaozhiyang---date:20250423---for:【QQYUN-12155】弹窗中勾选,再点取消,值被选中了
checkedKeys['value'] = [...selectValues['value']];
// update-end--author:liaozhiyang---date:20250423---for:【QQYUN-12155】弹窗中勾选,再点取消,值被选中了
selectRows['value'] = records;
}
/**
* 弹出框显示隐藏触发事件
*/
async function visibleChange(visible) {
if (visible) {
// update-begin--author:liaozhiyang---date:20250423---for:【QQYUN-12179】弹窗勾选了值,点击取消再次打开弹窗遗留了上次的勾选的值
checkedKeys['value'] = [...selectValues['value']];
// update-begin--author:liaozhiyang---date:20250423---for:【QQYUN-12179】弹窗勾选了值,点击取消再次打开弹窗遗留了上次的勾选的值
//设置列表默认选中
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,
},
];
}
@@ -0,0 +1,284 @@
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';
import { isEqual } from 'lodash-es';
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;
let prevSelectValues = [];
/**
* 监听selectValues变化
*/
watch(
selectValues,
({ value: values }: Recordable) => {
if(!values){
return;
}
// update-begin--author:liaozhiyang---date:20250604---for:【issues/8232】代码设置JSelectDept组件值没翻译
if (values.length > 0) {
// 防止多次请求
if (isEqual(values, prevSelectValues)) return;
prevSelectValues = values;
loadingEcho.value = isFirstLoadEcho;
isFirstLoadEcho = false;
onLoadData(null, values.join(',')).finally(() => {
loadingEcho.value = false;
});
// update-end--author:liaozhiyang---date:20250604---for:【issues/8232】代码设置JSelectDept组件值没翻译
}
},
{ 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不设置成id,sync开启异步的时候,点击上级下级不显示------------
params['pid'] = treeNode.value;
//update-end---author:wangshuai ---date:20220407 forrowkey不设置成id,sync开启异步的时候,点击上级下级不显示------------
}
if (ids) {
startPid = '';
params['ids'] = ids;
}
let record = await getList(params);
let optionData = record;
if (!props.serverTreeData) {
//前端处理数据为tree结构
record = listToTree(record, props, startPid);
if (record.length == 0 && treeNode) {
checkHasChild(startPid, treeData.value);
}
}
if (openModal.value == true) {
//弹框模式下加载全部数据
if (!treeNode) {
treeData.value = record;
} else {
return new Promise((resolve: (value?: unknown) => void) => {
if (!treeNode.children) {
resolve();
return;
}
const asyncTreeAction: TreeActionType | null = unref(treeRef);
if (asyncTreeAction) {
asyncTreeAction.updateNodeByKey(treeNode.eventKey, { children: record });
asyncTreeAction.setExpandedKeys([treeNode.eventKey, ...asyncTreeAction.getExpandedKeys()]);
}
resolve();
return;
});
}
expandTree();
} else {
const options = <any[]>[];
optionData.forEach((item) => {
//update-begin-author:taoyan date:2022-7-4 for: issues/I5F3P4 online配置部门选择后编辑,查看数据应该显示部门名称,不是部门代码
options.push({ label: item[props.labelKey], value: item[props.rowKey] });
//update-end-author:taoyan date:2022-7-4 for: issues/I5F3P4 online配置部门选择后编辑,查看数据应该显示部门名称,不是部门代码
});
selectOptions.value = options;
}
}
/**
* 异步加载时检测是否含有下级节点
* @param pid 父节点
* @param treeArray tree数据
*/
function checkHasChild(pid, treeArray) {
if (treeArray && treeArray.length > 0) {
for (let item of treeArray) {
if (item.key == pid) {
if (!item.child) {
item.isLeaf = true;
}
break;
} else {
checkHasChild(pid, item.children);
}
}
}
}
/**
* 获取已选择数据
*/
function getSelectTreeData(success) {
const options = <any[]>[];
const values = <any[]>[];
selectRows.value.forEach((item) => {
options.push({ label: item[props.labelKey], value: item[props.rowKey] });
});
checkedKeys.value.forEach((item) => {
values.push(item);
});
selectOptions.value = options;
success && success(options, values);
}
/**
* 弹出框显示隐藏触发事件
*/
async function visibleChange(visible) {
if (visible) {
//弹出框打开时加载全部数据
openModal.value = true;
await onLoadData(null, null);
} else {
openModal.value = false;
// update-begin--author:liaozhiyang---date:20240527---for:【TV360X-414】部门设置了默认值,查询重置变成空了(同步JSelectUser组件改法)
emit?.('close');
// update-end--author:liaozhiyang---date:20240527---for:【TV360X-414】部门设置了默认值,查询重置变成空了(同步JSelectUser组件改法)
}
}
return [
{
visibleChange,
selectOptions,
selectValues,
onLoadData,
onCheck,
onSelect,
checkALL,
expandAll,
checkedKeys,
selectRows,
treeData,
getCheckStrictly,
getSelectTreeData,
},
];
}

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