--添加测试模块

This commit is contained in:
2025-10-13 11:53:54 +08:00
parent c3c93fe308
commit e1326c7ce8
146 changed files with 11171 additions and 807 deletions

View File

@ -0,0 +1,4 @@
import { withInstall } from '/@/utils';
import customComponent from './src/CustomComponent.vue';
export const CustomComponent = withInstall(customComponent);

View File

@ -0,0 +1,60 @@
<template>
<component :is="component" v-model:value="modelValue" v-bind="props"/>
</template>
<script lang="ts" setup>
import { computed, ref, watch, defineAsyncComponent, onMounted} from 'vue';
const props = defineProps({
defaultValue: {
type: Object
},
value: {
type: Object
},
component: Object,
formData: Object, //主表
row: Object, //子表
});
const modelValue = ref(null);
const component = ref(
defineAsyncComponent({
loader: () => props.component
})
);
const emit = defineEmits(['update:value', 'change', 'blur']);
onMounted(() => {
})
watch(
() => props.defaultValue,
(val) => {
if(val){
emit('update:value', val);
}
},
{
immediate: true,
},
);
watch(
() => props.value,
(val) => {
modelValue.value = val;
},
{
immediate: true,
},
);
watch(
modelValue,
(val) => {
emit('update:value', val);
},
{
deep: true
}
)
</script>

View File

@ -37,7 +37,7 @@
</a-select>
</a-form-item>
<a-form-item :label="t('绑定字段')" required>
<a-select v-model:value="item.bindField" :placeholder="t('请选择表字段')" size="mini">
<a-select v-model:value="item.bindField" showSearch :placeholder="t('请选择表字段')" size="mini">
<a-select-option v-for="(field, idx) in tableField" :value="field.name" :key="idx" />
</a-select>
</a-form-item>

View File

@ -18,7 +18,7 @@
</a-select>
</a-form-item>
<a-form-item v-if="(data.type == 'input' && !data.options.isSave) || (!noHaveField.includes(data.type) && data.type !== 'input')" :label="t('绑定字段')">
<a-select v-model:value="data.bindField" :placeholder="t('请选择表字段')" size="mini">
<a-select v-model:value="data.bindField" showSearch :placeholder="t('请选择表字段')" size="mini">
<a-select-option v-for="(field, idx) in fieldsInfo" :key="idx" :value="field.name">
{{ field.name }}
<span>
@ -467,6 +467,15 @@
<a-form-item v-if="hasKey('span') && (!data.isSubFormChild || !data.isSingleFormChild)" label="标签宽度">
<a-input-number v-model:value="data.options.span" :max="24" :min="0" addonAfter="/ 24" @change="handleSpanChange" />
</a-form-item>
<a-form-item v-if="hasKey('mode')" label="模式选择">
<a-select
ref="select"
v-model:value="data.options.mode"
>
<a-select-option value="">单选</a-select-option>
<a-select-option value="multiple">多选</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-if="hasKey('range')" :label="t('双滑块模式')">
<a-switch v-model:checked="data.options.range" @change="handleSliderModeChange" />

View File

