--添加测试模块

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

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