产品管理实现物模型文件上传和下载

This commit is contained in:
ChenYi 2025-07-29 13:45:02 +08:00
parent 15dbbc6825
commit 4d7ee72d24
3 changed files with 172 additions and 87 deletions

View File

@ -2,7 +2,7 @@
import type { VbenFormProps } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { h, ref } from 'vue';
import { h, ref, nextTick } from 'vue';
import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
@ -17,6 +17,7 @@ import {
postOneNetProductListAsync,
postOneNetProductModifyAsync,
postFilesUpload,
postFilesDownload,
} from '#/api-client';
import { TableAction } from '#/components/table-action';
import { $t } from '#/locales';
@ -26,6 +27,7 @@ import {
editProductFormSchemaEdit,
querySchema,
tableSchema,
setFileSelectedCallback,
} from './schema';
defineOptions({
@ -70,13 +72,41 @@ const gridOptions: VxeGridProps<any> = {
const [Grid, gridApi] = useVbenVxeGrid({ formOptions, gridOptions });
const editRow: Record<string, any> = ref({});
//
let selectedFile: File | null = null;
//
setFileSelectedCallback((file) => {
selectedFile = file;
});
const [UserModal, userModalApi] = useVbenModal({
draggable: true,
footer: true,
showCancelButton: true,
showConfirmButton: true,
onConfirm: submit,
onBeforeClose: () => {
//
return true;
},
onOpen: () => {
//
selectedFile = null;
},
onOpenChange: (isOpen: boolean) => {
if (isOpen && editRow.value.id) {
//
nextTick(() => {
editFormApi.setValues({ ...editRow.value });
});
}
},
onCancel: () => {
//
selectedFile = null;
//
userModalApi.close();
},
});
const [AddForm, addFormApi] = useVbenForm({
@ -124,12 +154,44 @@ async function submit() {
const formValues = await formApi.getValues();
// DeviceThingModelUrl
if (!formValues.DeviceThingModelFileId) {
//
if (!formValues.deviceThingModelFileName) {
Message.error('请选择设备模型文件');
return;
}
//
if (selectedFile) {
try {
userModalApi.setState({ loading: true, confirmLoading: true });
const result = await postFilesUpload({ body: { files: [selectedFile] } });
if (result.status === 204 || result.status === 200) {
const fileInfo = result.data?.[0];
if (fileInfo && fileInfo.id) {
formValues.deviceThingModelFileId = fileInfo.id;
//
} else {
Message.error('文件上传成功但未获取到文件ID');
userModalApi.setState({ loading: false, confirmLoading: false });
return;
}
} else {
Message.error('文件上传失败');
userModalApi.setState({ loading: false, confirmLoading: false });
return;
}
} catch (error) {
Message.error('文件上传失败');
userModalApi.setState({ loading: false, confirmLoading: false });
return;
}
}
//
selectedFile = null;
//
const fetchParams: any = isEdit
? {
id: editRow.value.id,
@ -140,7 +202,6 @@ async function submit() {
};
try {
userModalApi.setState({ loading: true, confirmLoading: true });
const resp = await api({ body: fetchParams });
if (resp.data) {
Message.success(
@ -162,12 +223,15 @@ async function submit() {
async function onEdit(record: any) {
editRow.value = record;
userModalApi.open();
editFormApi.setValues({ ...record });
//
selectedFile = null;
}
const openAddModal = async () => {
editRow.value = {};
userModalApi.open();
//
selectedFile = null;
};
//
@ -176,6 +240,8 @@ async function onDel(record: any) {
const resp = await postOneNetProductDeleteAsync({ body: { id: record.id } });
if (resp.data) {
Message.success($t('common.deleteSuccess'));
//
selectedFile = null;
gridApi.reload();
} else {
Message.error($t('common.deleteFail'));
@ -184,6 +250,32 @@ async function onDel(record: any) {
Message.error($t('common.deleteFail'));
}
}
//
async function onDownloadFile(record: any) {
if (!record.deviceThingModelFileId) {
Message.error('文件ID不存在无法下载');
return;
}
try {
const { data } = await postFilesDownload({
body: { id: record.deviceThingModelFileId },
responseType: 'blob',
});
const url = window.URL.createObjectURL(new Blob([data as Blob]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', record.deviceThingModelFileName || 'device-model-file');
document.body.append(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
Message.success('文件下载成功');
} catch (error) {
Message.error('文件下载失败');
}
}
</script>
<template>
@ -213,6 +305,17 @@ async function onDel(record: any) {
/>
</template>
<template #deviceThingModelFileName="{ row }">
<a
v-if="row.deviceThingModelFileName && row.deviceThingModelFileId"
@click="onDownloadFile(row)"
style="color: #1890ff; cursor: pointer; text-decoration: underline;"
>
{{ row.deviceThingModelFileName }}
</a>
<span v-else>{{ row.deviceThingModelFileName || '-' }}</span>
</template>
<template #action="{ row }">
<TableAction
:actions="[

View File

@ -47,6 +47,12 @@ export const tableSchema: any = computed((): VxeGridProps['columns'] => [
title: $t('abp.OneNETManagement.CommunicationAddressTLS'),
minWidth: '150',
},
{
field: 'deviceThingModelFileName',
title: $t('abp.OneNETManagement.DeviceThingModelFileName'),
minWidth: '150',
slots: { default: 'deviceThingModelFileName' },
},
{
field: 'isEnabled',
title: $t('common.isEnable'),
@ -62,6 +68,16 @@ export const tableSchema: any = computed((): VxeGridProps['columns'] => [
},
]);
// 全局变量存储选择的文件
export let selectedFile: File | null = null;
// 文件选择回调函数
let _onFileSelected: ((file: File) => void) | null = null;
export function setFileSelectedCallback(callback: (file: File) => void) {
_onFileSelected = callback;
}
export const addProductFormSchema: any = computed(() => [
{
component: 'ApiSelect',
@ -108,7 +124,7 @@ export const addProductFormSchema: any = computed(() => [
},
{
component: 'Input',
fieldName: 'IoTPlatformProductId',
fieldName: 'ioTPlatformProductId',
label: $t('abp.OneNETManagement.OneNETProductId'),
rules: z.string().min(1, {
message: `${$t('common.pleaseInput')}${$t('common.info')}${$t('abp.OneNETManagement.OneNETProductId')}`,
@ -132,7 +148,7 @@ export const addProductFormSchema: any = computed(() => [
},
{
component: 'Input',
fieldName: 'CommunicationAddressTLS',
fieldName: 'communicationAddressTLS',
label: $t('abp.OneNETManagement.CommunicationAddressTLS'),
rules: z.string().min(1, {
message: `${$t('common.pleaseInput')}${$t('common.info')}${$t('abp.OneNETManagement.CommunicationAddressTLS')}`,
@ -140,7 +156,7 @@ export const addProductFormSchema: any = computed(() => [
},
{
component: 'Input',
fieldName: 'DeviceThingModelFileId',
fieldName: 'deviceThingModelFileName',
label: $t('abp.OneNETManagement.DeviceThingModelFileName'),
componentProps: {
placeholder: '请选择文件',
@ -151,48 +167,26 @@ export const addProductFormSchema: any = computed(() => [
onClick: () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '*/*';
input.onchange = async (e: any) => {
input.accept = '.json,.xlsx,.xls';
input.onchange = (e: any) => {
const file = e.target.files[0];
if (file) {
try {
const result = await postFilesUpload({
body: {
files: [file]
}
});
if (result.status === 204 || result.status === 200) {
// 假设返回的是文件信息数组取第一个文件的ID
const fileInfo = result.data?.[0];
if (fileInfo && fileInfo.id) {
// 查找当前输入框并更新值
const currentInput = document.querySelector('input[placeholder="请选择文件"]');
// 只显示文件名,不上传
const currentInput = document.querySelector('input[placeholder="请选择文件"]') as HTMLInputElement;
if (currentInput) {
(currentInput as HTMLInputElement).value = fileInfo.id;
currentInput.value = file.name;
// 触发change事件
currentInput.dispatchEvent(new Event('input', { bubbles: true }));
currentInput.dispatchEvent(new Event('change', { bubbles: true }));
// 同时设置文件名到隐藏字段
const fileNameInput = document.querySelector('input[name="DeviceThingModelFileName"]') ||
document.querySelector('input[data-field="DeviceThingModelFileName"]');
if (fileNameInput) {
(fileNameInput as HTMLInputElement).value = file.name;
fileNameInput.dispatchEvent(new Event('input', { bubbles: true }));
fileNameInput.dispatchEvent(new Event('change', { bubbles: true }));
// 存储文件对象到全局变量,用于后续上传
selectedFile = file;
// 调用回调函数
if (_onFileSelected) {
_onFileSelected(file);
}
}
console.log('文件上传成功文件ID:', fileInfo.id, '文件名:', file.name);
} else {
console.error('文件上传成功但未获取到文件ID');
}
} else {
console.error('文件上传失败');
}
} catch (error) {
console.error('文件上传失败:', error);
}
console.log('文件已选择:', file.name, '大小:', file.size, '字节');
}
};
input.click();
@ -205,7 +199,7 @@ export const addProductFormSchema: any = computed(() => [
},
{
component: 'Input',
fieldName: 'DeviceThingModelFileName',
fieldName: 'deviceThingModelFileId',
label: '',
componentProps: {
type: 'hidden',
@ -229,6 +223,7 @@ export const editProductFormSchemaEdit: any = computed(() => [
labelField: 'accountName',
valueField: 'oneNETAccountId',
immediate: true,
disabled: true, // 编辑时禁用
afterFetch: (res: any) => {
// 如果是 Axios 响应对象,提取 data
if (res && res.data) {
@ -259,8 +254,12 @@ export const editProductFormSchemaEdit: any = computed(() => [
},
{
component: 'Input',
fieldName: 'IoTPlatformProductId',
fieldName: 'ioTPlatformProductId',
label: $t('abp.OneNETManagement.OneNETProductId'),
disabled: true,
componentProps: {
readonly: true, // 编辑时只读
},
rules: z.string().min(1, {
message: `${$t('common.pleaseInput')}${$t('common.info')}${$t('abp.OneNETManagement.OneNETProductId')}`,
}),
@ -269,6 +268,10 @@ export const editProductFormSchemaEdit: any = computed(() => [
component: 'Input',
fieldName: 'productAccesskey',
label: $t('abp.OneNETManagement.ProductAccesskey'),
disabled: true,
componentProps: {
readonly: true, // 编辑时只读
},
rules: z.string().min(1, {
message: `${$t('common.pleaseInput')}${$t('common.info')}${$t('abp.OneNETManagement.ProductAccesskey')}`,
}),
@ -283,7 +286,7 @@ export const editProductFormSchemaEdit: any = computed(() => [
},
{
component: 'Input',
fieldName: 'CommunicationAddressTLS',
fieldName: 'communicationAddressTLS',
label: $t('abp.OneNETManagement.CommunicationAddressTLS'),
rules: z.string().min(1, {
message: `${$t('common.pleaseInput')}${$t('common.info')}${$t('abp.OneNETManagement.CommunicationAddressTLS')}`,
@ -291,7 +294,7 @@ export const editProductFormSchemaEdit: any = computed(() => [
},
{
component: 'Input',
fieldName: 'DeviceThingModelFileId',
fieldName: 'deviceThingModelFileName',
label: $t('abp.OneNETManagement.DeviceThingModelFileName'),
componentProps: {
placeholder: '请选择文件',
@ -302,48 +305,26 @@ export const editProductFormSchemaEdit: any = computed(() => [
onClick: () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '*/*';
input.onchange = async (e: any) => {
input.accept = '.json,.xlsx,.xls';
input.onchange = (e: any) => {
const file = e.target.files[0];
if (file) {
try {
const result = await postFilesUpload({
body: {
files: [file]
}
});
if (result.status === 204 || result.status === 200) {
// 假设返回的是文件信息数组取第一个文件的ID
const fileInfo = result.data?.[0];
if (fileInfo && fileInfo.id) {
// 查找当前输入框并更新值
const currentInput = document.querySelector('input[placeholder="请选择文件"]');
// 只显示文件名,不上传
const currentInput = document.querySelector('input[placeholder="请选择文件"]') as HTMLInputElement;
if (currentInput) {
(currentInput as HTMLInputElement).value = fileInfo.id;
currentInput.value = file.name;
// 触发change事件
currentInput.dispatchEvent(new Event('input', { bubbles: true }));
currentInput.dispatchEvent(new Event('change', { bubbles: true }));
// 同时设置文件名到隐藏字段
const fileNameInput = document.querySelector('input[name="DeviceThingModelFileName"]') ||
document.querySelector('input[data-field="DeviceThingModelFileName"]');
if (fileNameInput) {
(fileNameInput as HTMLInputElement).value = file.name;
fileNameInput.dispatchEvent(new Event('input', { bubbles: true }));
fileNameInput.dispatchEvent(new Event('change', { bubbles: true }));
// 存储文件对象到全局变量,用于后续上传
selectedFile = file;
// 调用回调函数
if (_onFileSelected) {
_onFileSelected(file);
}
}
console.log('文件上传成功文件ID:', fileInfo.id, '文件名:', file.name);
} else {
console.error('文件上传成功但未获取到文件ID');
}
} else {
console.error('文件上传失败');
}
} catch (error) {
console.error('文件上传失败:', error);
}
console.log('文件已选择:', file.name, '大小:', file.size, '字节');
}
};
input.click();
@ -356,7 +337,7 @@ export const editProductFormSchemaEdit: any = computed(() => [
},
{
component: 'Input',
fieldName: 'DeviceThingModelFileName',
fieldName: 'deviceThingModelFileId',
label: '',
componentProps: {
type: 'hidden',

View File

@ -175,6 +175,7 @@ const handleUpload = async () => {
//
if (result.status === 204 || result.status === 200) {
message.success(`文件上传成功!共上传 ${fileList.value.length} 个文件`);
fileList.value = [];
console.log('Emitting reload event...');
emit('reload');
modalApi.close();