feat: 更新代码

This commit is contained in:
华珑 2025-02-18 20:11:39 +08:00
parent 0516ee998e
commit 71f3e8d415
15 changed files with 383 additions and 75 deletions

View File

@ -12,6 +12,14 @@ body,
overflow: hidden;
}
/*
.ant-input-affix-wrapper-readonly,
.ant-input[readonly],
.ant-input-affix-wrapper-readonly .ant-input {
background-color: #fafafa;
cursor: default;
}
*/
@keyframes rotate {
0% {
transform: rotate(0deg)

View File

@ -2,13 +2,20 @@
import { watch, ref, onMounted } from 'vue';
import { Button } from '../../common';
import { Modal, Space } from 'ant-design-vue';
import { AsyncUploader, EditorData, PageData } from '@skyfox2000/webbase';
import { AnyData, IUrlInfo } from '@skyfox2000/fapi';
import { AsyncUploader, EditorData, gridRowUpdate, PageData } from '@skyfox2000/webbase';
import { AnyData, ApiResponse, IUrlInfo, ResStatus } from '@skyfox2000/fapi';
import UploadFileList from './uploadList.vue';
import { UploadFile } from '@skyfox2000/webbase';
import message from 'vue-m-message';
import { UploadStatus } from '@skyfox2000/webbase';
const props = defineProps<{
/**
* #### 使用模式
* - Row 数据行
* - Page 页面
*/
mode: 'Row' | 'Page';
/**
* 文件后缀限制
*/
@ -44,6 +51,20 @@ const maxCount = props.maxCount ?? 1;
const maxConcurrent = props.maxConcurrent ?? 3;
const fileList = ref<UploadFile[]>([]);
const emit = defineEmits<{
/**
* 显示预处理
*/
'before:file-list': [EditorData<any>, UploadFile[]];
/**
* 上传前预处理
*/
'before:upload': [UploadFile[]];
/**
* 上传结束处理上传后的文件
*/
'after:upload': [UploadFile[]];
}>();
watch(
() => uploaderForm.value.visible,
@ -62,36 +83,64 @@ const dialogUpload = async () => {
if (!url.api) url.api = props.pageData.api;
if (url.authorize === undefined) url.authorize = props.pageData.authorize;
emit('before:upload', fileList.value);
const uploader = new AsyncUploader(url, maxConcurrent);
uploaderForm.value.isFormLoading = true;
try {
if (fileList.value.length === 0) {
message.warning('请选择上传的文件!');
setTimeout(() => {
uploaderForm.value.isFormLoading = false;
}, 10000);
return;
}
//
await uploader.upload(
fileList.value,
(file) => {
console.log(`${file.name} 上传进度:${file.percent}% (${file.status})`);
},
(_) => {},
(files) => {
uploaderForm.value.isFormLoading = false;
console.log('所有文件上传结束:', files);
let err_count = 0;
for (const file of files) {
if (file.status === UploadStatus.Error) {
err_count++;
}
}
if (!err_count) message.success('全部文件上传成功!');
else if (err_count < files.length) message.error('上传结束,部分文件上传失败!');
else message.error('上传结束,所有文件上传失败!');
// formData
emit('after:upload', files);
dialogSave();
},
);
} catch (error) {
uploaderForm.value.isFormLoading = false;
console.error('上传错误:', error);
message.error('上传错误,请稍后再试!');
emit('after:upload', fileList.value);
}
};
const dialogSave = async () => {
switch (props.mode) {
case 'Row':
if (uploaderForm.value.formData) {
//
const result: ApiResponse<any> = await gridRowUpdate(props.pageData, uploaderForm.value.formData);
if (result.status === ResStatus.SUCCESS) {
uploaderForm.value.visible = false;
}
}
break;
case 'Page':
break;
}
};
onMounted(() => {
const list: UploadFile[] = [];
emit('before:file-list', uploaderForm.value, list);
fileList.value.push(...list);
open.value = uploaderForm.value.visible;
});
@ -106,11 +155,11 @@ const dialogClose = () => {
:wrapClassName="'modal mx-auto ' + ($attrs.width ? 'w-[' + $attrs.width + ']' : 'w-[430px]')"
@close="dialogClose"
>
<UploadFileList v-model:file-list="fileList" :max-count="maxCount" :file-ext="fileExt" />
<UploadFileList v-model:file-list="fileList" :page-data="pageData" :max-count="maxCount" :file-ext="fileExt" />
<template #footer>
<Space>
<Button @click="dialogClose">取消</Button>
<Button @click="dialogUpload" type="primary" :loading="uploaderForm.isFormSaving"> 上传文件 </Button>
<Button @click="dialogUpload" type="primary" :loading="uploaderForm.isFormSaving"> 上传文件并保存 </Button>
</Space>
</template>
</Modal>

View File

@ -1,30 +1,53 @@
<script setup lang="ts">
import { Button } from '@/components';
import { computed, ref } from 'vue';
import { computed, ref, watch } from 'vue';
import message from 'vue-m-message';
import type { UploadProps } from 'ant-design-vue';
import { Upload, Progress, Tag } from 'ant-design-vue';
import { UploadFile, UploadFileStatus } from '@skyfox2000/webbase';
import { watch } from 'vue';
import { PageData, UploadFile, UploadStatus, donwloadFromMinio, path } from '@skyfox2000/webbase';
import { IUrlInfo } from '@skyfox2000/fapi';
interface Props {
fileList: UploadFile<any>[];
placeholder?: string;
fileExt?: string[];
maxFileSize?: number;
maxCount?: number;
/**
* 是否自动上传
*/
autoUpload?: boolean;
pageData: PageData<any>;
/**
* 文件列表
*/
fileList: UploadFile<any>[];
/**
* 提示文字
*/
placeholder?: string;
/**
* 文件后缀列表
*/
fileExt?: string[];
/**
* 最大文件大小
*/
maxFileSize?: number;
/**
* 最大数量
*/
maxCount?: number;
/**
* 文件路径
*/
parentPath?: string;
}
const props = withDefaults(defineProps<Props>(), {
autoUpload: false,
fileList: () => [],
placeholder: '',
maxFileSize: 20,
maxCount: 5,
autoUpload: false,
});
const fileList = ref<UploadFile[]>([]);
const fileList = ref<UploadFile[]>(props.fileList);
const fileUploader = ref();
const emit = defineEmits(['update:file-list']);
@ -44,7 +67,7 @@ const beforeUpload: UploadProps['beforeUpload'] = (file) => {
return false;
}
if (fileList.value.length >= props.maxCount) {
if (props.maxCount > 1 && fileList.value.length >= props.maxCount) {
message.error(`最多上传 ${props.maxCount} 个文件`);
return false;
}
@ -52,10 +75,18 @@ const beforeUpload: UploadProps['beforeUpload'] = (file) => {
return props.autoUpload;
};
const updateFileList: UploadProps['onUpdate:fileList'] = (fileList) => {
fileList.forEach((file) => {
if (!file.fileName) file.fileName = file.name;
if (props.parentPath) file.name = path.join('/', props.parentPath, file.fileName);
});
};
const uploadProps = computed<UploadProps>(() => ({
accept: acceptString.value,
multiple: true,
fileList: fileList.value as UploadProps['fileList'],
'onUpdate:fileList': updateFileList,
beforeUpload: beforeUpload,
listType: 'text',
maxCount: props.maxCount,
@ -75,6 +106,36 @@ watch(
{ deep: true },
);
const downloadFile = (index: number) => {
const fileInfo = fileList.value[index].minioFile!;
const url: IUrlInfo = {
api: props.pageData.api,
authorize: props.pageData.authorize,
url: props.pageData.urls.download!.url,
params: {
Query: {
FileKey: fileInfo.Key,
},
},
};
donwloadFromMinio(url);
};
const onlineOrOffline = (file: UploadFile) => {
file.status = file.status === UploadStatus.Offline ? UploadStatus.Online : UploadStatus.Offline;
file.minioFile!.Status = file.status;
};
// const previewFile = (index: number) => {
// const fileInfo = fileList.value[index].minioFile;
// console.log(fileInfo);
// };
const removeFile = (index: number) => {
fileList.value.splice(index, 1);
};
const getPlaceholder = (): string => {
const typeMsg = props.fileExt && props.fileExt.length ? `文件必须为 ${props.fileExt.join('/')}` : '';
const sizeMsg = props.maxFileSize !== 0 ? `单文件最大 ${props.maxFileSize}MB` : '';
@ -83,30 +144,34 @@ const getPlaceholder = (): string => {
return [sizeMsg, typeMsg, countMsg].filter(Boolean).join('');
};
const removeFile = (index: number) => {
fileList.value.splice(index, 1);
};
const getStatusColor = (status?: UploadFileStatus) => {
const getStatusColor = (status?: UploadStatus) => {
switch (status) {
case UploadFileStatus.Uploading:
case UploadStatus.Uploading:
return 'blue';
case UploadFileStatus.Success:
case UploadStatus.Success:
return 'green';
case UploadFileStatus.Error:
case UploadStatus.Error:
return 'red';
case UploadStatus.Online:
return 'green';
case UploadStatus.Offline:
return 'pink';
default:
return 'cyan';
}
};
const getStatus = (status?: UploadFileStatus) => {
const getStatus = (status?: UploadStatus) => {
switch (status) {
case UploadFileStatus.Uploading:
case UploadStatus.Uploading:
return '上传中';
case UploadFileStatus.Success:
case UploadStatus.Success:
return '已完成';
case UploadFileStatus.Error:
case UploadStatus.Error:
return '上传失败';
case UploadStatus.Online:
return '在线';
case UploadStatus.Offline:
return '已下线';
default:
return '待上传';
}
@ -129,20 +194,36 @@ const getStatus = (status?: UploadFileStatus) => {
<div v-for="(file, index) in fileList" :key="index" class="mb-2 pb-1">
<div class="flex items-center justify-between">
<div class="flex items-center">
<span class="text-gray-700 mr-2">{{ file.name }}</span>
<span
class="text-gray-700 mr-2"
:class="[file.status == UploadStatus.Offline ? 'line-through' : '']"
>{{ file.fileName ?? file.name }}</span
>
<span>
<Tag :color="getStatusColor(file.status)">{{ getStatus(file.status) }}</Tag>
</span>
</div>
<div class="flex items-center">
<!-- <span class="text-blue-500 hover:text-blue-700 mr-4">预览</span> -->
<span
class="text-blue-500 hover:text-blue-700 mr-4 cursor-pointer"
v-if="file.status == UploadStatus.Offline || file.status == UploadStatus.Online"
@click="onlineOrOffline(file)"
>{{ file.status == UploadStatus.Offline ? '上线' : '下线' }}</span
>
<span
v-if="file.status == UploadStatus.Offline || file.status == UploadStatus.Online"
class="text-blue-500 hover:text-blue-700 mr-4 cursor-pointer"
@click="downloadFile(index)"
>下载</span
>
<!-- <span class="text-blue-500 hover:text-blue-700 mr-4" @click="previewFile(index)">预览</span> -->
<span class="text-red-500 hover:text-red-700 cursor-pointer" @click="removeFile(index)">删除</span>
</div>
</div>
<!-- 上传进度条 -->
<div>
<Progress :percent="file.percent" :stroke-width="2" :show-info="false" style="height: 2px" />
<Progress :percent="file.percent" :stroke-width="2" :show-info="false" style="height: 2px"></Progress>
</div>
</div>
</div>

View File

@ -86,6 +86,7 @@ const disabled = (item: ButtonTool) => {
<Space>
<template v-for="item in Buttons" :key="item.key">
<Popconfirm
v-if="getToolVisible(item, props.record)"
:disabled="!item.confirm"
cancelText="否"
okText="是"
@ -98,7 +99,7 @@ const disabled = (item: ButtonTool) => {
:key="item.key"
:type="item.type ?? 'text'"
:danger="item.danger"
v-if="getToolVisible(item)"
v-if="getToolVisible(item, props.record)"
:disabled="disabled(item)"
@click="onToolClicked(item, pageData, props.record)"
size="small"
@ -118,7 +119,7 @@ const disabled = (item: ButtonTool) => {
<Menu>
<template v-for="item in Menus" :key="item.key">
<MenuItem
v-if="getToolVisible(item)"
v-if="getToolVisible(item, props.record)"
:disabled="disabled(item)"
@click="onToolClicked(item, pageData, props.record)"
>

View File

@ -45,7 +45,7 @@ const onClear = () => {
</script>
<template>
<Input
:class="errClass === 'error' ? ['error', '!border-red-300', 'shadow-[0_0_3px_0px_#ff4d4f]'] : ''"
:class="[errClass === 'error' ? 'error !border-red-300 shadow-[0_0_3px_0px_#ff4d4f]' : '']"
v-model:value="innerValue"
@change="onClear"
:allow-clear="true"

12
src/types/global.d.ts vendored
View File

@ -53,6 +53,10 @@ declare global {
* ID
*/
Id: string | null;
/**
*
*/
OrderNo: string | null;
/**
*
*/
@ -61,6 +65,10 @@ declare global {
*
*/
MemberName: string | null;
/**
*
*/
Mobile: string | null;
/**
* ID
*/
@ -84,11 +92,11 @@ declare global {
/**
*
*/
ExpressInfo?: { [key: string]: any };
ExpressInfo?: Record;
/**
*
*/
ReportFileUrl?: string | null;
ReportFile?: MinioFile[];
/**
*
*/

View File

@ -24,6 +24,12 @@ export const SamplingHelperUrl: ApiUrls = {
save: {
url: '/api/RCSamplingHelperSrv/save',
},
/**
*
*/
update: {
url: '/api/RCSamplingHelperSrv/update',
},
/**
*
*/

View File

@ -10,14 +10,20 @@ const formData = ref<OrderEntity>(editorData.formData);
<template>
<Drawer title="订单信息" :page-data="pageData">
<Form>
<FormItem label="订单号">
<Input v-model:value="formData.OrderNo" disabled />
</FormItem>
<FormItem label="会员名">
<Input v-model:value="formData.MemberName" />
<Input v-model:value="formData.MemberName" disabled />
</FormItem>
<FormItem label="手机号">
<Input v-model:value="formData.Mobile" disabled />
</FormItem>
<FormItem label="产品名称">
<Input v-model:value="formData.ProductName" />
<Input v-model:value="formData.ProductName" disabled />
</FormItem>
<FormItem label="价码">
<Input v-model:value="formData.PriceCode" />
<Input v-model:value="formData.PriceCode" disabled />
</FormItem>
<FormItem label="快递号">
<Input v-model:value="formData.ExpressNumber" />

View File

@ -5,6 +5,8 @@ import UploadForm from '@/components/content/dialog/uploadForm.vue';
import Grid from './grid.vue';
import Search from './search.vue';
import { pageData, usePageInit } from './page';
import { beforeFileList, afterUpload, beforeUpload } from './upload';
usePageInit();
//
@ -18,7 +20,11 @@ const Editor = defineAsyncComponent(() => import('./editor.vue'));
<component :is="Editor" v-if="pageData.editor?.visible" />
<UploadForm
v-if="pageData.subEditor!.uploadForm.visible"
@before:file-list="beforeFileList"
@before:upload="beforeUpload"
@after:upload="afterUpload"
width="600px"
mode="Row"
:upload-form="pageData.subEditor!.uploadForm"
:max-count="1"
:page-data="pageData"

View File

@ -1,3 +1,4 @@
import { MinioFile } from '@skyfox2000/webbase';
/**
*
*/
@ -6,6 +7,10 @@ export interface OrderEntity {
* ID
*/
Id: string | null;
/**
*
*/
OrderNo: string | null;
/**
*
*/
@ -14,6 +19,10 @@ export interface OrderEntity {
*
*/
MemberName: string | null;
/**
*
*/
Mobile: string | null;
/**
* ID
*/
@ -37,11 +46,11 @@ export interface OrderEntity {
/**
*
*/
ExpressInfo?: { [key: string]: any };
ExpressInfo?: Record;
/**
*
*/
ReportFileUrl?: string | null;
ReportFile?: MinioFile[];
/**
*
*/

View File

@ -2,7 +2,15 @@
*
* API操作
*/
import { usePageFactory, ApiUrls, ValidateRule, ButtonTool, exportSelectedRows, EditorData } from '@skyfox2000/webbase';
import {
usePageFactory,
ApiUrls,
ValidateRule,
ButtonTool,
exportSelectedRows,
isEmpty,
PageData,
} from '@skyfox2000/webbase';
import { ref } from 'vue';
import message from 'vue-m-message';
@ -32,12 +40,16 @@ export const OrderUrl: ApiUrls = {
url: '/api/RCOrderSrv/remove',
},
upload: {
url: '',
header: {
'Content-Type': 'multipart/form-data',
update: {
url: '/api/RCOrderSrv/update',
},
authorize: true, // 需要授权
upload: {
url: '/api/FileSrv/upload',
},
download: {
url: '/api/FileSrv/download',
},
},
};
@ -47,15 +59,17 @@ export const OrderUrl: ApiUrls = {
*/
const defaultData: OrderEntity = {
Id: null,
OrderNo: null,
MemberId: null,
MemberName: null,
Mobile: null,
PriceId: null,
PriceCode: null,
ProductName: null,
Address: null,
ExpressNumber: null,
ExpressInfo: {},
ReportFileUrl: null,
ReportFile: [],
Status: null,
Remark: null,
Enabled: 1,
@ -98,25 +112,34 @@ const columns = ref([
{
title: '订单号',
dataIndex: 'OrderNo',
width: 120,
width: 100,
responsive: ['md'],
},
{
title: '会员名',
dataIndex: 'MemberName',
width: 120,
width: 60,
responsive: ['md'],
},
{
title: '手机号',
dataIndex: 'Mobile',
width: 120,
width: 110,
responsive: ['md'],
},
{
title: '价码',
title: '订单日期',
dataIndex: 'PriceCode',
width: 90,
customRender: ({ record }: { record: OrderEntity }) => {
return record.CreateTime!.substring(0, 10);
},
},
{
title: '产品',
dataIndex: 'ProductName',
width: 100,
ellipsis: true,
responsive: ['lg'],
},
{
@ -129,7 +152,7 @@ const columns = ref([
{
title: '订单状态',
dataIndex: 'Status',
width: 100,
width: 70,
responsive: ['md'],
},
{
@ -146,6 +169,7 @@ gridData.remotePage = false;
gridData.selectKeys = [];
gridData.selectRows = [];
gridData.selectable = true;
let download: ButtonTool = {
label: '下载',
key: 'download',
@ -159,18 +183,8 @@ let download: ButtonTool = {
}
},
};
gridData.buttons = ['New', download];
// 文件上传弹窗
let uploadForm: EditorData<OrderEntity> = {
visible: false,
formData: JSON.parse(JSON.stringify(defaultData)),
isFormSaving: false,
isFormLoading: false,
};
pageData.value.subEditor = {
uploadForm,
};
gridData.buttons = [download];
import { OrderEntity } from './model';
gridData.tools = ['Reload', 'Fullscreen'];
let upload: ButtonTool = {
@ -178,9 +192,44 @@ let upload: ButtonTool = {
key: 'upload',
type: 'default',
icon: 'icon-upload',
click: (_, row) => {
pageData.value.subEditor!.uploadForm.formData = row;
visible: (row: OrderEntity) => {
if (!row || isEmpty(row.ReportFile)) {
return true;
}
return false;
},
click: <T>(_: PageData<T>, row: T) => {
const rowData = row as OrderEntity;
pageData.value.subEditor!.uploadForm.formData = {
Id: rowData.Id,
OrderNo: rowData.OrderNo,
Mobile: rowData.Mobile,
ReportFile: rowData.ReportFile,
};
pageData.value.subEditor!.uploadForm.visible = !pageData.value.subEditor!.uploadForm.visible;
},
};
gridData.operates = [upload, 'Edit', 'Delete'];
let view: ButtonTool = {
label: '查看报告',
key: 'view',
type: 'default',
icon: 'icon-view',
visible: (row: OrderEntity) => {
if (row && !isEmpty(row.ReportFile)) {
return true;
}
return false;
},
click: <T>(_: PageData<T>, row: T) => {
const rowData = row as OrderEntity;
pageData.value.subEditor!.uploadForm.formData = {
Id: rowData.Id,
OrderNo: rowData.OrderNo,
Mobile: rowData.Mobile,
ReportFile: rowData.ReportFile,
};
pageData.value.subEditor!.uploadForm.visible = !pageData.value.subEditor!.uploadForm.visible;
},
};
gridData.operates = [upload, view, 'Edit', 'Delete'];

View File

@ -0,0 +1,67 @@
import { EditorData, isEmpty, MinioFile, path, UploadFile, UploadStatus } from '@skyfox2000/webbase';
import { OrderUrl, pageData } from './page';
import dayjs from 'dayjs';
// 文件上传弹窗
let uploadForm: EditorData<Record<string, any>> = {
visible: false,
formData: {},
isFormSaving: false,
isFormLoading: false,
saveUrl: OrderUrl.urls.update,
};
pageData.value.subEditor = {
uploadForm,
};
export const beforeFileList = (editorData: EditorData<OrderEntity>, fileList: UploadFile[]) => {
if (!isEmpty(editorData.formData?.ReportFile)) {
for (const reportFile of editorData.formData?.ReportFile!) {
const minioFile = reportFile as MinioFile;
const file: UploadFile = {
uid: minioFile.ETag,
name: minioFile.Key,
fileName: minioFile.FileName!,
lastModifiedDate: dayjs(minioFile.UpdateTime).toDate(),
status: minioFile.Status ?? UploadStatus.Online,
percent: 0,
params: {},
minioFile: minioFile,
};
minioFile.Status = file.status!;
fileList.push(file);
}
}
};
export const beforeUpload = (files: UploadFile[]) => {
files.forEach((file) => {
file.name = path.join(
'/Order/ReportFile',
uploadForm.formData.Mobile,
uploadForm.formData.OrderNo,
file.fileName!,
);
file.params = {
FileKey: file.name,
};
});
};
export const afterUpload = (files: UploadFile[]) => {
const uploadFiles: Record<string, any>[] = [];
files.forEach((file) => {
if (
(file.status == UploadStatus.Success ||
file.status == UploadStatus.Online ||
file.status == UploadStatus.Offline) &&
file.minioFile
) {
file.minioFile.FileName = file.fileName!;
uploadFiles.push(file.minioFile);
}
});
/// 为空的话,即删除所有上传的文件
uploadForm.formData!.ReportFile = uploadFiles;
};

View File

@ -24,6 +24,12 @@ export const PriceQRUrl: ApiUrls = {
save: {
url: '/api/RCPriceQRSrv/save',
},
/**
*
*/
update: {
url: '/api/RCPriceQRSrv/update',
},
/**
*
*/

View File

@ -24,6 +24,12 @@ export const AccountUrl: ApiUrls = {
save: {
url: '/api/RCAccountOpSrv/save',
},
/**
*
*/
update: {
url: '/api/RCAccountOpSrv/update',
},
/**
*
*/

View File

@ -28,6 +28,12 @@ export const DoctorUrl: ApiUrls = {
save: {
url: '/api/RCDoctorSrv/save',
},
/**
*
*/
update: {
url: '/api/RCDoctorSrv/update',
},
/**
*
*/