diff --git a/easyflow-starter/easyflow-starter-all/src/main/resources/application.yml b/easyflow-starter/easyflow-starter-all/src/main/resources/application.yml index 13f36c9..1a2fbce 100644 --- a/easyflow-starter/easyflow-starter-all/src/main/resources/application.yml +++ b/easyflow-starter/easyflow-starter-all/src/main/resources/application.yml @@ -116,7 +116,8 @@ dromara: secret-key: easyflowadmin123 end-point: http://127.0.0.1:39000 bucket-name: easyflow - domain: http://127.0.0.1:9000/easyflow/ + # minio 对象对外访问链接 + domain: http://127.0.0.1:39000/easyflow/ base-path: attachment # 自定义节点相关配置 diff --git a/easyflow-ui-admin/app/src/components/upload/DragFileUpload.vue b/easyflow-ui-admin/app/src/components/upload/DragFileUpload.vue index 2ea0f30..d563104 100644 --- a/easyflow-ui-admin/app/src/components/upload/DragFileUpload.vue +++ b/easyflow-ui-admin/app/src/components/upload/DragFileUpload.vue @@ -7,7 +7,9 @@ import { useAppConfig } from '@easyflow/hooks'; import { useAccessStore } from '@easyflow/stores'; import { UploadFilled } from '@element-plus/icons-vue'; -import { ElIcon, ElUpload } from 'element-plus'; +import { ElIcon, ElMessage, ElUpload } from 'element-plus'; + +import { normalizeUploadError, resolveUploadPath } from '#/utils/upload-response'; const props = defineProps({ action: { @@ -20,7 +22,7 @@ const props = defineProps({ }, }); -const emit = defineEmits(['success', 'onChange']); +const emit = defineEmits(['success', 'error', 'onChange']); const accessStore = useAccessStore(); const headers = ref({ 'easyflow-token': accessStore.accessToken, @@ -32,7 +34,18 @@ const uploadRef = ref>(); // 上传成功回调 const handleSuccess: UploadProps['onSuccess'] = (response) => { - emit('success', response.data.path); + try { + emit('success', resolveUploadPath(response)); + } catch (error) { + const normalizedError = normalizeUploadError(error); + ElMessage.error(normalizedError.message); + emit('error', normalizedError); + } +}; +const handleError: UploadProps['onError'] = (error) => { + const normalizedError = normalizeUploadError(error); + ElMessage.error(normalizedError.message); + emit('error', normalizedError); }; // 文件状态变化回调 @@ -66,6 +79,7 @@ defineExpose({ :headers="headers" :action="`${apiURL}${props.action}`" :on-success="handleSuccess" + :on-error="handleError" :on-change="handleChange" multiple :style="{ display: props.visible ? 'block' : 'none' }" diff --git a/easyflow-ui-admin/app/src/components/upload/Upload.vue b/easyflow-ui-admin/app/src/components/upload/Upload.vue index f9989bc..7d8486a 100644 --- a/easyflow-ui-admin/app/src/components/upload/Upload.vue +++ b/easyflow-ui-admin/app/src/components/upload/Upload.vue @@ -6,8 +6,9 @@ import { ref } from 'vue'; import { useAppConfig } from '@easyflow/hooks'; import { useAccessStore } from '@easyflow/stores'; -import { ElButton, ElUpload } from 'element-plus'; +import { ElButton, ElMessage, ElUpload } from 'element-plus'; +import { normalizeUploadError, resolveUploadPath } from '#/utils/upload-response'; import { $t } from '#/locales'; const props = defineProps({ @@ -31,6 +32,7 @@ const props = defineProps({ const emit = defineEmits([ 'success', // 文件上传成功 + 'error', 'handleDelete', 'handlePreview', 'beforeUpload', @@ -51,7 +53,18 @@ const handleRemove: UploadProps['onRemove'] = (file, uploadFiles) => { emit('handleDelete', file, uploadFiles); }; const handleSuccess: UploadProps['onSuccess'] = (response) => { - emit('success', response.data.path); + try { + emit('success', resolveUploadPath(response)); + } catch (error) { + const normalizedError = normalizeUploadError(error); + ElMessage.error(normalizedError.message); + emit('error', normalizedError); + } +}; +const handleError: UploadProps['onError'] = (error) => { + const normalizedError = normalizeUploadError(error); + ElMessage.error(normalizedError.message); + emit('error', normalizedError); }; @@ -66,6 +79,7 @@ const handleSuccess: UploadProps['onSuccess'] = (response) => { :on-remove="handleRemove" :limit="props.limit" :on-success="handleSuccess" + :on-error="handleError" > {{ $t('button.upload') }} diff --git a/easyflow-ui-admin/app/src/components/upload/UploadAvatar.vue b/easyflow-ui-admin/app/src/components/upload/UploadAvatar.vue index 710f96a..4dd7316 100644 --- a/easyflow-ui-admin/app/src/components/upload/UploadAvatar.vue +++ b/easyflow-ui-admin/app/src/components/upload/UploadAvatar.vue @@ -10,6 +10,7 @@ import { Plus } from '@element-plus/icons-vue'; import { ElIcon, ElImage, ElMessage, ElUpload } from 'element-plus'; import { $t } from '#/locales'; +import { normalizeUploadError, resolveUploadPath } from '#/utils/upload-response'; const props = defineProps({ action: { @@ -39,7 +40,7 @@ const props = defineProps({ }, }); -const emit = defineEmits(['success', 'update:modelValue']); +const emit = defineEmits(['success', 'error', 'update:modelValue']); const accessStore = useAccessStore(); const headers = ref({ 'easyflow-token': accessStore.accessToken, @@ -48,12 +49,24 @@ const headers = ref({ const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); const localImageUrl = ref(props.modelValue); const handleAvatarSuccess: UploadProps['onSuccess'] = ( - _response, + response, uploadFile, ) => { - localImageUrl.value = URL.createObjectURL(uploadFile.raw!); - emit('success', _response.data.path); - emit('update:modelValue', _response.data.path); + try { + const imageUrl = resolveUploadPath(response); + localImageUrl.value = URL.createObjectURL(uploadFile.raw!); + emit('success', imageUrl); + emit('update:modelValue', imageUrl); + } catch (error) { + const normalizedError = normalizeUploadError(error); + ElMessage.error(normalizedError.message); + emit('error', normalizedError); + } +}; +const handleAvatarError: UploadProps['onError'] = (error) => { + const normalizedError = normalizeUploadError(error); + ElMessage.error(normalizedError.message); + emit('error', normalizedError); }; watch( @@ -98,6 +111,7 @@ const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => { :headers="headers" :show-file-list="false" :on-success="handleAvatarSuccess" + :on-error="handleAvatarError" :before-upload="beforeAvatarUpload" > ; + +export async function syncProgrammaticFieldValidation( + formRef: FormRef, + fields?: Array, +) { + await nextTick(); + + if (!formRef.value) { + return; + } + + if (!fields || fields.length === 0) { + formRef.value.clearValidate(); + return; + } + + formRef.value.clearValidate(fields); +} diff --git a/easyflow-ui-admin/app/src/utils/upload-response.ts b/easyflow-ui-admin/app/src/utils/upload-response.ts new file mode 100644 index 0000000..bd10c32 --- /dev/null +++ b/easyflow-ui-admin/app/src/utils/upload-response.ts @@ -0,0 +1,59 @@ +import { $t } from '#/locales'; + +interface UploadResponseData { + path?: string; +} + +interface UploadResponseBody { + data?: UploadResponseData; + errorCode?: number; + message?: string; +} + +function getMessageFromUnknown(error: unknown) { + if (typeof error === 'string' && error) { + return error; + } + + if (!error || typeof error !== 'object') { + return ''; + } + + const raw = error as { + message?: unknown; + response?: unknown; + }; + + if (typeof raw.message === 'string' && raw.message) { + return raw.message; + } + + if (typeof raw.response === 'string' && raw.response) { + try { + const parsed = JSON.parse(raw.response) as UploadResponseBody; + return parsed.message || ''; + } catch { + return raw.response; + } + } + + return ''; +} + +export function normalizeUploadError(error: unknown) { + const message = getMessageFromUnknown(error); + return new Error(message || $t('cropper.message.uploadFailed')); +} + +export function resolveUploadPath(response: UploadResponseBody) { + if (!response || response.errorCode !== 0) { + throw new Error(response?.message || $t('cropper.message.uploadFailed')); + } + + const path = response.data?.path; + if (!path) { + throw new Error($t('cropper.message.notUrl')); + } + + return path; +} diff --git a/easyflow-ui-admin/app/src/views/ai/model/AddModelProviderModal.vue b/easyflow-ui-admin/app/src/views/ai/model/AddModelProviderModal.vue index 5798675..5191b36 100644 --- a/easyflow-ui-admin/app/src/views/ai/model/AddModelProviderModal.vue +++ b/easyflow-ui-admin/app/src/views/ai/model/AddModelProviderModal.vue @@ -9,6 +9,7 @@ import {ElForm, ElFormItem, ElIcon, ElInput, ElMessage, ElOption, ElSelect,} fro import {api} from '#/api/request'; import UploadAvatar from '#/components/upload/UploadAvatar.vue'; import {$t} from '#/locales'; +import {syncProgrammaticFieldValidation} from '#/utils/form-validation'; import ModelProviderBadge from '#/views/ai/model/ModelProviderBadge.vue'; import {getProviderPresetByValue, providerPresets,} from '#/views/ai/model/modelUtils/defaultIcon'; @@ -132,6 +133,14 @@ const applyPreset = (value: string) => { formData.chatPath = preset.options.chatPath || ''; formData.embedPath = preset.options.embedPath || ''; formData.rerankPath = preset.options.rerankPath || ''; + syncProgrammaticFieldValidation(formDataRef, [ + 'providerType', + 'providerName', + 'endpoint', + 'chatPath', + 'embedPath', + 'rerankPath', + ]); }; const save = async () => { diff --git a/easyflow-ui-admin/app/src/views/ai/resource/ResourceModal.vue b/easyflow-ui-admin/app/src/views/ai/resource/ResourceModal.vue index eb2744c..448f5d5 100644 --- a/easyflow-ui-admin/app/src/views/ai/resource/ResourceModal.vue +++ b/easyflow-ui-admin/app/src/views/ai/resource/ResourceModal.vue @@ -12,6 +12,7 @@ import { api } from '#/api/request'; import DictSelect from '#/components/dict/DictSelect.vue'; import Upload from '#/components/upload/Upload.vue'; import { $t } from '#/locales'; +import { syncProgrammaticFieldValidation } from '#/utils/form-validation'; const emit = defineEmits(['reload']); // vue @@ -20,50 +21,50 @@ defineExpose({ openDialog, }); const saveForm = ref(); +const createDefaultEntity = () => ({ + categoryId: '', + deptId: '', + fileSize: '', + options: '', + origin: '', + resourceName: '', + resourceType: '', + resourceUrl: '', + status: 0, + suffix: '', +}); // variables const dialogVisible = ref(false); const isAdd = ref(true); -const entity = ref({ - deptId: '', - resourceType: '', - resourceName: '', - suffix: '', - resourceUrl: '', - origin: '', - status: '', - options: '', - fileSize: '', -}); +const entity = ref(createDefaultEntity()); const btnLoading = ref(false); const rules = ref({ - deptId: [ - { required: true, message: $t('message.required'), trigger: 'blur' }, - ], resourceType: [ - { required: true, message: $t('message.required'), trigger: 'blur' }, + { required: true, message: $t('message.required'), trigger: 'change' }, ], resourceName: [ - { required: true, message: $t('message.required'), trigger: 'blur' }, - ], - suffix: [ - { required: true, message: $t('message.required'), trigger: 'blur' }, + { + required: true, + message: $t('message.required'), + trigger: ['blur', 'change'], + }, ], resourceUrl: [ - { required: true, message: $t('message.required'), trigger: 'blur' }, + { required: true, message: $t('message.required'), trigger: 'change' }, ], origin: [ - { required: true, message: $t('message.required'), trigger: 'blur' }, - ], - status: [ - { required: true, message: $t('message.required'), trigger: 'blur' }, + { required: true, message: $t('message.required'), trigger: 'change' }, ], }); + // functions function openDialog(row: any) { - if (row.id) { - isAdd.value = false; - } - entity.value = row; + isAdd.value = !row?.id; + entity.value = { + ...createDefaultEntity(), + ...row, + status: row?.status ?? 0, + }; dialogVisible.value = true; } function save() { @@ -92,20 +93,33 @@ function save() { function closeDialog() { saveForm.value?.resetFields(); isAdd.value = true; - entity.value = {}; + entity.value = createDefaultEntity(); dialogVisible.value = false; } function beforeUpload(f: any) { - const fName = f?.name?.split('.')[0]; - const fExt = f?.name?.split('.')[1]; + const fileName = f?.name || ''; + const fileNameParts = fileName.split('.'); + const fExt = fileNameParts.length > 1 ? fileNameParts.at(-1) || '' : ''; + const fName = + fileNameParts.length > 1 ? fileNameParts.slice(0, -1).join('.') : fileName; entity.value.resourceType = getResourceType(fExt); entity.value.resourceName = fName; entity.value.suffix = fExt; entity.value.fileSize = f.size; entity.value.origin = 0; + syncProgrammaticFieldValidation(saveForm, [ + 'origin', + 'resourceName', + 'resourceType', + ]); } function uploadSuccess(res: any) { entity.value.resourceUrl = res; + syncProgrammaticFieldValidation(saveForm, ['resourceUrl']); +} +function uploadError() { + entity.value.resourceUrl = ''; + syncProgrammaticFieldValidation(saveForm, ['resourceUrl']); } @@ -129,7 +143,11 @@ function uploadSuccess(res: any) { class="easyflow-modal-form easyflow-modal-form--compact" > - + diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/WorkflowModal.vue b/easyflow-ui-admin/app/src/views/ai/workflow/WorkflowModal.vue index 3db1b3c..e039b86 100644 --- a/easyflow-ui-admin/app/src/views/ai/workflow/WorkflowModal.vue +++ b/easyflow-ui-admin/app/src/views/ai/workflow/WorkflowModal.vue @@ -12,6 +12,7 @@ import DictSelect from '#/components/dict/DictSelect.vue'; // import Cropper from '#/components/upload/Cropper.vue'; import UploadAvatar from '#/components/upload/UploadAvatar.vue'; import { $t } from '#/locales'; +import { syncProgrammaticFieldValidation } from '#/utils/form-validation'; const emit = defineEmits(['reload']); // vue @@ -66,18 +67,18 @@ function openDialog(row: any, importMode = false) { const beforeUpload: UploadProps['beforeUpload'] = (file) => { jsonFile.value = file; uploadFileList.value = [file]; - saveForm.value?.clearValidate('jsonFile'); + syncProgrammaticFieldValidation(saveForm, ['jsonFile']); return false; }; const handleChange: UploadProps['onChange'] = (file, fileList) => { jsonFile.value = file.raw; uploadFileList.value = fileList.slice(-1); - saveForm.value?.clearValidate('jsonFile'); + syncProgrammaticFieldValidation(saveForm, ['jsonFile']); }; const handleRemove: UploadProps['onRemove'] = () => { jsonFile.value = null; uploadFileList.value = []; - saveForm.value?.clearValidate('jsonFile'); + syncProgrammaticFieldValidation(saveForm, ['jsonFile']); }; function save() { saveForm.value?.validate((valid) => {