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

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <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 { BpmProcessInstanceStatus, DICT_TYPE } from '@vben/constants';
import { UndoOutlined, ZoomInOutlined, ZoomOutOutlined } from '@vben/icons'; import { UndoOutlined, ZoomInOutlined, ZoomOutOutlined } from '@vben/icons';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -344,7 +344,7 @@ onBeforeUnmount(() => {
</script> </script>
<template> <template>
<Form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }"> <Form>
<FormItem label="规则类型" name="candidateStrategy"> <FormItem label="规则类型" name="candidateStrategy">
<Select <Select
v-model:value="userTaskForm.candidateStrategy" 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 { computed, nextTick, onMounted, ref, toRaw, watch } from 'vue';
import { import { IconifyIcon } from '@vben/icons';
CheckCircleFilled,
ExclamationCircleFilled,
IconifyIcon,
QuestionCircleFilled,
} from '@vben/icons';
import { Button, DatePicker, Input, Modal, Tooltip } from 'ant-design-vue'; import { Button, DatePicker, Input, Modal, Tooltip } from 'ant-design-vue';
@@ -240,7 +235,11 @@ watch(
循环 循环
</Button> </Button>
</Button.Group> </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>
<div style="display: flex; align-items: center; margin-top: 10px"> <div style="display: flex; align-items: center; margin-top: 10px">
<span>条件</span> <span>条件</span>
@@ -254,11 +253,15 @@ watch(
> >
<template #suffix> <template #suffix>
<Tooltip v-if="!valid" title="格式错误" placement="top"> <Tooltip v-if="!valid" title="格式错误" placement="top">
<ExclamationCircleFilled style="color: orange" /> <IconifyIcon
icon="ant-design:exclamation-circle-filled"
class="text-orange-400"
/>
</Tooltip> </Tooltip>
<Tooltip :title="helpText" placement="top"> <Tooltip :title="helpText" placement="top">
<QuestionCircleFilled <IconifyIcon
style="color: #409eff; cursor: pointer" icon="ant-design:question-circle-filled"
class="cursor-pointer text-[#409eff]"
@click="showHelp = true" @click="showHelp = true"
/> />
</Tooltip> </Tooltip>
@@ -351,7 +354,3 @@ watch(
</Modal> </Modal>
</div> </div>
</template> </template>
<style scoped>
/* 相关样式 */
</style>

View File

@@ -6,7 +6,7 @@ import type { FileUploadProps } from './typing';
import type { AxiosProgressEvent } from '#/api/infra/file'; 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 { CloudUpload } from '@vben/icons';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
@@ -22,8 +22,10 @@ defineOptions({ name: 'FileUpload', inheritAttrs: false });
const props = withDefaults(defineProps<FileUploadProps>(), { const props = withDefaults(defineProps<FileUploadProps>(), {
value: () => [], value: () => [],
modelValue: undefined,
directory: undefined, directory: undefined,
disabled: false, disabled: false,
drag: false,
helpText: '', helpText: '',
maxSize: 2, maxSize: 2,
maxNumber: 1, maxNumber: 1,
@@ -33,7 +35,14 @@ const props = withDefaults(defineProps<FileUploadProps>(), {
resultField: '', resultField: '',
showDescription: false, 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 { accept, helpText, maxNumber, maxSize } = toRefs(props);
const isInnerOperate = ref<boolean>(false); const isInnerOperate = ref<boolean>(false);
const { getStringAccept } = useUploadType({ const { getStringAccept } = useUploadType({
@@ -43,13 +52,25 @@ const { getStringAccept } = useUploadType({
maxSizeRef: maxSize, 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 fileList = ref<UploadProps['fileList']>([]);
const isLtMsg = ref<boolean>(true); // 文件大小错误提示 const isLtMsg = ref<boolean>(true); // 文件大小错误提示
const isActMsg = ref<boolean>(true); // 文件类型错误提示 const isActMsg = ref<boolean>(true); // 文件类型错误提示
const isFirstRender = ref<boolean>(true); // 是否第一次渲染 const isFirstRender = ref<boolean>(true); // 是否第一次渲染
const uploadNumber = ref<number>(0); // 上传文件计数器
const uploadList = ref<any[]>([]); // 临时上传列表
watch( watch(
() => props.value, currentValue,
(v) => { (v) => {
if (isInnerOperate.value) { if (isInnerOperate.value) {
isInnerOperate.value = false; isInnerOperate.value = false;
@@ -94,15 +115,40 @@ async function handleRemove(file: UploadFile) {
const value = getValue(); const value = getValue();
isInnerOperate.value = true; isInnerOperate.value = true;
emit('update:value', value); emit('update:value', value);
emit('update:modelValue', value);
emit('change', value); emit('change', value);
emit('delete', file); 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) { async function beforeUpload(file: File) {
const fileContent = await file.text(); const fileContent = await file.text();
emit('returnText', fileContent); 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 { maxSize, accept } = props;
const isAct = checkFileType(file, accept); const isAct = checkFileType(file, accept);
if (!isAct) { if (!isAct) {
@@ -110,6 +156,7 @@ async function beforeUpload(file: File) {
isActMsg.value = false; isActMsg.value = false;
// 防止弹出多个错误提示 // 防止弹出多个错误提示
setTimeout(() => (isActMsg.value = true), 1000); setTimeout(() => (isActMsg.value = true), 1000);
return Upload.LIST_IGNORE;
} }
const isLt = file.size / 1024 / 1024 > maxSize; const isLt = file.size / 1024 / 1024 > maxSize;
if (isLt) { if (isLt) {
@@ -117,8 +164,12 @@ async function beforeUpload(file: File) {
isLtMsg.value = false; isLtMsg.value = false;
// 防止弹出多个错误提示 // 防止弹出多个错误提示
setTimeout(() => (isLtMsg.value = true), 1000); 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>) { async function customRequest(info: UploadRequestOption<any>) {
@@ -133,17 +184,48 @@ async function customRequest(info: UploadRequestOption<any>) {
info.onProgress!({ percent }); info.onProgress!({ percent });
}; };
const res = await api?.(info.file as File, progressEvent); const res = await api?.(info.file as File, progressEvent);
// 处理上传成功后的逻辑
handleUploadSuccess(res, info.file as File);
info.onSuccess!(res); info.onSuccess!(res);
message.success($t('ui.upload.uploadSuccess')); message.success($t('ui.upload.uploadSuccess'));
// 更新文件
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('change', value);
} catch (error: any) { } catch (error: any) {
console.error(error); console.error(error);
info.onError!(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; return item?.url || item?.response?.url || item?.response;
}); });
// add by 芋艿:【特殊】单个文件的情况,获取首个元素,保证返回的是 String 类型
// 单个文件的情况,根据输入参数类型决定返回格式
if (props.maxNumber === 1) { 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> </script>
@@ -177,15 +274,34 @@ function getValue() {
:multiple="multiple" :multiple="multiple"
list-type="text" list-type="text"
:progress="{ showInfo: true }" :progress="{ showInfo: true }"
:show-upload-list="{
showPreviewIcon: true,
showRemoveIcon: true,
showDownloadIcon: true,
}"
@remove="handleRemove" @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> <Button>
<CloudUpload /> <CloudUpload />
{{ $t('ui.upload.upload') }} {{ $t('ui.upload.upload') }}
</Button> </Button>
</div> </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> <div class="text-primary mx-1 font-bold">{{ maxSize }}MB</div>
@@ -195,3 +311,35 @@ function getValue() {
</Upload> </Upload>
</div> </div>
</template> </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 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 { IconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
@@ -22,6 +22,7 @@ defineOptions({ name: 'ImageUpload', inheritAttrs: false });
const props = withDefaults(defineProps<FileUploadProps>(), { const props = withDefaults(defineProps<FileUploadProps>(), {
value: () => [], value: () => [],
modelValue: undefined,
directory: undefined, directory: undefined,
disabled: false, disabled: false,
listType: 'picture-card', listType: 'picture-card',
@@ -34,7 +35,12 @@ const props = withDefaults(defineProps<FileUploadProps>(), {
resultField: '', resultField: '',
showDescription: true, 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 { accept, helpText, maxNumber, maxSize } = toRefs(props);
const isInnerOperate = ref<boolean>(false); const isInnerOperate = ref<boolean>(false);
const { getStringAccept } = useUploadType({ const { getStringAccept } = useUploadType({
@@ -43,6 +49,16 @@ const { getStringAccept } = useUploadType({
maxNumberRef: maxNumber, maxNumberRef: maxNumber,
maxSizeRef: maxSize, 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 previewOpen = ref<boolean>(false); // 是否展示预览
const previewImage = ref<string>(''); // 预览图片 const previewImage = ref<string>(''); // 预览图片
const previewTitle = ref<string>(''); // 预览标题 const previewTitle = ref<string>(''); // 预览标题
@@ -51,9 +67,11 @@ const fileList = ref<UploadProps['fileList']>([]);
const isLtMsg = ref<boolean>(true); // 文件大小错误提示 const isLtMsg = ref<boolean>(true); // 文件大小错误提示
const isActMsg = ref<boolean>(true); // 文件类型错误提示 const isActMsg = ref<boolean>(true); // 文件类型错误提示
const isFirstRender = ref<boolean>(true); // 是否第一次渲染 const isFirstRender = ref<boolean>(true); // 是否第一次渲染
const uploadNumber = ref<number>(0); // 上传文件计数器
const uploadList = ref<any[]>([]); // 临时上传列表
watch( watch(
() => props.value, currentValue,
async (v) => { async (v) => {
if (isInnerOperate.value) { if (isInnerOperate.value) {
isInnerOperate.value = false; isInnerOperate.value = false;
@@ -122,6 +140,7 @@ async function handleRemove(file: UploadFile) {
const value = getValue(); const value = getValue();
isInnerOperate.value = true; isInnerOperate.value = true;
emit('update:value', value); emit('update:value', value);
emit('update:modelValue', value);
emit('change', value); emit('change', value);
emit('delete', file); emit('delete', file);
} }
@@ -133,6 +152,12 @@ function handleCancel() {
} }
async function beforeUpload(file: File) { 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 { maxSize, accept } = props;
const isAct = checkImgType(file, accept); const isAct = checkImgType(file, accept);
if (!isAct) { if (!isAct) {
@@ -140,6 +165,7 @@ async function beforeUpload(file: File) {
isActMsg.value = false; isActMsg.value = false;
// 防止弹出多个错误提示 // 防止弹出多个错误提示
setTimeout(() => (isActMsg.value = true), 1000); setTimeout(() => (isActMsg.value = true), 1000);
return Upload.LIST_IGNORE;
} }
const isLt = file.size / 1024 / 1024 > maxSize; const isLt = file.size / 1024 / 1024 > maxSize;
if (isLt) { if (isLt) {
@@ -147,8 +173,12 @@ async function beforeUpload(file: File) {
isLtMsg.value = false; isLtMsg.value = false;
// 防止弹出多个错误提示 // 防止弹出多个错误提示
setTimeout(() => (isLtMsg.value = true), 1000); 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>) { async function customRequest(info: UploadRequestOption<any>) {
@@ -163,20 +193,59 @@ async function customRequest(info: UploadRequestOption<any>) {
info.onProgress!({ percent }); info.onProgress!({ percent });
}; };
const res = await api?.(info.file as File, progressEvent); const res = await api?.(info.file as File, progressEvent);
// 处理上传成功后的逻辑
handleUploadSuccess(res, info.file as File);
info.onSuccess!(res); info.onSuccess!(res);
message.success($t('ui.upload.uploadSuccess')); message.success($t('ui.upload.uploadSuccess'));
// 更新文件
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('change', value);
} catch (error: any) { } catch (error: any) {
console.error(error); console.error(error);
info.onError!(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() { function getValue() {
const list = (fileList.value || []) const list = (fileList.value || [])
.filter((item) => item?.status === UploadResultStatus.DONE) .filter((item) => item?.status === UploadResultStatus.DONE)
@@ -186,11 +255,26 @@ function getValue() {
} }
return item?.url || item?.response?.url || item?.response; return item?.url || item?.response?.url || item?.response;
}); });
// add by 芋艿:【特殊】单个文件的情况,获取首个元素,保证返回的是 String 类型
// 单个文件的情况,根据输入参数类型决定返回格式
if (props.maxNumber === 1) { 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> </script>

View File

@@ -21,10 +21,12 @@ export interface FileUploadProps {
// 上传的目录 // 上传的目录
directory?: string; directory?: string;
disabled?: boolean; disabled?: boolean;
drag?: boolean; // 是否支持拖拽上传
helpText?: string; helpText?: string;
listType?: UploadListType; listType?: UploadListType;
// 最大数量的文件Infinity不限制 // 最大数量的文件Infinity不限制
maxNumber?: number; maxNumber?: number;
modelValue?: string | string[]; // v-model 支持
// 文件最大多少MB // 文件最大多少MB
maxSize?: number; 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 PlusOutlined = createIconifyIcon('ant-design:plus-outlined');
export const SelectOutlined = createIconifyIcon('ant-design:select-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',
);