feat: 完成工作流开始节点开场表单
- 增加开始节点 startFormMeta/startFormSchema 配置与运行参数解析 - 统一 Admin/UserCenter 开场表单渲染与文件集合输入 - 补充开始表单校验、引用迁移和前端工具测试
This commit is contained in:
@@ -12,6 +12,10 @@ const props = defineProps({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['choose']);
|
||||
@@ -27,7 +31,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(
|
||||
@@ -56,7 +60,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>
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { ElButton, ElLink, ElMessage } from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
import ChooseResource from '#/views/ai/resource/ChooseResource.vue';
|
||||
|
||||
import {
|
||||
appendWorkflowFileValues,
|
||||
buildWorkflowFileValueFromResource,
|
||||
buildWorkflowFileValueFromUpload,
|
||||
formatWorkflowFileSize,
|
||||
normalizeWorkflowFileValues,
|
||||
validateWorkflowFileSelection,
|
||||
validateWorkflowFileValues,
|
||||
WORKFLOW_FILE_LIMITS,
|
||||
} from './workflowFileValue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [Array, Object],
|
||||
default: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const uploadLoading = ref(false);
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const currentFiles = computed(() => normalizeWorkflowFileValues(props.modelValue));
|
||||
|
||||
function triggerSelectFile() {
|
||||
if (uploadLoading.value) {
|
||||
return;
|
||||
}
|
||||
fileInputRef.value?.click();
|
||||
}
|
||||
|
||||
async function handleNativeFileChange(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const files = Array.from(input.files || []);
|
||||
if (files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
uploadLoading.value = true;
|
||||
try {
|
||||
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;
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function handleChooseResource(resources: any) {
|
||||
try {
|
||||
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 removeFile(filePath: string) {
|
||||
const nextFiles = currentFiles.value.filter((item) => item.filePath !== filePath);
|
||||
emit('update:modelValue', nextFiles);
|
||||
}
|
||||
|
||||
function clearFiles() {
|
||||
emit('update:modelValue', []);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="workflow-file-input">
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
class="workflow-file-input__native"
|
||||
type="file"
|
||||
multiple
|
||||
@change="handleNativeFileChange"
|
||||
/>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="workflow-file-input__actions">
|
||||
<ElButton
|
||||
type="primary"
|
||||
plain
|
||||
:loading="uploadLoading"
|
||||
@click="triggerSelectFile"
|
||||
>
|
||||
{{ currentFiles.length > 0 ? '继续上传' : $t('button.upload') }}
|
||||
</ElButton>
|
||||
<ChooseResource attr-name="file" multiple @choose="handleChooseResource" />
|
||||
<ElButton
|
||||
v-if="currentFiles.length > 0"
|
||||
text
|
||||
type="danger"
|
||||
@click="clearFiles"
|
||||
>
|
||||
清空
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.workflow-file-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.workflow-file-input__native {
|
||||
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;
|
||||
color: var(--el-text-color-primary);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.workflow-file-input__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.workflow-file-input__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -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';
|
||||
@@ -34,9 +34,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;
|
||||
@@ -109,6 +174,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"
|
||||
@@ -120,11 +199,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>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
|
||||
import { $t } from '#/locales';
|
||||
import ChooseResource from '#/views/ai/resource/ChooseResource.vue';
|
||||
import WorkflowFileInput from '#/views/ai/workflow/components/WorkflowFileInput.vue';
|
||||
|
||||
const props = defineProps({
|
||||
parameters: {
|
||||
@@ -27,10 +28,19 @@ const props = defineProps({
|
||||
});
|
||||
const emit = defineEmits(['update:runParams']);
|
||||
function getContentType(item: any) {
|
||||
return item.contentType || 'text';
|
||||
if (item.contentType) {
|
||||
return item.contentType;
|
||||
}
|
||||
if (String(item.dataType || '').toLowerCase() === 'file') {
|
||||
return 'file';
|
||||
}
|
||||
return 'text';
|
||||
}
|
||||
function isResource(contentType: any) {
|
||||
return ['audio', 'file', 'image', 'video'].includes(contentType);
|
||||
return ['audio', 'image', 'video'].includes(contentType);
|
||||
}
|
||||
function isFileContentType(contentType: any) {
|
||||
return contentType === 'file';
|
||||
}
|
||||
function getCheckboxOptions(item: any) {
|
||||
if (item.enums) {
|
||||
@@ -43,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);
|
||||
@@ -58,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
|
||||
@@ -105,6 +138,12 @@ function choose(data: any, propName: string) {
|
||||
:placeholder="item.formPlaceholder"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="isFileContentType(getContentType(item))">
|
||||
<WorkflowFileInput
|
||||
:model-value="runParams[item.name]"
|
||||
@update:model-value="(val) => updateParam(item.name, val)"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="isResource(getContentType(item))">
|
||||
<ElInput
|
||||
:model-value="runParams[item.name]"
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* 工作流运行态单文件值。
|
||||
*/
|
||||
export interface WorkflowFileValue {
|
||||
fileName: string;
|
||||
filePath: string;
|
||||
contentType?: string;
|
||||
size?: number;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 素材对象最小字段约束。
|
||||
*/
|
||||
export interface WorkflowResourceLike {
|
||||
fileSize?: number | string;
|
||||
resourceName?: string;
|
||||
resourceUrl?: string;
|
||||
suffix?: string;
|
||||
}
|
||||
|
||||
export const WORKFLOW_FILE_LIMITS = {
|
||||
maxCount: 10,
|
||||
maxSingleSize: 5 * 1024 * 1024,
|
||||
maxTotalSize: 50 * 1024 * 1024,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 基于上传结果构建工作流文件值。
|
||||
*/
|
||||
export function buildWorkflowFileValueFromUpload(
|
||||
file: File,
|
||||
path: string,
|
||||
): WorkflowFileValue {
|
||||
const resolvedPath = String(path || '').trim();
|
||||
if (!resolvedPath) {
|
||||
throw new Error('上传结果缺少文件路径');
|
||||
}
|
||||
return {
|
||||
fileName: file.name,
|
||||
filePath: resolvedPath,
|
||||
contentType: file.type || '',
|
||||
size: file.size,
|
||||
url: resolvedPath,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于素材对象构建工作流文件值。
|
||||
*/
|
||||
export function buildWorkflowFileValueFromResource(
|
||||
resource: WorkflowResourceLike,
|
||||
): WorkflowFileValue {
|
||||
const resourceUrl = String(resource?.resourceUrl || '').trim();
|
||||
if (!resourceUrl) {
|
||||
throw new Error('素材缺少 resourceUrl');
|
||||
}
|
||||
const resourceName = String(resource?.resourceName || '').trim();
|
||||
const suffix = String(resource?.suffix || '').trim();
|
||||
const fallbackFileName = resourceUrl.split('/').pop()?.split('?')[0] || '';
|
||||
return {
|
||||
fileName:
|
||||
resourceName && suffix
|
||||
? `${resourceName}.${suffix}`
|
||||
: resourceName || fallbackFileName,
|
||||
filePath: resourceUrl,
|
||||
contentType: '',
|
||||
size: toNumber(resource?.fileSize),
|
||||
url: resourceUrl,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为有效的工作流文件值。
|
||||
*/
|
||||
export function isWorkflowFileValue(value: unknown): value is WorkflowFileValue {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return false;
|
||||
}
|
||||
const candidate = value as Partial<WorkflowFileValue>;
|
||||
return Boolean(
|
||||
String(candidate.fileName || '').trim() &&
|
||||
String(candidate.filePath || '').trim(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 归一化单文件或多文件值。
|
||||
*/
|
||||
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)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 友好格式化文件大小。
|
||||
*/
|
||||
export function formatWorkflowFileSize(size?: number): string {
|
||||
if (!size || Number.isNaN(size) || size <= 0) {
|
||||
return '-';
|
||||
}
|
||||
if (size < 1024) {
|
||||
return `${size} B`;
|
||||
}
|
||||
if (size < 1024 * 1024) {
|
||||
return `${(size / 1024).toFixed(1)} KB`;
|
||||
}
|
||||
if (size < 1024 * 1024 * 1024) {
|
||||
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
return `${(size / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
||||
}
|
||||
|
||||
function toNumber(value?: number | string) {
|
||||
if (typeof value === 'number') {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
Reference in New Issue
Block a user