huangxw před 10 měsíci
rodič
revize
e3e75ac954

+ 36 - 0
src/api/cdt/orders/index.ts

@@ -36,3 +36,39 @@ export const getTestOrderDetail = (orderId?: any): any => {
         method: 'get'
     });
 };
+/**
+ * 备注订单
+ * @param query
+ * @returns {*}
+ */
+export const testOrderRemark = (data?: any): any => {
+    return request({
+        url: '/dgtmedicine/testOrder/remark',
+        method: 'post',
+        data
+    });
+};
+/**
+ * 上传检测报告
+ * @param query
+ * @returns {*}
+ */
+export const uploadTestOrderReport = (data?: any): any => {
+    return request({
+        url: '/dgtmedicine/testOrder/uploadReport',
+        method: 'post',
+        data
+    });
+};
+/**
+ * 上传发票
+ * @param query
+ * @returns {*}
+ */
+export const uploadTestOrderInvoice = (data?: any): any => {
+    return request({
+        url: '/dgtmedicine/testOrder/uploadInvoice',
+        method: 'post',
+        data
+    });
+};

+ 194 - 139
src/components/FileUpload/index.vue

@@ -1,68 +1,80 @@
 <template>
-  <div class="upload-file flex1">
-    <el-upload
-      ref="fileUploadRef"
-      multiple
-      :action="uploadFileUrl"
-      :before-upload="handleBeforeUpload"
-      :file-list="fileList"
-      :limit="limit"
-      :on-error="handleUploadError"
-      :on-exceed="handleExceed"
-      :on-success="handleUploadSuccess"
-      :show-file-list="false"
-      :headers="headers"
-      class="upload-file-uploader"
-    >
-      <!-- 上传按钮 -->
-      <el-button type="primary">选取文件</el-button>
-    </el-upload>
-    <!-- 上传提示 -->
-    <div v-if="showTip" class="el-upload__tip">
-      请上传
-      <template v-if="fileSize">
-        大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
-      </template>
-      <template v-if="fileType">
-        格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b>
-      </template>
-      的文件
-    </div>
-    <!-- 文件列表 -->
-    <transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
-      <li v-for="(file, index) in fileList" :key="file.uid" class="el-upload-list__item ele-upload-list__item-content pd-8">
-        <el-link :href="`${file.url}`" :underline="false" target="_blank">
-          <span class="el-icon-document"> {{ getFileName(file.name) }} </span>
-        </el-link>
-        <div class="ele-upload-list__item-content-action">
-          <el-button type="danger" link @click="handleDelete(index)">删除</el-button>
-        </div>
-      </li>
+    <transition-group class="upload-file-list flex1 ov-hd" :class="{ white }" name="el-fade-in-linear" tag="div">
+        <el-upload v-if="fileList.length < limit || loading" :multiple="multiple" :action="uploadFileUrl" :before-upload="handleBeforeUpload" :file-list="fileList" :limit="limit" :on-error="handleUploadError" :on-exceed="handleExceed" :on-success="handleUploadSuccess" :show-file-list="false" :headers="headers" :accept="fileType.map(item => '.'+ item).toString()" class="upload-file-uploader" ref="fileUploadRef" :on-progress="handleOnProgress">
+            <!-- 上传按钮 -->
+            <!-- <div class="btn-file f-14 c-666">上传</div> -->
+            <el-button class="h-plain mb10" type="primary" plain>
+                <el-icon><Upload /></el-icon>
+                {{ btnText }}
+            </el-button>
+        </el-upload>
+        <el-row :gutter="60">
+            <el-col :span="span" v-for="(file, index) in fileList" :key="index">
+                <div class="upload-list-img d-flex a-c" :class="{ mt10: index }">
+                    <template v-if="file.url">
+                        <a v-if="['png', 'jpg', 'jpeg', 'bmp'].includes(fileExt(file.name))" class="flex1 right-wrap ov-hd" @click="show2 = true; lookIndex = index">
+                            <el-tooltip class="box-item" effect="dark" :content="file.name" placement="top">
+                                <div class="item-text sv-1 flex1">{{ file.name }}</div>
+                            </el-tooltip>
+                        </a>
+                        <a v-else class="flex1 right-wrap ov-hd" :href="`${file.url}`" :underline="false" target="_blank">
+                            <el-tooltip class="box-item" effect="dark" :content="file.name" placement="top">
+                                <div class="item-text sv-1 flex1">{{ file.name }}</div>
+                            </el-tooltip>
+                        </a>
+                    </template>
+                    <div v-else class="flex1 right-wrap ov-hd p-rtv">
+                        <el-tooltip class="box-item" effect="dark" :content="file.name" placement="top">
+                            <div class="item-text sv-1">{{ file.name }}</div>
+                        </el-tooltip>
+                        <div class="progress" :style="{ width: file.percentage + '%' }"></div>
+                    </div>
+                    <div class="delete-item d-flex a-c j-c" @click="handleDelete(index)">
+                        <el-icon color="#FF3A3A" :size="20"><Close /></el-icon>
+                    </div>
+                </div>
+            </el-col>
+            <el-col v-if="isShowTip" :span="span">
+                <div class="c-999 f-12" style="line-height: 1.6;padding-top: 10px;">
+                    {{ tipText }}
+                </div>
+            </el-col>
+        </el-row>
     </transition-group>
