feat: 完成工作流开始节点开场表单
- 增加开始节点 startFormMeta/startFormSchema 配置与运行参数解析 - 统一 Admin/UserCenter 开场表单渲染与文件集合输入 - 补充开始表单校验、引用迁移和前端工具测试
This commit is contained in:
@@ -14,6 +14,10 @@ const props = defineProps({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['choose']);
|
||||
@@ -29,7 +33,7 @@ function closeDialog() {
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
function confirm() {
|
||||
emit('choose', currentChoose.value, props.attrName);
|
||||
emit('choose', props.multiple ? chooseResources.value : currentChoose.value, props.attrName);
|
||||
closeDialog();
|
||||
}
|
||||
watch(
|
||||
@@ -55,7 +59,11 @@ watch(
|
||||
:page-sizes="[8, 12, 16, 20]"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<ResourceCardList v-model="chooseResources" :data="pageList" />
|
||||
<ResourceCardList
|
||||
v-model="chooseResources"
|
||||
:data="pageList"
|
||||
:multiple="props.multiple"
|
||||
/>
|
||||
</template>
|
||||
</PageData>
|
||||
<template #footer>
|
||||
|
||||
@@ -376,7 +376,7 @@ async function getWorkflowInfo(workflowId: any) {
|
||||
: {};
|
||||
tinyFlowData.value = isWorkflowDataEmpty(parsedContent)
|
||||
? createInitialWorkflowData()
|
||||
: parsedContent;
|
||||
: normalizeWorkflowStartNodes(parsedContent);
|
||||
syncNavTitle(workflowInfo.value?.title || '');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,15 +8,19 @@ import { $t } from '#/locales';
|
||||
import ChooseResource from '#/views/ai/resource/ChooseResource.vue';
|
||||
|
||||
import {
|
||||
appendWorkflowFileValues,
|
||||
buildWorkflowFileValueFromResource,
|
||||
buildWorkflowFileValueFromUpload,
|
||||
formatWorkflowFileSize,
|
||||
isWorkflowFileValue,
|
||||
normalizeWorkflowFileValues,
|
||||
validateWorkflowFileSelection,
|
||||
validateWorkflowFileValues,
|
||||
WORKFLOW_FILE_LIMITS,
|
||||
} from './workflowFileValue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object as () => Record<string, any> | undefined,
|
||||
type: [Array, Object],
|
||||
default: undefined,
|
||||
},
|
||||
});
|
||||
@@ -26,9 +30,7 @@ const emit = defineEmits(['update:modelValue']);
|
||||
const uploadLoading = ref(false);
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const currentFile = computed(() =>
|
||||
isWorkflowFileValue(props.modelValue) ? props.modelValue : undefined,
|
||||
);
|
||||
const currentFiles = computed(() => normalizeWorkflowFileValues(props.modelValue));
|
||||
|
||||
function triggerSelectFile() {
|
||||
if (uploadLoading.value) {
|
||||
@@ -39,17 +41,24 @@ function triggerSelectFile() {
|
||||
|
||||
async function handleNativeFileChange(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
if (!file) {
|
||||
const files = Array.from(input.files || []);
|
||||
if (files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
uploadLoading.value = true;
|
||||
try {
|
||||
const res = await api.upload('/api/v1/commons/upload', { file }, {});
|
||||
const fileValue = buildWorkflowFileValueFromUpload(file, res?.data?.path);
|
||||
emit('update:modelValue', fileValue);
|
||||
} catch (error) {
|
||||
ElMessage.error('文件上传失败');
|
||||
validateWorkflowFileSelection(currentFiles.value, files);
|
||||
const uploadedFiles = [];
|
||||
for (const file of files) {
|
||||
const res = await api.upload('/api/v1/commons/upload', { file }, {});
|
||||
uploadedFiles.push(buildWorkflowFileValueFromUpload(file, res?.data?.path));
|
||||
}
|
||||
const nextFiles = appendWorkflowFileValues(currentFiles.value, uploadedFiles);
|
||||
validateWorkflowFileValues(nextFiles);
|
||||
emit('update:modelValue', nextFiles);
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '文件上传失败');
|
||||
console.error('工作流文件上传失败', error);
|
||||
} finally {
|
||||
uploadLoading.value = false;
|
||||
@@ -57,17 +66,27 @@ async function handleNativeFileChange(event: Event) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleChooseResource(resource: any) {
|
||||
function handleChooseResource(resources: any) {
|
||||
try {
|
||||
const fileValue = buildWorkflowFileValueFromResource(resource || {});
|
||||
emit('update:modelValue', fileValue);
|
||||
const resourceList = Array.isArray(resources) ? resources : [resources];
|
||||
const fileValues = resourceList
|
||||
.map((resource) => buildWorkflowFileValueFromResource(resource || {}))
|
||||
.filter(Boolean);
|
||||
const nextFiles = appendWorkflowFileValues(currentFiles.value, fileValues);
|
||||
validateWorkflowFileValues(nextFiles);
|
||||
emit('update:modelValue', nextFiles);
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '素材文件选择失败');
|
||||
}
|
||||
}
|
||||
|
||||
function clearFile() {
|
||||
emit('update:modelValue', undefined);
|
||||
function removeFile(filePath: string) {
|
||||
const nextFiles = currentFiles.value.filter((item) => item.filePath !== filePath);
|
||||
emit('update:modelValue', nextFiles);
|
||||
}
|
||||
|
||||
function clearFiles() {
|
||||
emit('update:modelValue', []);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -77,23 +96,41 @@ function clearFile() {
|
||||
ref="fileInputRef"
|
||||
class="workflow-file-input__native"
|
||||
type="file"
|
||||
multiple
|
||||
@change="handleNativeFileChange"
|
||||
/>
|
||||
|
||||
<div v-if="currentFile" class="workflow-file-input__summary">
|
||||
<div class="workflow-file-input__name">
|
||||
{{ currentFile.fileName }}
|
||||
</div>
|
||||
<div class="workflow-file-input__meta">
|
||||
<span>{{ formatWorkflowFileSize(currentFile.size) }}</span>
|
||||
<ElLink
|
||||
v-if="currentFile.url || currentFile.filePath"
|
||||
:href="currentFile.url || currentFile.filePath"
|
||||
target="_blank"
|
||||
type="primary"
|
||||
>
|
||||
{{ $t('button.view') }}
|
||||
</ElLink>
|
||||
<div class="workflow-file-input__hint">
|
||||
最多 {{ WORKFLOW_FILE_LIMITS.maxCount }} 个文件,单个不超过
|
||||
{{ formatWorkflowFileSize(WORKFLOW_FILE_LIMITS.maxSingleSize) }},总计不超过
|
||||
{{ formatWorkflowFileSize(WORKFLOW_FILE_LIMITS.maxTotalSize) }}
|
||||
</div>
|
||||
|
||||
<div v-if="currentFiles.length > 0" class="workflow-file-input__list">
|
||||
<div
|
||||
v-for="item in currentFiles"
|
||||
:key="item.filePath"
|
||||
class="workflow-file-input__summary"
|
||||
>
|
||||
<div class="workflow-file-input__content">
|
||||
<div class="workflow-file-input__name">
|
||||
{{ item.fileName }}
|
||||
</div>
|
||||
<div class="workflow-file-input__meta">
|
||||
<span>{{ formatWorkflowFileSize(item.size) }}</span>
|
||||
<ElLink
|
||||
v-if="item.url || item.filePath"
|
||||
:href="item.url || item.filePath"
|
||||
target="_blank"
|
||||
type="primary"
|
||||
>
|
||||
{{ $t('button.view') }}
|
||||
</ElLink>
|
||||
</div>
|
||||
</div>
|
||||
<ElButton text type="danger" @click="removeFile(item.filePath)">
|
||||
{{ $t('button.delete') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -104,16 +141,16 @@ function clearFile() {
|
||||
:loading="uploadLoading"
|
||||
@click="triggerSelectFile"
|
||||
>
|
||||
{{ currentFile ? $t('button.replace') : $t('button.upload') }}
|
||||
{{ currentFiles.length > 0 ? '继续上传' : $t('button.upload') }}
|
||||
</ElButton>
|
||||
<ChooseResource attr-name="file" @choose="handleChooseResource" />
|
||||
<ChooseResource attr-name="file" multiple @choose="handleChooseResource" />
|
||||
<ElButton
|
||||
v-if="currentFile"
|
||||
v-if="currentFiles.length > 0"
|
||||
text
|
||||
type="danger"
|
||||
@click="clearFile"
|
||||
@click="clearFiles"
|
||||
>
|
||||
{{ $t('button.delete') }}
|
||||
清空
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -130,13 +167,34 @@ function clearFile() {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.workflow-file-input__hint {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.workflow-file-input__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.workflow-file-input__summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 10px;
|
||||
background: var(--el-fill-color-blank);
|
||||
}
|
||||
|
||||
.workflow-file-input__content {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.workflow-file-input__name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { computed, onUnmounted, ref } from 'vue';
|
||||
import { computed, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
import { Position } from '@element-plus/icons-vue';
|
||||
import { ElButton, ElForm, ElFormItem } from 'element-plus';
|
||||
@@ -35,9 +35,74 @@ defineExpose({
|
||||
const runForm = ref<FormInstance>();
|
||||
const runParams = ref<any>({});
|
||||
const submitLoading = ref(false);
|
||||
const parameters = computed(() => {
|
||||
return props.workflowParams.parameters;
|
||||
const startFormMeta = computed(() => {
|
||||
const meta = props.workflowParams?.startFormMeta || {};
|
||||
return {
|
||||
title: String(meta.title || '').trim() || props.workflowParams?.title || '',
|
||||
description:
|
||||
String(meta.description || '').trim() ||
|
||||
props.workflowParams?.description ||
|
||||
'',
|
||||
submitText: String(meta.submitText || '').trim() || $t('button.run'),
|
||||
};
|
||||
});
|
||||
const parameters = computed(() => {
|
||||
const schema = Array.isArray(props.workflowParams?.startFormSchema)
|
||||
? props.workflowParams.startFormSchema
|
||||
: [];
|
||||
if (schema.length === 0) {
|
||||
return props.workflowParams.parameters || [];
|
||||
}
|
||||
return schema.map((field: any) => {
|
||||
const type = String(field.type || '').trim() || 'text';
|
||||
return {
|
||||
name: field.key,
|
||||
formLabel: field.label || field.key,
|
||||
formDescription: field.description || '',
|
||||
formPlaceholder: field.placeholder || '',
|
||||
required: Boolean(field.required),
|
||||
defaultValue: field.defaultValue,
|
||||
enums: Array.isArray(field.options) ? field.options : [],
|
||||
contentType: type === 'file' ? 'file' : 'text',
|
||||
formType: type === 'text' ? 'input' : type === 'file' ? 'input' : type,
|
||||
dataType:
|
||||
type === 'checkbox' ? 'Array' : type === 'file' ? 'File' : 'String',
|
||||
};
|
||||
});
|
||||
});
|
||||
watch(
|
||||
parameters,
|
||||
(items) => {
|
||||
const nextRunParams = { ...runParams.value };
|
||||
let changed = false;
|
||||
for (const item of items || []) {
|
||||
if (nextRunParams[item.name] !== undefined) {
|
||||
continue;
|
||||
}
|
||||
if (item.defaultValue !== undefined && item.defaultValue !== '') {
|
||||
nextRunParams[item.name] = item.defaultValue;
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
if (item.formType === 'checkbox') {
|
||||
nextRunParams[item.name] = [];
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
if (item.contentType === 'file') {
|
||||
nextRunParams[item.name] = [];
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
runParams.value = nextRunParams;
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
const executeId = ref('');
|
||||
function resume(data: any) {
|
||||
data.executeId = executeId.value;
|
||||
@@ -110,6 +175,20 @@ onUnmounted(() => {
|
||||
<template>
|
||||
<div>
|
||||
<ElForm label-position="top" ref="runForm" :model="runParams">
|
||||
<div
|
||||
v-if="startFormMeta.title || startFormMeta.description"
|
||||
class="workflow-form__header"
|
||||
>
|
||||
<div v-if="startFormMeta.title" class="workflow-form__title">
|
||||
{{ startFormMeta.title }}
|
||||
</div>
|
||||
<div
|
||||
v-if="startFormMeta.description"
|
||||
class="workflow-form__description"
|
||||
>
|
||||
{{ startFormMeta.description }}
|
||||
</div>
|
||||
</div>
|
||||
<WorkflowFormItem
|
||||
v-model:run-params="runParams"
|
||||
:parameters="parameters"
|
||||
@@ -121,11 +200,28 @@ onUnmounted(() => {
|
||||
:loading="submitLoading"
|
||||
:icon="Position"
|
||||
>
|
||||
{{ $t('button.run') }}
|
||||
{{ startFormMeta.submitText }}
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.workflow-form__header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.workflow-form__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.workflow-form__description {
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -53,6 +53,31 @@ function getCheckboxOptions(item: any) {
|
||||
}
|
||||
return [];
|
||||
}
|
||||
function buildRules(item: any) {
|
||||
if (!item.required) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
required: true,
|
||||
validator: (_rule: any, value: any, callback: any) => {
|
||||
if (Array.isArray(value)) {
|
||||
callback(value.length > 0 ? undefined : new Error($t('message.required')));
|
||||
return;
|
||||
}
|
||||
if (value && typeof value === 'object') {
|
||||
callback(
|
||||
Object.keys(value).length > 0
|
||||
? undefined
|
||||
: new Error($t('message.required')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
callback(value ? undefined : new Error($t('message.required')));
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
function updateParam(name: string, value: any) {
|
||||
const newValue = { ...props.runParams, [name]: value };
|
||||
emit('update:runParams', newValue);
|
||||
@@ -68,9 +93,7 @@ function choose(data: any, propName: string) {
|
||||
:prop="`${propPrefix}${item.name}`"
|
||||
:key="idx"
|
||||
:label="item.formLabel || item.name"
|
||||
:rules="
|
||||
item.required ? [{ required: true, message: $t('message.required') }] : []
|
||||
"
|
||||
:rules="buildRules(item)"
|
||||
>
|
||||
<template v-if="getContentType(item) === 'text'">
|
||||
<ElInput
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
appendWorkflowFileValues,
|
||||
buildWorkflowFileValueFromResource,
|
||||
buildWorkflowFileValueFromUpload,
|
||||
formatWorkflowFileSize,
|
||||
isWorkflowFileValue,
|
||||
normalizeWorkflowFileValues,
|
||||
validateWorkflowFileSelection,
|
||||
validateWorkflowFileValues,
|
||||
} from '../workflowFileValue';
|
||||
|
||||
describe('workflowFileValue', () => {
|
||||
@@ -49,4 +53,99 @@ describe('workflowFileValue', () => {
|
||||
expect(isWorkflowFileValue({ fileName: 'demo.pdf' })).toBe(false);
|
||||
expect(formatWorkflowFileSize(2048)).toBe('2.0 KB');
|
||||
});
|
||||
|
||||
it('归一化并去重多文件值', () => {
|
||||
const result = normalizeWorkflowFileValues([
|
||||
{
|
||||
fileName: 'a.pdf',
|
||||
filePath: 'https://example.com/a.pdf',
|
||||
},
|
||||
{
|
||||
fileName: 'a-copy.pdf',
|
||||
filePath: 'https://example.com/a.pdf',
|
||||
},
|
||||
{
|
||||
fileName: 'b.pdf',
|
||||
filePath: 'https://example.com/b.pdf',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]?.fileName).toBe('a.pdf');
|
||||
expect(result[1]?.fileName).toBe('b.pdf');
|
||||
});
|
||||
|
||||
it('追加文件时按 filePath 去重', () => {
|
||||
const result = appendWorkflowFileValues(
|
||||
[
|
||||
{
|
||||
fileName: 'a.pdf',
|
||||
filePath: 'https://example.com/a.pdf',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
fileName: 'a-duplicate.pdf',
|
||||
filePath: 'https://example.com/a.pdf',
|
||||
},
|
||||
{
|
||||
fileName: 'b.pdf',
|
||||
filePath: 'https://example.com/b.pdf',
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[1]?.fileName).toBe('b.pdf');
|
||||
});
|
||||
|
||||
it('超出文件限制时抛出错误', () => {
|
||||
expect(() =>
|
||||
validateWorkflowFileValues(
|
||||
new Array(11).fill(null).map((_, index) => ({
|
||||
fileName: `file-${index}.pdf`,
|
||||
filePath: `https://example.com/${index}.pdf`,
|
||||
size: 1024,
|
||||
})),
|
||||
),
|
||||
).toThrow('最多上传 10 个文件');
|
||||
});
|
||||
|
||||
it('上传前校验文件数量限制', () => {
|
||||
const currentFiles = new Array(9).fill(null).map((_, index) => ({
|
||||
fileName: `file-${index}.pdf`,
|
||||
filePath: `https://example.com/${index}.pdf`,
|
||||
size: 1024,
|
||||
}));
|
||||
|
||||
expect(() =>
|
||||
validateWorkflowFileSelection(currentFiles, [
|
||||
new File(['a'], 'a.pdf'),
|
||||
new File(['b'], 'b.pdf'),
|
||||
]),
|
||||
).toThrow('最多上传 10 个文件');
|
||||
});
|
||||
|
||||
it('上传前校验单文件大小限制', () => {
|
||||
const oversizedFile = new File([new Uint8Array(6 * 1024 * 1024)], 'large.pdf');
|
||||
|
||||
expect(() =>
|
||||
validateWorkflowFileSelection([], [oversizedFile]),
|
||||
).toThrow('单个文件不能超过 5.0 MB');
|
||||
});
|
||||
|
||||
it('上传前校验总大小限制', () => {
|
||||
const currentFiles = [
|
||||
{
|
||||
fileName: 'existing.pdf',
|
||||
filePath: 'https://example.com/existing.pdf',
|
||||
size: 49 * 1024 * 1024,
|
||||
},
|
||||
];
|
||||
const incomingFile = new File([new Uint8Array(2 * 1024 * 1024)], 'new.pdf');
|
||||
|
||||
expect(() =>
|
||||
validateWorkflowFileSelection(currentFiles, [incomingFile]),
|
||||
).toThrow('文件总大小不能超过 50.0 MB');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,12 @@ export interface WorkflowResourceLike {
|
||||
suffix?: string;
|
||||
}
|
||||
|
||||
export const WORKFLOW_FILE_LIMITS = {
|
||||
maxCount: 10,
|
||||
maxSingleSize: 5 * 1024 * 1024,
|
||||
maxTotalSize: 50 * 1024 * 1024,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 基于上传结果构建工作流文件值。
|
||||
*/
|
||||
@@ -78,6 +84,87 @@ export function isWorkflowFileValue(value: unknown): value is WorkflowFileValue
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 归一化单文件或多文件值。
|
||||
*/
|
||||
export function normalizeWorkflowFileValues(value: unknown): WorkflowFileValue[] {
|
||||
const candidates = Array.isArray(value) ? value : [value];
|
||||
const result: WorkflowFileValue[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const candidate of candidates) {
|
||||
if (!isWorkflowFileValue(candidate)) {
|
||||
continue;
|
||||
}
|
||||
const key = String(candidate.filePath).trim();
|
||||
if (!key || seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
result.push(candidate);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加文件并按 filePath 去重。
|
||||
*/
|
||||
export function appendWorkflowFileValues(
|
||||
currentValue: unknown,
|
||||
incomingValues: WorkflowFileValue[],
|
||||
): WorkflowFileValue[] {
|
||||
return normalizeWorkflowFileValues([
|
||||
...normalizeWorkflowFileValues(currentValue),
|
||||
...incomingValues,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在上传前基于原生 File 列表校验文件限制,避免无效文件先落库。
|
||||
*/
|
||||
export function validateWorkflowFileSelection(
|
||||
currentValue: unknown,
|
||||
incomingFiles: File[],
|
||||
) {
|
||||
const currentFiles = normalizeWorkflowFileValues(currentValue);
|
||||
const totalCount = currentFiles.length + incomingFiles.length;
|
||||
if (totalCount > WORKFLOW_FILE_LIMITS.maxCount) {
|
||||
throw new Error(`最多上传 ${WORKFLOW_FILE_LIMITS.maxCount} 个文件`);
|
||||
}
|
||||
|
||||
let totalSize = currentFiles.reduce((sum, item) => sum + Number(item.size || 0), 0);
|
||||
for (const file of incomingFiles) {
|
||||
if (file.size > WORKFLOW_FILE_LIMITS.maxSingleSize) {
|
||||
throw new Error(`单个文件不能超过 ${formatWorkflowFileSize(WORKFLOW_FILE_LIMITS.maxSingleSize)}`);
|
||||
}
|
||||
totalSize += file.size;
|
||||
}
|
||||
|
||||
if (totalSize > WORKFLOW_FILE_LIMITS.maxTotalSize) {
|
||||
throw new Error(`文件总大小不能超过 ${formatWorkflowFileSize(WORKFLOW_FILE_LIMITS.maxTotalSize)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验最终文件列表是否满足工作流限制。
|
||||
*/
|
||||
export function validateWorkflowFileValues(values: WorkflowFileValue[]) {
|
||||
const normalized = normalizeWorkflowFileValues(values);
|
||||
if (normalized.length > WORKFLOW_FILE_LIMITS.maxCount) {
|
||||
throw new Error(`最多上传 ${WORKFLOW_FILE_LIMITS.maxCount} 个文件`);
|
||||
}
|
||||
let totalSize = 0;
|
||||
for (const item of normalized) {
|
||||
const size = Number(item.size || 0);
|
||||
if (size > WORKFLOW_FILE_LIMITS.maxSingleSize) {
|
||||
throw new Error(`单个文件不能超过 ${formatWorkflowFileSize(WORKFLOW_FILE_LIMITS.maxSingleSize)}`);
|
||||
}
|
||||
totalSize += size;
|
||||
}
|
||||
if (totalSize > WORKFLOW_FILE_LIMITS.maxTotalSize) {
|
||||
throw new Error(`文件总大小不能超过 ${formatWorkflowFileSize(WORKFLOW_FILE_LIMITS.maxTotalSize)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 友好格式化文件大小。
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user