This commit is contained in:
xingyu4j
2025-09-25 16:35:19 +08:00
22 changed files with 347 additions and 216 deletions

View File

@@ -4,17 +4,7 @@
// import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css'
// import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css'
// import 'bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css' // 右侧框样式
import {
computed,
defineEmits,
defineOptions,
defineProps,
h,
onBeforeUnmount,
onMounted,
provide,
ref,
} from 'vue';
import { computed, h, onBeforeUnmount, onMounted, provide, ref } from 'vue';
import {
AlignLeftOutlined,
@@ -655,7 +645,7 @@ onBeforeUnmount(() => {
type="file"
id="files"
ref="refFile"
style="display: none"
class="hidden"
accept=".xml, .bpmn"
@change="importLocalFile"
/>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { defineProps, h, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { h, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { BpmProcessInstanceStatus, DICT_TYPE } from '@vben/constants';
import { UndoOutlined, ZoomInOutlined, ZoomOutOutlined } from '@vben/icons';

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { Component } from 'vue';
import { defineOptions, defineProps, ref, watch } from 'vue';
import { ref, watch } from 'vue';
import { CustomConfigMap } from './data';

View File

@@ -1,13 +1,5 @@
<script lang="ts" setup>
import {
defineOptions,
defineProps,
inject,
nextTick,
ref,
toRaw,
watch,
} from 'vue';
import { inject, nextTick, ref, toRaw, watch } from 'vue';
import {
Divider,

View File

@@ -153,11 +153,7 @@ watch(
<template>
<div class="panel-tab__content">
<Form
:model="flowConditionForm"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<Form :model="flowConditionForm">
<Form.Item label="流转类型">
<Select v-model:value="flowConditionForm.type" @change="updateFlowType">
<Select.Option value="normal">普通流转路径</Select.Option>

View File

@@ -305,7 +305,7 @@ watch(
<template>
<div class="panel-tab__content">
<Form :label-col="{ style: { width: '80px' } }">
<Form>
<FormItem label="流程表单">
<!-- <Input v-model:value="formKey" @change="updateElementFormKey" />-->
<Select

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { inject, nextTick, ref, watch } from 'vue';
import { IconifyIcon, PlusOutlined } from '@vben/icons';
import { IconifyIcon } from '@vben/icons';
import { cloneDeep } from '@vben/utils';
import {
@@ -290,7 +290,7 @@ watch(
<div class="element-drawer__button">
<Button type="primary" size="small" @click="openListenerForm(null, -1)">
<template #icon>
<PlusOutlined />
<IconifyIcon icon="ep:plus" />
</template>
添加监听器
</Button>
@@ -309,12 +309,7 @@ watch(
:width="width as any"
:destroy-on-close="true"
>
<Form
:model="listenerForm"
ref="listenerFormRef"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<Form :model="listenerForm" ref="listenerFormRef">
<FormItem
label="事件类型"
name="event"
@@ -462,20 +457,23 @@ watch(
</template>
</Form>
<Divider />
<p class="listener-filed__title">
<span><IconifyIcon icon="ep:menu" />注入字段:</span>
<Button type="primary" @click="openListenerFieldForm(null, -1)">
<div class="mb-2 flex justify-between">
<span class="flex items-center">
<IconifyIcon icon="ep:menu" class="mr-2 text-gray-600" />
注入字段
</span>
<Button
type="primary"
title="添加字段"
@click="openListenerFieldForm(null, -1)"
>
<template #icon>
<IconifyIcon icon="ep:plus" />
</template>
添加字段
</Button>
</p>
<Table
:data-source="fieldsListOfListener"
size="small"
:scroll="{ y: 240 }"
:pagination="false"
bordered
style="flex: none"
>
</div>
<Table :data-source="fieldsListOfListener" size="small" bordered>
<TableColumn title="序号" width="50px">
<template #default="{ index }">
{{ index + 1 }}
@@ -492,12 +490,12 @@ watch(
/>
<TableColumn
title="字段值/表达式"
width="100px"
width="120px"
:custom-render="
({ record }: any) => record.string || record.expression
"
/>
<TableColumn title="操作" width="130px">
<TableColumn title="操作" width="80px" fixed="right">
<template #default="{ record, index }">
<Button
size="small"
@@ -532,13 +530,7 @@ watch(
width="600px"
:destroy-on-close="true"
>
<Form
:model="listenerFieldForm"
ref="listenerFieldFormRef"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
style="height: 136px"
>
<Form :model="listenerFieldForm" ref="listenerFieldFormRef">
<FormItem
label="字段名称"
name="name"

View File

@@ -89,7 +89,7 @@ const select = async (row: BpmProcessListenerApi.ProcessListener) => {
</template>
</Table.Column>
<Table.Column title="值" align="center" data-index="value" />
<Table.Column title="操作" align="center">
<Table.Column title="操作" align="center" fixed="right">
<template #default="{ record }">
<Button type="primary" @click="select(record)"> 选择 </Button>
</template>

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { inject, nextTick, ref, watch } from 'vue';
import { MenuOutlined, PlusOutlined, SelectOutlined } from '@vben/icons';
import { IconifyIcon } from '@vben/icons';
import { cloneDeep } from '@vben/utils';
import {
@@ -300,11 +300,11 @@ watch(
</Table>
<div class="element-drawer__button">
<Button size="small" type="primary" @click="openListenerForm(null)">
<template #icon><PlusOutlined /></template>
<template #icon> <IconifyIcon icon="ep:plus" /></template>
添加监听器
</Button>
<Button size="small" @click="openProcessListenerDialog">
<template #icon><SelectOutlined /></template>
<template #icon> <IconifyIcon icon="ep:select" /></template>
选择监听器
</Button>
</div>
@@ -316,12 +316,7 @@ watch(
:width="width"
:destroy-on-close="true"
>
<Form
:model="listenerForm"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
ref="listenerFormRef"
>
<Form :model="listenerForm" ref="listenerFormRef">
<FormItem
label="事件类型"
name="event"
@@ -458,16 +453,22 @@ watch(
</Form>
<Divider />
<p class="listener-filed__title">
<span><MenuOutlined />注入字段:</span>
<div class="mb-2 flex justify-between">
<span class="flex items-center">
<IconifyIcon icon="ep:menu" class="mr-2 text-gray-600" />
注入字段
</span>
<Button
size="small"
type="primary"
title="添加字段"
@click="openListenerFieldForm(null)"
>
<template #icon>
<IconifyIcon icon="ep:plus" />
</template>
添加字段
</Button>
</p>
</div>
<Table
:data="fieldsListOfListener"
size="small"
@@ -533,13 +534,7 @@ watch(
:width="600"
:destroy-on-close="true"
>
<Form
:model="listenerFieldForm"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
ref="listenerFieldFormRef"
style="height: 136px"
>
<Form :model="listenerFieldForm" ref="listenerFieldFormRef">
<FormItem
label="字段名称"
name="name"

View File

@@ -421,7 +421,7 @@ watch(
</RadioGroup>
<div v-else>除了UserTask以外节点的多实例待实现</div>
<!-- 与Simple设计器配置合并保留以前的代码 -->
<Form :label-col="{ span: 6 }" style="display: none">
<Form class="hidden">
<FormItem label="快捷配置">
<Button size="small" @click="() => changeConfig('依次审批')">
依次审批
@@ -467,7 +467,7 @@ watch(
/>
</FormItem>
<!-- add by 芋艿:由于「元素变量」暂时用不到,所以这里 display 为 none -->
<FormItem label="元素变量" key="elementVariable" style="display: none">
<FormItem label="元素变量" key="elementVariable" class="hidden">
<Input
v-model:value="loopInstanceForm.elementVariable"
allow-clear
@@ -485,7 +485,7 @@ watch(
/>
</FormItem>
<!-- add by 芋艿:由于「异步状态」暂时用不到,所以这里 display 为 none -->
<FormItem label="异步状态" key="async" style="display: none">
<FormItem label="异步状态" key="async" class="hidden">
<Checkbox
v-model:checked="loopInstanceForm.asyncBefore"
@change="() => updateLoopAsync('asyncBefore')"

View File

@@ -161,25 +161,15 @@ watch(
<template>
<div class="panel-tab__content">
<Table :data="elementPropertyList" :scroll="{ y: 240 }" bordered>
<Table :data="elementPropertyList" size="small" bordered>
<TableColumn title="序号" width="50">
<template #default="{ index }">
{{ index + 1 }}
</template>
</TableColumn>
<TableColumn
title="属性名"
data-index="name"
:min-width="100"
:ellipsis="{ showTitle: true }"
/>
<TableColumn
title="属性值"
data-index="value"
:min-width="100"
:ellipsis="{ showTitle: true }"
/>
<TableColumn title="操作" width="110">
<TableColumn title="属性名" data-index="name" />
<TableColumn title="属性值" data-index="value" />
<TableColumn title="操作">
<template #default="{ record, index }">
<Button
type="link"
@@ -215,11 +205,7 @@ watch(
:width="600"
:destroy-on-close="true"
>
<Form
:model="propertyForm"
ref="attributeFormRef"
:label-col="{ span: 6 }"
>
<Form :model="propertyForm" ref="attributeFormRef">
<FormItem label="属性名:" name="name">
<Input v-model:value="propertyForm.name" allow-clear />
</FormItem>

View File

@@ -84,8 +84,8 @@ onMounted(() => {
<template>
<div class="panel-tab__content">
<div class="panel-tab__content--title">
<span>
<IconifyIcon icon="ep:menu" style="margin-right: 8px; color: #555" />
<span class="flex items-center">
<IconifyIcon icon="ep:menu" class="mr-2 text-gray-600" />
消息列表
</span>
<Button type="primary" title="创建新消息" @click="openModel('message')">
@@ -95,33 +95,19 @@ onMounted(() => {
创建新消息
</Button>
</div>
<Table :data-source="messageList" :bordered="true" :pagination="false">
<Table :data-source="messageList" size="small" bordered>
<TableColumn title="序号" width="60px">
<template #default="{ index }">
{{ index + 1 }}
</template>
</TableColumn>
<TableColumn
title="消息ID"
data-index="id"
:width="300"
:ellipsis="{ showTitle: true }"
/>
<TableColumn
title="消息名称"
data-index="name"
:width="300"
:ellipsis="{ showTitle: true }"
/>
<TableColumn title="消息ID" data-index="id" />
<TableColumn title="消息名称" data-index="name" />
</Table>
<div
class="panel-tab__content--title"
style="padding-top: 8px; margin-top: 8px; border-top: 1px solid #eee"
>
<span>
<IconifyIcon icon="ep:menu" style="margin-right: 8px; color: #555">
信号列表
</IconifyIcon>
<div class="panel-tab__content--title mt-2 border-t border-gray-200 pt-2">
<span class="flex items-center">
<IconifyIcon icon="ep:menu" class="mr-2 text-gray-600" />
信号列表
</span>
<Button type="primary" title="创建新信号" @click="openModel('signal')">
<template #icon>
@@ -130,24 +116,14 @@ onMounted(() => {
创建新信号
</Button>
</div>
<Table :data-source="signalList" :bordered="true" :pagination="false">
<Table :data-source="signalList" size="small" bordered>
<TableColumn title="序号" width="60px">
<template #default="{ index }">
{{ index + 1 }}
</template>
</TableColumn>
<TableColumn
title="信号ID"
data-index="id"
:width="300"
:ellipsis="{ showTitle: true }"
/>
<TableColumn
title="信号名称"
data-index="name"
:width="300"
:ellipsis="{ showTitle: true }"
/>
<TableColumn title="信号ID" data-index="id" />
<TableColumn title="信号名称" data-index="name" />
</Table>
<Modal
@@ -157,11 +133,7 @@ onMounted(() => {
width="400px"
:destroy-on-close="true"
>
<Form
:model="modelObjectForm"
:label-col="{ span: 9 }"
:wrapper-col="{ span: 15 }"
>
<Form :model="modelObjectForm">
<FormItem :label="modelConfig.idLabel">
<Input v-model:value="modelObjectForm.id" allow-clear />
</FormItem>

View File

@@ -63,9 +63,9 @@ watch(
<template>
<div class="panel-tab__content">
<Form :label-col="{ span: 9 }" :wrapper-col="{ span: 15 }">
<Form>
<!-- add by 芋艿由于异步延续暂时用不到所以这里 display none -->
<FormItem label="异步延续" style="display: none">
<FormItem label="异步延续" class="hidden">
<Checkbox
v-model:checked="taskConfigForm.asyncBefore"
@change="changeTaskAsync"

View File

@@ -180,7 +180,7 @@ watch(
<template>
<div>
<Form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<Form>
<FormItem label="实例名称">
<Input
v-model:value="formData.processInstanceName"
@@ -341,12 +341,7 @@ watch(
@ok="saveVariable"
@cancel="variableDialogVisible = false"
>
<Form
:model="varialbeFormData"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
ref="varialbeFormRef"
>
<Form :model="varialbeFormData" ref="varialbeFormRef">
<FormItem label="源:" name="source">
<Input v-model:value="varialbeFormData.source" allow-clear />
</FormItem>

View File

@@ -143,7 +143,7 @@ watch(
width="400px"
:destroy-on-close="true"
>
<Form :model="newMessageForm" size="small" :label-col="{ span: 6 }">
<Form :model="newMessageForm" size="small">
<Form.Item label="消息ID">
<Input v-model:value="newMessageForm.id" allow-clear />
</Form.Item>

View File

@@ -1,13 +1,5 @@
<script lang="ts" setup>
import {
defineOptions,
defineProps,
nextTick,
onBeforeUnmount,
ref,
toRaw,
watch,
} from 'vue';
import { nextTick, onBeforeUnmount, ref, toRaw, watch } from 'vue';
import {
FormItem,

View File

@@ -344,7 +344,7 @@ onBeforeUnmount(() => {
</script>
<template>
<Form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<Form>
<FormItem label="规则类型" name="candidateStrategy">
<Select
v-model:value="userTaskForm.candidateStrategy"

View File

@@ -3,12 +3,7 @@ import type { Ref } from 'vue';
import { computed, nextTick, onMounted, ref, toRaw, watch } from 'vue';
import {
CheckCircleFilled,
ExclamationCircleFilled,
IconifyIcon,
QuestionCircleFilled,
} from '@vben/icons';
import { IconifyIcon } from '@vben/icons';
import { Button, DatePicker, Input, Modal, Tooltip } from 'ant-design-vue';
@@ -240,7 +235,11 @@ watch(
循环
</Button>
</Button.Group>
<CheckCircleFilled v-if="valid" style="color: green; margin-left: 8px" />
<IconifyIcon
icon="ant-design:check-circle-filled"
v-if="valid"
style="color: green; margin-left: 8px"
/>
</div>
<div style="display: flex; align-items: center; margin-top: 10px">
<span>条件</span>
@@ -254,11 +253,15 @@ watch(
>
<template #suffix>
<Tooltip v-if="!valid" title="格式错误" placement="top">
<ExclamationCircleFilled style="color: orange" />
<IconifyIcon
icon="ant-design:exclamation-circle-filled"
class="text-orange-400"
/>
</Tooltip>
<Tooltip :title="helpText" placement="top">
<QuestionCircleFilled
style="color: #409eff; cursor: pointer"
<IconifyIcon
icon="ant-design:question-circle-filled"
class="cursor-pointer text-[#409eff]"
@click="showHelp = true"
/>
</Tooltip>
@@ -351,7 +354,3 @@ watch(
</Modal>
</div>
</template>
<style scoped>
/* 相关样式 */
</style>

View File

@@ -6,7 +6,7 @@ import type { FileUploadProps } from './typing';
import type { AxiosProgressEvent } from '#/api/infra/file';
import { ref, toRefs, watch } from 'vue';
import { computed, ref, toRefs, watch } from 'vue';
import { CloudUpload } from '@vben/icons';
import { $t } from '@vben/locales';
@@ -22,8 +22,10 @@ defineOptions({ name: 'FileUpload', inheritAttrs: false });
const props = withDefaults(defineProps<FileUploadProps>(), {
value: () => [],
modelValue: undefined,
directory: undefined,
disabled: false,
drag: false,
helpText: '',
maxSize: 2,
maxNumber: 1,
@@ -33,7 +35,14 @@ const props = withDefaults(defineProps<FileUploadProps>(), {
resultField: '',
showDescription: false,
});
const emit = defineEmits(['change', 'update:value', 'delete', 'returnText']);
const emit = defineEmits([
'change',
'update:value',
'update:modelValue',
'delete',
'returnText',
'preview',
]);
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
const isInnerOperate = ref<boolean>(false);
const { getStringAccept } = useUploadType({
@@ -43,13 +52,25 @@ const { getStringAccept } = useUploadType({
maxSizeRef: maxSize,
});
// 计算当前绑定的值,优先使用 modelValue
const currentValue = computed(() => {
return props.modelValue === undefined ? props.value : props.modelValue;
});
// 判断是否使用 modelValue
const isUsingModelValue = computed(() => {
return props.modelValue !== undefined;
});
const fileList = ref<UploadProps['fileList']>([]);
const isLtMsg = ref<boolean>(true); // 文件大小错误提示
const isActMsg = ref<boolean>(true); // 文件类型错误提示
const isFirstRender = ref<boolean>(true); // 是否第一次渲染
const uploadNumber = ref<number>(0); // 上传文件计数器
const uploadList = ref<any[]>([]); // 临时上传列表
watch(
() => props.value,
currentValue,
(v) => {
if (isInnerOperate.value) {
isInnerOperate.value = false;
@@ -94,15 +115,40 @@ async function handleRemove(file: UploadFile) {
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('update:modelValue', value);
emit('change', value);
emit('delete', file);
}
}
// 处理文件预览
function handlePreview(file: UploadFile) {
emit('preview', file);
}
// 处理文件数量超限
function handleExceed() {
message.error($t('ui.upload.maxNumber', [maxNumber.value]));
}
// 处理上传错误
function handleUploadError(error: any) {
console.error('上传错误:', error);
message.error($t('ui.upload.uploadError'));
// 上传失败时减少计数器
uploadNumber.value = Math.max(0, uploadNumber.value - 1);
}
async function beforeUpload(file: File) {
const fileContent = await file.text();
emit('returnText', fileContent);
// 检查文件数量限制
if (fileList.value!.length >= props.maxNumber) {
message.error($t('ui.upload.maxNumber', [props.maxNumber]));
return Upload.LIST_IGNORE;
}
const { maxSize, accept } = props;
const isAct = checkFileType(file, accept);
if (!isAct) {
@@ -110,6 +156,7 @@ async function beforeUpload(file: File) {
isActMsg.value = false;
// 防止弹出多个错误提示
setTimeout(() => (isActMsg.value = true), 1000);
return Upload.LIST_IGNORE;
}
const isLt = file.size / 1024 / 1024 > maxSize;
if (isLt) {
@@ -117,8 +164,12 @@ async function beforeUpload(file: File) {
isLtMsg.value = false;
// 防止弹出多个错误提示
setTimeout(() => (isLtMsg.value = true), 1000);
return Upload.LIST_IGNORE;
}
return (isAct && !isLt) || Upload.LIST_IGNORE;
// 只有在验证通过后才增加计数器
uploadNumber.value++;
return true;
}
async function customRequest(info: UploadRequestOption<any>) {
@@ -133,17 +184,48 @@ async function customRequest(info: UploadRequestOption<any>) {
info.onProgress!({ percent });
};
const res = await api?.(info.file as File, progressEvent);
// 处理上传成功后的逻辑
handleUploadSuccess(res, info.file as File);
info.onSuccess!(res);
message.success($t('ui.upload.uploadSuccess'));
// 更新文件
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('change', value);
} catch (error: any) {
console.error(error);
info.onError!(error);
handleUploadError(error);
}
}
// 处理上传成功
function handleUploadSuccess(res: any, file: File) {
// 删除临时文件
const index = fileList.value?.findIndex((item) => item.name === file.name);
if (index !== -1) {
fileList.value?.splice(index!, 1);
}
// 添加到临时上传列表
const fileUrl = res?.url || res?.data || res;
uploadList.value.push({
name: file.name,
url: fileUrl,
status: UploadResultStatus.DONE,
uid: file.name + Date.now(),
});
// 检查是否所有文件都上传完成
if (uploadList.value.length >= uploadNumber.value) {
fileList.value?.push(...uploadList.value);
uploadList.value = [];
uploadNumber.value = 0;
// 更新值
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('update:modelValue', value);
emit('change', value);
}
}
@@ -156,11 +238,26 @@ function getValue() {
}
return item?.url || item?.response?.url || item?.response;
});
// add by 芋艿:【特殊】单个文件的情况,获取首个元素,保证返回的是 String 类型
// 单个文件的情况,根据输入参数类型决定返回格式
if (props.maxNumber === 1) {
return list.length > 0 ? list[0] : '';
const singleValue = list.length > 0 ? list[0] : '';
// 如果原始值是字符串或 modelValue 是字符串,返回字符串
if (
isString(props.value) ||
(isUsingModelValue.value && isString(props.modelValue))
) {
return singleValue;
}
return singleValue;
}
return list;
// 多文件情况,根据输入参数类型决定返回格式
if (isUsingModelValue.value) {
return Array.isArray(props.modelValue) ? list : list.join(',');
}
return Array.isArray(props.value) ? list : list.join(',');
}
</script>
@@ -177,15 +274,34 @@ function getValue() {
:multiple="multiple"
list-type="text"
:progress="{ showInfo: true }"
:show-upload-list="{
showPreviewIcon: true,
showRemoveIcon: true,
showDownloadIcon: true,
}"
@remove="handleRemove"
@preview="handlePreview"
@reject="handleExceed"
>
<div v-if="fileList && fileList.length < maxNumber">
<div v-if="drag" class="upload-drag-area">
<p class="ant-upload-drag-icon">
<CloudUpload />
</p>
<p class="ant-upload-text">点击或拖拽文件到此区域上传</p>
<p class="ant-upload-hint">
支持{{ accept.join('/') }}格式文件不超过{{ maxSize }}MB
</p>
</div>
<div v-else-if="fileList && fileList.length < maxNumber">
<Button>
<CloudUpload />
{{ $t('ui.upload.upload') }}
</Button>
</div>
<div v-if="showDescription" class="mt-2 flex flex-wrap items-center">
<div
v-if="showDescription && !drag"
class="mt-2 flex flex-wrap items-center"
>
请上传不超过
<div class="text-primary mx-1 font-bold">{{ maxSize }}MB</div>
@@ -195,3 +311,35 @@ function getValue() {
</Upload>
</div>
</template>
<style scoped>
.upload-drag-area {
border: 2px dashed #d9d9d9;
border-radius: 8px;
padding: 20px;
text-align: center;
background-color: #fafafa;
transition: border-color 0.3s;
}
.upload-drag-area:hover {
border-color: #1890ff;
}
.ant-upload-drag-icon {
font-size: 48px;
color: #d9d9d9;
margin-bottom: 16px;
}
.ant-upload-text {
font-size: 16px;
color: #666;
margin-bottom: 8px;
}
.ant-upload-hint {
font-size: 14px;
color: #999;
}
</style>

View File

@@ -6,7 +6,7 @@ import type { FileUploadProps } from './typing';
import type { AxiosProgressEvent } from '#/api/infra/file';
import { ref, toRefs, watch } from 'vue';
import { computed, ref, toRefs, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales';
@@ -22,6 +22,7 @@ defineOptions({ name: 'ImageUpload', inheritAttrs: false });
const props = withDefaults(defineProps<FileUploadProps>(), {
value: () => [],
modelValue: undefined,
directory: undefined,
disabled: false,
listType: 'picture-card',
@@ -34,7 +35,12 @@ const props = withDefaults(defineProps<FileUploadProps>(), {
resultField: '',
showDescription: true,
});
const emit = defineEmits(['change', 'update:value', 'delete']);
const emit = defineEmits([
'change',
'update:value',
'update:modelValue',
'delete',
]);
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
const isInnerOperate = ref<boolean>(false);
const { getStringAccept } = useUploadType({
@@ -43,6 +49,16 @@ const { getStringAccept } = useUploadType({
maxNumberRef: maxNumber,
maxSizeRef: maxSize,
});
// 计算当前绑定的值,优先使用 modelValue
const currentValue = computed(() => {
return props.modelValue === undefined ? props.value : props.modelValue;
});
// 判断是否使用 modelValue
const isUsingModelValue = computed(() => {
return props.modelValue !== undefined;
});
const previewOpen = ref<boolean>(false); // 是否展示预览
const previewImage = ref<string>(''); // 预览图片
const previewTitle = ref<string>(''); // 预览标题
@@ -51,9 +67,11 @@ const fileList = ref<UploadProps['fileList']>([]);
const isLtMsg = ref<boolean>(true); // 文件大小错误提示
const isActMsg = ref<boolean>(true); // 文件类型错误提示
const isFirstRender = ref<boolean>(true); // 是否第一次渲染
const uploadNumber = ref<number>(0); // 上传文件计数器
const uploadList = ref<any[]>([]); // 临时上传列表
watch(
() => props.value,
currentValue,
async (v) => {
if (isInnerOperate.value) {
isInnerOperate.value = false;
@@ -122,6 +140,7 @@ async function handleRemove(file: UploadFile) {
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('update:modelValue', value);
emit('change', value);
emit('delete', file);
}
@@ -133,6 +152,12 @@ function handleCancel() {
}
async function beforeUpload(file: File) {
// 检查文件数量限制
if (fileList.value!.length >= props.maxNumber) {
message.error($t('ui.upload.maxNumber', [props.maxNumber]));
return Upload.LIST_IGNORE;
}
const { maxSize, accept } = props;
const isAct = checkImgType(file, accept);
if (!isAct) {
@@ -140,6 +165,7 @@ async function beforeUpload(file: File) {
isActMsg.value = false;
// 防止弹出多个错误提示
setTimeout(() => (isActMsg.value = true), 1000);
return Upload.LIST_IGNORE;
}
const isLt = file.size / 1024 / 1024 > maxSize;
if (isLt) {
@@ -147,8 +173,12 @@ async function beforeUpload(file: File) {
isLtMsg.value = false;
// 防止弹出多个错误提示
setTimeout(() => (isLtMsg.value = true), 1000);
return Upload.LIST_IGNORE;
}
return (isAct && !isLt) || Upload.LIST_IGNORE;
// 只有在验证通过后才增加计数器
uploadNumber.value++;
return true;
}
async function customRequest(info: UploadRequestOption<any>) {
@@ -163,20 +193,59 @@ async function customRequest(info: UploadRequestOption<any>) {
info.onProgress!({ percent });
};
const res = await api?.(info.file as File, progressEvent);
// 处理上传成功后的逻辑
handleUploadSuccess(res, info.file as File);
info.onSuccess!(res);
message.success($t('ui.upload.uploadSuccess'));
// 更新文件
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('change', value);
} catch (error: any) {
console.error(error);
info.onError!(error);
handleUploadError(error);
}
}
// 处理上传成功
function handleUploadSuccess(res: any, file: File) {
// 删除临时文件
const index = fileList.value?.findIndex((item) => item.name === file.name);
if (index !== -1) {
fileList.value?.splice(index!, 1);
}
// 添加到临时上传列表
const fileUrl = res?.url || res?.data || res;
uploadList.value.push({
name: file.name,
url: fileUrl,
status: UploadResultStatus.DONE,
uid: file.name + Date.now(),
});
// 检查是否所有文件都上传完成
if (uploadList.value.length >= uploadNumber.value) {
fileList.value?.push(...uploadList.value);
uploadList.value = [];
uploadNumber.value = 0;
// 更新值
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('update:modelValue', value);
emit('change', value);
}
}
// 处理上传错误
function handleUploadError(error: any) {
console.error('上传错误:', error);
message.error($t('ui.upload.uploadError'));
// 上传失败时减少计数器
uploadNumber.value = Math.max(0, uploadNumber.value - 1);
}
function getValue() {
const list = (fileList.value || [])
.filter((item) => item?.status === UploadResultStatus.DONE)
@@ -186,11 +255,26 @@ function getValue() {
}
return item?.url || item?.response?.url || item?.response;
});
// add by 芋艿:【特殊】单个文件的情况,获取首个元素,保证返回的是 String 类型
// 单个文件的情况,根据输入参数类型决定返回格式
if (props.maxNumber === 1) {
return list.length > 0 ? list[0] : '';
const singleValue = list.length > 0 ? list[0] : '';
// 如果原始值是字符串或 modelValue 是字符串,返回字符串
if (
isString(props.value) ||
(isUsingModelValue.value && isString(props.modelValue))
) {
return singleValue;
}
return singleValue;
}
return list;
// 多文件情况,根据输入参数类型决定返回格式
if (isUsingModelValue.value) {
return Array.isArray(props.modelValue) ? list : list.join(',');
}
return Array.isArray(props.value) ? list : list.join(',');
}
</script>

View File

@@ -21,10 +21,12 @@ export interface FileUploadProps {
// 上传的目录
directory?: string;
disabled?: boolean;
drag?: boolean; // 是否支持拖拽上传
helpText?: string;
listType?: UploadListType;
// 最大数量的文件Infinity不限制
maxNumber?: number;
modelValue?: string | string[]; // v-model 支持
// 文件最大多少MB
maxSize?: number;
// 是否支持多选

View File

@@ -61,15 +61,3 @@ export const MenuOutlined = createIconifyIcon('ant-design:menu-outlined');
export const PlusOutlined = createIconifyIcon('ant-design:plus-outlined');
export const SelectOutlined = createIconifyIcon('ant-design:select-outlined');
export const CheckCircleFilled = createIconifyIcon(
'ant-design:check-circle-filled',
);
export const ExclamationCircleFilled = createIconifyIcon(
'ant-design:exclamation-circle-filled',
);
export const QuestionCircleFilled = createIconifyIcon(
'ant-design:question-circle-filled',
);