-  </div>
+    <ImageViewer v-model:show="show2" :imgs="fileList.map((item: any) => item.url)" :index="lookIndex"></ImageViewer>
 </template>
 
 <script setup lang="ts">
+import { listByIds, delOss } from '@/api/system/oss';
 import { propTypes } from '@/utils/propTypes';
-import { delOss, listByIds } from '@/api/system/oss/index';
 import { globalHeaders } from '@/utils/request';
+import { fileExt } from '@/utils/ruoyi';
+import { ImageViewer } from '@/views/models';
+
 const props = defineProps({
-  modelValue: {
-    type: [String, Object, Array],
-    default: () => []
-  },
+  modelValue: [String, Object, Array],
   // 数量限制
-  limit: propTypes.number.def(5),
+  limit: propTypes.number.def(1),
   // 大小限制(MB)
   fileSize: propTypes.number.def(5),
-  // 文件类型, 例如['png', 'jpg', 'jpeg']
+  // 文件类型, 例如['png', 'jpg', 'jpeg', 'bmp']
   fileType: propTypes.array.def(['doc', 'xls', 'ppt', 'txt', 'pdf']),
   // 是否显示提示
-  isShowTip: propTypes.bool.def(true)
+  isShowTip: propTypes.bool.def(true),
+  tipText: propTypes.string.def(''),
+  span: propTypes.number.def(24),
+  isObject: propTypes.bool.def(false),
+  multiple: propTypes.bool.def(true),
+  white: propTypes.bool.def(false),
+  // 上传数据格式
+  format: propTypes.string.def('id'),
+  btnText: propTypes.string.def('上传文件')
 });
-
+const loading = ref(false);
+const lookIndex = ref(0);
+const show2 = ref(false);
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-const emit = defineEmits(['update:modelValue']);
+const emit = defineEmits(['update:modelValue', 'change']);
 const number = ref(0);
 const uploadList = ref<any[]>([]);
 
@@ -74,36 +86,35 @@ const fileList = ref<any[]>([]);
 const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));
 
 const fileUploadRef = ref<ElUploadInstance>();
-
+const progressList = ref<any>([]);
 watch(
   () => props.modelValue,
   async (val) => {
-    if (val) {
-      let temp = 1;
-      // 首先将值转为数组
-      let list: any[] = [];
-      if (Array.isArray(val)) {
-        list = val;
+      if (val) {
+          let temp = 1;
+          // 首先将值转为数组
+          let list = [];
+           if (props.format === 'array' && Array.isArray(val)) {
+              list = val;
+           } else if (props.format === 'object' || props.isObject) {
+              list = [val];
+          } else {
+              const res = await listByIds(val as string);
+              list = res.data.map((oss) => {
+                  const data = { name: oss.originalName, url: oss.url, ossId: oss.ossId };
+                  return data;
+              });
+          }
+          // 然后将数组转为对象数组
+          fileList.value = list.map((item) => {
+              item = { name: item.fileName ? item.fileName : item.name, url: item.url, fileSize: item.fileSize, fileType: item.fileType };
+              item.uid = item.uid || new Date().getTime() + temp++;
+              return item;
+          });
       } else {
-        const res = await listByIds(val);
-        list = res.data.map((oss) => {
-          return {
-            name: oss.originalName,
-            url: oss.url,
-            ossId: oss.ossId
-          };
-        });
+          fileList.value = [];
+          return [];
       }
-      // 然后将数组转为对象数组
-      fileList.value = list.map((item) => {
-        item = { name: item.name, url: item.url, ossId: item.ossId };
-        item.uid = item.uid || new Date().getTime() + temp++;
-        return item;
-      });
-    } else {
-      fileList.value = [];
-      return [];
-    }
   },
   { deep: true, immediate: true }
 );
@@ -112,28 +123,25 @@ watch(
 const handleBeforeUpload = (file: any) => {
   // 校检文件类型
   if (props.fileType.length) {
-    const fileName = file.name.split('.');
-    const fileExt = fileName[fileName.length - 1];
-    const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
-    if (!isTypeOk) {
-      proxy?.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join('/')}格式文件!`);
-      return false;
-    }
-  }
-  // 校检文件名是否包含特殊字符
-  if (file.name.includes(',')) {
-    proxy?.$modal.msgError('文件名不正确,不能包含英文逗号!');
-    return false;
+      loading.value = true;
+      const fileName = file.name.split('.');
+      const fileExt = fileName[fileName.length - 1];
+      const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
+      if (!isTypeOk) {
+          proxy?.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join('/')}格式文件!`);
+          loading.value = false;
+          return false;
+      }
   }
   // 校检文件大小
   if (props.fileSize) {
-    const isLt = file.size / 1024 / 1024 < props.fileSize;
-    if (!isLt) {
-      proxy?.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
-      return false;
-    }
+      const isLt = file.size / 1024 / 1024 < props.fileSize;
+      if (!isLt) {
+          loading.value = false;
+          proxy?.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
+          return false;
+      }
   }
-  proxy?.$modal.loading('正在上传文件,请稍候...');
   number.value++;
   return true;
 };