@ -247,6 +247,7 @@ export const advanceComponents = [
showSearch: false,
clearable: false,
disabled: false,
mode: 'multiple',
staticOptions: [
{
key: 1,

View File

@ -24,6 +24,7 @@ import SelectDepartment from './components/SelectDepartment.vue';
import SelectDepartmentV2 from './components/SelectDepartmentV2.vue';
import SelectUser from './components/SelectUser.vue';
import SelectUserV2 from './components/SelectUserV2.vue';
import SelectUserShowTree from './components/SelectUserShowTree.vue';
import CommonInfo from './components/CommonInfo.vue';
import SelectArea from './components/SelectArea.vue';
import AutoCodeRule from './components/AutoCodeRule.vue';
@ -66,6 +67,7 @@ import { XjrDatePicker } from '/@/components/DatePicker';
import { Slider } from '/@/components/Slider';
import { CodeTextArea } from '/@/components/Input';
import { OneForOne } from '/@/components/OneForOne';
import { CustomComponent } from '/@/components/CustomComponent';
import SubForm from './components/SubFormV2.vue';
import ErpApply from './components/ErpApply.vue';
import ErpUpload from './components/ErpUpload.vue';
@ -102,6 +104,7 @@ componentMap.set('Rate', Rate);
componentMap.set('DeptTree', SelectDepartment);
componentMap.set('Dept', SelectDepartmentV2);
componentMap.set('User', SelectUserV2);
componentMap.set('UserTree', SelectUserShowTree);
componentMap.set('Info', CommonInfo);
componentMap.set('Area', SelectArea);
componentMap.set('SubForm', SubForm);
@ -145,6 +148,7 @@ componentMap.set('ErpApply', ErpApply);
componentMap.set('ErpUpload', ErpUpload);
componentMap.set('ErpCheck', ErpCheck);
componentMap.set('AutoComplete', AutoComplete);
componentMap.set('CustomComponent', CustomComponent);
export function add(compName: ComponentType, component: Component) {
componentMap.set(compName, component);

View File

@ -1,12 +1,12 @@
<template>
<div class="field-readonly">
<div :class="isWordWrap ? '' : 'field-readonly'" :title="fieldValue">
<div v-if="schema.component === 'RichTextEditor'" v-html="htmlValue"> </div>
{{ fieldValue }}
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
import { ref, watch, computed } from 'vue';
const props = defineProps({
value: String,
@ -17,6 +17,14 @@
const textComponents = ['Input', 'AutoCodeRule', 'DatePicker', 'TimePicker', 'Text', 'InputTextArea', 'InputNumber'];
const fieldValue = ref(genFieldValue(props.value));
const htmlValue = ref(getHtmlValue(props.value));
const isWordWrap = computed(() => {
const schema = props.schema;
const { componentProps } = schema;
if (componentProps?.wordWrap) {
return componentProps.wordWrap;
}
return false;
})
function parseRangeVal(val, component) {
if (component !== 'RangePicker' || !val) {
@ -75,8 +83,10 @@
);
</script>
<style>
<style lang="less" scoped>
.field-readonly {
white-space: pre-wrap;
white-space: nowrap;
overflow: hidden;
text-overflow:ellipsis
}
</style>

View File

@ -1,6 +1,9 @@
<template>
<div class="depart-select" style="width: 100%" @click="show">
<a-input v-model:value="departNames" :bordered="bordered" :disabled="disabled" :placeholder="placeholder" :size="size" readonly>
<div v-if="disabled && !disabledShowBorder" :class="wordWrap ? '' : 'field-readonly'" :title="departNames">
{{departNames}}
</div>
<a-input v-model:value="departNames" v-if="disabledShowBorder || !disabled" :bordered="bordered" :disabled="disabled" :placeholder="placeholder" :size="size" readonly>
<template v-if="prefix" #prefix>
<Icon :icon="prefix" />
</template>
@ -81,7 +84,16 @@
justCompany: {
type: Boolean,
default: false
}
},
wordWrap:{
type: Boolean,
default: false
},
disabledShowBorder: {
type: Boolean,
default: false
},
formData:Object
});
const selectedNodes = ref([]);
const loading = ref(true);
@ -181,7 +193,11 @@
visible.value = true;
loading.value = true;
if(props.defaultDeptField) {
defaultDepts.value = props.row[props.defaultDeptField]
if(props.row) {
defaultDepts.value = props.row[props.defaultDeptField];
}else{
defaultDepts.value = props.formData[props.defaultDeptField];
}
}
if (props.value) {
if(props.isArray && !props.value.length) {
@ -227,6 +243,11 @@
}
</style>
<style lang="less" scoped>
.field-readonly {
white-space: nowrap;
overflow: hidden;
text-overflow:ellipsis
}
.choose-dep-box {
display: flex;
height: 95%;

View File

@ -1,21 +1,24 @@
<template>
<div class="user-select-list">
<div class="user-select-item" v-for="item in data" @click="selectItem(item)">
<div class="user-select-item-left" v-if="showDep && item.departmentPathName">
{{ `${item.name}${item.departmentPathName}` }}
</div>
<div class="user-select-item-left" v-else>
{{ `${item.name}` }}
</div>
<div class="user-select-item-right">
<div class="select-circle" :class="item.selected ? 'selected' : ''" v-if="!viewList">
<check-outlined v-if="item.selected" />
<template v-for="item in data">
<div class="user-select-item" @click="selectItem(item)" :class="isDisabledItem(item) ? 'disabled-item' : ''">
<div class="user-select-item-left" v-if="showDep && item.departmentPathName">
{{ `${item.name}${item.departmentPathName}` }}
</div>
<div class="delete-circle" v-if="canDel" @click="delItem(item)">
<CloseOutlined />
<div class="user-select-item-left" v-else>
{{ `${item.name}` }}
</div>
<div class="user-select-item-right" v-if="!isDisabledItem(item)">
<div class="select-circle" :class="item.selected ? 'selected' : ''" v-if="!viewList">
<check-outlined v-if="item.selected" />
</div>
<div class="delete-circle" v-if="canDel" @click="delItem(item)">
<CloseOutlined />
</div>
</div>
</div>
</div>
</template>
<div class="empty-box" v-if="!data.length">
<a-empty :image="simpleImage">
<template #description>
@ -58,8 +61,18 @@ const props = defineProps({
type: Boolean,
default: false
},
disabledSelectList: {
type: Array,
default: () => []
},
})
function isDisabledItem(item) {
return props.disabledSelectList.includes(item.id)
}
function selectItem(item) {
if(isDisabledItem(item)) {
return
}
emits('selectId', item.id)
}
function delItem(item) {
@ -83,6 +96,9 @@ function delItem(item) {
// margin: auto;
}
}
.disabled-item {
background: #e3e3e3;
}
@ -102,11 +118,14 @@ function delItem(item) {
overflow: hidden; //超出隐藏
text-overflow: ellipsis; //文本超出时显示省略号
white-space: nowrap; //设置文本不换行
flex: 1;
}
.user-select-item-right {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
.select-circle {
width: 16px;

View File

@ -0,0 +1,143 @@
<template>
<div class="selectUser-show-tree">
<div class="content">
<div class="user-list" v-if="treeData.length" :style="{height: height, width: width}">
<a-tree v-model:expandedKeys="expandedKeys" :treeData="treeData" :key="treeKey" :fieldNames="{
children: 'children',
title: 'name',
key: 'id'
}">
</a-tree>
</div>
<a-button style="margin-top: 10px" @click="showDialog" v-if="!disabled">选择用户</a-button>
</div>
<SelectUserV2
ref="selectUser"
v-model:value="selected"
v-bind="filteredProps"
justDialog
@change="changeSelect"
/>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import SelectUserV2 from './SelectUserV2.vue'
import { getUserByDepartTree } from '/@/api/system/user';
import { cloneDeep } from 'lodash-es';
const emits = defineEmits(['update:value', 'change']);
const props = defineProps({
value: {
type: String,
default: ''
},
prefix: String,
suffix: String,
placeholder: String,
readonly: Boolean,
disabled: Boolean,
size: String,
justDialog: {
type: Boolean,
default: false
},
multiple: {
type: Boolean,
default: true
},
row: Object, // 行数据,在表格里用的到
sepTextField: String, // 将文本表示存储在独立字段
onlyUserCompany: { // 仅在登录用户公司范围内筛选
type: Boolean,
default: false
},
buttonShow: {
type: Boolean,
default: false
},
defaultDeptField: { // 默认选择公司范围key值
type: String,
default: ''
},
justCompany: {
type: Boolean,
default: false
},
isOnlyCurrentDepartment: { // 仅当前部门,无下级部门
type: Boolean,
default: true
},
height: {
type: String,
default: '500px'
},
width: {
type: String,
default: '100%'
}
});
// 过滤掉value属性避免冲突
const filteredProps = computed(() => {
const { value, ...rest } = props;
return rest;
});
const treeKey = ref(1)
const selected = ref(props.value || [])
const selectUser = ref()
const expandedKeys = ref([])
const treeData = ref([])
const changeSelect = async (ids, options) => {
emits('update:value', ids)
emits('change', ids, options)
}
async function getTreeData(list) {
let tree = await getUserByDepartTree(list)
expandedKeys.value = []
setTreeData(tree)
treeData.value = tree
treeKey.value = Date.now()
}
const setTreeData = (list, indexList = []) => {
list.forEach((item, index) => {
let list = cloneDeep(indexList)
list.push(index)
item.id = list.join('-')
expandedKeys.value.push(item.id)
if(item.children && item.children.length){
setTreeData(item.children, cloneDeep(list))
} else if (item.users.length) {
item.children = item.users
}
})
}
const showDialog = () => {
selectUser.value.show()
}
// 监听props.value变化
watch(() => props.value, (newVal) => {
selected.value = newVal
if(selected.value) {
getTreeData(selected.value)
}
})
</script>
<style lang="less" scoped>
.selectUser-show-tree {
.content{
.user-list {
overflow: auto;
border: 1px solid #e5e5e5;
}
}
}
</style>

View File

@ -1,7 +1,10 @@
<template>
<div :class="{ disabled }" class="form-select-user" @click="show">
<a-button type="primary" v-if="buttonShow">{{ t('添加人员') }}</a-button>
<a-input v-model:value="userNames" v-if="!buttonShow" :disabled="disabled" :placeholder="placeholder" :size="size" readonly>
<a-button type="primary" v-if="buttonShow && !justDialog">{{ t('添加人员') }}</a-button>
<div v-if="disabled && !buttonShow && !justDialog && !disabledShowBorder" :class="wordWrap ? '' : 'field-readonly'" :title="userNames">
{{userNames}}
</div>
<a-input v-model:value="userNames" v-if="!buttonShow && !justDialog && (disabledShowBorder || !disabled)" :placeholder="placeholder" :size="size" readonly>
<template v-if="prefix" #prefix>
<Icon :icon="prefix" />
</template>
@ -9,7 +12,7 @@
<Icon :icon="suffix" />
</template>
</a-input>
<ModalPanel :title="t('选择人员')" :visible="visible" :width="900" class="select-user-model" @close="close" @submit="submit">
<ModalPanel :title="t('选择人员')" :visible="visible" :width="900" class="select-user-model" @close="close" @submit="submit" :confirmLoading="confirmLoading">
<div class="select-user">
<div class="select-user-left">
<a-tabs v-model:activeKey="activeKey" :tabBarGutter="20">
@ -29,20 +32,20 @@
</div>
</div>
<div class="user-select-box">
<div class="select-depart-all" @click="selectAll">
<div class="select-depart-all" @click="selectAll" v-if="multiple">
<span>全选</span>
<div class="select-circle" :class="departAllSelected ? 'selected' : ''" v-if="!viewList">
<check-outlined v-if="departAllSelected" />
</div>
</div>
<SelectUserListV2 :multiple="multiple" :data="searchDepartMemberList" emptyDescription="暂无人员" @selectId="changeDepMemberSelect"></SelectUserListV2>
<SelectUserListV2 :multiple="multiple" :data="searchDepartMemberList" emptyDescription="暂无人员" @selectId="changeDepMemberSelect" :disabledSelectList="disabledSelectList"></SelectUserListV2>
<div v-if="false" class="user-select-pagination">
<a-pagination v-model:current="searchDepartMemberParams.limit" :pageSize="searchDepartMemberParams.size" :total="searchDepartMemberTotal" />
</div>
</div>
</div>
<div v-show="activeKey === 'allPerson'" class="all-user-select-box">
<SelectUserListV2 :multiple="multiple" :data="searchAllMemberList" @selectId="changeMemberSelect" show-dep></SelectUserListV2>
<SelectUserListV2 :multiple="multiple" :data="searchAllMemberList" @selectId="changeMemberSelect" show-dep :disabledSelectList="disabledSelectList"></SelectUserListV2>
<div v-if="searchAllMemberTotal > 25" class="all-user-select-pagination">
<a-form-item label="" name="pagination">
<a-pagination
@ -64,14 +67,14 @@
<!-- <div class="selected-user-title sub-title">
已选列表
</div> -->
<SelectUserListV2 :data="selectedMemberList" canDel emptyDescription="暂无已选择人员<br> 请从左侧添加人员" viewList @delId="delMember"></SelectUserListV2>
<SelectUserListV2 :data="selectedMemberList" canDel emptyDescription="暂无已选择人员<br> 请从左侧添加人员" viewList @delId="delMember" :disabledSelectList="disabledSelectList"></SelectUserListV2>
</div>
</div>
</ModalPanel>
</div>
</template>
<script setup>
import { watch, ref } from 'vue';
import { watch, ref, defineExpose } from 'vue';
import { cloneDeep } from 'lodash-es';
import { useI18n } from '/@/hooks/web/useI18n';
import { ModalPanel } from '/@/components/ModalPanel/index';
@ -104,6 +107,10 @@
readonly: Boolean,
disabled: Boolean,
size: String,
justDialog: {
type: Boolean,
default: false
},
multiple: {
type: Boolean,
default: true
@ -129,6 +136,26 @@
isOnlyCurrentDepartment: { // 仅当前部门,无下级部门
type: Boolean,
default: true
},
submitClose: {
type: Boolean,
default: true
},
disabledShowBorder: {
type: Boolean,
default: false
},
wordWrap: {
type: Boolean,
default: false
},
lastSelectedDisabled: {
type: Boolean,
default: false
},
confirmLoading: {
type: Boolean,
default: false
}
});
let timeoutId = null;
@ -136,6 +163,7 @@
const searchPlaceholder = '请输入姓名或工号搜索';
const defaultDepts = ref('')
const departAllSelected = ref(false)
const disabledSelectList = ref([])
if(props.onlyUserCompany) {
const userStore = useUserStore();
const userInfo = userStore.getUserInfo;
@ -180,6 +208,9 @@
watch(
props,
async () => {
if(props.lastSelectedDisabled) {
disabledSelectList.value = props.value.split(',')
}
if (props.value && !valChanged.value && props.sepTextField) {
const idArr = props.value.split(',');
let valStr = props.row[camelCaseString(props.sepTextField)];
@ -196,7 +227,7 @@
resetMemberList = cloneDeep(initValue);
return;
}
if (props.value && !resetMemberList.length) {
if (props.value && !Array.isArray(props.value) && !resetMemberList.length) {
const list = await getUserMulti(props.value);
selectedMemberList.value = list;
resetMemberList = cloneDeep(list);
@ -282,11 +313,17 @@
async function getUserList(params) {
return await getUserPageListNew(params);
}
function isDisabledItem(item) {
return disabledSelectList.value.includes(item.id)
}
function selectAll() {
departAllSelected.value = !departAllSelected.value
if(departAllSelected.value) {
searchDepartMemberList.value.forEach(item => {
if(isDisabledItem(item)) {
return
}
if(!item.selected) {
selectedMemberList.value.push(item)
}
@ -294,6 +331,9 @@
})
} else {
searchDepartMemberList.value.forEach(item => {
if(isDisabledItem(item)) {
return
}
if(item.selected) {
selectedMemberList.value = selectedMemberList.value.filter(m => m.id !== item.id)
}
@ -380,7 +420,9 @@
emits('selectedId', ids);
emits('change', ids, selectedMemberList.value);
valChanged.value = false;
close();
if(props.submitClose) {
close();
}
}
function close() {
@ -390,6 +432,10 @@
searchDepartMemberList.value = [];
searchAllMemberList.value = [];
}
defineExpose({
show,
close
})
</script>
<style lang="less">
.select-user-model {
@ -407,6 +453,11 @@
justify-content: space-between;
color: rgba(144, 147, 153, 0.7);
}
.field-readonly {
white-space: nowrap;
overflow: hidden;
text-overflow:ellipsis
}
.select-user {
display: flex;

View File

@ -52,22 +52,33 @@
<template v-else-if="column.key !== 'index'">
<component
v-show="column.width!==0"
:is="componentMap.get(column.componentType)"
:key="column.dataIndex + record['_key_']"
v-model:value="record[column.dataIndex]"
:title="readonlySupport(column.componentType) ? record[column.dataIndex] : ''"
:bordered="showComponentBorder"
:index="index"
:mainKey="mainKey"
:row="record"
v-bind="getComponentsProps(column.componentProps, column.dataIndex, record, index)"
v-bind="{...getComponentsProps(column.componentProps, column.dataIndex, record, index), placeholder: getComponentsProps(column.componentProps, column.dataIndex, record, index).disabled ? '' : getComponentsProps(column.componentProps, column.dataIndex, record, index).placeholder}"
@blur="onFieldBlur(column, record, index)"
@change="onFieldChange(column, record, index)"
/>
</template>
</FormItem>
</template>
<template v-if="column.key === 'action' && !disabled">
<MinusCircleOutlined style="padding-bottom: 20px" @click="remove(record)" />
<template v-if="column.key === 'action'">
<template v-if="column.actions?.length > 0">
<div class="tag-wrapper">
<template v-for="action in column.actions">
<a-tag class="custom-tag" v-if="showButton(action.show,{record,records:data,formModel,disabled})" @click="action.type === 'delete' ? remove(record) : action.click({record,records:data,formModel,disabled})" :color="action?.color ? action.color : 'blue'">{{action.label}}</a-tag>
</template>
</div>
</template>
<template v-else-if="!disabled">
<MinusCircleOutlined style="padding-bottom: 20px" @click="remove(record)" />
</template>
</template>
</template>
</a-table>
@ -92,6 +103,7 @@
:isSubFormUse="true"
:params="{ itemId }"
popupType="preload"
:backPagination="backPagination"
@submit="renderSubFormList"
/>
</FormItemRest>
@ -182,6 +194,8 @@
},
// 是否开启分页
showPagination: Boolean,
//后端分页
backPagination: Boolean,
/**
* 选数据按钮名称
*/
@ -197,7 +211,8 @@
//add after hooks
addAfter: Function,
//表头合并数据
multipleHeads: { type: Array as PropType<MutipleHeadInfo[]> }
multipleHeads: { type: Array as PropType<MutipleHeadInfo[]> },
backPagination: { type: Boolean }
});
const data = ref<Recordable[]>([]);
@ -207,14 +222,18 @@
const headColums = ref<MutipleHeadInfo[]>([]); // 多表头
const originHeads = ref<MutipleHeadInfo[]>([]); // 多表头源数据
const columns = ref<SubFormColumn[]>(props.columns);
const allColumns = ref([])
// 注入表单数据
const formModel = inject<any>('formModel', null);
function readonlySupport(name) {
return /^(Input|AutoCodeRule|DatePicker|Text|TimePicker|Range|RichTextEditor|TimeRangePicker|RangePicker)$/.test(name);
}
const onFieldChange = (column, row, rowIndex) => {
const evt = column?.componentProps?.events?.change;
if (evt) {
evt({ column, row, rowIndex, formModel });
evt({ column, row, rowIndex, formModel, columns: headColums.length > 0 ? headColums : columns });
}
};
@ -235,7 +254,7 @@
}
function addDataKey(rows) {
rows.forEach((row) => {
rows?.forEach((row) => {
if (!row['_key_']) {
row['_key_'] = buildUUID();
}
@ -255,6 +274,7 @@
});
}
setColWidth(columns);
allColumns.value = cloneDeep(columns.value)
columns.value = filterColum(columns.value);
nextTick(() => {
//处理多表头
@ -271,6 +291,7 @@
watch(
() => props.columns,
(val) => {
allColumns.value = cloneDeep(val)
columns.value = filterColum(val);
setColWidth(columns);
}
@ -382,11 +403,20 @@
emit('update:value', unref(data));
};
const showButton=(show,obj)=>{
if(show===undefined||show===null||show===''||show===true||show==='true'){
return true;
}else if(isFunction(show)){
return show(obj);
}
return false;
}
const renderSubFormList = async (list) => {
list?.forEach((x) => {
const dataObj = {};
columns.value?.map((item) => {
if (!item?.dataIndex) return;
allColumns.value?.map((item) => {
if (!item?.dataIndex || !item.componentProps.isShow) return;
dataObj[item.dataIndex as string] = item.componentProps?.prestrainField ? x[item.componentProps.prestrainField] : null;
});
@ -652,4 +682,14 @@
.tbl-toolbar {
margin-top: 10px;
}
.tag-wrapper{
display: flex;
gap: 5px;
}
.custom-tag{
cursor: pointer;
display: inline-block;
}
</style>

View File

@ -168,6 +168,10 @@
type: Boolean,
default: true
},
/**
* 按钮组 [{name: '按钮', click: function(){}}]
*/
buttons: Array,
// 是否开启分页
showPagination: Boolean,
/**
@ -197,6 +201,7 @@
const headColums = ref<MutipleHeadInfo[]>([]); // 多表头
const originHeads = ref<MutipleHeadInfo[]>([]); // 多表头源数据
const columns = ref<SubFormColumn[]>(props.columns);
const allColumns = ref([])
// 注入表单数据
const formModel = inject<any>('formModel', null);
@ -224,6 +229,9 @@
}
function addDataKey(rows) {
if (!rows) {
return [];
}
rows.forEach((row) => {
if (!row['_key_']) {
row['_key_'] = Math.random();
@ -244,6 +252,7 @@
});
}
setColWidth(columns);
allColumns.value = cloneDeep(columns.value)
columns.value = filterColum(columns.value);
nextTick(() => {
//处理多表头
@ -260,6 +269,7 @@
watch(
() => props.columns,
(val) => {
allColumns.value = cloneDeep(val)
columns.value = filterColum(val);
setColWidth(columns);
}
@ -327,6 +337,29 @@
}
});
}
const removeById = (id) => {
data.value = deleteNodeById(data.value, id);
};
function deleteNodeById(tree, idToDelete) {
function deepCloneAndFilter(nodes) {
return nodes.map((node) => {
if (node.id === idToDelete) {
return null; // 当前节点要删除
}
// 递归处理 children
const newChildren = node.children ? deepCloneAndFilter(node.children) : [];
return {
...node,
children: newChildren.length ? newChildren : undefined
};
}).filter(Boolean);
}
return deepCloneAndFilter(tree);
}
const add = () => {
//给各个组件赋默认值
@ -364,17 +397,25 @@
}
};
const remove = (index) => {
data.value.splice(index, 1);
emit('change', unref(data));
emit('update:value', unref(data));
const remove = (record) => {
let index;
if (typeof record === 'number' || typeof record === 'string') {
index = Number(record);
} else {
index = data.value.findIndex((r) => r._key_ === record._key_);
}
if (index !== -1 && index < data.value.length) {
data.value.splice(index, 1);
emit('change', unref(data));
emit('update:value', unref(data));
}
};
const renderSubFormList = async (list) => {
list?.forEach((x) => {
const dataObj = {};
columns.value?.map((item) => {
if (!item?.dataIndex) return;
allColumns.value?.map((item) => {
if (!item?.dataIndex || !item.componentProps.isShow) return;
dataObj[item.dataIndex as string] = item.componentProps?.prestrainField ? x[item.componentProps.prestrainField] : null;
});
@ -562,7 +603,8 @@
data,
headColums,
columns,
renderSubFormList
renderSubFormList,
removeById
}
}
}

View File

@ -48,7 +48,7 @@
</a-upload>
</div>
<a-upload
:file-list="fileList"
:file-list="fileListWithHeader"
:maxCount="maxNumber"
:accept="accept"
:name="name"
@ -62,7 +62,7 @@
@preview="handlePreview"
@click="handleClick"
v-else
>
>
<plus-outlined v-if="listType == 'picture-card'" />
<div :style="style" v-else>
<a-button :loading="loading" :disabled="loading" v-if="!disabled">
@ -75,15 +75,45 @@
</div>
<template #itemRender="{ file, actions }">
<a-space class="file-space">
<template v-if="file.__header&&showDownloadIcon">
<div class="file-list-header" style="display: flex; align-items: center; padding: 4px 0;">
<input
type="checkbox"
:checked="isAllSelected"
@change="toggleSelectAll"
style="margin-right: 8px;"
/>全选
<a-button
type="primary"
size="small"
:disabled="!selectedIds.length"
@click="handleBatchDownload"
style="margin-left: 8px;"
>批量打包下载</a-button>
</div>
</template>
<template v-else>
<a-space class="file-space">
<input
v-if="showDownloadIcon"
type="checkbox"
:checked="selectedIds.includes(file.id)"
@change="e => toggleSelectOne(file.id, e)"
style="margin-right: 8px;"
/>
<PaperClipOutlined/>
<span class="file-name-span" @click="actions.preview">{{ file.name }}</span>
<a-tooltip v-if="showDownloadIcon" title="下载"><span @click="actions.download" class="file-outlined-span"><DownloadOutlined /></span></a-tooltip>
<a-tooltip v-if="!disabled && showRemoveIcon" title="删除"><span @click="actions.remove" class="file-outlined-span"><DeleteOutlined /></span></a-tooltip>
<a-tooltip v-if="'.doc,.docx,.xls,.xlsx,.pdf'.includes(file.fileType)" title="编辑文档">
<span @click="editFile(file)" class="file-outlined-span"><EditOutlined /></span>
<a-tooltip v-if="showDownloadIcon" title="下载">
<span @click="actions.download" class="file-outlined-span"><DownloadOutlined /></span>
</a-tooltip>
</a-space>
<a-tooltip v-if="!disabled && showRemoveIcon" title="删除">
<span @click="actions.remove" class="file-outlined-span"><DeleteOutlined /></span>
</a-tooltip>
<a-tooltip v-if="'.doc,.docx,.xls,.xlsx,.pdf'.includes(file.fileType)" title="编辑文档">
<span @click="editFile(file)" class="file-outlined-span"><EditOutlined /></span>
</a-tooltip>
</a-space>
</template>
</template>
</a-upload>
@ -110,11 +140,11 @@
</div>
</template>
<script lang="ts" setup>
import { nextTick, ref, watch } from 'vue';
import { nextTick, ref, watch, computed } from 'vue';
import { Upload } from 'ant-design-vue';
import { UploadOutlined, PlusOutlined, DownloadOutlined, DeleteOutlined, EditOutlined, PaperClipOutlined } from '@ant-design/icons-vue';
import { useMessage } from '/@/hooks/web/useMessage';
import {deleteSingleFile, getAppToken, getFileList, getOnlineEditUrl} from '/@/api/system/file';
import {deleteSingleFile, getAppToken, getFileList, getOnlineEditUrl, getZipFiles} from '/@/api/system/file';
import { downloadByUrl } from '/@/utils/file/download';
import { uploadMultiApi } from '/@/api/sys/upload';
import Icon from '/@/components/Icon/index';
@ -122,8 +152,12 @@
import { getAppEnvConfig } from '/@/utils/env';
import WebOfficeSDK from "/@/assets/libs/open-jssdk-v0.1.3.es.js";
import {getToken} from "/@/utils/auth";
import { useRoute } from 'vue-router';
const route = useRoute();
const { VITE_GLOB_UPLOAD_ALERT_TIP } = getAppEnvConfig();
const { createSuccessModal, } = useMessage();
const props = defineProps({
value: String,
maxNumber: Number,
@ -346,6 +380,52 @@
previewVisible.value = false;
previewTitle.value = '';
};
const selectedIds = ref<string[]>([]);
const isAllSelected = computed(() => fileList.value.length > 0 && selectedIds.value.length === fileList.value.length);
const fileListWithHeader = computed(() => {
// 只在有文件时插入头部
if (fileList.value.length) {
return [{ __header: true, uid: '__header__' }, ...fileList.value];
}
return fileList.value;
});
function toggleSelectAll(e: Event) {
const checked = (e.target as HTMLInputElement).checked;
selectedIds.value = checked ? fileList.value.map(f => f.id) : [];
}
function toggleSelectOne(id: string, e: Event) {
const checked = (e.target as HTMLInputElement).checked;
if (checked) {
selectedIds.value = [...selectedIds.value, id];
} else {
selectedIds.value = selectedIds.value.filter(item => item !== id);
}
}
async function handleBatchDownload() {
if (!selectedIds.value.length) return;
// getZipFiles 返回下载url
let formName = '';
try {
formName = route.query.formName as string || '';
// 获取当前页面得form name
} catch (error) {
console.warn(error);
}
const res = await getZipFiles({ fileIds: selectedIds.value.join(',') , insertionFileName: formName});
if (!res) {
notification.error({
message: 'Tip',
description: '批量下载失败,请稍后重试!',
});
return;
} else if (res.type === 'async') {
createSuccessModal({ title: 'Tip', content: res.msg });
return;
} else if (res.type === 'synced') {
downloadByUrl({ url: res.url, fileName: res.name || 'files.zip' });
}
}
</script>
<style lang="less" scoped>
.list-upload {

View File

@ -127,6 +127,7 @@ export type ComponentType =
| 'DeptTree'
| 'Dept'
| 'User'
| 'UserTree'
| 'Info'
| 'Area'
| 'SubForm'
@ -158,6 +159,7 @@ export type ComponentType =
| 'ErpCheck'
| 'FormView'
| 'XjrIframe'
| 'CustomComponent'
| 'TableLayout';
/**

View File

@ -1,15 +1,34 @@
<template>
<a-modal
ref="modalRef"
:visible="visible"
:title="title"
:maskClosable="false"
:width="hasLeftSlot ? 1200 : width || 600"
:okText="t('确定')"
:cancelText="t('取消')"
@ok="$emit('submit')"
@cancel="$emit('close')"
:confirmLoading="confirmLoading"
@ok="handleOk"
@cancel="handleCancel"
:getContainer="draggable ? undefined : 'body'"
:wrap-style="draggable ? { overflow: 'hidden' } : {}"
>
<slot name="header"></slot>
<template #title>
<div
ref="modalTitleRef"
:style="{ width: '100%', cursor: draggable ? 'move' : 'default' }"
>
{{ title }}
</div>
</template>
<template #modalRender="{ originVNode }">
<div :style="draggable ? transformStyle : {}">
<component :is="originVNode" />
</div>
</template>
<div class="content">
<div :class="['left', isDeptSelect ? 'left-box' : '']" v-if="hasLeftSlot">
<slot name="left"></slot>
@ -21,19 +40,99 @@
</a-modal>
</template>
<script setup lang="ts">
import { computed, useSlots } from 'vue';
import { computed, useSlots, ref, watch } from 'vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { useDraggable } from '@vueuse/core';
const { t } = useI18n();
defineEmits(['submit', 'close']);
defineProps({
const emit = defineEmits(['submit', 'close']);
const props = defineProps({
title: String,
width: Number,
visible: { type: Boolean, default: false },
isDeptSelect: { type: Boolean, default: false },
confirmLoading: { type: Boolean, default: false },
draggable: { type: Boolean, default: false }
});
const modalRef = ref(null);
const modalTitleRef = ref(null);
const hasLeftSlot = computed(() => {
return !!useSlots().left;
});
const startX = ref(0);
const startY = ref(0);
const startedDrag = ref(false);
const transformX = ref(0);
const transformY = ref(0);
const preTransformX = ref(0);
const preTransformY = ref(0);
const dragRect = ref({ left: 0, right: 0, top: 0, bottom: 0 });
const { x, y, isDragging } = useDraggable(modalTitleRef, {
enabled: computed(() => props.draggable)
});
const handleOk = () => {
emit('submit');
};
const handleCancel = () => {
emit('close');
};
watch([x, y], () => {
if (!props.draggable || !startedDrag.value) {
startX.value = x.value;
startY.value = y.value;
const bodyRect = document.body.getBoundingClientRect();
if (modalTitleRef.value) {
const titleRect = modalTitleRef.value.getBoundingClientRect();
dragRect.value.right = bodyRect.width - titleRect.width;
dragRect.value.bottom = bodyRect.height - titleRect.height;
}
preTransformX.value = transformX.value;
preTransformY.value = transformY.value;
}
startedDrag.value = true;
});
// 监听拖拽状态变化
watch(isDragging, (newVal) => {
if (!newVal) {
startedDrag.value = false;
}
});
watch(
() => props.visible,
(newVisible) => {
if (!newVisible) {
transformX.value = 0;
transformY.value = 0;
}
}
);
const transformStyle = computed(() => {
if (!props.draggable) return {};
if (startedDrag.value) {
transformX.value =
preTransformX.value +
Math.min(Math.max(dragRect.value.left, x.value), dragRect.value.right) -
startX.value;
transformY.value =
preTransformY.value +
Math.min(Math.max(dragRect.value.top, y.value), dragRect.value.bottom) -
startY.value;
}
return {
transform: `translate(${transformX.value}px, ${transformY.value}px)`,
};
});
</script>
<style lang="less" scoped>
.content {
@ -49,6 +148,7 @@
.right {
flex: 1;
width: 100%;
}
}

View File

@ -1,7 +1,11 @@
<template>
<div :class="{ disabled }" class="multiple-popup">
<div v-if="disabled && !disabledShowBorder" :class="wordWrap ? '' : 'field-readonly'" :title="popupValue">
{{popupValue}}
</div>
<a-input
v-model:value="popupValue"
v-if="disabledShowBorder || !disabled"
:addonAfter="addonAfter"
:addonBefore="addonBefore"
:bordered="bordered"
@ -35,6 +39,7 @@
:mainKey="mainKey"
:params="params"
:popupType="popupType"
:popupTitle="popupTitle"
:subTableIndex="index"
:valueField="valueField"
:backPagination="backPagination"
@ -68,6 +73,7 @@
const props = defineProps({
popupType: { type: String },
popupTitle: { type: String },
value: { type: String },
labelField: { type: String, default: 'label' },
valueField: { type: String, default: 'value' },
@ -247,3 +253,11 @@
}
};
</script>
<style lang="less" scoped>
.field-readonly {
white-space: nowrap;
overflow: hidden;
text-overflow:ellipsis
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<a-modal centered :width="1250" :visible="props.multipleDialog" :title="title" :destroyOnClose="true"
@ok="submitDialog" @cancel="closeDialog" :okText="t('确认')" :cancelText="t('取消')" :bodyStyle="{ padding: '20px' }">
@ok="submitDialog" @cancel="closeDialog" :okText="t('确认')" :cancelText="t('取消')" :bodyStyle="{ padding: '20px' }" v-loading="loading">
<a-row :gutter="12" style="margin-bottom: 10px">
<a-col :span="8">
<a-input v-model:value="state.searchText" :placeholder="t('请输入要查询的关键字')" />
@ -28,10 +28,10 @@
<a-table :dataSource="state.dataSourceList" :columns="state.sourceColumns" :row-selection="{
selectedRowKeys: state.selectedRowKeys,
onChange: onSelectChange,
}" :pagination="paginationProps" :scroll="{ y: '420px' }" />
}" :pagination="paginationProps" :scroll="{ y: '420px' }" :loading="loading"/>
</a-tab-pane>
<a-tab-pane key="2" :tab="t('已选记录')" force-render>
<a-table :dataSource="selectedList" :columns="state.selectedColumns" :scroll="{ y: '400px' }">
<a-table :dataSource="selectedList" :columns="state.selectedColumns" :scroll="{ y: '400px' }" :loading="loading">
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'delete'">
<Icon icon="ant-design:delete-outlined" color="#f56c6c" @click="deleteSelected(record, index)"
@ -41,7 +41,7 @@
</a-table>
</a-tab-pane>
</a-tabs>
<a-table v-else-if="popupType === 'associate'" :dataSource="state.dataSourceList" :columns="state.sourceColumns"
<a-table v-else-if="popupType === 'associate'" :dataSource="state.dataSourceList" :columns="state.sourceColumns" :loading="loading"
:pagination="paginationProps" :row-selection="{
selectedRowKeys: state.selectedRowKeys,
onChange: onSelectChange,
@ -64,6 +64,7 @@ const { t } = useI18n();
const props = defineProps({
multipleDialog: { type: Boolean },
popupType: { type: String },
popupTitle: { type: String },
dataSourceOptions: { type: Array },
params: {
type: [Array, Object, String, Number],
@ -105,6 +106,7 @@ onMounted(async () => {
await getDatasourceList(1);
}
});
const loading = ref(false)
const state = reactive({
selectedRowKeys: [] as any[],
@ -127,6 +129,9 @@ const paginationProps = reactive({
const formModel = inject<any>('formModel', null);
const isCamelCase = inject<boolean>('isCamelCase', false);
const title = computed(() => {
if(props.popupTitle){
return t(props.popupTitle);
};
switch (props.popupType) {
case 'multiple':
return t('多选弹层-选择记录');
@ -214,6 +219,9 @@ const resetSearch = () => {
};
const closeDialog = () => {
emit('update:multipleDialog', false);
selectedList.value = [];
state.selectedRowKeys = [];
state.dataSourceList = [];
};
const submitDialog = () => {
@ -291,12 +299,18 @@ const setFormModel = (isNull?) => {
let bindField = !isCamelCase ? item.bindField : camelCaseString(item.bindField);
dataObj[bindField as string] = item.prestrainField ? x[item.prestrainField] : null;
});
if (formModel[table]) formModel[table].push(dataObj);
if (formModel[table]) {
formModel[table].push(dataObj)
} else {
formModel[table] = []
formModel[table].push(dataObj)
};
});
}
};
const getDatasourceList = async (limit = 1) => {
loading.value = true;
paginationProps.current = limit;
let api;
if (props.datasourceType) {
@ -342,8 +356,8 @@ const getDatasourceList = async (limit = 1) => {
}
}
}
loading.value = false;
if (!api || !isFunction(api)) return;
state.dataSourceList = [];
try {
let dataParams = {};
const pageParams = { order: 'desc', size: 10, limit, keyword: state.searchText };
@ -359,9 +373,11 @@ const getDatasourceList = async (limit = 1) => {
pageParams,
);
}
loading.value = true;
const res = await api(dataParams);
state.dataSourceList = res.list;
paginationProps.total = Number(res.total);
loading.value = false;
} catch (error) {
console.warn(error);
}

View File

@ -15,7 +15,7 @@
<span class="node-name">{{node.activityName}}</span>
<a-switch :checked="node.chooseNode" v-if="!node.hiddenNode" style="margin-left: 10px;" @change="agreeNodeChange(node)"></a-switch>
</div>
<a-form-item :required="(flowNextNodes.length === 1 || node.chooseNode)" v-if="_action === 'agree' && !isEnd" :label="'审批人'">
<a-form-item :required="(flowNextNodes.length === 1 || node.chooseNode)" v-if="(_action === 'agree' || _action == 'disagree') && !isEnd" :label="'审批人'">
<a-select v-show="node.chooseAssign" v-model:value="node.assignees" :options="node.nextAssignees" :disabled="loading"
:placeholder="'审批人'" max-tag-count="responsive"
:mode="node.isChooseMulti? 'multiple' : ''"
@ -30,10 +30,12 @@
<a-select-option v-for="(item, index) in rejectNodeList" :key="index" :value="item.activityId">{{ item.activityName }}</a-select-option>
</a-select>
</a-form-item>
<template v-for="node in rejectNodeList">
<a-form-item v-if="_action === 'reject'&&rejectNodeId===node.activityId" label="审批人">
<a-select v-show="node.chooseAssign" v-model:value="node.assignees" :options="node.nextAssignees" :disabled="loading"
:placeholder="'请选择' + node.activityName + '的审批人'" max-tag-count="responsive" mode="multiple"
<template v-for="node in rejectNodeList" :key="node.activityId">
<a-form-item required v-if="_action === 'reject'&&rejectNodeId===node.activityId" label="审批人">
<a-select v-show="node.chooseAssign" v-model:value="node.assignees" :options="node.nextAssignees"
:placeholder="'请选择' + node.activityName + '的审批人'" max-tag-count="responsive"
:disabled="loading"
:mode="node.rejectIsChooseMulti? 'multiple' : ''"
:filterOption="search"
></a-select>
<span v-show="!node.chooseAssign">{{ getAssigneeText(node) }}</span>
@ -81,7 +83,7 @@
}
function getNextNodesName() {
return flowNextNodes.value.length > 1 ? '多个节点,请选择流向节点' : flowNextNodes?.value[0]?.activityName;
return flowNextNodes.value.length > 1 ? '多个节点,请确认流向节点' : flowNextNodes?.value[0]?.activityName;
}
function toggleDialog({ isClose, action, callback, rejectCancel, processId, taskId, nextNodes } = {}) {
@ -251,7 +253,7 @@
}
rejectNodeList.value.forEach((nNode) => {
if(nNode.activityId==rejectNodeId.value){
nextTaskUser[nNode.activityId] = isEnd.value ? '' : nNode.assignees.join(',');
nextTaskUser[nNode.activityId] = isEnd.value ? '' : (typeof(nNode.assignees) == 'string' ? nNode.assignees : nNode.assignees.join(','));
}
});
}

View File

@ -1,5 +1,5 @@
<template>
<a-modal :mask-closable="false" :title="dialogTitle" :visible="isOpen" :width="500" class="geg" centered>
<a-modal :mask-closable="false" :title="dialogTitle" :visible="isOpen" :width="500" class="geg" centered @cancel="onClickCancel">
<template #footer>
<a-button :disabled="loading" @click="onClickCancel">取消</a-button>
<a-button :loading="loading" type="primary" @click="onClickOK">确定</a-button>

View File

@ -1,5 +1,9 @@
<template>
<a-select v-model:value="selectedValue" :filter-option="handleFilterOption" :mode="mode" :options="getOptions" :placeholder="placeholder" allowClear v-bind="$attrs" @change="handleChange" @dropdown-visible-change="handleFetch">
<div>
<div v-if="disabled && !disabledShowBorder" :class="wordWrap ? '' : 'field-readonly'" :title="departNames">
{{departNames}}
</div>
<a-select v-model:value="selectedValue" :filter-option="handleFilterOption" :mode="mode" :options="getOptions" :placeholder="placeholder" allowClear v-bind="$attrs" @change="handleChange" @dropdown-visible-change="handleFetch" v-else>
<template v-for="item in Object.keys($slots)" #[item]="data">
<slot :name="item" v-bind="data || {}"></slot>
</template>
@ -13,6 +17,7 @@
</span>
</template>
</a-select>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, computed, unref, watch, inject, onMounted, watchEffect } from 'vue';
@ -63,7 +68,15 @@
mainKey: String,
index: Number,
sepTextField: String, // 独立存储文本部分
row: Object // 行数据,在明细表里生效
row: Object, // 行数据,在明细表里生效
wordWrap: {
type: Boolean,
default: false
},
disabledShowBorder: {
type: Boolean,
default: false
}
},
emits: ['options-change', 'change', 'update:value'],
setup(props, { emit }) {
@ -80,6 +93,7 @@
const valChanged = ref(false);
// label分开存储时第一次懒加载会同时处罚两次fetch所以这里延迟执行
const fetch = debounce(_fetch, 200, { leading: false, trailing: true });
const selectName = ref('')
const getOptions = computed(() => {
const { labelField, valueField, numberToString } = props;
@ -156,6 +170,7 @@
() => props.value,
() => {
selectedValue.value = ((typeof props.value === 'string' && !!props.value ? props.value?.split(',') : props.value) || undefined) as any;
updateSepTextField(Array.isArray(selectedValue.value) ? selectedValue.value : [selectedValue.value]);
},
{
immediate: true
@ -163,15 +178,16 @@
);
function updateSepTextField(arr) {
if (!props.sepTextField || !props.row) {
return;
}
const options = unref(getOptions);
const txtArr = options
.filter((opt) => {
return arr.includes(opt.value);
})
.map((item) => item.label);
selectName.value = txtArr.join(',')
if (!props.sepTextField || !props.row) {
return;
}
props.row[camelCaseString(props.sepTextField)] = txtArr.join(',');
}
@ -260,7 +276,6 @@
emit('update:value', val);
emit('change', val, args);
selectedValue.value = props.value === undefined ? val : (((typeof props.value === 'string' && !!props.value ? props.value?.split(',') : props.value) || undefined) as any);
updateSepTextField(Array.isArray(value) ? value : [value]);
}
return {
@ -276,3 +291,11 @@
}
});
</script>
<style lang="less" scoped>
.field-readonly {
white-space: nowrap;
overflow: hidden;
text-overflow:ellipsis
}
</style>

View File

@ -52,12 +52,13 @@
import { PostInfo } from '/@/api/system/post/model';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
const emits = defineEmits(['change']);
const emits = defineEmits(['change','selectedNodes']);
const props = withDefaults(
defineProps<{
selectedIds: Array<string>;
multiple?: Boolean;
needNode?: Boolean;
}>(),
{
selectedIds: () => {
@ -66,6 +67,9 @@
disabledIds: () => {
return [];
},
needNode: () => {
return false;
},
},
);
let data: {
@ -106,7 +110,10 @@
data.visible = true;
}
function submit() {
emits('change', data.selectedIds);
emits('change', data.selectedIds, data.selectedList);
if(props.needNode && props.needNode === true) {
emits('selectedNodes', data.selectedList);
}
close();
}
function close() {
@ -136,6 +143,7 @@
data.selectedList = [];
} else {
data.selectedIds = [item.id];
data.selectedList = [item]
}
}
}

View File

@ -86,7 +86,9 @@
});
const getSchemas = computed<FormSchema[]>(() => {
return (unref(getProps).schemas as any) || unref(schemaRef);
let schemas = (unref(getProps).schemas as any) || unref(schemaRef)
getComponent(schemas);
return schemas;
});
// Get the basic configuration of the form
@ -110,7 +112,7 @@
}
function getColWidth(schema: any) {
const compProps = schema.componentProps;
const compProps = schema?.componentProps;
if (compProps?.responsive) {
if (compProps.respNewRow) {
return 24; // 响应式布局下独立成行
@ -125,7 +127,7 @@
}
}
}
return schema.colProps?.span;
return schema?.colProps?.span;
}
const debGetWrapSize = debounce(getWrapSize, 300);
@ -148,7 +150,7 @@
});
function showComponent(schema) {
return props.isWorkFlow ? !noShowWorkFlowComponents.includes(schema.type) : !noShowGenerateComponents.includes(schema.type);
return props.isWorkFlow ? !noShowWorkFlowComponents.includes(schema?.type) : !noShowGenerateComponents.includes(schema?.type);
}
function getIsShow(schema: FormSchema, itemValue: any): boolean {
@ -175,22 +177,24 @@
}
function getIfShow(schema: FormSchema, itemValue: any): boolean {
const { ifShow } = schema;
let isIfShow = true;
if (isBoolean(ifShow)) {
isIfShow = ifShow;
const { componentProps, show } = schema;
let isShow = true;
if (isBoolean(componentProps?.isShow)) {
isShow = componentProps?.isShow;
}
if (isFunction(ifShow)) {
isIfShow = ifShow({
// if (isBoolean(show)) {
// isShow = show;
// }
if (isFunction(componentProps?.isShow)) {
isShow = componentProps.isShow({
values: itemValue,
model: formModel!,
schema: schema,
field: schema.field
});
}
return isIfShow;
return isShow;
}
const formModel = reactive<Recordable>(props.formModel);
@ -218,8 +222,6 @@
const refreshFieldObj = ref<object>({});
getComponent(getSchemas.value);
function getComponent(component) {
const layoutComponents = ['tab', 'grid', 'card'];
component?.map((info) => {
@ -253,8 +255,10 @@
function setComponentDefault(item) {
if ((staticDataComponents.includes(item.component) && (item.componentProps as any)?.datasourceType === 'staticData') || (needDicDefaultValue.includes(item.type) && (item.componentProps as any)?.datasourceType === 'dic')) {
let { defaultSelect } = item.componentProps as any;
formModel[item.field] = defaultSelect;
if(formModel[item.field]==undefined) {
let { defaultSelect } = item.componentProps as any;
formModel[item.field] = defaultSelect;
}
return;
}
let { defaultValue } = item;
@ -277,7 +281,9 @@
}
return;
} else {
formModel[item.field] = item.component === 'SubForm' ? [] : defaultValue;
if(formModel[item.field]==undefined){
formModel[item.field] = item.component === 'SubForm' ? [] : defaultValue;
}
return;
}
}
@ -413,7 +419,8 @@
* 修改bug #5090
* 为了保证表单赋值触发所有组件的change事件
*/
const executeEvent = (allSchemas: FormSchema[]) => {
const executeEvent = (allSchemas: FormSchema[] = []) => {
if(!allSchemas) return;
for (const schema of allSchemas) {
//如果是这几个组件 需要查询子级
if (['Card', 'Tab', 'Grid'].includes(schema.component)) {
@ -442,9 +449,9 @@
try {
if (typeof handler === 'string') {
const event = new Function('schema', 'formModel', 'formActionType', 'extParams', handler);
event(schema, formModel, formApi, { formData, allSchemas });
event(schema, formModel, formApi, { formData, allSchemas, isPersonChange: false });
} else if (typeof handler === 'function') {
handler(schema, formModel, formApi, { formData, allSchemas });
handler(schema, formModel, formApi, { formData, allSchemas, isPersonChange: false });
}
} catch (error) {
console.log('error', error);
@ -675,7 +682,7 @@
schema.componentProps.style = { ...schema.componentProps.style, ...style };
};
const formApi: FormActionType = {
const formApi = {
submit,
validate,
clearValidate,
@ -694,9 +701,10 @@
httpRequest,
refreshAPI,
changeStyle,
setDefaultValue
setDefaultValue,
formModel
};
//将表单方法 导出 给父组件使用。
defineExpose<FormActionType>(formApi);
defineExpose(formApi);
</script>

View File

@ -110,7 +110,7 @@
}
function getColWidth(schema: any) {
const compProps = schema.componentProps;
const compProps = schema?.componentProps;
if (compProps?.responsive) {
if (compProps.respNewRow) {
return 24; // 响应式布局下独立成行
@ -125,7 +125,7 @@
}
}
}
return schema.colProps?.span;
return schema?.colProps?.span;
}
const debGetWrapSize = debounce(getWrapSize, 300);
@ -148,7 +148,7 @@
});
function showComponent(schema) {
return props.isWorkFlow ? !noShowWorkFlowComponents.includes(schema.type) : !noShowGenerateComponents.includes(schema.type);
return props.isWorkFlow ? !noShowWorkFlowComponents.includes(schema.type) : !noShowGenerateComponents.includes(schema?.type);
}
function getIsShow(schema: FormSchema, itemValue: any): boolean {
@ -175,22 +175,15 @@
}
function getIfShow(schema: FormSchema, itemValue: any): boolean {
const { ifShow } = schema;
let isIfShow = true;
if (isBoolean(ifShow)) {
isIfShow = ifShow;
const { componentProps, show } = schema;
let isShow = true;
if (isBoolean(componentProps?.isShow)) {
isShow = componentProps?.isShow;
}
if (isFunction(ifShow)) {
isIfShow = ifShow({
values: itemValue,
model: formModel!,
schema: schema,
field: schema.field
});
}
return isIfShow;
// if (isBoolean(show)) {
// isShow = show;
// }
return isShow;
}
const formModel = reactive<Recordable>(props.formModel);
@ -413,7 +406,8 @@
* 修改bug #5090
* 为了保证表单赋值触发所有组件的change事件
*/
const executeEvent = (allSchemas: FormSchema[]) => {
const executeEvent = (allSchemas: FormSchema[] = []) => {
if(!allSchemas) return;
for (const schema of allSchemas) {
//如果是这几个组件 需要查询子级
if (['Card', 'Tab', 'Grid'].includes(schema.component)) {
@ -442,9 +436,9 @@
try {
if (typeof handler === 'string') {
const event = new Function('schema', 'formModel', 'formActionType', 'extParams', handler);
event(schema, formModel, formApi, { formData, allSchemas });
event(schema, formModel, formApi, { formData, allSchemas, isPersonChange: false });
} else if (typeof handler === 'function') {
handler(schema, formModel, formApi, { formData, allSchemas });
handler(schema, formModel, formApi, { formData, allSchemas, isPersonChange: false });
}
} catch (error) {
console.log('error', error);
@ -672,12 +666,15 @@
httpRequest,
refreshAPI,
changeStyle,
setDefaultValue
setDefaultValue,
formModel
};
//将表单方法 导出 给父组件使用。
expose({
...formApi
...formApi,
formModel,
getSchemas
});
return {

View File

@ -31,6 +31,9 @@
:labelAlign="formProps?.labelAlign"
:name="schema.field"
:wrapperCol="itemLabelWidthProp.wrapperCol"
:style="{
overflow:'hidden'
}"
>
<component :is="formComponent(schema)" v-model:value="formModel![schema.field]" :disabled="getDisable" :size="formProps?.size" v-bind="schema.componentProps" />
</FormItem>
@ -84,7 +87,7 @@
:wrapperCol="itemLabelWidthProp.wrapperCol"
>
<template v-if="getDisable && readonlySupport(schema.component)">
<readonly :model="formModel" :schema="schema" />
<readonly :schema="schema" :model="formModel"/>
<component
:is="componentMap.get(schema.component)"
v-show="false"
@ -164,11 +167,11 @@
:validateTrigger="['blur', 'change']"
:wrapperCol="itemLabelWidthProp.wrapperCol"
>
<template v-if="getDisable && readonlySupport(schema.component)">
<template v-if="getDisable && readonlySupport(schema.component) && !getDisabledShowBorder">
<readonly :schema="schema" :value="formModel![schema.field]" />
</template>
<template v-else>
<component :is="defaultComponent(schema)" :key="refreshFieldObj[schema.field]" v-model:value="formModel![schema.field]" :disabled="getDisable" :formData="formModel" :size="formProps?.size" v-bind="getComponentsProps" />
<component :is="defaultComponent(schema)" :key="refreshFieldObj[schema.field]" v-model:value="formModel![schema.field]" :disabled="getDisable" :formData="formModel" :size="formProps?.size" v-bind="{...getComponentsProps, placeholder: getDisable ? '' : getComponentsProps.placeholder, disabledShowBorder: getDisabledShowBorder}" :title="readonlySupport(schema.component) ? formModel![schema.field] : ''"/>
</template>
</FormItem>
</template>
@ -218,7 +221,8 @@
const tabActiveKey = inject<Ref<number>>('tabActiveKey', ref(0));
const activeKey = ref<number>(0);
const isCamelCase = inject<boolean>('isCamelCase', false);
// 注入整个表单的配置formProps是个计算属性不能修改formData则来自每个业务的表单页面。
const formData = inject('formData', { noInject: true });
watch(
() => tabActiveKey?.value,
(val) => {
@ -262,6 +266,17 @@
return disabled;
});
const getDisabledShowBorder = computed(() => {
let disabledShowBorder = false
if (getComponentsProps.value?.disabledShowBorder) {
disabledShowBorder = true;
}
if(import.meta.env?.VITE_GLOB_READ_ONLY_BORDER_DISABLED) {
disabledShowBorder = import.meta.env?.VITE_GLOB_READ_ONLY_BORDER_DISABLED == 'true'
}
return disabledShowBorder
})
const getComponentsProps = computed(() => {
let { componentProps = {} } = props.schema;
@ -356,9 +371,9 @@
// console.log('formitem watch!!!!!!!!');
//填值以后需要手动校验的组件
const validateComponents = ['User', 'RichTextEditor', 'Upload', 'SelectMap'];
if (validateComponents.includes(props.schema.component) && formModel![props.schema.field]) {
if (validateComponents.includes(props.schema?.component) && formModel![props.schema?.field]) {
setTimeout(() => {
props.formApi?.validateFields([props.schema.field]);
props.formApi?.validateFields([props.schema?.field]);
}, 100);
}
},
@ -394,32 +409,53 @@
};
function showComponent(schema) {
return props.isWorkFlow ? !noShowWorkFlowComponents.includes(schema.type) : !noShowGenerateComponents.includes(schema.type);
return props.isWorkFlow ? !noShowWorkFlowComponents.includes(schema?.type) : !noShowGenerateComponents.includes(schema?.type);
}
function readonlySupport(name) {
return /^(Input|AutoCodeRule|DatePicker|Text|TimePicker|Range|RichTextEditor|TimeRangePicker|RangePicker|InputTextArea)$/.test(name);
return /^(Input|AutoCodeRule|DatePicker|Text|TimePicker|Range|RichTextEditor|TimeRangePicker|RangePicker)$/.test(name);
}
function getShow(schema: FormSchema): boolean {
const { show } = schema;
let isIfShow = true;
if (isBoolean(show)) {
isIfShow = show;
}
return isIfShow;
}
function getIsShow(schema: FormSchema): boolean {
const { componentProps, show } = schema as any;
const { componentProps, show } = schema;
let isShow = true;
if (isBoolean(componentProps?.isShow)) {
isShow = componentProps?.isShow;
}
// if (isBoolean(show)) {
// isShow = show;
// }
if (isFunction(componentProps?.isShow)) {
isShow = componentProps.isShow({
values:formModel![schema.field],
model: formModel!,
schema: schema,
field: schema.field
});
}
return isShow;
}
function getIsShow(schema: FormSchema): boolean {
const { show } = schema;
let isShow = true;
if (isBoolean(show)) {
isShow = show;
}
if (isFunction(show)) {
isShow = show({
values:formModel![schema.field],
model: formModel!,
schema: schema,
field: schema.field
});
}
isShow = isShow;
return isShow;
}
</script>

View File

@ -53,6 +53,8 @@
const tabActiveKey = inject<Ref<number>>('tabActiveKey', ref(0));
const activeKey = ref<number>(0);
const isCamelCase = inject<boolean>('isCamelCase', false);
// 注入整个表单的配置formProps是个计算属性不能修改formData则来自每个业务的表单页面。
const formData = inject('formData', { noInject: true });
watch(
() => tabActiveKey?.value,
(val) => {
@ -122,7 +124,7 @@
let field = camelCaseString(item);
if (field) cloneFormModel[field] = cloneFormModel[item];
}
event(props.schema, isCamelCase ? cloneFormModel : formModel, props.formApi, {});
event(props.schema, isCamelCase ? cloneFormModel : formModel, props.formApi, { formData });
if (isCamelCase) {
for (let item in formModel) {
@ -188,7 +190,7 @@
// console.log('formitem watch!!!!!!!!');
//填值以后需要手动校验的组件
const validateComponents = ['User', 'RichTextEditor', 'Upload', 'SelectMap'];
if (validateComponents.includes(props.schema.component) && formModel![props.schema.field]) {
if (validateComponents.includes(props.schema?.component) && formModel![props.schema.field]) {
setTimeout(() => {
props.formApi?.validateFields([props.schema.field]);
}, 100);
@ -209,7 +211,7 @@
};
function showComponent(schema) {
return props.isWorkFlow ? !noShowWorkFlowComponents.includes(schema.type) : !noShowGenerateComponents.includes(schema.type);
return props.isWorkFlow ? !noShowWorkFlowComponents.includes(schema?.type) : !noShowGenerateComponents.includes(schema?.type);
}
function readonlySupport(name) {
@ -217,24 +219,35 @@
}
function getShow(schema: FormSchema): boolean {
const { show } = schema;
let isIfShow = true;
if (isBoolean(show)) {
isIfShow = show;
}
return isIfShow;
}
function getIsShow(schema: FormSchema): boolean {
const { componentProps, show } = schema as any;
const { componentProps, show } = schema;
let isShow = true;
if (isBoolean(componentProps?.isShow)) {
isShow = componentProps?.isShow;
}
// if (isBoolean(show)) {
// isShow = show;
// }
return isShow;
}
function getIsShow(schema: FormSchema): boolean {
const { show } = schema;
let isShow = true;
if (isBoolean(show)) {
isShow = show;
}
if (isFunction(show)) {
isShow = show({
values: itemValue,
model: formModel!,
schema: schema,
field: schema.field
});
}
isShow = isShow;
return isShow;
}

View File

@ -1,5 +1,5 @@
<template>
<div v-if="visible">
<div v-if="visible" class="system-form">
<component
:is="componentName"
v-if="visible"
@ -28,7 +28,10 @@
import { GeneratorConfig } from '/@/model/generator/generatorConfig';
import { createFormEvent, loadFormEvent } from '/@/hooks/web/useFormEvent';
import { changeFormJson } from '/@/hooks/web/useWorkFlowForm';
import { useUserStore } from '/@/store/modules/user';
import {message} from "ant-design-vue";
const userStore = useUserStore();
const userInfo = userStore.getUserInfo;
const props = defineProps({
systemComponent: {
@ -131,7 +134,7 @@
onMounted(() => {
visible.value = true;
approvalData.value = inject("approvalData");
approvalData.value = inject("approvalData");
});
//
@ -176,6 +179,13 @@
async function getFieldsValue(){
return SystemFormRef.value.getFieldsValue();
}
async function getFormModels() {
try {
return (SystemFormRef.value?.getFormModel && SystemFormRef.value?.getFormModel()) || await validate()
} catch (error) {
throw new Error(error);
}
}
async function getValue(){
let values = null;
if(approvalData.value?.approvedResult === ApproveCode.FINISH){
@ -198,40 +208,56 @@
// 提交表单
if (visible.value) {
let id = await submit(saveRowKey);
if(!id) {
throw new Error(`提交表单失败`);
}
let rowKey = getRowKey();
values[rowKey] = id;
values['_id'] = id;
//重新查一遍
let newValues=await SystemFormRef.value.setFormDataFromId(id,true);
let newValues=await SystemFormRef.value.setFormDataFromId(id);
if(newValues){
values=newValues;
} else {
throw new Error(`获取表单失败`);
}
}
return values;
} catch (error) {}
} catch (error) {
console.error(error)
throw new Error(error);
}
}
async function submit(saveRowKey) {
let saveValId = '';
let values = await SystemFormRef.value.validate();
let rowKey = getRowKey();
if (props.workflowConfig.formModel[rowKey]) {
values[rowKey] = props.workflowConfig.formModel[rowKey];
}
if (values[rowKey]) {
// 编辑
await SystemFormRef.value.update({ values, rowId: values[rowKey] });
saveValId = values[rowKey];
} else {
// 新增
saveValId = await SystemFormRef.value.add(values);
if (saveRowKey) {
// 把rowKey写回去新建流程的时候防止取消了重复提交
props.workflowConfig.formModel[rowKey] = saveValId;
try {
let saveValId = '';
let values = await SystemFormRef.value.validate();
let rowKey = getRowKey();
if (props.workflowConfig.formModel[rowKey]) {
values[rowKey] = props.workflowConfig.formModel[rowKey];
}
if (values[rowKey]) {
// 编辑
let res = await SystemFormRef.value.update({ values, rowId: values[rowKey] });
if(!res) {
throw new Error(`提交表单失败`);
}
saveValId = values[rowKey];
} else {
// 新增
saveValId = await SystemFormRef.value.add(values);
if (saveRowKey) {
// 把rowKey写回去新建流程的时候防止取消了重复提交
props.workflowConfig.formModel[rowKey] = saveValId;
}
}
return saveValId;
} catch(e) {
}
return saveValId;
}
async function setDisabledForm(isDisabled) {
@ -245,13 +271,22 @@
async function handleDelete(id) {
let ret;
try {
if(!SystemFormRef.value?.handleDelete) {
throw new Error(`表单未配置删除`);
}
ret = await SystemFormRef.value.handleDelete(id);
} catch (e) {
message.error('表单未配置删除');
return null;
throw new Error(e);
}
return ret;
}
function handleInnerFun(funcName) {
if(!SystemFormRef.value?.[funcName]) {
message.error(`表单未配置${funcName}方法`);
return
}
SystemFormRef.value[funcName]()
}
defineExpose({
workflowSubmit,
@ -263,7 +298,9 @@
getFieldsValue,
getIsOldSystem,
setDisabledForm,
handleDelete
handleDelete,
getFormModels,
handleInnerFun
});
</script>

View File

@ -4,7 +4,7 @@
background-color: @component-background;
.ant-tree-treenode {
align-items: center;
align-items: center!important;
padding-bottom: 0;
}

View File

@ -39,6 +39,7 @@ import { ref, reactive, onMounted, } from 'vue';
import Preview from '/@/views/workflow/design/Preview.vue';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
import { h } from 'vue';
const props = withDefaults(
defineProps<{
@ -89,6 +90,25 @@ const configColumns = [
title: t('备注'),
dataIndex: 'remark',
width: 180,
customRender: ({ text }) => {
return h(
'a-tooltip',
{ title: text || '' },
[
h('div', {
style: {
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
textOverflow: 'ellipsis',
wordBreak: 'break-all',
cursor: 'pointer',
}
}, text || '')
]
);
},
},
{
title: t('创建时间'),
@ -147,6 +167,15 @@ const getHistoryList = async () => {
if (item.chooseCount == 0) {
item.chooseCount = '';//置空
}
// 渲染备注 由json返回
if (item.jsonContent) {
try {
const remarkObj = JSON.parse(item.jsonContent || '{}');
item.remark = remarkObj?.processConfig?.remark || '';
} catch (e) {
item.remark = '';
}
}
});
}
catch (error) {}