@@ -145,53 +153,74 @@ const handleExceed = () => {
 
 // 上传失败
 const handleUploadError = () => {
+  loading.value = false;
   proxy?.$modal.msgError('上传文件失败');
 };
 
 // 上传成功回调
 const handleUploadSuccess = (res: any, file: UploadFile) => {
+  loading.value = false;
   if (res.code === 200) {
-    uploadList.value.push({
-      name: res.data.fileName,
-      url: res.data.url,
-      ossId: res.data.ossId
-    });
-    uploadedSuccessfully();
+      uploadList.value.push({
+          name: res.data.fileName,
+          url: res.data.url,
+          ossId: res.data.ossId,
+          fileSize: file?.raw.size,
+          fileType: file?.raw.type
+      });
+      uploadedSuccessfully();
   } else {
-    number.value--;
-    proxy?.$modal.closeLoading();
-    proxy?.$modal.msgError(res.msg);
-    fileUploadRef.value?.handleRemove(file);
-    uploadedSuccessfully();
+      number.value--;
+      proxy?.$modal.msgError(res.msg);
+      fileUploadRef.value?.handleRemove(file);
+      uploadedSuccessfully();
   }
 };
 
-// 删除文件
-const handleDelete = (index: number) => {
-  let ossId = fileList.value[index].ossId;
-  delOss(ossId);
-  fileList.value.splice(index, 1);
-  emit('update:modelValue', listToString(fileList.value));
-};
-
 // 上传结束处理
 const uploadedSuccessfully = () => {
   if (number.value > 0 && uploadList.value.length === number.value) {
-    fileList.value = fileList.value.filter((f) => f.url !== undefined).concat(uploadList.value);
-    uploadList.value = [];
-    number.value = 0;
-    emit('update:modelValue', listToString(fileList.value));
-    proxy?.$modal.closeLoading();
+      fileList.value = fileList.value.filter((f) => f.url !== undefined).concat(uploadList.value);
+      uploadList.value = [];
+      number.value = 0;
+      const valuef = fileList.value.map(({ name, url, fileType, fileSize, ossId }) => ({ fileName: name, url, fileSize, fileType, ossId }));
+      if (props.format === 'id') {
+         emit('change', listToString(valuef));
+         emit('update:modelValue', listToString(valuef));
+      } else if (props.isObject || props.format === 'object') {
+          emit('change', valuef[0])
+          emit('update:modelValue', valuef[0]);
+      } else {
+          emit('change', valuef)
+          emit('update:modelValue', valuef);
+      }
   }
 };
-
+const handleOnProgress = (event: any, file: any, list: any) => {
+  const progress = Math.round((event.loaded / event.total) * 100);
+  fileList.value = [...list];
+};
+// 删除文件
+const handleDelete = (index: number) => {
+  fileUploadRef.value?.abort(fileList.value[index]);
+  fileList.value.splice(index, 1);
+  const valuef = fileList.value.map(({ name, url, fileType, fileSize }) => ({ fileName: name, url, fileSize, fileType }));
+  if (props.isObject) {
+      emit('change', null)
+      emit('update:modelValue', null);
+  } else {
+      emit('change', valuef)
+      emit('update:modelValue', valuef);
+  }
+  loading.value = false;
+};
 // 获取文件名称
 const getFileName = (name: string) => {
   // 如果是url那么取最后的名字 如果不是直接返回
   if (name.lastIndexOf('/') > -1) {
-    return name.slice(name.lastIndexOf('/') + 1);
+      return name.slice(name.lastIndexOf('/') + 1);
   } else {
-    return name;
+      return name;
   }
 };
 
@@ -200,34 +229,60 @@ const listToString = (list: any[], separator?: string) => {
   let strs = '';
   separator = separator || ',';
   list.forEach((item) => {
-    if (item.ossId) {
-      strs += item.ossId + separator;
-    }
+      if (item.ossId) {
+          strs += item.ossId + separator;
+      }
   });
   return strs != '' ? strs.substring(0, strs.length - 1) : '';
 };
 </script>
 
 <style scoped lang="scss">
-.upload-file-uploader {
-  margin-bottom: 5px;
+.btn-file {
+  width: 90px;
+  height: 40px;
+  line-height: 40px;
+  border-radius: 8px;
+  color: #666;
+  text-align: center;
+  background-color: #f7f7f7;
 }
+.upload-list-img {
+  background-color: #f7f7f7;
+  border-radius: 8px;
 
-.upload-file-list .el-upload-list__item {
-  border: 1px solid #e4e7ed;
-  line-height: 2;
-  margin-bottom: 10px;
-  position: relative;
+  &.mt10 {
+      margin-top: 10px;
+  }
 }
-
-.upload-file-list .ele-upload-list__item-content {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  color: inherit;
+.right-item {
+  padding: 10px 16px;
+  cursor: pointer;
+  &:hover {
+      color: var(--el-color-primary);
+  }
 }
-
-.ele-upload-list__item-content-action .el-link {
-  margin-right: 10px;
+.delete-item {
+  width: 40px;
+  cursor: pointer;
+}
+.upload-file-list.white {
+  .btn-file {
+      background-color: #fff;
+  }
+  .upload-list-img {
+      background-color: #fff;
+  }
+}
+.right-wrap {
+  padding: 6px 10px;
+  box-sizing: border-box;
+}
+.progress {
+  position: absolute;
+  left: 0;
+  bottom: 0;
+  height: 1px;
+  background-color: var(--el-color-primary);
 }
 </style>

+ 192 - 188
src/utils/ruoyi.ts

@@ -1,46 +1,46 @@
 // 日期格式化
 export function parseTime(time: any, pattern?: string) {
-  if (arguments.length === 0 || !time) {
-    return null;
-  }
-  const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}';
-  let date;
-  if (typeof time === 'object') {
-    date = time;
-  } else {
-    if (typeof time === 'string' && /^[0-9]+$/.test(time)) {
-      time = parseInt(time);
-    } else if (typeof time === 'string') {
-      time = time
-        .replace(new RegExp(/-/gm), '/')
-        .replace('T', ' ')
-        .replace(new RegExp(/\.[\d]{3}/gm), '');
-    }
-    if (typeof time === 'number' && time.toString().length === 10) {
-      time = time * 1000;
-    }
-    date = new Date(time);
-  }
-  const formatObj: { [key: string]: any } = {
-    y: date.getFullYear(),
-    m: date.getMonth() + 1,
-    d: date.getDate(),
-    h: date.getHours(),
-    i: date.getMinutes(),
-    s: date.getSeconds(),
-    a: date.getDay()
-  };
-  return format.replace(/{(y|m|d|h|i|s|a)+}/g, (result: string, key: string) => {
-    let value = formatObj[key];
-    // Note: getDay() returns 0 on Sunday
-    if (key === 'a') {
-      return ['日', '一', '二', '三', '四', '五', '六'][value];
-    }
-    if (result.length > 0 && value < 10) {
-      value = '0' + value;
-    }
-    return value || 0;
-  });
+    if (arguments.length === 0 || !time) {
+        return null;
+    }
+    const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}';
+    let date;
+    if (typeof time === 'object') {
+        date = time;
+    } else {
+        if (typeof time === 'string' && /^[0-9]+$/.test(time)) {
+            time = parseInt(time);
+        } else if (typeof time === 'string') {
+            time = time
+                .replace(new RegExp(/-/gm), '/')
+                .replace('T', ' ')
+                .replace(new RegExp(/\.[\d]{3}/gm), '');
+        }
+        if (typeof time === 'number' && time.toString().length === 10) {
+            time = time * 1000;
+        }
+        date = new Date(time);
+    }
+    const formatObj: { [key: string]: any } = {
+        y: date.getFullYear(),
+        m: date.getMonth() + 1,
+        d: date.getDate(),
+        h: date.getHours(),
+        i: date.getMinutes(),
+        s: date.getSeconds(),
+        a: date.getDay()
+    };
+    return format.replace(/{(y|m|d|h|i|s|a)+}/g, (result: string, key: string) => {
+        let value = formatObj[key];
+        // Note: getDay() returns 0 on Sunday
+        if (key === 'a') {
+            return ['日', '一', '二', '三', '四', '五', '六'][value];
+        }
+        if (result.length > 0 && value < 10) {
+            value = '0' + value;
+        }
+        return value || 0;
+    });
 }
 
 /**
@@ -50,102 +50,102 @@ export function parseTime(time: any, pattern?: string) {
  * @param propName
  */
 export const addDateRange = (params: any, dateRange: any[], propName?: string) => {
-  const search = params;
-  search.params = typeof search.params === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {};
-  dateRange = Array.isArray(dateRange) ? dateRange : [];
-  if (typeof propName === 'undefined') {
-    search.params['beginTime'] = dateRange[0];
-    search.params['endTime'] = dateRange[1];
-  } else {
-    search.params['begin' + propName] = dateRange[0];
-    search.params['end' + propName] = dateRange[1];
-  }
-  return search;
+    const search = params;
+    search.params = typeof search.params === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {};
+    dateRange = Array.isArray(dateRange) ? dateRange : [];
+    if (typeof propName === 'undefined') {
+        search.params['beginTime'] = dateRange[0];
+        search.params['endTime'] = dateRange[1];
+    } else {
+        search.params['begin' + propName] = dateRange[0];
+        search.params['end' + propName] = dateRange[1];
+    }
+    return search;
 };
 
 // 回显数据字典
 export const selectDictLabel = (datas: any, value: number | string) => {
-  if (value === undefined) {
-    return '';
-  }
-  const actions: Array<string | number> = [];
-  Object.keys(datas).some((key) => {
-    if (datas[key].value == '' + value) {
-      actions.push(datas[key].label);
-      return true;
-    }
-  });
-  if (actions.length === 0) {
-    actions.push(value);
-  }
-  return actions.join('');
+    if (value === undefined) {
+        return '';
+    }
+    const actions: Array<string | number> = [];
+    Object.keys(datas).some((key) => {
+        if (datas[key].value == '' + value) {
+            actions.push(datas[key].label);
+            return true;
+        }
+    });
+    if (actions.length === 0) {
+        actions.push(value);
+    }
+    return actions.join('');
 };
 
 // 回显数据字典(字符串数组)
 export const selectDictLabels = (datas: any, value: any, separator: any) => {
-  if (value === undefined || value.length === 0) {
-    return '';
-  }
-  if (Array.isArray(value)) {
-    value = value.join(',');
-  }
-  const actions: any[] = [];
-  const currentSeparator = undefined === separator ? ',' : separator;
-  const temp = value.split(currentSeparator);
-  Object.keys(value.split(currentSeparator)).some((val) => {
-    let match = false;
-    Object.keys(datas).some((key) => {
-      if (datas[key].value == '' + temp[val]) {
-        actions.push(datas[key].label + currentSeparator);
-        match = true;
-      }
-    });
-    if (!match) {
-      actions.push(temp[val] + currentSeparator);
+    if (value === undefined || value.length === 0) {
+        return '';
+    }
+    if (Array.isArray(value)) {
+        value = value.join(',');
     }
-  });
-  return actions.join('').substring(0, actions.join('').length - 1);
+    const actions: any[] = [];
+    const currentSeparator = undefined === separator ? ',' : separator;
+    const temp = value.split(currentSeparator);
+    Object.keys(value.split(currentSeparator)).some((val) => {
+        let match = false;
+        Object.keys(datas).some((key) => {
+            if (datas[key].value == '' + temp[val]) {
+                actions.push(datas[key].label + currentSeparator);
+                match = true;
+            }
+        });
+        if (!match) {
+            actions.push(temp[val] + currentSeparator);
+        }
+    });
+    return actions.join('').substring(0, actions.join('').length - 1);
 };
 
 // 字符串格式化(%s )
 export function sprintf(str: string) {
-  if (arguments.length !== 0) {
-    let flag = true,
-      i = 1;
-    str = str.replace(/%s/g, function () {
-      const arg = arguments[i++];
-      if (typeof arg === 'undefined') {
-        flag = false;
-        return '';
-      }
-      return arg;
-    });
-    return flag ? str : '';
-  }
+    if (arguments.length !== 0) {
+        let flag = true,
+            i = 1;
+        str = str.replace(/%s/g, function () {
+            const arg = arguments[i++];
+            if (typeof arg === 'undefined') {
+                flag = false;
+                return '';
+            }
+            return arg;
+        });
+        return flag ? str : '';
+    }
 }
 
 // 转换字符串,undefined,null等转化为""
 export const parseStrEmpty = (str: any) => {
-  if (!str || str == 'undefined' || str == 'null') {
-    return '';
-  }
-  return str;
+    if (!str || str == 'undefined' || str == 'null') {
+        return '';
+    }
+    return str;
 };
 
 // 数据合并
 export const mergeRecursive = (source: any, target: any) => {
-  for (const p in target) {
-    try {
-      if (target[p].constructor == Object) {
-        source[p] = mergeRecursive(source[p], target[p]);
-      } else {
-        source[p] = target[p];
-      }
-    } catch (e) {
-      source[p] = target[p];
-    }
-  }
-  return source;
+    for (const p in target) {
+        try {
+            if (target[p].constructor == Object) {
+                source[p] = mergeRecursive(source[p], target[p]);
+            } else {
+                source[p] = target[p];
+            }
+        } catch (e) {
+            source[p] = target[p];
+        }
+    }
+    return source;
 };
 
 /**
@@ -156,51 +156,51 @@ export const mergeRecursive = (source: any, target: any) => {
  * @param {*} children 孩子节点字段 默认 'children'
  */
 export const handleTree = <T>(data: any[], id?: string, parentId?: string, children?: string): T[] => {
-  const config: {
-    id: string;
-    parentId: string;
-    childrenList: string;
-  } = {
-    id: id || 'id',
-    parentId: parentId || 'parentId',
-    childrenList: children || 'children'
-  };
-
-  const childrenListMap: any = {};
-  const nodeIds: any = {};
-  const tree: T[] = [];
-
-  for (const d of data) {
-    const parentId = d[config.parentId];
-    if (childrenListMap[parentId] == null) {
-      childrenListMap[parentId] = [];
-    }
-    nodeIds[d[config.id]] = d;
-    childrenListMap[parentId].push(d);
-  }
-
-  for (const d of data) {
-    const parentId = d[config.parentId];
-    if (nodeIds[parentId] == null) {
-      tree.push(d);
-    }
-  }
-  const adaptToChildrenList = (o: any) => {
-    if (childrenListMap[o[config.id]] !== null) {
-      o[config.childrenList] = childrenListMap[o[config.id]];
-    }
-    if (o[config.childrenList]) {
-      for (const c of o[config.childrenList]) {
-        adaptToChildrenList(c);
-      }
-    }
-  };
-
-  for (const t of tree) {
-    adaptToChildrenList(t);
-  }
-
-  return tree;
+    const config: {
+        id: string;
+        parentId: string;
+        childrenList: string;
+    } = {
+        id: id || 'id',
+        parentId: parentId || 'parentId',
+        childrenList: children || 'children'
+    };
+
+    const childrenListMap: any = {};
+    const nodeIds: any = {};
+    const tree: T[] = [];
+
+    for (const d of data) {
+        const parentId = d[config.parentId];
+        if (childrenListMap[parentId] == null) {
+            childrenListMap[parentId] = [];
+        }
+        nodeIds[d[config.id]] = d;
+        childrenListMap[parentId].push(d);
+    }
+
+    for (const d of data) {
+        const parentId = d[config.parentId];
+        if (nodeIds[parentId] == null) {
+            tree.push(d);
+        }
+    }
+    const adaptToChildrenList = (o: any) => {
+        if (childrenListMap[o[config.id]] !== null) {
+            o[config.childrenList] = childrenListMap[o[config.id]];
+        }
+        if (o[config.childrenList]) {
+            for (const c of o[config.childrenList]) {
+                adaptToChildrenList(c);
+            }
+        }
+    };
+
+    for (const t of tree) {
+        adaptToChildrenList(t);
+    }
+
+    return tree;
 };
 
 /**
@@ -208,44 +208,48 @@ export const handleTree = <T>(data: any[], id?: string, parentId?: string, child
  * @param {*} params  参数
  */
 export const tansParams = (params: any) => {
-  let result = '';
-  for (const propName of Object.keys(params)) {
-    const value = params[propName];
-    const part = encodeURIComponent(propName) + '=';
-    if (value !== null && value !== '' && typeof value !== 'undefined') {
-      if (typeof value === 'object') {
-        for (const key of Object.keys(value)) {
-          if (value[key] !== null && value[key] !== '' && typeof value[key] !== 'undefined') {
-            const params = propName + '[' + key + ']';
-            const subPart = encodeURIComponent(params) + '=';
-            result += subPart + encodeURIComponent(value[key]) + '&';
-          }
+    let result = '';
+    for (const propName of Object.keys(params)) {
+        const value = params[propName];
+        const part = encodeURIComponent(propName) + '=';
+        if (value !== null && value !== '' && typeof value !== 'undefined') {
+            if (typeof value === 'object') {
+                for (const key of Object.keys(value)) {
+                    if (value[key] !== null && value[key] !== '' && typeof value[key] !== 'undefined') {
+                        const params = propName + '[' + key + ']';
+                        const subPart = encodeURIComponent(params) + '=';
+                        result += subPart + encodeURIComponent(value[key]) + '&';
+                    }
+                }
+            } else {
+                result += part + encodeURIComponent(value) + '&';
+            }
         }
-      } else {
-        result += part + encodeURIComponent(value) + '&';
-      }
     }
-  }
-  return result;
+    return result;
 };
 
 // 返回项目路径
 export const getNormalPath = (p: string): string => {
-  if (p.length === 0 || !p || p === 'undefined') {
-    return p;
-  }
-  const res = p.replace('//', '/');
-  if (res[res.length - 1] === '/') {
-    return res.slice(0, res.length - 1);
-  }
-  return res;
+    if (p.length === 0 || !p || p === 'undefined') {
+        return p;
+    }
+    const res = p.replace('//', '/');
+    if (res[res.length - 1] === '/') {
+        return res.slice(0, res.length - 1);
+    }
+    return res;
 };
 
 // 验证是否为blob格式
 export const blobValidate = (data: any) => {
-  return data.type !== 'application/json';
+    return data.type !== 'application/json';
 };
 
 export default {
-  handleTree
+    handleTree
+};
+export const fileExt = (fileName: string) => {
+    // 获取文件后缀, 从后面到第一个点取 不包括.
+    return fileName.slice(fileName.lastIndexOf('.') + 1);
 };

+ 68 - 0
src/views/cdt/models/EditOrderRemark.vue

@@ -0,0 +1,68 @@
+<template>
+    <vxe-modal v-model="dialogVisible" :title="title" show-zoom resize show-footer destroy-on-close transfer @hide="close" :width="width" :z-index="100">
+        <template #default>
+            <el-form ref="formRef" :model="form" label-width="auto">
+                <el-form-item label="订单备注:" prop="remark">
+                    <el-input v-model="form.remark" :rows="4" type="textarea" placeholder="请输入订单备注" clearable style="width: 100%" />
+                </el-form-item>
+            </el-form>
+        </template>
+        <template #footer>
+            <el-button @click="close">取消</el-button>
+            <el-button type="primary" @click="submitForm">确认添加</el-button>
+        </template>
+    </vxe-modal>
+</template>
+
+<script setup name="EditOrderRemark" lang="ts">
+import { testOrderRemark } from '@/api/cdt/orders';
+import { editValidityDate } from '@/api/dgtmedicine/member/index';
+import { propTypes } from '@/utils/propTypes';
+import { FormInstance } from 'element-plus';
+const emit = defineEmits(['update:show', 'close', 'success']);
+const props = defineProps({
+    show: propTypes.bool.def(false),
+    title: propTypes.string.def('添加备注'),
+    width: propTypes.number.def(560),
+    info: propTypes.any.def(null),
+    dict: propTypes.object.def({}),
+});
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const dialogVisible = ref(false);
+const form = ref<any>({
+    remark: ''
+});
+const formRef = ref<FormInstance>();
+const close = () => {
+    formRef.value?.resetFields();
+    emit('update:show', false);
+    emit('close', false);
+};
+const submitForm = async () => {
+    try {
+        await formRef.value?.validate();
+        const res = await testOrderRemark({ ...form.value, orderId: props.info?.id });
+        if (res) {
+            close();
+            emit('success', true);
+        }
+    } catch (error) {
+        console.error(error);
+    }
+};
+
+watch(
+    () => props.show,
+    (val) => {
+        dialogVisible.value = val;
+    },
+    { immediate: true }
+);
+watch(
+    () => props.info,
+    (val) => {
+        form.value = { remark: val?.remark };
+    },
+    { immediate: true }
+);
+</script>

+ 3 - 0
src/views/cdt/models/index.ts

@@ -2,3 +2,6 @@ export { default as TransferItems } from './transferItems.vue'; // 选择检测
 export { default as rowItems } from './rowItems.vue'; // 查看检测项目
 export { default as menuInfo } from './menuInfo.vue'; // 套餐信息
 export { default as orderInfo } from './orderInfo.vue'; // 订单信息
+export { default as EditOrderRemark } from './EditOrderRemark.vue'; // 订单信息
+export { default as uploadReportForm } from './uploadReportForm.vue'; // 上传报告
+export { default as uploadInvoiceForm } from './uploadInvoiceForm.vue'; // 上传发票

+ 62 - 0
src/views/cdt/models/uploadInvoiceForm.vue

@@ -0,0 +1,62 @@
+<template>
+    <vxe-modal v-model="dialogVisible" :title="title" show-zoom resize show-footer destroy-on-close transfer @hide="close" :width="width">
+        <template #default>
+            <el-form ref="formRef" :model="form" :rules="rules" label-width="auto" hide-required-asterisk>
+                <el-form-item label="发票:" prop="invoice">
+                    <file-upload v-model="form.invoice" :limit="1" isObject :fileType="['pdf']" :span="24" />
+                </el-form-item>
+            </el-form>
+        </template>
+        <template #footer>
+            <el-button @click="close">取消</el-button>
+            <el-button type="primary" @click="submitForm">确定</el-button>
+        </template>
+    </vxe-modal>
+</template>
+
+<script setup name="uploadInvoiceForm" lang="ts">
+import { uploadTestOrderInvoice } from '@/api/cdt/orders';
+import { propTypes } from '@/utils/propTypes';
+import { FormInstance } from 'element-plus';
+const emit = defineEmits(['update:show', 'close', 'success']);
+const props = defineProps({
+    show: propTypes.bool.def(false),
+    title: propTypes.string.def('上传检测发票'),
+    width: propTypes.number.def(560),
+    info: propTypes.any.def(null),
+});
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const dialogVisible = ref(false);
+const form = ref<any>({
+    invoice: null
+});
+const rules = reactive({
+    invoice: [{ required: true, message: '请上传发票', trigger: 'change' }],
+})
+const formRef = ref<FormInstance>();
+const close = () => {
+    formRef.value?.resetFields();
+    emit('update:show', false);
+    emit('close', false);
+};
+const submitForm = async () => {
+    try {
+        await formRef.value?.validate();
+        const res = await uploadTestOrderInvoice({ ...form.value, orderId: props.info?.id });
+        if (res) {
+            close();
+            emit('success', true);
+        }
+    } catch (error) {
+        console.error(error);
+    }
+};
+
+watch(
+    () => props.show,
+    (val) => {
+        dialogVisible.value = val;
+    },
+    { immediate: true }
+);
+</script>

+ 63 - 0
src/views/cdt/models/uploadReportForm.vue

@@ -0,0 +1,63 @@
+<template>
+    <vxe-modal v-model="dialogVisible" :title="title" show-zoom resize show-footer destroy-on-close transfer @hide="close" :width="width">
+        <template #default>
+            <el-form ref="formRef" :model="form" :rules="rules" label-width="auto" hide-required-asterisk>
+                <el-form-item label="报告:" prop="report">
+                    <file-upload v-model="form.report" :limit="1" isObject :fileType="['pdf']" :span="24" />
+                </el-form-item>
+            </el-form>
+        </template>
+        <template #footer>
+            <el-button @click="close">取消</el-button>
+            <el-button type="primary" @click="submitForm">确定</el-button>
+        </template>
+    </vxe-modal>
+</template>
+
+<script setup name="uploadReportForm" lang="ts">
+import { uploadTestOrderReport } from '@/api/cdt/orders';
+import { propTypes } from '@/utils/propTypes';
+import { FormInstance } from 'element-plus';
+const emit = defineEmits(['update:show', 'close', 'success']);
+const props = defineProps({
+    show: propTypes.bool.def(false),
+    title: propTypes.string.def('上传检测报告'),
+    width: propTypes.number.def(560),
+    info: propTypes.any.def(null),
+});
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const dialogVisible = ref(false);
+const form = ref<any>({
+    report: null
+});
+const rules = reactive({
+    report: [{ required: true, message: '请上传发票', trigger: 'change' }],
+})
+const formRef = ref<FormInstance>();
+const close = () => {
+    formRef.value?.resetFields();
+    emit('update:show', false);
+    emit('close', false);
+};
+const submitForm = async () => {
+    try {
+        await formRef.value?.validate();
+        const res = await uploadTestOrderReport({ ...form.value, orderId: props.info?.id });
+        if (res) {
+            proxy.$modal.msgSuccess('上传成功');
+            close();
+            emit('success', true);
+        }
+    } catch (error) {
+        console.error(error);
+    }
+};
+
+watch(
+    () => props.show,
+    (val) => {
+        dialogVisible.value = val;
+    },
+    { immediate: true }
+);
+</script>

+ 34 - 4
src/views/cdt/orders/index.vue

@@ -63,11 +63,19 @@
                             </template>
                         </vxe-column>
                         <vxe-column title="订单备注" field="remark" min-width="100" fixed="right" :formatter="colNoData" />
-                        <vxe-column title="操作" width="180" fixed="right">
+                        <vxe-column title="操作" width="260" fixed="right">
                             <template #default="{ row }">
-                                <el-button text class="small-btn-font" type="primary" size="small">添加备注</el-button>
+                                <el-button @click="EidtRemark(row)" text class="small-btn-font" type="primary" size="small">添加备注</el-button>
+                                <template v-if="row?.status === '4'">
+                                    <span></span>
+                                    <el-button @click="clickRowReport(row)" text class="small-btn-font" type="primary" size="small">上传报告</el-button>
+                                </template>
+                                <template v-if="row?.status === '5'">
+                                    <span></span>
+                                    <el-button @click="clickRownvoice(row)" text class="small-btn-font" type="primary" size="small">上传发票</el-button>
+                                </template>
                                 <span></span>
-                                <el-button @click="router.push({ path: 'order-detail', query: { orderId: row?.id} })" text class="small-btn-font" type="primary" size="small">查看详情</el-button>
+                                <el-button @click="router.push({ path: 'order-detail', query: { orderId: row?.id} })" style="color: #0079fe;" text class="small-btn-font" type="primary" size="small">查看详情</el-button>
                             </template>
                         </vxe-column>
                     </vxe-table>
@@ -78,13 +86,16 @@
         </div>
     </div>
     <rowItems v-if="showRowItems" v-model:show="showRowItems" :packageId="rowId"></rowItems>
+    <EditOrderRemark v-if="showRemark" v-model:show="showRemark" :info="rowInfo" @success="getList();getTabsCount()"></EditOrderRemark>
+    <uploadReportForm v-if="showReport" v-model:show="showReport" :info="rowInfo" @success="getList();getTabsCount()"></uploadReportForm>
+    <uploadInvoiceForm v-if="showInvoice" v-model:show="showInvoice" :info="rowInfo" @success="getList();getTabsCount()"></uploadInvoiceForm>
 </template>
 <script setup name="orders" lang="ts">
 import { testOrderList, testOrderListCount } from '@/api/cdt/orders';
 import { colNoData } from '@/utils/noData';
 import { DateRange, searchTabs } from '@/views/models';
 import NP from 'number-precision';
-import { rowItems } from '../models';
+import { EditOrderRemark, rowItems, uploadReportForm, uploadInvoiceForm } from '../models';
 import { debounce } from 'lodash';
 import { selectDictLabel } from '@/utils/ruoyi';
 const router = useRouter();
@@ -98,6 +109,8 @@ const queryParams = ref<any>({
 });
 const queryFormRef = ref<any>();
 const showRowItems = ref(false);
+const showReport = ref(false);
+const showInvoice = ref(false);
 const loading = ref(false);
 const total = ref(0);
 const list = ref<any>([]);
@@ -130,6 +143,23 @@ const getTabsCount = debounce(async () => {
     // this.tabsList = res.rows;
     tabs.value = res.data.map(({ status, statusCount }: any) => ({ label: selectDictLabel(test_order_status_bg.value, status), value: status, count: statusCount }));
 }, 500);
+// 添加备注
+const rowInfo = ref<any>({});
+const showRemark = ref(false);
+const EidtRemark = (row: any) => {
+    rowInfo.value = row;
+    showRemark.value = true;
+};
+// 上传报告
+const clickRowReport = (row: any) => {
+    rowInfo.value = row;
+    showReport.value = true;
+};
+// 上传发票
+const clickRownvoice = (row: any) => {
+    rowInfo.value = row;
+    showInvoice.value = true;
+};
 onMounted(() => {
     getList();
     getTabsCount()

+ 1 - 1
src/views/dgtmedicine/model/uploadInvoiceForm.vue

@@ -3,7 +3,7 @@
         <template #default>
             <el-form ref="formRef" :model="form" :rules="rules" label-width="auto" hide-required-asterisk>
                 <el-form-item label="发票:" prop="invoiceUrl">
-                    <file-upload v-model="form.invoiceUrl" :limit="1" :isString="true" :fileType="['pdf']" />
+                    <file-upload v-model="form.invoiceUrl" :span="24" :limit="1" :isString="true" :fileType="['pdf']" />
                 </el-form-item>
             </el-form>
         </template>

+ 24 - 0
src/views/models/ImageViewer.vue

@@ -0,0 +1,24 @@
+<template>
+    <el-image-viewer v-if="show" :url-list="(imgs as string[])" :initialIndex="index" @close="close" @cover-click="close" :z-index="10000" teleported></el-image-viewer>
+</template>
+
+<script setup name="CardItem" lang="ts">
+import { propTypes } from '@/utils/propTypes';
+
+const emit = defineEmits(['update:show']);
+const props = defineProps({
+    show: Boolean,
+    imgs: Array,
+    index: propTypes.number.def(0)
+});
+const close = () => {
+    emit('update:show', false);
+};
+onMounted(() => {
+    document.addEventListener('click', function (e: any) {
+        if (e.target.className == 'el-image-viewer__mask') {
+            close();
+        }
+    });
+});
+</script>

+ 1 - 0
src/views/models/index.ts

@@ -4,3 +4,4 @@ export { default as DescCol } from './DescCol.vue'; // 描述组件
 export { default as BtnRadio } from './BtnRadio.vue'; // 按钮单选
 export { default as AddBtn } from './AddBtn.vue'; // 新增按钮
 export { default as SearchSelect } from './SearchSelect.vue'; // 搜索选择服务端获取数据
+export { default as ImageViewer } from './ImageViewer.vue'; // 图片预览