소스 검색

Merge branch 'master' of http://git.yujin.shuziyunyao.com/yujin/digital-medicine-front

longmh 10 달 전
부모
커밋
4673a28d75
51개의 변경된 파일3252개의 추가작업 그리고 1011개의 파일을 삭제
  1. 3 0
      .env.development
  2. 1 0
      .eslintrc.js
  3. 4 4
      README.md
  4. 2 1
      package.json
  5. 120 5
      src/api/cdt/menus/index.ts
  6. 74 0
      src/api/cdt/orders/index.ts
  7. BIN
      src/assets/images/pdf_icon.png
  8. BIN
      src/assets/images/sf_icon.png
  9. 4 0
      src/assets/styles/element-ui.scss
  10. 1 1
      src/assets/styles/ruoyi.scss
  11. 2 2
      src/assets/styles/variables.module.scss
  12. 28 0
      src/assets/styles/vxe-table.scss
  13. 194 139
      src/components/FileUpload/index.vue
  14. 23 37
      src/components/ImageUpload/index.vue
  15. 1 1
      src/directive/common/copyText.ts
  16. 80 84
      src/layout/components/Settings/index.vue
  17. 3 12
      src/main.ts
  18. 7 0
      src/plugins/vxe-table.ts
  19. 57 57
      src/settings.ts
  20. 121 119
      src/utils/request.ts
  21. 217 188
      src/utils/ruoyi.ts
  22. 4 9
      src/views/cdt/discount/index.vue
  23. 22 14
      src/views/cdt/items/index.vue
  24. 117 0
      src/views/cdt/menus/detail/index.vue
  25. 374 0
      src/views/cdt/menus/form/index.vue
  26. 155 19
      src/views/cdt/menus/index.vue
  27. 68 0
      src/views/cdt/models/EditOrderRemark.vue
  28. 7 0
      src/views/cdt/models/index.ts
  29. 93 0
      src/views/cdt/models/menuInfo.vue
  30. 256 0
      src/views/cdt/models/orderInfo.vue
  31. 88 0
      src/views/cdt/models/rowItems.vue
  32. 233 0
      src/views/cdt/models/transferItems.vue
  33. 62 0
      src/views/cdt/models/uploadInvoiceForm.vue
  34. 63 0
      src/views/cdt/models/uploadReportForm.vue
  35. 75 0
      src/views/cdt/orders/detail/index.vue
  36. 167 0
      src/views/cdt/orders/index.vue
  37. 1 2
      src/views/dgtmedicine/model/index.ts
  38. 1 1
      src/views/dgtmedicine/model/uploadInvoiceForm.vue
  39. 154 205
      src/views/login.vue
  40. 29 0
      src/views/models/AddBtn.vue
  41. 115 0
      src/views/models/FileLook.vue
  42. 24 0
      src/views/models/ImageViewer.vue
  43. 109 0
      src/views/models/SearchSelect.vue
  44. 5 2
      src/views/models/index.ts
  45. 10 10
      src/views/models/searchTabs.vue
  46. 1 1
      src/views/register.vue
  47. 1 1
      src/views/tool/gen/genInfoForm.vue
  48. 1 1
      src/views/tool/gen/index.vue
  49. 20 30
      uno.config.ts
  50. 54 65
      vite.config.ts
  51. 1 1
      vite/plugins/compression.ts

+ 3 - 0
.env.development

@@ -28,6 +28,9 @@ VITE_APP_RSA_PRIVATE_KEY = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3C
 # 客户端id
 VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'
 
+# appid
+VITE_APP_APPID = '1890328853823459329'
+
 # websocket 开关 默认使用sse推送
 VITE_APP_WEBSOCKET = false
 

+ 1 - 0
.eslintrc.js

@@ -1,4 +1,5 @@
 module.exports = {
+  "root": true,
   env: {
     browser: true,
     es2021: true,

+ 4 - 4
README.md

@@ -1,10 +1,10 @@
 ## 平台简介
 
 - 本仓库为前端技术栈 [Vue3](https://v3.cn.vuejs.org) + [TS](https://www.typescriptlang.org/) + [Element Plus](https://element-plus.org/zh-CN) + [Vite](https://cn.vitejs.dev) 版本。
-- 成员项目: 基于 vben5(ant-design-vue) 的前端项目 [ruoyi-plus-vben5](https://gitee.com/dapppp/ruoyi-plus-vben5)
+- 成员项目: 基于 vben5(ant-design-vue) 的前端项目 [yujin-plus-vben5](https://gitee.com/dapppp/yujin-plus-vben5)
 - 配套后端代码仓库地址
 - [数字云药 5.X(注意版本号)](https://gitee.com/dromara/数字云药)
-- [RuoYi-Cloud-Plus 2.X(注意版本号)](https://gitee.com/dromara/RuoYi-Cloud-Plus)
+- [yujin-Cloud-Plus 2.X(注意版本号)](https://gitee.com/dromara/yujin-Cloud-Plus)
 
 ## 前端运行
 
@@ -24,9 +24,9 @@ npm run build:prod
 # 前端访问地址 http://localhost:80
 ```
 
-## 本框架与RuoYi的业务差异
+## 本框架与yujin的业务差异
 
-| 业务         | 功能说明                                                      | 本框架 | RuoYi                         |
+| 业务         | 功能说明                                                      | 本框架 | yujin                         |
 | ------------ | ------------------------------------------------------------- | ------ | ----------------------------- |
 | 租户管理     | 系统内租户的管理 如:租户套餐、过期时间、用户数量、企业信息等  | 支持   | 无                            |
 | 租户套餐管理 | 系统内租户所能使用的套餐管理 如:套餐内所包含的菜单等          | 支持   | 无                            |

+ 2 - 1
package.json

@@ -39,6 +39,7 @@
     "js-cookie": "3.0.5",
     "jsencrypt": "3.3.2",
     "nprogress": "0.2.0",
+    "number-precision": "^1.6.0",
     "pinia": "2.2.6",
     "screenfull": "6.0.2",
     "vue": "3.5.13",
@@ -47,7 +48,7 @@
     "vue-json-pretty": "2.4.0",
     "vue-router": "4.4.5",
     "vue-types": "5.1.3",
-    "vxe-table": "^4.11.18"
+    "vxe-table": "4.6.17"
   },
   "devDependencies": {
     "@eslint/js": "9.15.0",

+ 120 - 5
src/api/cdt/menus/index.ts

@@ -8,9 +8,124 @@ import { AxiosPromise } from 'axios';
  */
 
 export const testPackageList = (query?: any): any => {
-  return request({
-    url: '/dgtmedicine/testPackage/list',
-    method: 'get',
-    params: query
-  });
+    return request({
+        url: '/dgtmedicine/testPackage/list',
+        method: 'get',
+        params: query
+    });
+};
+/**
+ * 查询检测套餐列表统计
+ * @param query
+ * @returns {*}
+ */
+export const testPackageListCount = (query?: any): any => {
+    return request({
+        url: '/dgtmedicine/testPackage/queryPackCount',
+        method: 'get',
+        params: query
+    });
+};
+/**
+ * 获取企业信息列表
+ * @param id
+ * @returns {*}
+ */
+export const getEnterpriseList = (query?: any): any => {
+    return request({
+        url: '/dgtmedicine/member/list',
+        method: 'get',
+        params: query
+    });
+};
+/**
+ * 新增检测套餐
+ * @param id
+ * @returns {*}
+ */
+export const addTestPackage = (data?: any): any => {
+    return request({
+        url: '/dgtmedicine/testPackage/add',
+        method: 'post',
+        data
+    });
+};
+/**
+ * 修改检测套餐
+ * @param id
+ * @returns {*}
+ */
+export const editTestPackage = (data?: any): any => {
+    return request({
+        url: '/dgtmedicine/testPackage/edit',
+        method: 'post',
+        data
+    });
+};
+/**
+ * 上架检测套餐
+ * @param id
+ * @returns {*}
+ */
+export const testPackageSale = (id?: string): any => {
+    return request({
+        url: `/dgtmedicine/testPackage/onSale/${id}`,
+        method: 'post'
+    });
+};
+/**
+ * 下架套餐
+ * @param id
+ * @returns {*}
+ */
+export const testPackageUnSale = (id?: string): any => {
+    return request({
+        url: `/dgtmedicine/testPackage/offSale/${id}`,
+        method: 'post'
+    });
+};
+/**
+ * 复制套餐
+ * @param id
+ * @returns {*}
+ */
+export const copyTestPackage = (id?: string): any => {
+    return request({
+        url: `/dgtmedicine/testPackage/copy/${id}`,
+        method: 'post'
+    });
+};
+/**
+ * 获取检测套餐详细信息
+ * @param id
+ * @returns {*}
+ */
+export const getTestPackage = (id?: string): any => {
+    return request({
+        url: `/dgtmedicine/testPackage/getInfo/${id}`,
+        method: 'get'
+    });
+};
+/**
+ * 查询检测套餐项目列表
+ * @param id
+ * @returns {*}
+ */
+export const getTestPackageItems = (query?: any): any => {
+    return request({
+        url: `/dgtmedicine/testItem/list`,
+        method: 'get',
+        params: query
+    });
+};
+/**
+ * 企业id查企业详情
+ * @param id
+ * @returns {*}
+ */
+export const getEnterpriseDetail = (id?: string): any => {
+    return request({
+        url: `/dgtmedicine/member//getMemberInfoById/${id}`,
+        method: 'get'
+    });
 };

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

@@ -0,0 +1,74 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+
+/**
+ * 查询订单列表
+ * @param query
+ * @returns {*}
+ */
+export const testOrderList = (query?: any): any => {
+    return request({
+        url: '/dgtmedicine/testOrder/list',
+        method: 'get',
+        params: query
+    });
+};
+/**
+ * 查询订单状态统计
+ * @param query
+ * @returns {*}
+ */
+export const testOrderListCount = (query?: any): any => {
+    return request({
+        url: '/dgtmedicine/testOrder/statusCount',
+        method: 'get',
+        params: query
+    });
+};
+/**
+ * 获取检测订单详细信息
+ * @param query
+ * @returns {*}
+ */
+export const getTestOrderDetail = (orderId?: any): any => {
+    return request({
+        url: `/dgtmedicine/testOrder/getInfo/${orderId}`,
+        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
+    });
+};

BIN
src/assets/images/pdf_icon.png


BIN
src/assets/images/sf_icon.png


+ 4 - 0
src/assets/styles/element-ui.scss

@@ -160,3 +160,7 @@
      color: var(--el-color-primary);
   }
 }
+
+.small-btn-font.el-button--small {
+  font-size: 14px;
+}

+ 1 - 1
src/assets/styles/ruoyi.scss

@@ -1,6 +1,6 @@
 /**
  * 通用css样式布局处理
- * Copyright (c) 2019 ruoyi
+ * Copyright (c) 2019 yujin
  */
 
 /** 基础通用 **/

+ 2 - 2
src/assets/styles/variables.module.scss

@@ -4,7 +4,7 @@
   --menuColor: #bfcbd9;
   --menuActiveText: #f4f4f5;
   --menuHover: #263445;
-
+  --el-color-primary: #2c9049;
   --subMenuBg: #1f2d3d;
   --subMenuActiveText: #f4f4f5;
   --subMenuHover: #001528;
@@ -106,7 +106,7 @@ $fixed-header-bg: var(--fixedHeaderBg);
 $table-header-bg: var(--tableHeaderBg);
 $table-header-text-color: var(--tableHeaderTextColor);
 
-$--color-primary: #409eff;
+$--color-primary: #2c9049;
 $--color-success: #67c23a;
 $--color-warning: #e6a23c;
 $--color-danger: #f56c6c;

+ 28 - 0
src/assets/styles/vxe-table.scss

@@ -0,0 +1,28 @@
+$vxe-primary-color: #2c9049;
+@import 'vxe-table/styles/index.scss';
+[data-vxe-table-theme="default"] {
+    --vxe-modal-header-background-color: #fff;
+    --vxe-table-row-hover-background-color:#fafafa;
+    
+    --vxe-font-family: PingFang, PingFang SC, Microsoft YaHei, Arial, sans-serif, Helvetica Neue, Helvetica, Hiragino Sans GB;
+}
+[data-vxe-ui-theme=light] {
+    --vxe-table-row-checkbox-checked-background-color: #e6f7ff;
+    --vxe-table-row-hover-checkbox-checked-background-color: #e6f7ff;
+    --vxe-font-family: PingFang, PingFang SC, Microsoft YaHei, Arial, sans-serif, Helvetica Neue, Helvetica, Hiragino Sans GB;
+}
+.table-box-row-highlight {
+    --vxe-table-row-radio-checked-background-color: #e6f7ff;
+}
+.vxe-header--row.tb-hd-inner {
+    background-color: #fafcfb;
+}
+.vxe-body--row.tb-ce-td{
+    background-color: #fafcfb;
+}
+
+.is--expand-row.row--hover {
+    & + .vxe-body--expanded-row {
+        background-color: var(--vxe-table-row-hover-background-color);
+    }
+}

+ 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>

+ 23 - 37
src/components/ImageUpload/index.vue

@@ -1,42 +1,28 @@
 <template>
-  <div class="component-upload-image">
-    <el-upload
-      ref="imageUpload"
-      multiple
-      :action="uploadImgUrl"
-      list-type="picture-card"
-      :on-success="handleUploadSuccess"
-      :before-upload="handleBeforeUpload"
-      :limit="limit"
-      :on-error="handleUploadError"
-      :on-exceed="handleExceed"
-      :before-remove="handleDelete"
-      :show-file-list="true"
-      :headers="headers"
-      :file-list="fileList"
-      :on-preview="handlePictureCardPreview"
-      :class="{ hide: fileList.length >= limit }"
-    >
-      <el-icon class="avatar-uploader-icon">
-        <plus />
-      </el-icon>
-    </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>
+    <div class="component-upload-image">
+        <el-upload ref="imageUpload" multiple :action="uploadImgUrl" list-type="picture-card" :on-success="handleUploadSuccess" :before-upload="handleBeforeUpload" :limit="limit" :on-error="handleUploadError" :on-exceed="handleExceed" :before-remove="handleDelete" :show-file-list="true" :headers="headers" :file-list="fileList" :on-preview="handlePictureCardPreview" :class="{ hide: fileList.length >= limit }">
+            <el-icon class="avatar-uploader-icon">
+                <plus />
+            </el-icon>
+        </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>
 
-    <el-dialog v-model="dialogVisible" title="预览" width="800px" append-to-body>
-      <img :src="dialogImageUrl" style="display: block; max-width: 100%; margin: 0 auto" />
-    </el-dialog>
-  </div>
+        <el-dialog v-model="dialogVisible" title="预览" width="800px" append-to-body>
+            <img :src="dialogImageUrl" style="display: block; max-width: 100%; margin: 0 auto" />
+        </el-dialog>
+    </div>
 </template>
 
 <script setup lang="ts">

+ 1 - 1
src/directive/common/copyText.ts

@@ -1,6 +1,6 @@
 /**
  * v-copyText 复制文本内容
- * Copyright (c) 2022 ruoyi
+ * Copyright (c) 2022 yujin
  */
 import { DirectiveBinding } from 'vue';
 

+ 80 - 84
src/layout/components/Settings/index.vue

@@ -1,90 +1,86 @@
 <template>
-  <el-drawer v-model="showSettings" :with-header="false" direction="rtl" size="300px" close-on-click-modal>
-    <h3 class="drawer-title">主题风格设置</h3>
-
-    <div class="setting-drawer-block-checbox">
-      <div class="setting-drawer-block-checbox-item" @click="handleTheme(SideThemeEnum.DARK)">
-        <img src="@/assets/images/dark.svg" alt="dark" />
-        <div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block">
-          <i aria-label="图标: check" class="anticon anticon-check">
-            <svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
-              <path
-                d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
-              />
-            </svg>
-          </i>
+    <el-drawer v-model="showSettings" :with-header="false" direction="rtl" size="300px" close-on-click-modal>
+        <h3 class="drawer-title">主题风格设置</h3>
+
+        <div class="setting-drawer-block-checbox">
+            <div class="setting-drawer-block-checbox-item" @click="handleTheme(SideThemeEnum.DARK)">
+                <img src="@/assets/images/dark.svg" alt="dark" />
+                <div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block">
+                    <i aria-label="图标: check" class="anticon anticon-check">
+                        <svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
+                            <path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" />
+                        </svg>
+                    </i>
+                </div>
+            </div>
+            <div class="setting-drawer-block-checbox-item" @click="handleTheme(SideThemeEnum.LIGHT)">
+                <img src="@/assets/images/light.svg" alt="light" />
+                <div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block">
+                    <i aria-label="图标: check" class="anticon anticon-check">
+                        <svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
+                            <path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" />
+                        </svg>
+                    </i>
+                </div>
+            </div>
         </div>
-      </div>
-      <div class="setting-drawer-block-checbox-item" @click="handleTheme(SideThemeEnum.LIGHT)">
-        <img src="@/assets/images/light.svg" alt="light" />
-        <div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block">
-          <i aria-label="图标: check" class="anticon anticon-check">
-            <svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
-              <path
-                d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
-              />
-            </svg>
-          </i>
+        <div class="drawer-item">
+            <span>主题颜色</span>
+            <span class="comp-style">
+                <el-color-picker v-model="theme" :predefine="predefineColors" @change="themeChange" />
+            </span>
         </div>
-      </div>
-    </div>
-    <div class="drawer-item">
-      <span>主题颜色</span>
-      <span class="comp-style">
-        <el-color-picker v-model="theme" :predefine="predefineColors" @change="themeChange" />
-      </span>
-    </div>
-    <div class="drawer-item">
-      <span>深色模式</span>
-      <span class="comp-style">
-        <el-switch v-model="isDark" class="drawer-switch" @change="toggleDark" />
-      </span>
-    </div>
-
-    <el-divider />
-
-    <h3 class="drawer-title">系统布局配置</h3>
-
-    <div class="drawer-item">
-      <span>开启 TopNav</span>
-      <span class="comp-style">
-        <el-switch v-model="settingsStore.topNav" class="drawer-switch" @change="topNavChange" />
-      </span>
-    </div>
-
-    <div class="drawer-item">
-      <span>开启 Tags-Views</span>
-      <span class="comp-style">
-        <el-switch v-model="settingsStore.tagsView" class="drawer-switch" />
-      </span>
-    </div>
-
-    <div class="drawer-item">
-      <span>固定 Header</span>
-      <span class="comp-style">
-        <el-switch v-model="settingsStore.fixedHeader" class="drawer-switch" />
-      </span>
-    </div>
-
-    <div class="drawer-item">
-      <span>显示 Logo</span>
-      <span class="comp-style">
-        <el-switch v-model="settingsStore.sidebarLogo" class="drawer-switch" />
-      </span>
-    </div>
-
-    <div class="drawer-item">
-      <span>动态标题</span>
-      <span class="comp-style">
-        <el-switch v-model="settingsStore.dynamicTitle" class="drawer-switch" @change="dynamicTitleChange" />
-      </span>
-    </div>
-
-    <el-divider />
-
-    <el-button type="primary" plain icon="DocumentAdd" @click="saveSetting">保存配置</el-button>
-    <el-button plain icon="Refresh" @click="resetSetting">重置配置</el-button>
-  </el-drawer>
+        <div class="drawer-item">
+            <span>深色模式</span>
+            <span class="comp-style">
+                <el-switch v-model="isDark" class="drawer-switch" @change="toggleDark" />
+            </span>
+        </div>
+
+        <el-divider />
+
+        <h3 class="drawer-title">系统布局配置</h3>
+
+        <div class="drawer-item">
+            <span>开启 TopNav</span>
+            <span class="comp-style">
+                <el-switch v-model="settingsStore.topNav" class="drawer-switch" @change="topNavChange" />
+            </span>
+        </div>
+
+        <div class="drawer-item">
+            <span>开启 Tags-Views</span>
+            <span class="comp-style">
+                <el-switch v-model="settingsStore.tagsView" class="drawer-switch" />
+            </span>
+        </div>
+
+        <div class="drawer-item">
+            <span>固定 Header</span>
+            <span class="comp-style">
+                <el-switch v-model="settingsStore.fixedHeader" class="drawer-switch" />
+            </span>
+        </div>
+
+        <div class="drawer-item">
+            <span>显示 Logo</span>
+            <span class="comp-style">
+                <el-switch v-model="settingsStore.sidebarLogo" class="drawer-switch" />
+            </span>
+        </div>
+
+        <div class="drawer-item">
+            <span>动态标题</span>
+            <span class="comp-style">
+                <el-switch v-model="settingsStore.dynamicTitle" class="drawer-switch" @change="dynamicTitleChange" />
+            </span>
+        </div>
+
+        <el-divider />
+
+        <el-button type="primary" plain icon="DocumentAdd" @click="saveSetting">保存配置</el-button>
+        <el-button plain icon="Refresh" @click="resetSetting">重置配置</el-button>
+    </el-drawer>
 </template>
 
 <script setup lang="ts">

+ 3 - 12
src/main.ts

@@ -15,11 +15,7 @@ import directive from './directive';
 // 注册插件
 import plugins from './plugins/index'; // plugins
 
-// 高亮组件
-// import 'highlight.js/styles/a11y-light.css';
-import 'highlight.js/styles/atom-one-dark.css';
-import 'highlight.js/lib/common';
-import HighLight from '@highlightjs/vue-plugin';
+
 
 // svg图标
 import 'virtual:svg-icons-register';
@@ -32,24 +28,19 @@ import './permission';
 import i18n from '@/lang/index';
 
 // vxeTable
-import VXETable from 'vxe-table';
-import 'vxe-table/lib/style.css';
-VXETable.config({
-  zIndex: 999999
-});
 
+import { useTable } from './plugins/vxe-table';
 // 修改 el-dialog 默认点击遮照为不关闭
 import { ElDialog } from 'element-plus';
 ElDialog.props.closeOnClickModal.default = false;
 
 const app = createApp(App);
 
-app.use(HighLight);
+app.use(useTable);
 app.use(ElementIcons);
 app.use(router);
 app.use(store);
 app.use(i18n);
-app.use(VXETable);
 app.use(plugins);
 // 自定义指令
 directive(app);

+ 7 - 0
src/plugins/vxe-table.ts

@@ -0,0 +1,7 @@
+import { App } from 'vue';
+import VXETable from 'vxe-table';
+import '@/assets/styles/vxe-table.scss';
+
+export function useTable(app: App) {
+    app.use(VXETable);
+}

+ 57 - 57
src/settings.ts

@@ -1,62 +1,62 @@
 import { LanguageEnum } from '@/enums/LanguageEnum';
 
 const setting: DefaultSettings = {
-  /**
-   * 网页标题
-   */
-  title: import.meta.env.VITE_APP_TITLE,
-
-  theme: '#2c9049',
-
-  /**
-   * 侧边栏主题 深色主题theme-dark,浅色主题theme-light
-   */
-  sideTheme: 'theme-dark',
-  /**
-   * 是否系统布局配置
-   */
-  showSettings: false,
-
-  /**
-   * 是否显示顶部导航
-   */
-  topNav: false,
-
-  /**
-   * 是否显示 tagsView
-   */
-  tagsView: false,
-
-  /**
-   * 是否固定头部
-   */
-  fixedHeader: false,
-
-  /**
-   * 是否显示logo
-   */
-  sidebarLogo: true,
-
-  /**
-   * 是否显示动态标题
-   */
-  dynamicTitle: false,
-
-  /**
-   * @type {string | array} 'production' | ['production', 'development']
-   * @description Need show err logs component.
-   * The default is only used in the production env
-   * If you want to also use it in dev, you can pass ['production', 'development']
-   */
-  errorLog: 'production',
-
-  animationEnable: false,
-
-  dark: false,
-  language: LanguageEnum.zh_CN,
-
-  size: 'default',
-
-  layout: ''
+    /**
+     * 网页标题
+     */
+    title: import.meta.env.VITE_APP_TITLE,
+
+    theme: '#2c9049',
+
+    /**
+     * 侧边栏主题 深色主题theme-dark,浅色主题theme-light
+     */
+    sideTheme: 'theme-dark',
+    /**
+     * 是否系统布局配置
+     */
+    showSettings: false,
+
+    /**
+     * 是否显示顶部导航
+     */
+    topNav: false,
+
+    /**
+     * 是否显示 tagsView
+     */
+    tagsView: false,
+
+    /**
+     * 是否固定头部
+     */
+    fixedHeader: false,
+
+    /**
+     * 是否显示logo
+     */
+    sidebarLogo: true,
+
+    /**
+     * 是否显示动态标题
+     */
+    dynamicTitle: false,
+
+    /**
+     * @type {string | array} 'production' | ['production', 'development']
+     * @description Need show err logs component.
+     * The default is only used in the production env
+     * If you want to also use it in dev, you can pass ['production', 'development']
+     */
+    errorLog: 'production',
+
+    animationEnable: false,
+
+    dark: false,
+    language: LanguageEnum.zh_CN,
+
+    size: 'default',
+
+    layout: ''
 };
 export default setting;

+ 121 - 119
src/utils/request.ts

@@ -17,116 +17,118 @@ let downloadLoadingInstance: LoadingInstance;
 // 是否显示重新登录
 export const isRelogin = { show: false };
 export const globalHeaders = () => {
-  return {
-    Authorization: 'Bearer ' + getToken(),
-    clientid: import.meta.env.VITE_APP_CLIENT_ID
-  };
+    return {
+        Authorization: 'Bearer ' + getToken(),
+        clientid: import.meta.env.VITE_APP_CLIENT_ID,
+        Appid: import.meta.env.VITE_APP_APPID
+    };
 };
 
 axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8';
 axios.defaults.headers['clientid'] = import.meta.env.VITE_APP_CLIENT_ID;
+axios.defaults.headers['Appid'] = import.meta.env.VITE_APP_APPID;
 // 创建 axios 实例
 const service = axios.create({
-  baseURL: import.meta.env.VITE_APP_BASE_API,
-  timeout: 50000
+    baseURL: import.meta.env.VITE_APP_BASE_API,
+    timeout: 50000
 });
 
 // 请求拦截器
 service.interceptors.request.use(
-  (config: InternalAxiosRequestConfig) => {
-    // 对应国际化资源文件后缀
-    config.headers['Content-Language'] = getLanguage();
+    (config: InternalAxiosRequestConfig) => {
+        // 对应国际化资源文件后缀
+        config.headers['Content-Language'] = getLanguage();
 
-    const isToken = config.headers?.isToken === false;
-    // 是否需要防止数据重复提交
-    const isRepeatSubmit = config.headers?.repeatSubmit === false;
-    // 是否需要加密
-    const isEncrypt = config.headers?.isEncrypt === 'true';
+        const isToken = config.headers?.isToken === false;
+        // 是否需要防止数据重复提交
+        const isRepeatSubmit = config.headers?.repeatSubmit === false;
+        // 是否需要加密
+        const isEncrypt = config.headers?.isEncrypt === 'true';
 
-    if (getToken() && !isToken) {
-      config.headers['Authorization'] = 'Bearer ' + getToken(); // 让每个请求携带自定义token 请根据实际情况自行修改
-    }
-    // get请求映射params参数
-    if (config.method === 'get' && config.params) {
-      let url = config.url + '?' + tansParams(config.params);
-      url = url.slice(0, -1);
-      config.params = {};
-      config.url = url;
-    }
+        if (getToken() && !isToken) {
+            config.headers['Authorization'] = 'Bearer ' + getToken(); // 让每个请求携带自定义token 请根据实际情况自行修改
+        }
+        // get请求映射params参数
+        if (config.method === 'get' && config.params) {
+            let url = config.url + '?' + tansParams(config.params);
+            url = url.slice(0, -1);
+            config.params = {};
+            config.url = url;
+        }
 
-    if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
-      const requestObj = {
-        url: config.url,
-        data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
-        time: new Date().getTime()
-      };
-      const sessionObj = cache.session.getJSON('sessionObj');
-      if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
-        cache.session.setJSON('sessionObj', requestObj);
-      } else {
-        const s_url = sessionObj.url; // 请求地址
-        const s_data = sessionObj.data; // 请求数据
-        const s_time = sessionObj.time; // 请求时间
-        const interval = 500; // 间隔时间(ms),小于此时间视为重复提交
-        if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
-          const message = '数据正在处理,请勿重复提交';
-          console.warn(`[${s_url}]: ` + message);
-          return Promise.reject(new Error(message));
-        } else {
-          cache.session.setJSON('sessionObj', requestObj);
+        if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
+            const requestObj = {
+                url: config.url,
+                data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
+                time: new Date().getTime()
+            };
+            const sessionObj = cache.session.getJSON('sessionObj');
+            if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
+                cache.session.setJSON('sessionObj', requestObj);
+            } else {
+                const s_url = sessionObj.url; // 请求地址
+                const s_data = sessionObj.data; // 请求数据
+                const s_time = sessionObj.time; // 请求时间
+                const interval = 500; // 间隔时间(ms),小于此时间视为重复提交
+                if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
+                    const message = '数据正在处理,请勿重复提交';
+                    console.warn(`[${s_url}]: ` + message);
+                    return Promise.reject(new Error(message));
+                } else {
+                    cache.session.setJSON('sessionObj', requestObj);
+                }
+            }
         }
-      }
-    }
-    if (import.meta.env.VITE_APP_ENCRYPT === 'true') {
-      // 当开启参数加密
-      if (isEncrypt && (config.method === 'post' || config.method === 'put')) {
-        // 生成一个 AES 密钥
-        const aesKey = generateAesKey();
-        config.headers[encryptHeader] = encrypt(encryptBase64(aesKey));
-        config.data = typeof config.data === 'object' ? encryptWithAes(JSON.stringify(config.data), aesKey) : encryptWithAes(config.data, aesKey);
-      }
-    }
-    // FormData数据去请求头Content-Type
-    if (config.data instanceof FormData) {
-      delete config.headers['Content-Type'];
+        if (import.meta.env.VITE_APP_ENCRYPT === 'true') {
+            // 当开启参数加密
+            if (isEncrypt && (config.method === 'post' || config.method === 'put')) {
+                // 生成一个 AES 密钥
+                const aesKey = generateAesKey();
+                config.headers[encryptHeader] = encrypt(encryptBase64(aesKey));
+                config.data = typeof config.data === 'object' ? encryptWithAes(JSON.stringify(config.data), aesKey) : encryptWithAes(config.data, aesKey);
+            }
+        }
+        // FormData数据去请求头Content-Type
+        if (config.data instanceof FormData) {
+            delete config.headers['Content-Type'];
+        }
+        return config;
+    },
+    (error: any) => {
+        return Promise.reject(error);
     }
-    return config;
-  },
-  (error: any) => {
-    return Promise.reject(error);
-  }
 );
 
 // 响应拦截器
 service.interceptors.response.use(
-  (res: AxiosResponse) => {
-    if (import.meta.env.VITE_APP_ENCRYPT === 'true') {
-      // 加密后的 AES 秘钥
-      const keyStr = res.headers[encryptHeader];
-      // 加密
-      if (keyStr != null && keyStr != '') {
-        const data = res.data;
-        // 请求体 AES 解密
-        const base64Str = decrypt(keyStr);
-        // base64 解码 得到请求头的 AES 秘钥
-        const aesKey = decryptBase64(base64Str.toString());
-        // aesKey 解码 data
-        const decryptData = decryptWithAes(data, aesKey);
-        // 将结果 (得到的是 JSON 字符串) 转为 JSON
-        res.data = JSON.parse(decryptData);
-      }
-    }
-    // 未设置状态码则默认成功状态
-    const code = res.data.code || HttpStatus.SUCCESS;
-    // 获取错误信息
-    const msg = errorCode[code] || res.data.msg || errorCode['default'];
-    // 二进制数据则直接返回
-    if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
-      return res.data;
-    }
-    if (code === 401) {
-      // prettier-ignore
-      if (!isRelogin.show) {
+    (res: AxiosResponse) => {
+        if (import.meta.env.VITE_APP_ENCRYPT === 'true') {
+            // 加密后的 AES 秘钥
+            const keyStr = res.headers[encryptHeader];
+            // 加密
+            if (keyStr != null && keyStr != '') {
+                const data = res.data;
+                // 请求体 AES 解密
+                const base64Str = decrypt(keyStr);
+                // base64 解码 得到请求头的 AES 秘钥
+                const aesKey = decryptBase64(base64Str.toString());
+                // aesKey 解码 data
+                const decryptData = decryptWithAes(data, aesKey);
+                // 将结果 (得到的是 JSON 字符串) 转为 JSON
+                res.data = JSON.parse(decryptData);
+            }
+        }
+        // 未设置状态码则默认成功状态
+        const code = res.data.code || HttpStatus.SUCCESS;
+        // 获取错误信息
+        const msg = errorCode[code] || res.data.msg || errorCode['default'];
+        // 二进制数据则直接返回
+        if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
+            return res.data;
+        }
+        if (code === 401) {
+            // prettier-ignore
+            if (!isRelogin.show) {
         isRelogin.show = true;
         ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
           confirmButtonText: '重新登录',
@@ -146,38 +148,38 @@ service.interceptors.response.use(
           isRelogin.show = false;
         });
       }
-      return Promise.reject('无效的会话,或者会话已过期,请重新登录。');
-    } else if (code === HttpStatus.SERVER_ERROR) {
-      ElMessage({ message: msg, type: 'error' });
-      return Promise.reject(new Error(msg));
-    } else if (code === HttpStatus.WARN) {
-      ElMessage({ message: msg, type: 'warning' });
-      return Promise.reject(new Error(msg));
-    } else if (code !== HttpStatus.SUCCESS) {
-      ElNotification.error({ title: msg });
-      return Promise.reject('error');
-    } else {
-      return Promise.resolve(res.data);
-    }
-  },
-  (error: any) => {
-    let { message } = error;
-    if (message == 'Network Error') {
-      message = '后端接口连接异常';
-    } else if (message.includes('timeout')) {
-      message = '系统接口请求超时';
-    } else if (message.includes('Request failed with status code')) {
-      message = '系统接口' + message.substr(message.length - 3) + '异常';
+            return Promise.reject('无效的会话,或者会话已过期,请重新登录。');
+        } else if (code === HttpStatus.SERVER_ERROR) {
+            ElMessage({ message: msg, type: 'error' });
+            return Promise.reject(new Error(msg));
+        } else if (code === HttpStatus.WARN) {
+            ElMessage({ message: msg, type: 'warning' });
+            return Promise.reject(new Error(msg));
+        } else if (code !== HttpStatus.SUCCESS) {
+            ElNotification.error({ title: msg });
+            return Promise.reject('error');
+        } else {
+            return Promise.resolve(res.data);
+        }
+    },
+    (error: any) => {
+        let { message } = error;
+        if (message == 'Network Error') {
+            message = '后端接口连接异常';
+        } else if (message.includes('timeout')) {
+            message = '系统接口请求超时';
+        } else if (message.includes('Request failed with status code')) {
+            message = '系统接口' + message.substr(message.length - 3) + '异常';
+        }
+        ElMessage({ message: message, type: 'error', duration: 5 * 1000 });
+        return Promise.reject(error);
     }
-    ElMessage({ message: message, type: 'error', duration: 5 * 1000 });
-    return Promise.reject(error);
-  }
 );
 // 通用下载方法
 export function download(url: string, params: any, fileName: string) {
-  downloadLoadingInstance = ElLoading.service({ text: '正在下载数据,请稍候', background: 'rgba(0, 0, 0, 0.7)' });
-  // prettier-ignore
-  return service.post(url, params, {
+    downloadLoadingInstance = ElLoading.service({ text: '正在下载数据,请稍候', background: 'rgba(0, 0, 0, 0.7)' });
+    // prettier-ignore
+    return service.post(url, params, {
       transformRequest: [
         (params: any) => {
           return tansParams(params);

+ 217 - 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,73 @@ 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);
+};
+export const changeByte = (byte: number) => {
+    let size = '';
+    if (byte < 0.1 * 1024) {
+        // 小于0.1KB,则转化成B
+        size = `${byte.toFixed(2)}B`;
+    } else if (byte < 0.1 * 1024 * 1024) {
+        // 小于0.1MB,则转化成KB
+        size = `${(byte / 1024).toFixed(2)}KB`;
+    } else if (byte < 0.1 * 1024 * 1024 * 1024) {
+        // 小于0.1GB,则转化成MB
+        size = `${(byte / (1024 * 1024)).toFixed(2)}MB`;
+    } else {
+        // 其他转化成GB
+        size = `${(byte / (1024 * 1024 * 1024)).toFixed(2)}GB`;
+    }
+
+    const sizeStr = `${size}`; // 转成字符串
+    const index = sizeStr.indexOf('.'); // 获取小数点处的索引
+    const dou = sizeStr.substring(index + 1, index + 3); // 获取小数点后两位的值
+    if (dou == '00') {
+        // 判断后两位是否为00,如果是则删除00
+        return sizeStr.substring(0, index);
+    }
+    return size;
 };

+ 4 - 9
src/views/cdt/discount/index.vue

@@ -9,17 +9,14 @@
                     <!-- 序号 -->
                     <vxe-column type="seq" width="60" title="序号" align="center" />
                     <vxe-column title="会员级别" align="center" field="name" min-width="100" :formatter="colNoData" />
-                    <vxe-column title="折扣标准" align="center"  min-width="100">
-                        <template #default="{ row }">
-                           {{ row?.discount }}折
-                        </template>
+                    <vxe-column title="折扣标准" align="center" min-width="100">
+                        <template #default="{ row }">{{ NP.times(row?.discount, 10) }}折</template>
                     </vxe-column>
                     <vxe-column title="创建人" align="center" field="createByName" min-width="100" :formatter="colNoData" />
                     <vxe-column title="创建时间" align="center" field="createTime" min-width="100" :formatter="colNoData" />
                 </vxe-table>
             </div>
-            <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
-                v-model:limit="queryParams.pageSize" @pagination="getList" />
+            <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
             <div class="pd-5"></div>
         </div>
     </div>
@@ -27,12 +24,10 @@
 <script setup name="Discount" lang="ts">
 import { vipDiscountList } from '@/api/cdt/discount';
 import { colNoData } from '@/utils/noData';
+import NP from 'number-precision';
 const queryParams = ref<any>({
     pageNum: 1,
     pageSize: 10,
-    name: '',
-    itemTypeId: '',
-    packageId: ''
 });
 const loading = ref(false);
 const total = ref(0);

+ 22 - 14
src/views/cdt/items/index.vue

@@ -18,20 +18,26 @@
             </div>
             <div class="flex1 ov-hd d-flex content-border">
                 <div class="tree-wrap">
-                   <div v-show="tabActive === '1'">
-                    <el-tree ref="treeItemsRef" class="base-tree-tabs" auto-expand-parent default-expand-all node-key="id" :data="itemsData" :props="{ label: 'name' }" highlight-current @node-click="itemsClick" accordion>
-                        <template #default="{ node, data }">
-                            <span>{{ node.label }} <template v-if="!data?.children">({{ data?.itemCount || '0' }})</template></span>
-                        </template>
-                    </el-tree>
-                   </div>
-                   <div v-show="tabActive === '2'">
-                    <el-tree ref="treeStandardsRef" class="base-tree-tabs" auto-expand-parent :data="standards" node-key="id" :props="{ label: 'name' }" highlight-current @node-click="standardsClick" accordion>
-                        <template #default="{ node, data }">
-                            <span>{{ node.label }} <template v-if="!data?.children">({{ data?.itemCount || '0' }})</template></span>
-                        </template>
-                    </el-tree>
-                   </div>
+                    <div v-show="tabActive === '1'">
+                        <el-tree ref="treeItemsRef" class="base-tree-tabs" auto-expand-parent default-expand-all node-key="id" :data="itemsData" :props="{ label: 'name' }" highlight-current @node-click="itemsClick" accordion>
+                            <template #default="{ node, data }">
+                                <span>
+                                    {{ node.label }}
+                                    <template v-if="!data?.children">({{ data?.itemCount || '0' }})</template>
+                                </span>
+                            </template>
+                        </el-tree>
+                    </div>
+                    <div v-show="tabActive === '2'">
+                        <el-tree ref="treeStandardsRef" class="base-tree-tabs" auto-expand-parent :data="standards" node-key="id" :props="{ label: 'name' }" highlight-current @node-click="standardsClick" accordion>
+                            <template #default="{ node, data }">
+                                <span>
+                                    {{ node.label }}
+                                    <template v-if="!data?.children">({{ data?.itemCount || '0' }})</template>
+                                </span>
+                            </template>
+                        </el-tree>
+                    </div>
                 </div>
                 <div class="flex1 ov-hd d-flex flex-cln">
                     <div class="flex1 ov-hd">
@@ -103,12 +109,14 @@ const getTabsTree = async () => {
 const itemsClick = (data: any) => {
     if (!data.children) {
         queryParams.value.itemTypeId = data.id;
+        queryParams.value.packageId = '';
     }
     handleQuery()
 };
 const standardsClick = (data: any) => {
     if (!data.children) {
         queryParams.value.packageId = data.id;
+        queryParams.value.itemTypeId = '';
     }
     handleQuery()
 };

+ 117 - 0
src/views/cdt/menus/detail/index.vue

@@ -0,0 +1,117 @@
+<template>
+    <div class="p-3">
+        <div class="bg-fff flex1 ov-hd d-flex flex-cln">
+            <div class="d-flex a-c pd-16 border-bottom">
+                <div class="f-s-20 c-333 f-w-7 mr-10">套餐详情</div>
+                <el-button @click="router.go(-1)" type="primary" text>
+                    <el-icon>
+                        <Back />
+                    </el-icon>
+                    返回上一级
+                </el-button>
+            </div>
+            <div class="flex1 over-auto">
+                <div class="pd-16">
+                    <div class="info-title mb-16">套餐基本信息</div>
+                    <el-descriptions :column="4">
+                        <el-descriptions-item label="套餐名称:">{{ form?.name || '-' }}</el-descriptions-item>
+                        <el-descriptions-item label="套餐类型:">{{ selectDictLabel(dm_package_type, form?.publicFlag) || '-' }}</el-descriptions-item>
+                        <el-descriptions-item label="制定规则:">{{ selectDictLabel(dm_permit_type,form?.permitType ) }}</el-descriptions-item>
+                        <el-descriptions-item v-if="form?.permitType === '1'" label="适用类型:">{{ selectDictLabels(vip_level, form?.permit, ',') || '-' }}</el-descriptions-item>
+                        <el-descriptions-item v-if="form?.permitType === '2'" label="适用企业:">{{ form?.permitCpyNames.toString()|| '-' }}</el-descriptions-item>
+                        <el-descriptions-item label="检测周期:">{{ form?.period || '-' }}天</el-descriptions-item>
+                        <el-descriptions-item label="上架数量:">{{ form?.totalCount || '-' }}</el-descriptions-item>
+                        <el-descriptions-item label="执行标准:">{{ form?.standard || '-' }}</el-descriptions-item>
+                        <el-descriptions-item label="送样信息:" :span="2">{{ form?.description || '-' }}</el-descriptions-item>
+                        <el-descriptions-item label="购买须知:" :span="2">{{ form?.purchaseNotes || '-' }}</el-descriptions-item>
+                        <el-descriptions-item label="失效日期:">{{ form?.validUntil || '-' }}</el-descriptions-item>
+                    </el-descriptions>
+                    <el-divider />
+                    <el-descriptions :column="4" direction="vertical">
+                        <el-descriptions-item label="封面图:">
+                            <el-image style="width: 100px; height: 100px" :src="form?.coverImg" :zoom-rate="1.2" :max-scale="7" :min-scale="0.2" :preview-src-list="[form?.coverImg]" fit="cover"></el-image>
+                        </el-descriptions-item>
+                        <el-descriptions-item label="产品图:">
+                            <template v-for="(item, index) in form?.proImg.split(',')" :key="index">
+                                <el-image style="width: 100px; height: 100px;margin-right: 12px;" :src="item" fit="cover" :zoom-rate="1.2" :max-scale="7" :min-scale="0.2" :initial-index="index" :preview-src-list="[item]"></el-image>
+                            </template>
+                        </el-descriptions-item>
+                    </el-descriptions>
+                    <el-divider />
+                    <div class="info-title mb-16 d-flex">
+                        <span>检测项目明细</span>
+                        <span class="f-s-14">(共{{itemsInfo?.length}}项, 合计{{ totalCountPrice }}元)</span>
+                    </div>
+                    <vxe-table ref="tableRightRef" border :data="itemsInfo" :column-config="{ resizable: true }">
+                        <vxe-column type="seq" width="60" title="序号" align="center" />
+                        <vxe-column title="检测项目" align="center" field="name" min-width="100" :formatter="colNoData" />
+                        <vxe-column title="单价(元、批次)" align="center" field="price" min-width="100" :formatter="colNoData" />
+                        <vxe-column title="备注" align="center" field="description" min-width="100" :formatter="colNoData" />
+                    </vxe-table>
+                    <el-divider />
+                    <div class="info-title mb-16">套餐价格</div>
+                    <vxe-table ref="tableRightRef" border :data="form?.priceDetail" :column-config="{ resizable: true }">
+                        <vxe-column type="seq" width="60" title="序号" align="center" />
+                        <!-- <vxe-column v-if="form.permitType === '2'" title="企业名称" align="center" min-width="100" :formatter="colNoData">
+                            <template #default="{ row }">{{ row?.cpyName }}</template>
+                        </vxe-column> -->
+                        <vxe-column title="适用类型" align="center" field="memberLevelName" min-width="100" :formatter="colNoData" />
+                        <vxe-column title="折扣标准" align="center" min-width="100">
+                            <template #default="{ row }">{{ NP.times(row?.memberDiscount, 10) }}折</template>
+                        </vxe-column>
+                        <vxe-column title="最终售价" align="center" min-width="100">
+                            <template #default="{ row }">{{ row.price || '-' }}元</template>
+                        </vxe-column>
+                    </vxe-table>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup name="Menus-detail" lang="ts">
+import NP from 'number-precision';
+import { colNoData } from '@/utils/noData';
+import { debounce } from 'lodash';
+import { getTestPackage, getTestPackageItems } from '@/api/cdt/menus';
+const { query }: any = useRoute();
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { dm_package_type, dm_permit_type, vip_level } = toRefs<any>(proxy?.useDict('dm_package_type', 'dm_permit_type', 'vip_level'));
+const router = useRouter();
+const form = ref<any>({
+   items: [],
+   priceDetail: [],
+   permitCpys: [],
+});
+const getDetail = async () => {
+    proxy?.$modal.loading('加载中...');
+    const res = await getTestPackage(query?.id).finally(() => {
+        proxy?.$modal.closeLoading();
+    });
+    if (!res || res.code !== 200) return;
+    console.log(res);
+    form.value = {
+        ...res.data
+    };
+};
+const itemsInfo = ref<any>([]);
+const getItems = async () => {
+    const res = await getTestPackageItems({
+        pageNum: 1,
+        pageSize: 1000,
+        packageId: query?.id
+    });
+    if (!res || res.code !== 200) return;
+    itemsInfo.value = res.rows;
+};
+// 合计价格
+const totalCountPrice = computed(() => {
+    return itemsInfo.value.reduce((total: number, item: any) => {
+        return NP.plus(total, item.price);
+    }, 0);
+});
+onMounted(() => {
+    getDetail()
+    getItems()
+});
+</script>

+ 374 - 0
src/views/cdt/menus/form/index.vue

@@ -0,0 +1,374 @@
+<template>
+    <div class="p-3">
+        <div class="bg-fff flex1 ov-hd d-flex flex-cln">
+            <div class="d-flex a-c pd-16 border-bottom">
+                <div class="f-s-20 c-333 f-w-7 mr-10">新增套餐</div>
+                <el-button @click="router.go(-1)" type="primary" text>
+                    <el-icon>
+                        <Back />
+                    </el-icon>
+                    返回上一级
+                </el-button>
+            </div>
+            <div class="flex1 over-auto">
+                <el-form ref="formRef" label-width="auto" label-position="top" :model="form" :rules="rules">
+                    <div class="pd-16 border-bottom">
+                        <el-form-item prop="items">
+                            <div class="flex1 ov-hd">
+                                <template v-if="!form.items.length">
+                                    <add-btn @click="showSelectItems = true" content="选择检测项目"></add-btn>
+                                </template>
+                                <template v-else>
+                                    <div class="d-flex mb-10">
+                                        <div class="flex1 ov-hd f-s-16 c-333 f-w-5">已选择{{ form.items.length }}项检测项目,合计{{ totalCountPrice }}元</div>
+                                        <div>
+                                            <el-button @click="addItems" type="primary" text>
+                                                <el-icon><Plus /></el-icon>
+                                                继续添加
+                                            </el-button>
+                                            <el-button @click="clearItemsAdd" type="danger" text>
+                                                <el-icon><DeleteFilled /></el-icon>
+                                                清空重选
+                                            </el-button>
+                                        </div>
+                                    </div>
+                                    <vxe-table ref="tableRightRef" border :data="form.items" :column-config="{ resizable: true }">
+                                        <vxe-column type="seq" width="60" title="序号" align="center" />
+                                        <vxe-column title="检测项目" align="center" field="name" min-width="100" :formatter="colNoData" />
+                                        <vxe-column title="单价(元、批次)" align="center" field="price" min-width="100" :formatter="colNoData" />
+                                        <vxe-column title="备注" align="center" field="description" min-width="100" :formatter="colNoData" />
+                                        <vxe-column title="操作" align="center">
+                                            <template #default="{ $rowIndex }">
+                                                <el-button type="danger" @click="deleteRow($rowIndex)" text>删除</el-button>
+                                            </template>
+                                        </vxe-column>
+                                    </vxe-table>
+                                </template>
+                            </div>
+                        </el-form-item>
+                    </div>
+                    <div class="pd-16 border-bottom ov-hd">
+                        <div class="info-title mb-10">套餐基本信息</div>
+                        <el-row :gutter="90">
+                            <el-col :span="8">
+                                <el-form-item label="套餐名称" prop="name">
+                                    <el-input v-model="form.name" clearable placeholder="请输入套餐名称"></el-input>
+                                </el-form-item>
+                            </el-col>
+                            <el-col :span="8">
+                                <el-form-item label="套餐类型" prop="publicFlag">
+                                    <el-select v-model="form.publicFlag" clearable placeholder="请选择套餐类型">
+                                        <el-option v-for="item in dm_package_type" :key="item.value" :label="item.label" :value="item.value"></el-option>
+                                    </el-select>
+                                </el-form-item>
+                            </el-col>
+                            <el-col :span="8">
+                                <el-form-item label="制定规则" prop="permitType">
+                                    <el-radio-group v-model="form.permitType" @change="changePermitType">
+                                        <el-radio v-for="item in dm_permit_type" :key="item.value" :label="item.value">{{ item.label }}</el-radio>
+                                    </el-radio-group>
+                                </el-form-item>
+                            </el-col>
+                            <template v-if="form.permitType === '1'">
+                                <el-col :span="8">
+                                    <el-form-item label="适用类型" prop="permit">
+                                        <el-select v-model="form.permit" @change="changePermit" clearable multiple placeholder="请选择适用类型">
+                                            <el-option v-for="item in vip_level" :key="item.value" :label="item.label" :value="item.value"></el-option>
+                                        </el-select>
+                                    </el-form-item>
+                                </el-col>
+                            </template>
+                            <template v-if="form.permitType === '2'">
+                                <el-col :span="8">
+                                    <el-form-item label="适用企业" prop="permitCpys">
+                                        <SearchSelect v-model="form.permitCpys" :limit="1" :params="{ reviewStatus: '1' }" @changeItem="changeItemCpy"></SearchSelect>
+                                    </el-form-item>
+                                </el-col>
+                            </template>
+                            <el-col :span="8">
+                                <el-form-item label="检测周期" prop="period">
+                                    <el-input v-model.number="form.period" clearable placeholder="请输入检测周期">
+                                        <template #suffix>个工作日</template>
+                                    </el-input>
+                                </el-form-item>
+                            </el-col>
+                            <el-col :span="8">
+                                <el-form-item label="上架数量" prop="totalCount">
+                                    <el-input v-model.number="form.totalCount" clearable placeholder="请输入上架数量"></el-input>
+                                </el-form-item>
+                            </el-col>
+                            <el-col :span="8">
+                                <el-form-item label="执行标准" prop="standard">
+                                    <el-input type="textarea" v-model="form.standard" placeholder="请输入执行标准" :rows="4" show-word-limit maxlength="100"></el-input>
+                                </el-form-item>
+                            </el-col>
+                            <el-col :span="8">
+                                <el-form-item label="送样信息" prop="description">
+                                    <el-input type="textarea" v-model="form.description" placeholder="请输入送样信息" :rows="4" show-word-limit maxlength="100"></el-input>
+                                </el-form-item>
+                            </el-col>
+                            <el-col :span="8">
+                                <el-form-item label="购买须知" prop="purchaseNotes">
+                                    <el-input type="textarea" v-model="form.purchaseNotes" placeholder="请输入购买须知" :rows="4" show-word-limit maxlength="100"></el-input>
+                                </el-form-item>
+                            </el-col>
+                            <el-col :span="8">
+                                <el-form-item label="失效日期" prop="validUntil">
+                                    <el-date-picker v-model="form.validUntil" value-format="YYYY-MM-DD" type="date" clearable placeholder="请选择失效日期"></el-date-picker>
+                                </el-form-item>
+                            </el-col>
+                            <el-col :span="8">
+                                <el-form-item label="封面图" prop="coverImg">
+                                    <imageUpload v-model="form.coverImg" :limit="1" isString :isShowTip="false"></imageUpload>
+                                </el-form-item>
+                            </el-col>
+                            <el-col :span="8">
+                                <el-form-item label="产品图" prop="proImg">
+                                    <imageUpload v-model="form.proImg" :limit="5" isString :isShowTip="false"></imageUpload>
+                                </el-form-item>
+                            </el-col>
+                        </el-row>
+                    </div>
+                    <div v-if="form.permit?.length" class="pd-16">
+                        <div class="info-title mb-10">套餐价格确认</div>
+                        <vxe-table ref="tableRightRef" border :data="form.priceDetail" :column-config="{ resizable: true }">
+                            <vxe-column type="seq" width="60" title="序号" align="center" />
+                            <vxe-column v-if="form.permitType === '2'" title="企业名称" align="center" min-width="100" :formatter="colNoData">
+                                <template #default="{ row, $rowIndex }">{{ row?.cpyName || form?.permitCpyNames[$rowIndex] || '-' }}</template>
+                            </vxe-column>
+                            <vxe-column title="适用类型" align="center" field="memberLevelName" min-width="100" :formatter="colNoData" />
+                            <vxe-column title="折扣标准" align="center" min-width="100">
+                                <template #default="{ row }">{{ NP.times(row?.memberDiscount, 10) }}折</template>
+                            </vxe-column>
+                            <vxe-column title="折扣后售价" align="center" field="name" min-width="100">
+                                <template #default="{ row }">{{ NP.times(row?.originalPrice, row?.memberDiscount) }}元</template>
+                            </vxe-column>
+                            <vxe-column title="最终售价" align="center" min-width="100">
+                                <template #default="{ row }">
+                                    <el-input v-model="row.price" clearable placeholder="请输入最终售价">
+                                        <template #suffix>元</template>
+                                    </el-input>
+                                </template>
+                            </vxe-column>
+                        </vxe-table>
+                    </div>
+                </el-form>
+            </div>
+            <div class="d-flex a-c j-c pd-16">
+                <el-button @click="router.go(-1)">取消</el-button>
+                <el-button @click="save" color="#0079fe">保存</el-button>
+                <el-button @click="submitGround" type="warning">立即上架</el-button>
+            </div>
+        </div>
+    </div>
+    <TransferItems ref="TransferItemsRef" v-model:show="showSelectItems" @change="changeItems"></TransferItems>
+</template>
+
+<script setup name="Menus-form" lang="ts">
+import { AddBtn, SearchSelect } from '@/views/models';
+import { TransferItems } from '../../models';
+import NP from 'number-precision';
+import { colNoData } from '@/utils/noData';
+import { vipDiscountList } from '@/api/cdt/discount';
+import { debounce } from 'lodash';
+import { addTestPackage, editTestPackage, getTestPackage, getTestPackageItems, testPackageSale } from '@/api/cdt/menus';
+const { query }: any = useRoute();
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { dm_package_type, dm_permit_type, vip_level } = toRefs<any>(proxy?.useDict('dm_package_type', 'dm_permit_type', 'vip_level'));
+// 字典
+const TransferItemsRef = ref<any>();
+const router = useRouter();
+const showSelectItems = ref(false);
+const form = ref<any>({
+   items: [],
+   priceDetail: [],
+   permitCpys: []
+});
+const rules = reactive<any>({
+    items: [
+        { required: true,  type: 'array', message: '请选择检测项目', trigger: 'change' },
+    ],
+    name: [
+        { required: true, message: '请输入套餐名称', trigger: 'blur' }
+    ],
+    publicFlag: [
+        { required: true, message: '请选择套餐类型', trigger: 'change' }
+    ],
+    permitType: [
+        { required: true, message: '请选择制定规则', trigger: 'change' }
+    ],
+    permit: [
+        { required: true, message: '请选择适用类型', trigger: 'change' }
+    ],
+    permitCpys: [
+        {type: 'array', required: true, message: '请选择适用企业', trigger: 'change' }
+    ],
+    period: [
+        { required: true, message: '请输入检测周期', trigger: 'blur' }
+    ],
+    totalCount: [
+        { required: true, message: '请输入上架数量', trigger: 'blur' }
+    ],
+    standard: [
+        { required: true, message: '请输入执行标准', trigger: 'blur' }
+    ],
+    description: [
+        { required: true, message: '请输入送样信息', trigger: 'blur' }
+    ],
+    purchaseNotes: [
+        { required: true, message: '请输入购买须知', trigger: 'blur' }
+    ],
+    validUntil: [
+        { required: true, message: '请选择失效日期', trigger: 'change' }
+    ],
+    coverImg: [
+        { required: true, message: '请上传封面图', trigger: 'blur' }
+    ],
+    proImg: [
+        { required: true, message: '请上传产品图', trigger: 'blur' }
+    ]
+});
+const formRef = ref<any>();
+const save = debounce(() => {
+   submitSever(0)
+}, 500)
+const submitGround = debounce(async () => {
+   await formRef.value.validate()
+   submitSever(1)
+}, 500)
+const submitSever = async (type) => {
+   proxy.$modal.loading('提交中...');
+   const params = {
+        ...form.value,
+        testItemIds: form.value.items.map((item: any) => item.id),
+        priceDetail: form.value.priceDetail.map((item: any) => {
+            return {
+                memberLevel: item.memberLevel,
+                memberDiscount: item.memberDiscount,
+                originalPrice: item.originalPrice,
+                memberLevelName: item.memberLevelName,
+                price: item.price
+            }
+        })
+   }
+   delete params.items;
+   let res = null;
+   if (form.value.id) {
+       res = await editTestPackage(params).finally(() => {
+           proxy.$modal.closeLoading();
+       });
+   } else {
+       res = await addTestPackage(params).finally(() => {
+           proxy.$modal.closeLoading();
+       });
+   }
+   if (!res || res.code !== 200) return;
+   if (type) {
+    await testPackageSale(res.data?.id || form.value?.id);
+   }
+    proxy.$modal.msgSuccess('提交成功!')
+    router.go(-1);
+};
+// 合计价格
+const totalCountPrice = computed(() => {
+    return form.value.items.reduce((total: number, item: any) => {
+        return NP.plus(total, item.price);
+    }, 0);
+});
+const changeItems = (val: any) => {
+    form.value.items = val;
+    const standards = val.map((item: any) => item.standardForPackage).join(',').split(',');
+    // 去重
+    form.value.standard = Array.from(new Set(standards)).join(',');
+    changePermitType();
+};
+const changeItemCpy = (val: any[]) => {
+   const permits = val.map((item: any) => item.vipLevel || '0');
+    form.value.permit = [...permits];
+    changePermitCpys(val);
+};
+// 继续添加方法
+const addItems = () => {
+    // 保留之前选中
+    TransferItemsRef.value?.setSelectItems(form.value.items);
+    showSelectItems.value = true;
+};
+// 清空重选方法
+const clearItemsAdd = () => {
+    form.value.items = [];
+    TransferItemsRef.value?.clearAll();
+    showSelectItems.value = true;
+};
+// 删除行
+const deleteRow = (index: number) => {
+    form.value.items.splice(index, 1);
+};
+const discounts = ref<any>([]);
+const getDiscount = async () => {
+    // 获取折扣
+    const res = await vipDiscountList({ pageSize: 1000, pageNum: 1 });
+    if (!res || res.code !== 200) return;
+    discounts.value = res.rows;
+};
+const changePermit = (val: string[]) => {
+    form.value.priceDetail = discounts.value.filter(({ level }) => val.includes(level + '')).map(item => {
+        return {
+            ...item,
+            memberLevel: item.level,
+            memberDiscount: item.discount,
+            memberLevelName: item.name,
+            originalPrice: totalCountPrice.value,
+            price: NP.times(item.discount, totalCountPrice.value) || undefined
+        };
+    });
+};
+const changePermitCpys = (vals: any[]) => {
+    form.value.priceDetail = vals.map(item => {
+        const discountItem = getDiscountItem(item.vipLevel || '0');
+        return {
+            cpyName: item.cpyName,
+            memberLevelName: discountItem?.name,
+            memberLevel: discountItem?.level,
+            memberDiscount: discountItem?.discount,
+            originalPrice: totalCountPrice.value,
+            price: NP.times(discountItem?.discount, totalCountPrice.value) || undefined
+        };
+    });
+};
+// 获取discountsItem
+const getDiscountItem = (val: string) => {
+    return discounts.value.find(item => +item.level === +val);
+};
+const changePermitType = () => {
+    form.value.permit = [];
+    form.value.permitCpys = [];
+    form.value.priceDetail = [];
+};
+const getDetail = async () => {
+    proxy?.$modal.loading('加载中...');
+    const res = await getTestPackage(query?.id).finally(() => {
+        proxy?.$modal.closeLoading();
+    });
+    if (!res || res.code !== 200) return;
+    form.value = {
+        ...res.data,
+        items: [],
+    };
+    getItems();
+};
+const getItems = async () => {
+    const res = await getTestPackageItems({
+        pageNum: 1,
+        pageSize: 1000,
+        packageId: query?.id
+    });
+    if (!res || res.code !== 200) return;
+    form.value.items = res.rows;
+};
+onMounted(() => {
+    getDiscount();
+    if (query?.id) {
+        getDetail();
+    }
+});
+</script>

+ 155 - 19
src/views/cdt/menus/index.vue

@@ -4,8 +4,8 @@
             <div class="pd-16 pb-4 border-bottom">
                 <div class="f-s-20 c-333 f-w-7 mb-16">套餐管理</div>
                 <div class="d-flex">
-                    <div class="">
-                        <el-button type="primary">新增套餐</el-button>
+                    <div class="mr-40">
+                        <el-button @click="router.push({ path: 'menus-form' })" type="primary">新增套餐</el-button>
                     </div>
                     <div class="flex1 ov-hd d-flex j-ed">
                         <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="auto">
@@ -14,16 +14,14 @@
                             </el-form-item>
                             <el-form-item label="套餐状态" prop="status">
                                 <el-select style="width: 160px" v-model="queryParams.status" clearable placeholder="请选择套餐状态" @change="handleQuery">
-                                    <el-option label="全部" value=""></el-option>
-                                    <el-option label="启用" value="1"></el-option>
-                                    <el-option label="停用" value="0"></el-option>
+                                    <el-option label="未上架" value="0"></el-option>
+                                    <el-option label="在售" value="1"></el-option>
+                                    <el-option label="已下架" value="2"></el-option>
                                 </el-select>
                             </el-form-item>
-                            <el-form-item label="适用对象" prop="applyType">
-                                <el-select style="width: 160px" v-model="queryParams.applyType" clearable placeholder="请选择适用对象" @change="handleQuery">
-                                    <el-option label="全部" value=""></el-option>
-                                    <el-option label="个人" value="1"></el-option>
-                                    <el-option label="企业" value="2"></el-option>
+                            <el-form-item label="制定规则" prop="permitType">
+                                <el-select style="width: 160px" v-model="queryParams.permitType" clearable placeholder="请选择制定规则" @change="handleQuery">
+                                    <el-option v-for="item in dm_permit_type" :key="item.value" :label="item.label" :value="item.value"></el-option>
                                 </el-select>
                             </el-form-item>
                             <el-form-item label="创建人" prop="createByName">
@@ -37,35 +35,106 @@
                     </div>
                 </div>
             </div>
-            <div class="flex1 ov-hd pd-16">
-                <vxe-table :loading="loading" border :data="list" min-height="0" max-height="100%">
-                    <!-- 序号 -->
-                    <vxe-column type="seq" width="60" title="序号" align="center" />
-                </vxe-table>
+            <div class="flex1 ov-hd pd-16 d-flex flex-cln">
+                <searchTabs v-model="queryParams.publicFlag" @change="handleQuery" :list="tabs" key-label="name" key-value="type" key-count="num"></searchTabs>
+                <div class="pd-8"></div>
+                <div class="flex1 ov-hd">
+                    <vxe-table :loading="loading" border :data="list" min-height="0" max-height="100%">
+                        <!-- 序号 -->
+                        <vxe-column type="seq" fixed="left" width="60" title="序号" align="center" />
+                        <vxe-column title="套餐名称" fixed="left" align="center" field="name" min-width="100" :formatter="colNoData" />
+                        <vxe-column title="适用对象" field="applyType" min-width="100">
+                            <template #default="{ row }">
+                                <view class="d-flex flex-cln">
+                                    <template v-for="(item, index) in row?.priceDetail" :key="index">
+                                        <view>
+                                            {{ item?.memberLevelName }}-{{ NP.times(item?.memberDiscount, 10) }}折
+                                            <span class="c-333 f-w-5">({{ item?.price }})</span>
+                                        </view>
+                                    </template>
+                                </view>
+                            </template>
+                        </vxe-column>
+                        <vxe-column title="检测项目" min-width="140">
+                            <template #default="{ row }">
+                                <view v-if="row?.items">
+                                    {{ row?.items }}
+                                    <el-button @click="queryRowItems(row)" type="primary" text>点击查看详情{{ '>' }}</el-button>
+                                </view>
+                                <view v-else>-</view>
+                            </template>
+                        </vxe-column>
+                        <vxe-column title="检测周期" width="120">
+                            <template #default="{ row }">{{ row?.period }}个工作日</template>
+                        </vxe-column>
+                        <vxe-column title="上架数量" field="totalCount" width="80" :formatter="colNoData"></vxe-column>
+                        <vxe-column title="销量" field="orderedCount" width="80" :formatter="colNoData"></vxe-column>
+                        <vxe-column title="套餐状态" align="center" width="90" fixed="right">
+                            <template #default="{ row }">
+                                <span v-if="+row?.status === 0" class="c-danger">未上架</span>
+                                <span v-else-if="+row?.status === 1" class="c-primary">在售</span>
+                                <span v-else-if="+row?.status === 2" class="c-999">已下架</span>
+                            </template>
+                        </vxe-column>
+                        <vxe-column title="操作" fixed="right" width="260">
+                            <template #default="{ row }">
+                                <template v-if="+row?.status === 0">
+                                    <el-button @click="putaway(row)" text type="primary">上架</el-button>
+                                    <span></span>
+                                    <el-button @click="router.push({ path: 'menus-form', query: { id: row?.id } })" text type="primary">编辑</el-button>
+                                </template>
+                                <template v-if="+row?.status === 1">
+                                    <el-button @click="soldOut(row)" text type="danger">下架</el-button>
+                                    <span></span>
+                                    <el-button text type="primary">分享</el-button>
+                                </template>
+                                <template v-if="+row?.status === 2">
+                                    <el-button @click="putaway(row)" text type="primary">上架</el-button>
+                                    <span></span>
+                                    <el-button text style="color: #999;" disabled>分享</el-button>
+                                </template>
+                                <span></span>
+                                <el-button @click="copyItem(row)" text type="primary">复制</el-button>
+                                <span></span>
+                                <el-button @click="router.push({ path: 'menus-detail', query: { id: row?.id } })" text type="primary">详情</el-button>
+                            </template>
+                        </vxe-column>
+                    </vxe-table>
+                </div>
             </div>
             <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
         </div>
     </div>
+    <rowItems v-if="showRowItems" v-model:show="showRowItems" :packageId="rowId"></rowItems>
 </template>
 <script setup name="Menus" lang="ts">
-import { testPackageList } from '@/api/cdt/menus';
+import { copyTestPackage, testPackageList, testPackageListCount, testPackageSale, testPackageUnSale } from '@/api/cdt/menus';
 import { colNoData } from '@/utils/noData';
+import { searchTabs } from '@/views/models';
+import NP from 'number-precision';
+import { rowItems } from '../models';
+import { debounce } from 'lodash';
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { dm_package_type, dm_permit_type, vip_level } = toRefs<any>(proxy?.useDict('dm_package_type', 'dm_permit_type', 'vip_level'));
+const router = useRouter();
 const queryParams = ref<any>({
     pageNum: 1,
-    pageSize: 10
+    pageSize: 10,
+    publicFlag: '1'
 });
 const queryFormRef = ref<any>();
+const showRowItems = ref(false);
 const loading = ref(false);
 const total = ref(0);
 const list = ref<any>([]);
-const getList = async () => {
+const getList = debounce(async () => {
     loading.value = true;
     const res = await testPackageList(queryParams.value);
     if (!res || res.code !== 200) return;
     list.value = res.rows;
     total.value = res.total;
     loading.value = false;
-};
+}, 500);
 const handleQuery = () => {
     queryParams.value.pageNum = 1;
     getList();
@@ -74,7 +143,74 @@ const resetQuery = () => {
     queryFormRef.value?.resetFields();
     handleQuery();
 };
+// 获取tabs统计数据
+const tabs = ref<any[]>([]);
+const getTabsCount = debounce(async () => {
+    const res = await testPackageListCount({ ...queryParams.value });
+    if (!res || res.code !== 200) return;
+    // this.tabsList = res.rows;
+    tabs.value = res.data;
+}, 500);
+// 上架套餐
+const putaway = async (row: any) => {
+    console.log(row);
+    ElMessageBox({
+        title: '上架提示',
+        showCancelButton: true,
+        confirmButtonText: '确认上架',
+        cancelButtonText: '取消',
+        message: h('p', null, [h('div', null, `是否确认上架:${row?.name}`), h('div', { style: 'color: #2A6D52;' }, '上架后,即可分享并在小程序上显示该套餐!')]),
+        callback: async (action: string) => {
+            if (action === 'confirm') {
+                proxy.$modal.loading('上架中...');
+                const res = await testPackageSale(row?.id).finally(() => proxy.$modal.closeLoading());
+                if (!res || res.code !== 200) return;
+                proxy.$modal.msgSuccess('上架成功!');
+                getList();
+            }
+        }
+    });
+};
+// 下架套餐
+const soldOut = async (row: any) => {
+    console.log(row);
+    ElMessageBox({
+        title: '下架提示',
+        showCancelButton: true,
+        confirmButtonText: '确认下架',
+        cancelButtonText: '取消',
+        message: h('p', null, [h('div', null, `是否确认下架:${row?.name}`), h('div', { style: 'color: #F56C6C;' }, '下架后,该套餐将不再小程序上显示,已购买企业不受影响!')]),
+        callback: async (action: string) => {
+            if (action === 'confirm') {
+                proxy.$modal.loading('下架中...');
+                const res = await testPackageUnSale(row?.id).finally(() => proxy.$modal.closeLoading());
+                if (!res || res.code !== 200) return;
+                proxy.$modal.msgSuccess('下架成功!');
+                getList();
+            }
+        }
+    });
+};
+// 复制套餐
+const copyItem = async (row: any) => {
+    console.log(row);
+    proxy.$modal.loading('复制中...');
+    const res = await copyTestPackage(row?.id).finally(() => proxy.$modal.closeLoading());
+    if (!res || res.code !== 200) return;
+    proxy.$modal.msgSuccess('复制成功!');
+    getList();
+};
+const rowId = ref('');
+const queryRowItems = (row: any) => {
+    rowId.value = row?.id;
+    showRowItems.value = true;
+};
 onMounted(() => {
+    getTabsCount();
+    getList();
+});
+onActivated(() => {
+    getTabsCount();
     getList();
 });
 </script>

+ 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>

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

@@ -0,0 +1,7 @@
+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'; // 上传发票

+ 93 - 0
src/views/cdt/models/menuInfo.vue

@@ -0,0 +1,93 @@
+<template>
+    <div class="flex1 over-auto">
+        <div class="pd-16">
+            <div class="info-title">套餐基本信息</div>
+            <el-descriptions :column="4">
+                <el-descriptions-item label="套餐名称:">{{ form?.name || '-' }}</el-descriptions-item>
+                <el-descriptions-item label="套餐类型:">{{ selectDictLabel(dm_package_type, form?.publicFlag) || '-' }}</el-descriptions-item>
+                <el-descriptions-item label="制定规则:">{{ selectDictLabel(dm_permit_type,form?.permitType ) }}</el-descriptions-item>
+                <el-descriptions-item v-if="form?.permitType === '1'" label="适用类型:">{{ selectDictLabels(vip_level, form?.permit, ',') || '-' }}</el-descriptions-item>
+                <el-descriptions-item v-if="form?.permitType === '2'" label="适用企业:">{{ form?.permitCpyNames.toString()|| '-' }}</el-descriptions-item>
+                <el-descriptions-item label="检测周期:">{{ form?.period || '-' }}天</el-descriptions-item>
+                <el-descriptions-item label="上架数量:">{{ form?.totalCount || '-' }}</el-descriptions-item>
+                <el-descriptions-item label="执行标准:">{{ form?.standard || '-' }}</el-descriptions-item>
+                <el-descriptions-item label="送样信息:" :span="2">{{ form?.description || '-' }}</el-descriptions-item>
+                <el-descriptions-item label="购买须知:" :span="2">{{ form?.purchaseNotes || '-' }}</el-descriptions-item>
+                <el-descriptions-item label="失效日期:">{{ form?.validUntil || '-' }}</el-descriptions-item>
+            </el-descriptions>
+            <el-divider />
+            <el-descriptions :column="4" direction="vertical">
+                <el-descriptions-item label="封面图:">
+                    <el-image style="width: 100px; height: 100px" :src="form?.coverImg" :zoom-rate="1.2" :max-scale="7" :min-scale="0.2" :preview-src-list="[form?.coverImg]" fit="cover"></el-image>
+                </el-descriptions-item>
+                <el-descriptions-item v-if="form?.proImg" label="产品图:">
+                    <template v-for="(item, index) in form?.proImg.split(',')" :key="index">
+                        <el-image style="width: 100px; height: 100px;margin-right: 12px;" :src="item" fit="cover" :zoom-rate="1.2" :max-scale="7" :min-scale="0.2" :initial-index="index" :preview-src-list="[item]"></el-image>
+                    </template>
+                </el-descriptions-item>
+            </el-descriptions>
+            <el-divider />
+            <div class="info-title mb-16 d-flex">
+                <span>检测项目明细</span>
+                <span class="f-s-14">(共{{itemsInfo?.length}}项, 合计{{ totalCountPrice }}元)</span>
+            </div>
+            <vxe-table ref="tableRightRef" border :data="itemsInfo" :column-config="{ resizable: true }">
+                <vxe-column type="seq" width="60" title="序号" align="center" />
+                <vxe-column title="检测项目" align="center" field="name" min-width="100" :formatter="colNoData" />
+                <vxe-column title="单价(元、批次)" align="center" field="price" min-width="100" :formatter="colNoData" />
+                <vxe-column title="备注" align="center" field="description" min-width="100" :formatter="colNoData" />
+            </vxe-table>
+        </div>
+    </div>
+</template>
+<script setup lang="ts">
+import { propTypes } from '@/utils/propTypes';
+
+import NP from 'number-precision';
+import { colNoData } from '@/utils/noData';
+import { debounce } from 'lodash';
+import { getTestPackage, getTestPackageItems } from '@/api/cdt/menus';
+const props = defineProps({
+    info: propTypes.any.def({})
+});
+const { query }: any = useRoute();
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { dm_package_type, dm_permit_type, vip_level } = toRefs<any>(proxy?.useDict('dm_package_type', 'dm_permit_type', 'vip_level'));
+const router = useRouter();
+const form = ref<any>({
+   items: [],
+   priceDetail: [],
+   permitCpys: [],
+});
+const getDetail = async () => {
+    proxy?.$modal.loading('加载中...');
+    const res = await getTestPackage(props?.info?.packageId).finally(() => {
+        proxy?.$modal.closeLoading();
+    });
+    if (!res || res.code !== 200) return;
+    console.log(res);
+    form.value = {
+        ...res.data
+    };
+};
+const itemsInfo = ref<any>([]);
+const getItems = async () => {
+    const res = await getTestPackageItems({
+        pageNum: 1,
+        pageSize: 1000,
+        packageId: props?.info?.packageId
+    });
+    if (!res || res.code !== 200) return;
+    itemsInfo.value = res.rows;
+};
+// 合计价格
+const totalCountPrice = computed(() => {
+    return itemsInfo.value.reduce((total: number, item: any) => {
+        return NP.plus(total, item.price);
+    }, 0);
+});
+onMounted(() => {
+    getDetail()
+    getItems()
+});
+</script>

+ 256 - 0
src/views/cdt/models/orderInfo.vue

@@ -0,0 +1,256 @@
+<template>
+    <div v-if="info" class="flex1 ov-hd d-flex flex-cln pd-16">
+        <div class="info-title f-w-5 mb-15">订单信息</div>
+        <vxe-table ref="vxeTableRef" border :data="info?.testOrderDetailList" :merge-cells="mergeCells">
+            <vxe-column title="订单号" fixed="left" width="120">
+                <template #default>{{ info?.orderNo }}</template>
+            </vxe-column>
+            <vxe-column title="企业名称" min-width="100">
+                <template #default>
+                    <div>{{ info?.cpyName }}</div>
+                    <div class="c-999 f-s-12">{{ info?.creditCode }}</div>
+                </template>
+            </vxe-column>
+            <vxe-column title="会员级别" width="100">
+                <template #default>
+                    {{ info?.vipLevelName }}
+                </template>
+            </vxe-column>
+            <vxe-column title="套餐详情" min-width="100">
+                <template #default="{ row }">
+                    {{ row?.packageName }}
+                </template>
+            </vxe-column>
+            <vxe-column title="购买数量" width="90">
+                <template #default="{ row }">
+                    {{ row?.quantity }}
+                </template>
+            </vxe-column>
+            <vxe-column title="实收款" width="100">
+                <template #default="{ row }">{{ row?.amount }}元</template>
+            </vxe-column>
+            <vxe-column title="下单人" min-width="100">
+                <template #default>
+                    <div>{{ info?.createName }}</div>
+                </template>
+            </vxe-column>
+            <vxe-column title="下单时间" min-width="100">
+                <template #default>
+                    <div>{{ info?.createTime }}</div>
+                </template>
+            </vxe-column>
+            <vxe-column title="订单状态" width="100">
+                <template #default>
+                    <div>{{ selectDictLabel(test_order_status_bg, info?.status) }}</div>
+                </template>
+            </vxe-column>
+        </vxe-table>
+        <div class="pd-8"></div>
+        <div class="d-flex flex1 ov-hd">
+            <div class="left-step-box" style="width: 230px">
+                <div class="steps-box">
+                    <template v-for="(item, index) in info?.logList" :key="index">
+                        <div class="steps-item d-flex">
+                            <div class="pr-16 steps-icon-box p-rtv">
+                                <div class="steps-line"></div>
+                                <el-icon class="steps-icon"><CircleCheckFilled /></el-icon>
+                            </div>
+                            <div class="steps-content d-flex flex-cln flex1 ov-hd">
+                                <view class="f-s-16 c-666 mb-5">{{ item.operationDescription }}</view>
+                                <view class="steps-desc">{{ item?.operationTime }}</view>
+                            </div>
+                        </div>
+                    </template>
+                </div>
+            </div>
+            <div class="flex1 over-auto">
+                <div class="pd-16 ov-hd" style="box-sizing: border-box;">
+                    <template v-if="+info.status >= 6 && info?.status !== '90'">
+                        <el-descriptions title="发票信息" :column="3">
+                            <el-descriptions-item label="上传人:">{{ info?.invoiceLog?.operator }}</el-descriptions-item>
+                            <el-descriptions-item label="上传时间:">{{ info?.invoiceLog?.operationTime }}</el-descriptions-item>
+                            <el-descriptions-item v-if="info?.invoice" label="发票:">
+                                <FileLook v-model="info.invoice" isObject></FileLook>
+                            </el-descriptions-item>
+                        </el-descriptions>
+                        <el-divider />
+                    </template>
+                    <template v-if="+info.status >= 5 && info?.status !== '90'">
+                        <el-descriptions title="检测报告" :column="4">
+                            <el-descriptions-item label="上传人:">{{ info?.reportLog?.operator}}</el-descriptions-item>
+                            <el-descriptions-item label="上传时间:">{{ info?.reportLog?.operationTime }}</el-descriptions-item>
+                            <el-descriptions-item label="检测单位:">{{ info?.testOrg }}</el-descriptions-item>
+                            <el-descriptions-item v-if="info?.report" class-name="zy-desc" label="检测报告:">
+                                <FileLook v-model="info.report" isObject></FileLook>
+                            </el-descriptions-item>
+                        </el-descriptions>
+                        <el-divider />
+                    </template>
+                    <template v-if="+info.status >= 2 && info?.status !== '90' && info?.postInfo?.length">
+                        <div class="f-s-16 c-333 f-w-6 mb-16">物流信息</div>
+                        <div class="d-flex a-c mb-10">
+                            <img class="mr-10" style="width: 30px; height: 30px;" src="@/assets/images/sf_icon.png" />
+                            <div>
+                                <span class="f-s-14 mr-16">顺丰快递</span>
+                                <span class="c-999 f-s-12">{{ info?.postInfo[0]?.mailno }}</span>
+                            </div>
+                        </div>
+                        <el-steps direction="vertical" :active="1">
+                            <template v-for="(item, index) in info?.postInfo" :key="index">
+                                <el-step>
+                                    <template #icon>
+                                        <el-button v-if="item.opCode === '80'" type="primary" circle size="small">收</el-button>
+                                        <div class="dot" :class="{ ['active' + index]: true }"></div>
+                                    </template>
+                                    <template #title>
+                                        <span>{{ selectDictLabel(sf_router_code, item?.opCode) }}</span>
+                                        <span>{{ item?.acceptTime }}</span>
+                                    </template>
+                                    <template #description>
+                                        <div class="pt-10 pb-10">
+                                            <span class="f-s-14 c-666">[{{ item.acceptAddress }}]</span>
+                                            <span class="f-s-14 c-666">{{ item?.remark }}</span>
+                                        </div>
+                                    </template>
+                                </el-step>
+                            </template>
+                        </el-steps>
+                        <el-divider />
+                    </template>
+                    <template v-if="+info.status >= 2  && +info?.originStatus >= 2">
+                        <div class="f-s-16 c-333 f-w-6 mb-16">寄样信息</div>
+                        <div class="d-flex a-c">
+                            <div class="card-blcok pd-16 d-flex flex1">
+                                <div class="mr-16">
+                                    <el-button type="danger" circle>取</el-button>
+                                </div>
+                                <div class="flex1 ov-hd">
+                                    <div class="f-s-14 c-333 f-w-5 mb-5">{{ info?.sendAddress?.province + info?.sendAddress?.city + info?.sendAddress?.district + info?.sendAddress?.address }}</div>
+                                    <div class="f-s-14 c-999">
+                                        <span class="mr-30">{{ info?.sendAddress?.contactName }}</span>
+                                        <span>{{ info?.sendAddress?.contactPhone }}</span>
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="c-primary f-w-6 pd-20" style="font-size: 30px;">
+                                <el-icon><Right /></el-icon>
+                            </div>
+                            <div class="card-blcok pd-16 d-flex flex1">
+                                <div class="mr-16">
+                                    <el-button type="primary" circle>收</el-button>
+                                </div>
+                                <div class="flex1 ov-hd">
+                                    <div class="f-s-14 c-333 f-w-5 mb-5">{{ info?.orgAddress?.province + info?.orgAddress?.city + info?.orgAddress?.district + info?.orgAddress?.address }}</div>
+                                    <div class="f-s-14 c-999">
+                                        <span class="mr-30">{{ info?.orgAddress?.contactName }}</span>
+                                        <span>{{ info?.orgAddress?.contactPhone }}</span>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                        <div class="pd-8"></div>
+                        <el-descriptions :column="4">
+                            <el-descriptions-item label="取件时间:">{{ info?.reservationTime}}</el-descriptions-item>
+                        </el-descriptions>
+                        <el-divider />
+                    </template>
+
+                    <el-descriptions title="订单信息" :column="4">
+                        <el-descriptions-item label="订单号:">{{ info?.orderNo }}</el-descriptions-item>
+                        <el-descriptions-item label="下单人:">{{ info?.createName }}</el-descriptions-item>
+                        <el-descriptions-item label="下单时间:">{{ info?.createTime }}</el-descriptions-item>
+                        <el-descriptions-item label="收货地址:">{{ info?.reciveAddress?.province + info?.reciveAddress?.city + info?.reciveAddress?.district + info?.reciveAddress?.address }}</el-descriptions-item>
+                        <el-descriptions-item label="收件人:">{{ info?.reciveAddress?.contactName  }}</el-descriptions-item>
+                        <el-descriptions-item label="联系电话:">{{ info?.reciveAddress?.contactPhone  }}</el-descriptions-item>
+                    </el-descriptions>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+<script setup lang="ts">
+import { propTypes } from '@/utils/propTypes';
+import { VxeTablePropTypes } from 'vxe-table';
+import { colNoData } from '@/utils/noData';
+import { FileLook } from '@/views/models';
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { test_order_status_bg, sf_router_code } = toRefs<any>(proxy?.useDict('test_order_status_bg', 'sf_router_code'));
+const vxeTableRef = ref<any>(null);
+const list = ref<any[]>([]);
+const props = defineProps({
+    info: propTypes.any.def(null)
+});
+// 合并表格
+const mergeCells = ref<VxeTablePropTypes.MergeCells>([
+    { row: 0, col: 0, rowspan: 2, colspan: 0 },
+    { row: 0, col: 1, rowspan: 2, colspan: 0 },
+    { row: 0, col: 2, rowspan: 2, colspan: 0 },
+    { row: 0, col: 6, rowspan: 2, colspan: 0 },
+    { row: 0, col: 7, rowspan: 2, colspan: 0 },
+    { row: 0, col: 8, rowspan: 2, colspan: 0 }
+]);
+const steps = ref([
+    { label: '付款信息', name: '待寄样', value: '1' },
+    { label: '寄样信息', name: '待寄样', value: '2' },
+    { label: '物流信息', name: '已在途', value: '3' },
+    { label: '检测报告', name: '待开票', value: '4' },
+    { label: '发票信息', name: '已完成', value: '5' }
+]);
+</script>
+<style lang="scss" scoped>
+.card-blcok {
+    border: 1px solid #f2f2f2;
+    border-radius: 4px;
+}
+.dot {
+    width: 20px;
+    height: 20px;
+    background-color: #d7d7d7;
+    border-radius: 50%;
+
+    &.active0 {
+        background-color: #2A6D52;
+    }
+    &.active1 {
+        background-color: #333;
+    }
+}
+.zy-desc {
+    display: inline-block;
+}
+.left-step-box {
+    box-sizing: border-box;
+    padding: 16px;
+    border-right: 1px solid #d7d7d7;
+}
+
+.steps-box {
+    .steps-item {
+        cursor: pointer;
+
+        &:last-child {
+            .steps-line {
+                border-color: transparent;
+            }
+        }
+    }
+    .steps-desc {
+        margin-bottom: 20px;
+        color: #bbb;
+        font-size: 14px;
+    }
+    .steps-icon {
+        font-size: 22px;
+        color: #2c9049;
+    }
+
+    .steps-line {
+        position: absolute;
+        top: 22px;
+        left: 10px;
+        bottom: 0;
+        width: 1px;
+        border: 1px dashed #f4f4f4;
+    }
+}
+</style>

+ 88 - 0
src/views/cdt/models/rowItems.vue

@@ -0,0 +1,88 @@
+<template>
+    <vxe-modal v-model="dialogVisible" :title="title" show-zoom resize show-footer destroy-on-close transfer @hide="close" :width="width">
+        <template #default>
+            <div class="d-flex flex-cln" style="height: 60vh;">
+                <div class="flex1 ov-hd">
+                    <vxe-table :loading="loading" border :data="list">
+                        <vxe-column type="seq" width="60" title="序号" align="center" />
+                        <vxe-column title="检测项目" align="center" field="name" min-width="100" :formatter="colNoData" />
+                        <vxe-column title="单价(元、批次)" align="center" field="price" min-width="100" :formatter="colNoData" />
+                        <vxe-column title="备注" align="center" field="description" min-width="100" :formatter="colNoData" />
+                    </vxe-table>
+                </div>
+                <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+            </div>
+        </template>
+        <template #footer>
+            <el-button type="primary" @click="close">知道了</el-button>
+        </template>
+    </vxe-modal>
+</template>
+<script setup name="Pay-log" lang="ts">
+import { getTestPackageItems } from '@/api/cdt/menus';
+import { colNoData } from '@/utils/noData';
+import { propTypes } from '@/utils/propTypes';
+const emit = defineEmits(['update:show', 'close']);
+const props = defineProps({
+    packageId: propTypes.string.def(''),
+    title: propTypes.string.def('检测项目明细'),
+    width: propTypes.number.def(800),
+    show: propTypes.bool.def(false)
+})
+const dialogVisible = ref(false);
+const { query }: any = useRoute()
+const router = useRouter();
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const list = ref<any[]>([]);
+const loading = ref(true);
+const total = ref(0);
+const queryFormRef = ref<ElFormInstance>();
+const data = reactive<any>({
+    queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+    }
+});
+const { queryParams } = toRefs(data);
+
+/** 查询会员信息列表 */
+const getList = async () => {
+    loading.value = true;
+    const res = await getTestPackageItems({ ...queryParams.value, packageId: props?.packageId });
+    list.value = res.rows;
+    total.value = res.total;
+    loading.value = false;
+};
+
+/** 搜索按钮操作 */
+const handleQuery = (level?: any) => {
+    queryParams.value.pageNum = 1;
+    getList();
+};
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+    queryFormRef.value?.resetFields();
+    queryParams.value.startDate = '';
+    queryParams.value.endDate = '';
+    handleQuery();
+};
+
+const close = () => {
+    // formRef.value?.resetFields();
+    emit('update:show', false);
+    emit('close', false);
+};
+
+
+watch(
+    () => props.show,
+    (val) => {
+        dialogVisible.value = val;
+    },
+    { immediate: true }
+);
+onMounted(() => {
+    getList();
+});
+</script>

+ 233 - 0
src/views/cdt/models/transferItems.vue

@@ -0,0 +1,233 @@
+<template>
+    <vxe-modal v-model="dialogVisible" :title="title" show-zoom resize show-footer destroy-on-close transfer @hide="close" :width="width">
+        <template #default>
+            <div style="height: 80vh" class="d-flex flex-cln">
+                <div class="pd-16 border-bottom d-flex j-sb pb-4">
+                    <div>
+                        <searchTabs v-model="tabActive" :list="[{ label: '按检测项目', value: '1' }, { label: '按执行标准', value: '2' }]" :is-num="false"></searchTabs>
+                    </div>
+                    <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="auto">
+                        <el-form-item label="检测项名称:" prop="name">
+                            <el-input v-model="queryParams.name" placeholder="请输入检测项名称关键字" clearable style="width: 180px" @keyup.enter="handleQuery" />
+                        </el-form-item>
+                        <el-form-item>
+                            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+                            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+                        </el-form-item>
+                    </el-form>
+                </div>
+                <div class="flex1 ov-hd d-flex content-border">
+                    <div class="tree-wrap">
+                        <div v-show="tabActive === '1'">
+                            <el-tree ref="treeItemsRef" class="base-tree-tabs" auto-expand-parent default-expand-all node-key="id" :data="itemsData" :props="{ label: 'name' }" highlight-current @node-click="itemsClick" accordion>
+                                <template #default="{ node, data }">
+                                    <span>
+                                        {{ node.label }}
+                                        <template v-if="!data?.children">({{ data?.itemCount || '0' }})</template>
+                                    </span>
+                                </template>
+                            </el-tree>
+                        </div>
+                        <div v-show="tabActive === '2'">
+                            <el-tree ref="treeStandardsRef" class="base-tree-tabs" auto-expand-parent :data="standards" node-key="id" :props="{ label: 'name' }" highlight-current @node-click="standardsClick" accordion>
+                                <template #default="{ node, data }">
+                                    <span>
+                                        {{ node.label }}
+                                        <template v-if="!data?.children">({{ data?.itemCount || '0' }})</template>
+                                    </span>
+                                </template>
+                            </el-tree>
+                        </div>
+                    </div>
+                    <div class="flex1 ov-hd d-flex flex-cln">
+                        <div class="flex1 ov-hd">
+                            <vxe-table ref="tableLeftRef" :loading="loading" border :data="list" height="100%" :column-config="{ resizable: true }" :row-config="{keyField: 'id',isCurrent: true, isHover: true}" :checkbox-config="{ highlight: true, range: true, trigger: 'row', reserve: true }">
+                                <vxe-column type="checkbox" width="60"></vxe-column>
+                                <!-- 序号 -->
+                                <vxe-column type="seq" width="60" title="序号" align="center" />
+                                <vxe-column title="检测项目" align="center" field="name" min-width="100" :formatter="colNoData" />
+                                <vxe-column title="单价(元、批次)" align="center" field="price" min-width="100" :formatter="colNoData" />
+                            </vxe-table>
+                        </div>
+                    </div>
+                    <div class="d-flex j-c a-c flex-cln pd-10">
+                        <el-button type="primary" @click="transferRight">
+                            <el-icon>
+                                <DArrowRight />
+                            </el-icon>
+                        </el-button>
+                        <div class="pd-10"></div>
+                        <el-button type="primary" @click="transferLeft">
+                            <el-icon>
+                                <DArrowLeft />
+                            </el-icon>
+                        </el-button>
+                    </div>
+                    <div class="flex1 ov-hd d-flex flex-cln">
+                        <div class="flex1 ov-hd">
+                            <vxe-table ref="tableRightRef" :loading="loading" border :data="targetList" height="100%" :column-config="{ resizable: true }" :row-config="{ keyField: 'id', isCurrent: true, isHover: true }" :checkbox-config="{ highlight: true, range: true, trigger: 'row', reserve: true }">
+                                <vxe-column type="checkbox" width="60"></vxe-column>
+                                <vxe-column type="seq" width="60" title="序号" align="center" />
+                                <vxe-column title="检测项目" align="center" field="name" min-width="100" :formatter="colNoData" />
+                                <vxe-column title="单价(元、批次)" align="center" field="price" min-width="100" :formatter="colNoData" />
+                            </vxe-table>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </template>
+        <template #footer>
+            <el-button @click="close">取消</el-button>
+            <el-button type="primary" @click="submitForm">确认添加</el-button>
+        </template>
+    </vxe-modal>
+</template>
+
+<script setup name="transferItems" lang="ts">
+import { itemsList, itemsListByTree, itemsStandardList } from '@/api/cdt/items';
+import { colNoData } from '@/utils/noData';
+import { propTypes } from '@/utils/propTypes';
+import { searchTabs } from '@/views/models';
+import { FormInstance } from 'element-plus';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const dialogVisible = ref(false);
+const emit = defineEmits(['update:show', 'close', 'change']);
+const props = defineProps({
+    show: propTypes.bool.def(false),
+    title: propTypes.string.def('选择检测项目'),
+    width: propTypes.string.def('80vw'),
+    info: propTypes.any.def(null)
+});
+const treeItemsRef = ref<any>();
+const treeStandardsRef = ref<any>();
+const tabActive = ref('1');
+const queryParams = ref<any>({
+    pageNum: 1,
+    pageSize: 300,
+    name: '',
+    itemTypeId: '',
+    packageId: ''
+});
+const loading = ref(false);
+const total = ref(0);
+const list = ref<any>([]);
+const targetList = ref<any>([]);
+const itemsData = ref<any>([]);
+const getList = async () => {
+    loading.value = true;
+    const res = await itemsList(queryParams.value);
+    if (!res || res.code !== 200) return;
+    list.value = res.rows;
+    total.value = res.total;
+    loading.value = false;
+};
+const handleQuery = () => {
+    queryParams.value.pageNum = 1;
+    getList();
+};
+const resetQuery = () => {
+    queryParams.value = {
+        pageNum: 1,
+        pageSize: 10,
+        name: '',
+        itemTypeId: '',
+        packageId: ''
+    };
+    // 重置树高亮
+    treeItemsRef.value?.setCurrentKey(null, true);
+    treeStandardsRef.value?.setCurrentKey(null, true);
+    handleQuery();
+};
+const getTabsTree = async () => {
+    const res = await itemsListByTree();
+    if (!res || res.code !== 200) return;
+    itemsData.value = res.data;
+};
+const itemsClick = (data: any) => {
+    if (!data.children) {
+        queryParams.value.itemTypeId = data.id;
+        queryParams.value.packageId = '';
+    }
+    handleQuery();
+};
+const standardsClick = (data: any) => {
+    if (!data.children) {
+        queryParams.value.packageId = data.id;
+        queryParams.value.itemTypeId = '';
+    }
+    handleQuery();
+};
+// 查询执行标准列表
+const standards = ref<any>([]);
+const getStandardList = async () => {
+    const res = await itemsStandardList({ pageNum: 1, pageSize: 10000 });
+    if (!res || res.code !== 200) return;
+    standards.value = res.rows;
+};
+const formRef = ref<FormInstance>();
+const close = () => {
+    emit('update:show', false);
+    emit('close', false);
+};
+const submitForm = async () => {
+    try {
+        if (!targetList.value.length) {
+            // 抛出错误
+            return proxy.$modal.msgWarning('请选择检测项目');
+        }
+        emit('change', targetList.value);
+        close();
+    } catch (error) {
+        proxy.$modal.msgWarning(error);
+    }
+};
+const tableLeftRef = ref<any>();
+const tableRightRef = ref<any>();
+const transferRight = () => {
+    targetList.value = tableLeftRef.value?.getCheckboxReserveRecords(true).concat(tableLeftRef.value?.getCheckboxRecords());
+};
+const transferLeft = () => {
+    targetList.value = targetList.value.filter((item: any) => !tableRightRef.value?.getCheckboxRecords().includes(item));
+    tableLeftRef.value?.setCheckboxRow(tableRightRef.value?.getCheckboxRecords(), false);
+    tableRightRef.value.setCheckboxRow(tableRightRef.value?.getCheckboxRecords(), false);
+};
+// 清空全部已选
+const clearAll = () => {
+    tableLeftRef.value?.clearCheckboxRow();
+    tableRightRef.value?.clearCheckboxRow();
+    targetList.value = [];
+};
+// 设置已选
+const setSelectItems = (val: any) => {
+    targetList.value = val;
+    tableLeftRef.value?.setCheckboxRow(val, true);
+};
+watch(
+    () => props.show,
+    (val) => {
+        dialogVisible.value = val;
+        if (val) {
+            getTabsTree();
+            getStandardList();
+        }
+    },
+    { immediate: true }
+);
+defineExpose({
+    clearAll,
+    setSelectItems
+});
+</script>
+<style lang="scss" scoped>
+.tree-wrap {
+    width: 310px;
+    box-sizing: border-box;
+    border-right: 1px solid #ebeef5;
+    padding: 16px;
+}
+
+.content-border {
+    border: 1px solid var(--border-color);
+}
+</style>

+ 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>

+ 75 - 0
src/views/cdt/orders/detail/index.vue

@@ -0,0 +1,75 @@
+<template>
+    <div class="p-3">
+        <div class="bg-fff flex1 ov-hd d-flex flex-cln">
+            <div class="d-flex a-c pd-16">
+                <div class="f-s-20 c-333 f-w-7 mr-10">订单详情</div>
+                <el-button @click="router.go(-1)" type="primary" text>
+                    <el-icon>
+                        <Back />
+                    </el-icon>
+                    返回上一级
+                </el-button>
+            </div>
+            <div class="border-botttom pl-20">
+                <div class="d-flex">
+                    <template v-for="(item, index) in tabs" :key="index">
+                        <div class="tabs-item" @click="() => { activeName = item.value }" :class="{ checked: activeName === item.value }">{{ item.label }}</div>
+                    </template>
+                </div>
+            </div>
+            <template v-if="orderItem">
+                <div v-show="activeName === '1'" class="flex1 ov-hd d-flex flex-cln">
+                    <orderInfo :info="orderItem"></orderInfo>
+                </div>
+                <div v-show="activeName === '2'" class="flex1 ov-hd">
+                    <menuInfo :info="orderItem?.testOrderDetailList[0]"></menuInfo>
+                </div>
+            </template>
+        </div>
+    </div>
+</template>
+
+<script setup name="Member-detail" lang="ts">
+import { getTestOrderDetail } from '@/api/cdt/orders';
+import { orderInfo, menuInfo } from '../../models';
+const { query }:any = useRoute()
+const router = useRouter();
+const activeName = ref('1');
+// 获取详细信息
+const orderItem = ref<any>(null);
+const getDetail = async () => {
+    const res = await getTestOrderDetail(query?.orderId);
+    if (!res || res.code !== 200) return;
+    orderItem.value = res.data;
+};
+const tabs = ref([
+    { label: '订单信息', value: '1' },
+    { label: '套餐信息', value: '2' },
+])
+onMounted(() => {
+    getDetail()
+});
+</script>
+<style lang="scss" scoped>
+.tabs-item {
+    margin-right: 20px;
+   padding: 8px 20px;
+   font-size: 14px;
+   border-color:  #d7d7d7;
+   border-style: solid;
+   border-width: 1px 1px 0 1px;
+   cursor: pointer;
+   user-select: none;
+
+   &.checked {
+    color: #fff;
+    border-color: var(--el-color-primary);
+    background-color: var(--el-color-primary);
+   }
+}
+.border-botttom {
+    border-bottom: 1px solid #d7d7d7;
+}
+
+
+</style>

+ 167 - 0
src/views/cdt/orders/index.vue

@@ -0,0 +1,167 @@
+<template>
+    <div class="p-3">
+        <div class="bg-fff flex1 ov-hd d-flex flex-cln">
+            <div class="pd-16 border-bottom">
+                <div class="f-s-20 c-333 f-w-7 mb-12">订单管理</div>
+                <searchTabs v-model="queryParams.status" @change="handleQuery" :list="tabs"></searchTabs>
+            </div>
+            <div class="flex1 ov-hd pd-16 d-flex flex-cln">
+                <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="auto">
+                    <el-form-item label="订单号:" prop="orderNo">
+                        <el-input v-model="queryParams.orderNo" placeholder="搜订单号" clearable style="width: 180px" @keyup.enter="handleQuery" />
+                    </el-form-item>
+                    <el-form-item label="下单时间段" prop="dateRange">
+                        <div class="d-flex" style="width: 180px">
+                            <DateRange v-model="queryParams.dateRange" v-model:start-date="queryParams.startDate" v-model:end-date="queryParams.endDate" @change="handleQuery"></DateRange>
+                        </div>
+                    </el-form-item>
+                    <el-form-item label="套餐名称:" prop="packageName">
+                        <el-input v-model="queryParams.packageName" placeholder="请输入套餐名称关键字" clearable style="width: 160px" @keyup.enter="handleQuery" />
+                    </el-form-item>
+                    <el-form-item label="会员等级" prop="vipLevel">
+                        <el-select style="width: 160px" v-model="queryParams.vipLevel" clearable placeholder="请选择会员等级" @change="handleQuery">
+                            <el-option v-for="item in vip_level" :key="item.value" :label="item.label" :value="item.value"></el-option>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item label="买家名称:" prop="cpyName">
+                        <el-input v-model="queryParams.cpyName" placeholder="搜买家名称及关键字" clearable style="width: 160px" @keyup.enter="handleQuery" />
+                    </el-form-item>
+                    <el-form-item>
+                        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+                        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+                    </el-form-item>
+                </el-form>
+                <div class="flex1 ov-hd">
+                    <vxe-table :loading="loading" border :data="list" min-height="0" max-height="100%">
+                        <!-- 序号 -->
+                        <vxe-column type="seq" width="60" title="序号" fixed="left" align="center" />
+                        <vxe-column title="订单号" field="orderNo" fixed="left" min-width="100" :formatter="colNoData" />
+                        <vxe-column title="套餐名称" min-width="100">
+                            <template #default="{ row }">{{ row?.testOrderDetailList[0]?.packageName }}</template>
+                        </vxe-column>
+                        <vxe-column title="买家" field="cpyName" min-width="100" :formatter="colNoData" />
+                        <vxe-column title="会员等级" field="vipLevelName" min-width="100" :formatter="colNoData" />
+                        <vxe-column title="检测项目" min-width="200">
+                            <template #default="{ row }">
+                                <div>
+                                    {{ row?.testOrderDetailList[0]?.itemNames }}等{{ row?.testOrderDetailList[0]?.itemCount }}项
+                                    <el-button @click="queryRowItems(row?.testOrderDetailList[0])" type="primary" text>点击查看详情{{ '>' }}</el-button>
+                                </div>
+                            </template>
+                        </vxe-column>
+                        <vxe-column title="检测周期" width="90">
+                            <template #default="{ row }">{{ row?.testOrderDetailList[0]?.period }}个工作日</template>
+                        </vxe-column>
+                        <vxe-column title="实付款" width="90">
+                            <template #default="{ row }">{{ row?.totalAmount }}元</template>
+                        </vxe-column>
+                        <vxe-column title="下单人" field="createName" width="90" :formatter="colNoData" />
+                        <vxe-column title="下单时间" field="createTime" width="130" :formatter="colNoData" />
+                        <vxe-column title="订单状态" width="100" fixed="right">
+                            <template #default="{ row }">
+                                <div class="c-primary">{{ selectDictLabel(test_order_status_bg, row?.status) }}</div>
+                            </template>
+                        </vxe-column>
+                        <vxe-column title="订单备注" field="remark" min-width="100" fixed="right" :formatter="colNoData" />
+                        <vxe-column title="操作" width="260" fixed="right">
+                            <template #default="{ row }">
+                                <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} })" style="color: #0079fe;" text class="small-btn-font" type="primary" size="small">查看详情</el-button>
+                            </template>
+                        </vxe-column>
+                    </vxe-table>
+                </div>
+            </div>
+            <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+            <div class="pd-5"></div>
+        </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 { EditOrderRemark, rowItems, uploadReportForm, uploadInvoiceForm } from '../models';
+import { debounce } from 'lodash';
+import { selectDictLabel } from '@/utils/ruoyi';
+const router = useRouter();
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { dm_package_type, dm_permit_type, vip_level, test_order_status_bg } = toRefs<any>(proxy?.useDict('dm_package_type', 'dm_permit_type', 'vip_level', 'test_order_status_bg'));
+const queryParams = ref<any>({
+    pageNum: 1,
+    pageSize: 10,
+    status: '2',
+    dateRange: []
+});
+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>([]);
+const getList = debounce(async () => {
+    loading.value = true;
+    const res = await testOrderList(queryParams.value);
+    if (!res || res.code !== 200) return;
+    list.value = res.rows;
+    total.value = res.total;
+    loading.value = false;
+}, 500);
+const handleQuery = () => {
+    queryParams.value.pageNum = 1;
+    getList();
+};
+const resetQuery = () => {
+    queryFormRef.value?.resetFields();
+    handleQuery();
+};
+const rowId = ref('');
+const queryRowItems = (row: any) => {
+    rowId.value = row?.packageId;
+    showRowItems.value = true;
+};
+// 获取tabs统计数据
+const tabs = ref<any[]>([]);
+const getTabsCount = debounce(async () => {
+    const res = await testOrderListCount();
+    if (!res || res.code !== 200) return;
+    // 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()
+});
+</script>

+ 1 - 2
src/views/dgtmedicine/model/index.ts

@@ -1,8 +1,7 @@
-// 上传发票组件
 export { default as uploadInvoiceForm } from './uploadInvoiceForm.vue'; // 上传发票
 export { default as CheckPass } from './CheckPass.vue'; // 审核为通过弹框
 export { default as CheckNoPass } from './CheckNoPass.vue'; // 审核为不通过弹框
 export { default as MemberInfo } from './MemberInfo.vue'; // 会员信息
 export { default as MemberPayLog } from './MemberPayLog.vue'; // 会员缴费记录
 export { default as EditVipLevel } from './EditVipLevel.vue'; // 修改级别
-export { default as EditVipEndDate } from './EditVipEndDate.vue'; // 修改会员有效期时间
+export { default as EditVipEndDate } from './EditVipEndDate.vue'; // 修改会员有效期时间

+ 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>

+ 154 - 205
src/views/login.vue

@@ -1,67 +1,56 @@
 <template>
-  <div class="login">
-    <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
-      <div class="title-box">
-        <h3 class="title">数字云药管理后台</h3>
-        <lang-select />
-      </div>
-      <el-form-item prop="username">
-        <el-input v-model="loginForm.username" type="text" size="large" auto-complete="off" :placeholder="proxy.$t('login.username')">
-          <template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
-        </el-input>
-      </el-form-item>
-      <el-form-item prop="password">
-        <el-input
-          v-model="loginForm.password"
-          type="password"
-          size="large"
-          auto-complete="off"
-          :placeholder="proxy.$t('login.password')"
-          @keyup.enter="handleLogin"
-        >
-          <template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
-        </el-input>
-      </el-form-item>
-      <el-form-item v-if="captchaEnabled" prop="code">
-        <el-input
-          v-model="loginForm.code"
-          size="large"
-          auto-complete="off"
-          :placeholder="proxy.$t('login.code')"
-          style="width: 63%"
-          @keyup.enter="handleLogin"
-        >
-          <template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
-        </el-input>
-        <div class="login-code">
-          <img :src="codeUrl" class="login-code-img" @click="getCode" />
+    <div class="login">
+        <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
+            <div class="title-box">
+                <h3 class="title">数字云药管理后台</h3>
+                <lang-select />
+            </div>
+            <el-form-item prop="username">
+                <el-input v-model="loginForm.username" type="text" size="large" auto-complete="off"
+                    :placeholder="proxy.$t('login.username')">
+                    <template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
+                </el-input>
+            </el-form-item>
+            <el-form-item prop="password">
+                <el-input v-model="loginForm.password" type="password" size="large" auto-complete="off"
+                    :placeholder="proxy.$t('login.password')" @keyup.enter="handleLogin">
+                    <template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
+                </el-input>
+            </el-form-item>
+            <el-form-item v-if="captchaEnabled" prop="code">
+                <el-input v-model="loginForm.code" size="large" auto-complete="off" :placeholder="proxy.$t('login.code')"
+                    style="width: 63%" @keyup.enter="handleLogin">
+                    <template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
+                </el-input>
+                <div class="login-code">
+                    <img :src="codeUrl" class="login-code-img" @click="getCode" />
+                </div>
+            </el-form-item>
+            <el-checkbox v-model="loginForm.rememberMe" style="margin: 0 0 25px 0">{{ proxy.$t('login.rememberPassword')
+            }}</el-checkbox>
+            <el-form-item style="width: 100%">
+                <el-button :loading="loading" size="large" type="primary" style="width: 100%" @click.prevent="handleLogin">
+                    <span v-if="!loading">{{ proxy.$t('login.login') }}</span>
+                    <span v-else>{{ proxy.$t('login.logging') }}</span>
+                </el-button>
+                <div v-if="register" style="float: right">
+                    <router-link class="link-type" :to="'/register'">{{ proxy.$t('login.switchRegisterPage')
+                    }}</router-link>
+                </div>
+            </el-form-item>
+        </el-form>
+        <!--  底部  -->
+        <div class="el-login-footer">
+            <span>Copyright © 2025-2026 钰津供应链科技 All Rights Reserved.</span>
         </div>
-      </el-form-item>
-      <el-checkbox v-model="loginForm.rememberMe" style="margin: 0 0 25px 0">{{ proxy.$t('login.rememberPassword') }}</el-checkbox>
-      <el-form-item style="width: 100%">
-        <el-button :loading="loading" size="large" type="primary" style="width: 100%" @click.prevent="handleLogin">
-          <span v-if="!loading">{{ proxy.$t('login.login') }}</span>
-          <span v-else>{{ proxy.$t('login.logging') }}</span>
-        </el-button>
-        <div v-if="register" style="float: right">
-          <router-link class="link-type" :to="'/register'">{{ proxy.$t('login.switchRegisterPage') }}</router-link>
-        </div>
-      </el-form-item>
-    </el-form>
-    <!--  底部  -->
-    <div class="el-login-footer">
-      <span>Copyright © 2018-2024 疯狂的狮子Li All Rights Reserved.</span>
     </div>
-  </div>
 </template>
 
 <script setup lang="ts">
-import { getCodeImg, getTenantList } from '@/api/login';
-import { authBinding } from '@/api/system/social/auth';
+import { getCodeImg } from '@/api/login';
 import { useUserStore } from '@/store/modules/user';
 import { LoginData, TenantVO } from '@/api/types';
 import { to } from 'await-to-js';
-import { HttpStatus } from '@/enums/RespEnum';
 import { useI18n } from 'vue-i18n';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -71,20 +60,19 @@ const router = useRouter();
 const { t } = useI18n();
 
 const loginForm = ref<LoginData>({
-  // tenantId: '190040',
-   tenantId: '000000',
-  username: 'admin',
-  password: 'admin123',
-  rememberMe: false,
-  code: '',
-  uuid: ''
+    tenantId: '190040',
+    username: '',
+    password: '',
+    rememberMe: false,
+    code: '',
+    uuid: ''
 } as LoginData);
 
 const loginRules: ElFormRules = {
-  tenantId: [{ required: true, trigger: 'blur', message: t('login.rule.tenantId.required') }],
-  username: [{ required: true, trigger: 'blur', message: t('login.rule.username.required') }],
-  password: [{ required: true, trigger: 'blur', message: t('login.rule.password.required') }],
-  code: [{ required: true, trigger: 'change', message: t('login.rule.code.required') }]
+    tenantId: [{ required: true, trigger: 'blur', message: t('login.rule.tenantId.required') }],
+    username: [{ required: true, trigger: 'blur', message: t('login.rule.username.required') }],
+    password: [{ required: true, trigger: 'blur', message: t('login.rule.password.required') }],
+    code: [{ required: true, trigger: 'change', message: t('login.rule.code.required') }]
 };
 
 const codeUrl = ref('');
@@ -102,189 +90,150 @@ const loginRef = ref<ElFormInstance>();
 const tenantList = ref<TenantVO[]>([]);
 
 watch(
-  () => router.currentRoute.value,
-  (newRoute: any) => {
-    redirect.value = newRoute.query && newRoute.query.redirect && decodeURIComponent(newRoute.query.redirect);
-  },
-  { immediate: true }
+    () => router.currentRoute.value,
+    (newRoute: any) => {
+        redirect.value = newRoute.query && newRoute.query.redirect && decodeURIComponent(newRoute.query.redirect);
+    },
+    { immediate: true }
 );
 
 const handleLogin = () => {
-  loginRef.value?.validate(async (valid: boolean, fields: any) => {
-    if (valid) {
-      loading.value = true;
-      // 勾选了需要记住密码设置在 localStorage 中设置记住用户名和密码
-      if (loginForm.value.rememberMe) {
-        localStorage.setItem('tenantId', String(loginForm.value.tenantId));
-        localStorage.setItem('username', String(loginForm.value.username));
-        localStorage.setItem('password', String(loginForm.value.password));
-        localStorage.setItem('rememberMe', String(loginForm.value.rememberMe));
-      } else {
-        // 否则移除
-        localStorage.removeItem('tenantId');
-        localStorage.removeItem('username');
-        localStorage.removeItem('password');
-        localStorage.removeItem('rememberMe');
-      }
-      // 调用action的登录方法
-      const [err] = await to(userStore.login(loginForm.value));
-      if (!err) {
-        const redirectUrl = redirect.value || '/';
-        await router.push(redirectUrl);
-        loading.value = false;
-      } else {
-        loading.value = false;
-        // 重新获取验证码
-        if (captchaEnabled.value) {
-          await getCode();
+    loginRef.value?.validate(async (valid: boolean, fields: any) => {
+        if (valid) {
+            loading.value = true;
+            // 勾选了需要记住密码设置在 localStorage 中设置记住用户名和密码
+            if (loginForm.value.rememberMe) {
+                localStorage.setItem('username', String(loginForm.value.username));
+                localStorage.setItem('password', String(loginForm.value.password));
+                localStorage.setItem('rememberMe', String(loginForm.value.rememberMe));
+            } else {
+                // 否则移除
+                localStorage.removeItem('tenantId');
+                localStorage.removeItem('username');
+                localStorage.removeItem('password');
+                localStorage.removeItem('rememberMe');
+            }
+            // 调用action的登录方法
+            const [err] = await to(userStore.login(loginForm.value));
+            if (!err) {
+                const redirectUrl = redirect.value || '/';
+                await router.push(redirectUrl);
+                loading.value = false;
+            } else {
+                loading.value = false;
+                // 重新获取验证码
+                if (captchaEnabled.value) {
+                    await getCode();
+                }
+            }
+        } else {
+            console.log('error submit!', fields);
         }
-      }
-    } else {
-      console.log('error submit!', fields);
-    }
-  });
+    });
 };
 
 /**
  * 获取验证码
  */
 const getCode = async () => {
-  const res = await getCodeImg();
-  const { data } = res;
-  captchaEnabled.value = data.captchaEnabled === undefined ? true : data.captchaEnabled;
-  if (captchaEnabled.value) {
-    codeUrl.value = 'data:image/gif;base64,' + data.img;
-    loginForm.value.uuid = data.uuid;
-  }
-};
-
-const getLoginData = () => {
-  const tenantId = localStorage.getItem('tenantId');
-  const username = localStorage.getItem('username');
-  const password = localStorage.getItem('password');
-  const rememberMe = localStorage.getItem('rememberMe');
-  loginForm.value = {
-    tenantId: tenantId === null ? String(loginForm.value.tenantId) : tenantId,
-    username: username === null ? String(loginForm.value.username) : username,
-    password: password === null ? String(loginForm.value.password) : String(password),
-    rememberMe: rememberMe === null ? false : Boolean(rememberMe)
-  } as LoginData;
-};
-
-/**
- * 获取租户列表
- */
-const initTenantList = async () => {
-  const { data } = await getTenantList(false);
-  tenantEnabled.value = data.tenantEnabled === undefined ? true : data.tenantEnabled;
-  if (tenantEnabled.value) {
-    tenantList.value = data.voList;
-    if (tenantList.value != null && tenantList.value.length !== 0) {
-      loginForm.value.tenantId = tenantList.value[0].tenantId;
+    const res = await getCodeImg();
+    const { data } = res;
+    captchaEnabled.value = data.captchaEnabled === undefined ? true : data.captchaEnabled;
+    if (captchaEnabled.value) {
+        codeUrl.value = 'data:image/gif;base64,' + data.img;
+        loginForm.value.uuid = data.uuid;
     }
-  }
 };
 
-/**
- * 第三方登录
- * @param type
- */
-const doSocialLogin = (type: string) => {
-  authBinding(type, loginForm.value.tenantId).then((res: any) => {
-    if (res.code === HttpStatus.SUCCESS) {
-      // 获取授权地址跳转
-      window.location.href = res.data;
-    } else {
-      ElMessage.error(res.msg);
-    }
-  });
-};
 
 onMounted(() => {
-  getCode();
-  // initTenantList();
-  // getLoginData();
+    getCode();
+    // 获取路由地址参数tenantId
+    const tenantId = router.currentRoute.value.query.tenantId;
+    if (tenantId) {
+        loginForm.value.tenantId = tenantId as string;
+    }
 });
 </script>
 
 <style lang="scss" scoped>
 .login {
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  height: 100%;
-  background-image: url('../assets/images/login-background.jpg');
-  background-size: cover;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 100%;
+    background-image: url('../assets/images/login-background.jpg');
+    background-size: cover;
 }
 
 .title-box {
-  display: flex;
+    display: flex;
 
-  .title {
-    margin: 0px auto 30px auto;
-    text-align: center;
-    color: #707070;
-  }
+    .title {
+        margin: 0px auto 30px auto;
+        text-align: center;
+        color: #707070;
+    }
 
-  :deep(.lang-select--style) {
-    line-height: 0;
-    color: #7483a3;
-  }
+    :deep(.lang-select--style) {
+        line-height: 0;
+        color: #7483a3;
+    }
 }
 
 .login-form {
-  border-radius: 6px;
-  background: #ffffff;
-  width: 400px;
-  padding: 25px 25px 5px 25px;
+    border-radius: 6px;
+    background: #ffffff;
+    width: 400px;
+    padding: 25px 25px 5px 25px;
 
-  .el-input {
-    height: 40px;
+    .el-input {
+        height: 40px;
 
-    input {
-      height: 40px;
+        input {
+            height: 40px;
+        }
     }
-  }
 
-  .input-icon {
-    height: 39px;
-    width: 14px;
-    margin-left: 0px;
-  }
+    .input-icon {
+        height: 39px;
+        width: 14px;
+        margin-left: 0px;
+    }
 }
 
 .login-tip {
-  font-size: 13px;
-  text-align: center;
-  color: #bfbfbf;
+    font-size: 13px;
+    text-align: center;
+    color: #bfbfbf;
 }
 
 .login-code {
-  width: 33%;
-  height: 40px;
-  float: right;
+    width: 33%;
+    height: 40px;
+    float: right;
 
-  img {
-    cursor: pointer;
-    vertical-align: middle;
-  }
+    img {
+        cursor: pointer;
+        vertical-align: middle;
+    }
 }
 
 .el-login-footer {
-  height: 40px;
-  line-height: 40px;
-  position: fixed;
-  bottom: 0;
-  width: 100%;
-  text-align: center;
-  color: #fff;
-  font-family: Arial, serif;
-  font-size: 12px;
-  letter-spacing: 1px;
+    height: 40px;
+    line-height: 40px;
+    position: fixed;
+    bottom: 0;
+    width: 100%;
+    text-align: center;
+    color: #fff;
+    font-family: Arial, serif;
+    font-size: 12px;
+    letter-spacing: 1px;
 }
 
 .login-code-img {
-  height: 40px;
-  padding-left: 12px;
+    height: 40px;
+    padding-left: 12px;
 }
 </style>

+ 29 - 0
src/views/models/AddBtn.vue

@@ -0,0 +1,29 @@
+<template>
+    <div @click.stop="$emit('click')" class="add-btn d-flex a-c j-c u-s-n" :style="{ height: height  }">
+        <slot>
+            <el-icon size="18"><CirclePlusFilled /></el-icon>
+            <span class="pl5 f-16">{{ content }}</span>
+        </slot>
+    </div>
+</template>
+<script setup name="AddBtn" lang="ts">
+import { propTypes } from '@/utils/propTypes';
+
+const props = defineProps({
+    content: propTypes.string.def(''),
+    height: propTypes.string.def('66px')
+});
+</script>
+<style lang="scss" scoped>
+.add-btn {
+    height: 66px;
+    border-radius: 4px;
+    color: var(--el-color-primary);
+    border: 1px dashed var(--el-color-primary);
+    cursor: pointer;
+
+    &:hover {
+        opacity: .8;
+    }
+}
+</style>

+ 115 - 0
src/views/models/FileLook.vue

@@ -0,0 +1,115 @@
+<template>
+    <transition-group class="upload-file-list" name="el-fade-in-linear flex1 ov-hd" tag="div">
+        <el-row :gutter="60">
+            <el-col :span="span" v-for="(file, index) in fileList" :key="index">
+                <div class="upload-list-item">
+                    <el-link class="flex1 right-item" :href="`${file.url}`" :underline="false" target="_blank">
+                        <img style="width: 36px; height: 36px;" class="mr5" src="@/assets/images/pdf_icon.png" alt="" />
+                        <div>
+                            <div class="item-text">{{ file.fileName }}</div>
+                            <div v-if="file.fileSize" class="item-text">{{ changeByte(file.fileSize) }}</div>
+                        </div>
+                    </el-link>
+                    <a class="delete-item" @click="download(file.url, {}, file.fileName)" :underline="false">
+                        <div class="d-flex a-c j-c flex-cln">
+                            <el-icon size="16"><Download /></el-icon>
+                            <div style="padding-top: 6px;">下载</div>
+                        </div>
+                    </a>
+                </div>
+            </el-col>
+        </el-row>
+        <div v-if="!fileList.length">-</div>
+    </transition-group>
+</template>
+
+<script setup lang="ts">
+import { download } from '@/utils/request';
+import { propTypes } from '@/utils/propTypes';
+import { changeByte } from '@/utils/ruoyi';
+
+const props = defineProps({
+    modelValue: [String, Object, Array],
+    span: propTypes.number.def(18),
+    isObject: propTypes.bool.def(true)
+});
+
+const fileList = ref<any>([]);
+watch(
+    () => props.modelValue,
+    async (val) => {
+        if (val) {
+            let list = [];
+            if (Array.isArray(val)) {
+                list = val;
+            } else if (props.isObject) {
+                list = [val];
+            }
+            fileList.value = [...list];
+        } else {
+            fileList.value = [];
+            return [];
+        }
+    },
+    { deep: true, immediate: true }
+);
+const handleDownload = () => {};
+</script>
+
+<style scoped lang="scss">
+.upload-file-uploader {
+    margin-bottom: 5px;
+}
+
+.upload-list-item {
+    position: relative;
+    display: flex;
+    align-items: center;
+    border: 1px solid #e8e8e8;
+    border-radius: 2px;
+    padding-right: 60px;
+    box-sizing: border-box;
+    margin-bottom: 12px;
+
+    &:hover {
+        border-color: var(--el-color-primary);
+        background-color: rgba(#0f8240, 0.06);
+        .delete-item {
+            opacity: 1;
+        }
+    }
+}
+
+.item-text {
+    line-height: 1.2;
+    font-size: 14px;
+    word-break:break-all;
+}
+
+.delete-item {
+    width: 48px;
+    position: absolute;
+    top: -1px;
+    bottom: -1px;
+    right: 0;
+    display: flex;
+    justify-content: center;
+    font-size: 12px;
+    align-items: center;
+    color: #fff;
+    border: 1px solid var(--el-color-primary);
+    cursor: pointer;
+    background-color: var(--el-color-primary);
+    opacity: 0;
+}
+
+.right-item {
+    padding: 8px 10px;
+    justify-content: start;
+    box-sizing: border-box;
+}
+
+.upload-file {
+    width: 100%;
+}
+</style>

+ 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>

+ 109 - 0
src/views/models/SearchSelect.vue

@@ -0,0 +1,109 @@
+<template>
+    <el-select ref="selectDropRef" :multiple-limit="limit" @change="changeSelect" multiple v-model="value" filterable popper-class="custom-dropdown" remote reserve-keyword :remote-method="remoteMethod" remote-show-suffix clearable placeholder="请选择适用企业">
+        <div class="optionBox" infinite-scroll-delay="500" infinite-scroll-distance="20" v-infinite-scroll="load">
+            <template v-if="optionsV.length">
+                <el-option v-for="item in optionsV" :key="item.value" :label="item.cpyName" :value="item.cpyid"></el-option>
+            </template>
+            <template v-for="item in options" :key="item.cpyid">
+                <el-option v-if="!isOptionsV(item.cpyid)" :label="item.cpyName" :value="item.cpyid"></el-option>
+            </template>
+        </div>
+    </el-select>
+</template>
+<script setup lang="ts">
+import { getEnterpriseDetail, getEnterpriseList } from '@/api/cdt/menus'
+import { propTypes } from '@/utils/propTypes';
+// 防抖
+import { debounce } from 'lodash';
+const props = defineProps({
+    modelValue: propTypes.any.def([]),
+    params: {
+        type: Object,
+        default: () => ({})
+    },
+    limit: {
+        type: Number,
+        default: 0
+    }
+})
+const emit = defineEmits(['change', 'update:modelValue', 'changeItem'])
+const value = ref<string[]>([])
+const options = ref([])
+const optionsV = ref([])
+const total = ref(0)
+const selectDropRef = ref(null)
+const queryParams = ref({
+    pageSize: 10,
+    pageNum: 1,
+    cpyName: '',
+})
+const getList = async (hasRx = true) => {
+    if (hasRx) {
+        queryParams.value.pageNum = 1
+        options.value = []
+    }
+    const res = await getEnterpriseList({ ...queryParams.value, ...props.params })
+    total.value = res.total;
+    if (hasRx) {
+        options.value = res.rows
+    } else {
+        options.value = options.value.concat(res.rows)
+    }
+}
+const remoteMethod = (text: string) => {
+    if (text) {
+        queryParams.value.cpyName = text
+        selectSearch()
+    }
+}
+const load = () => {
+    if (options.value.length < total.value) {
+        queryParams.value.pageNum++
+        getList(false)
+    }
+}
+const changeSelect = (val: string[]) => {
+    const itemInfo = options.value.filter(item => val.includes(item.cpyid))
+    emit('update:modelValue', val)
+    emit('change', val)
+    emit('changeItem', itemInfo)
+}
+// 判断是否在optionsV
+const isOptionsV = (val: string) => {
+    return optionsV.value.some((item) => item.cpyid === val)
+}
+// 获取当前id对应的企业信息
+const selectSearch = debounce(() => {
+    getList()
+}, 500)
+// 根据values获取下拉框的options
+const getOptions = async (values: string[]) => {
+    if (!values.length) {
+        return
+    }
+    values.forEach(async (item) => {
+        if ([...optionsV.value, ...options.value].find((v) => v.cpyid === item)) {
+            return
+        }
+        const res = await getEnterpriseDetail(item)
+        res?.data && optionsV.value.push(res.data)
+    })
+}
+onMounted(() => {
+    getList()
+    nextTick(() => {
+      // 获取下拉框的dom
+      const dropdown = document.querySelector('.custom-dropdown .el-select-dropdown__wrap');
+      dropdown.addEventListener('scroll', () => {
+        if (dropdown.scrollTop + dropdown.clientHeight >= dropdown.scrollHeight) {
+          // 在这里执行你的操作
+          load()
+        }
+      });
+    });
+})
+watch(() => props.modelValue, (val) => {
+    value.value = val
+    getOptions(val)
+}, { immediate: true })
+</script>

+ 5 - 2
src/views/models/index.ts

@@ -1,5 +1,8 @@
-
 export { default as DateRange } from './DateRange.vue'; // 包装任务
 export { default as searchTabs } from './searchTabs.vue'; // tabs组件
 export { default as DescCol } from './DescCol.vue'; // 描述组件
-export { default as BtnRadio } from './BtnRadio.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'; // 图片预览
+export { default as FileLook } from './FileLook.vue'; // 文件查看

+ 10 - 10
src/views/models/searchTabs.vue

@@ -1,13 +1,13 @@
 <template>
-  <div class="ridio-info d-flex f-w-w">
-    <!-- <div class="ridio-item f-12" @click="change('')" :class="{ checked: !activeName }">全部({{ total }})</div> -->
-    <template v-for="item in list" :key="item.value">
-      <div class="ridio-item f-s-12 f-w-5" @click="change(item[keyValue])" :class="{ checked: (item[keyValue] || '') === activeName }">
-        <template v-if="isNum"> {{ item[keyLabel] }}({{ item[keyCount] }}) </template>
-        <template v-else> {{ item[keyLabel] }} </template>
-      </div>
-    </template>
-  </div>
+    <div class="ridio-info d-flex f-w-w">
+        <!-- <div class="ridio-item f-12" @click="change('')" :class="{ checked: !activeName }">全部({{ total }})</div> -->
+        <template v-for="item in list" :key="item.value">
+            <div class="ridio-item f-s-12 f-w-5" @click="change(item[keyValue])" :class="{ checked: (item[keyValue] || '') === activeName }">
+                <template v-if="isNum">{{ item[keyLabel] }}({{ item[keyCount] }})</template>
+                <template v-else>{{ item[keyLabel] }}</template>
+            </div>
+        </template>
+    </div>
 </template>
 <script setup name="searchTabs" lang="ts">
 import { propTypes } from '@/utils/propTypes';
@@ -23,7 +23,7 @@ const props = defineProps({
 });
 const change = (event: any) => {
   console.log(event);
-  
+
   activeName.value = event;
 
   emit('update:modelValue', activeName.value);

+ 1 - 1
src/views/register.vue

@@ -67,7 +67,7 @@
     </el-form>
     <!--  底部  -->
     <div class="el-register-footer">
-      <span>Copyright © 2018-2024 疯狂的狮子Li All Rights Reserved.</span>
+      <span>Copyright © 2018-2024 钰津供应链科技 All Rights Reserved.</span>
     </div>
   </div>
 </template>

+ 1 - 1
src/views/tool/gen/genInfoForm.vue

@@ -15,7 +15,7 @@
         <el-form-item prop="packageName">
           <template #label>
             生成包路径
-            <el-tooltip content="生成在哪个java包下,例如 com.ruoyi.system" placement="top">
+            <el-tooltip content="生成在哪个java包下,例如 com.yujin.system" placement="top">
               <el-icon><question-filled /></el-icon>
             </el-tooltip>
           </template>

+ 1 - 1
src/views/tool/gen/index.vue

@@ -185,7 +185,7 @@ const handleGenTable = async (row?: TableVO) => {
     await genCode(row.tableId);
     proxy?.$modal.msgSuccess('成功生成到自定义路径:' + row.genPath);
   } else {
-    proxy?.$download.zip('/tool/gen/batchGenCode?tableIdStr=' + tbIds, 'ruoyi.zip');
+    proxy?.$download.zip('/tool/gen/batchGenCode?tableIdStr=' + tbIds, 'yujin.zip');
   }
 };
 /** 同步数据库操作 */

+ 20 - 30
uno.config.ts

@@ -1,33 +1,23 @@
-import {
-  defineConfig,
-  presetAttributify,
-  presetIcons,
-  presetTypography,
-  presetUno,
-  presetWebFonts,
-  transformerDirectives,
-  transformerVariantGroup
-} from 'unocss';
+import { defineConfig, presetAttributify, presetIcons, presetTypography, presetUno, presetWebFonts, transformerDirectives, transformerVariantGroup } from 'unocss';
 
 export default defineConfig({
-  shortcuts: {
-    'panel-title':
-      'pb-[5px] font-sans leading-[1.1] font-medium text-base text-[#6379bb] border-b border-b-solid border-[var(--el-border-color-light)] mb-5 mt-0'
-  },
-  theme: {
-    colors: {
-      primary: 'var(--el-color-primary)',
-      primary_dark: 'var(--el-color-primary-light-5)'
-    }
-  },
-  presets: [
-    presetUno(),
-    presetAttributify(),
-    presetIcons(),
-    presetTypography(),
-    presetWebFonts({
-      fonts: {}
-    })
-  ],
-  transformers: [transformerDirectives(), transformerVariantGroup()]
+    shortcuts: {
+        'panel-title': 'pb-[5px] font-sans leading-[1.1] font-medium text-base text-[#6379bb] border-b border-b-solid border-[var(--el-border-color-light)] mb-5 mt-0'
+    },
+    theme: {
+        colors: {
+            primary: 'var(--el-color-primary)',
+            primary_dark: 'var(--el-color-primary-light-5)'
+        }
+    },
+    presets: [
+        presetUno(),
+        presetAttributify(),
+        presetIcons(),
+        presetTypography(),
+        presetWebFonts({
+            fonts: {}
+        })
+    ],
+    transformers: [transformerDirectives(), transformerVariantGroup()]
 });

+ 54 - 65
vite.config.ts

@@ -4,71 +4,60 @@ import createPlugins from './vite/plugins';
 
 import path from 'path';
 export default defineConfig(({ mode, command }: ConfigEnv): UserConfig => {
-  const env = loadEnv(mode, process.cwd());
-  return {
-    // 部署生产环境和开发环境下的URL。
-    // 默认情况下,vite 会假设你的应用是被部署在一个域名的根路径上
-    // 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。
-    base: env.VITE_APP_CONTEXT_PATH,
-    resolve: {
-      alias: {
-        '~': path.resolve(__dirname, './'),
-        '@': path.resolve(__dirname, './src')
-      },
-      extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
-    },
-    // https://cn.vitejs.dev/config/#resolve-extensions
-    plugins: createPlugins(env, command === 'build'),
-    server: {
-      host: '0.0.0.0',
-      port: Number(env.VITE_APP_PORT),
-      open: true,
-      proxy: {
-        [env.VITE_APP_BASE_API]: {
-          // target: 'http://dm.yujin.shuziyunyao.com/api',
-          // target: 'http://dm.yujin.shuziyunyao.com/api',
-          target: 'http://192.168.1.68:8080',
-          changeOrigin: true,
-          ws: true,
-          rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')
-        }
-      }
-    },
-    css: {
-      preprocessorOptions: {
-        scss: {
-          javascriptEnabled: true
-        }
-      },
-      postcss: {
-        plugins: [
-          {
-            postcssPlugin: 'internal:charset-removal',
-            AtRule: {
-              charset: (atRule) => {
-                if (atRule.name === 'charset') {
-                  atRule.remove();
+    const env = loadEnv(mode, process.cwd());
+    return {
+        // 部署生产环境和开发环境下的URL。
+        // 默认情况下,vite 会假设你的应用是被部署在一个域名的根路径上
+        // 例如 https://www.yujin.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.yujin.vip/admin/,则设置 baseUrl 为 /admin/。
+        base: env.VITE_APP_CONTEXT_PATH,
+        resolve: {
+            alias: {
+                '~': path.resolve(__dirname, './'),
+                '@': path.resolve(__dirname, './src')
+            },
+            extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
+        },
+        // https://cn.vitejs.dev/config/#resolve-extensions
+        plugins: createPlugins(env, command === 'build'),
+        server: {
+            host: '0.0.0.0',
+            port: Number(env.VITE_APP_PORT),
+            open: true,
+            proxy: {
+                [env.VITE_APP_BASE_API]: {
+                    // target: 'http://dm.yujin.shuziyunyao.com/api',
+                    target: 'http://dm.yujin.shuziyunyao.com/api',
+                    // target: 'http://192.168.1.68:8080',
+                    changeOrigin: true,
+                    ws: true,
+                    rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')
                 }
-              }
             }
-          }
-        ]
-      }
-    },
-    // 预编译
-    optimizeDeps: {
-      include: [
-        'vue',
-        'vue-router',
-        'pinia',
-        'axios',
-        '@vueuse/core',
-        'echarts',
-        'vue-i18n',
-        '@vueup/vue-quill',
-        'image-conversion',
-        'element-plus/es/components/**/css'
-      ]
-    }
-  };
+        },
+        css: {
+            preprocessorOptions: {
+                scss: {
+                    javascriptEnabled: true
+                }
+            },
+            postcss: {
+                plugins: [
+                    {
+                        postcssPlugin: 'internal:charset-removal',
+                        AtRule: {
+                            charset: (atRule) => {
+                                if (atRule.name === 'charset') {
+                                    atRule.remove();
+                                }
+                            }
+                        }
+                    }
+                ]
+            }
+        },
+        // 预编译
+        optimizeDeps: {
+            include: ['vue', 'vue-router', 'pinia', 'axios', '@vueuse/core', 'echarts', 'vue-i18n', '@vueup/vue-quill', 'image-conversion', 'element-plus/es/components/**/css']
+        }
+    };
 });

+ 1 - 1
vite/plugins/compression.ts

@@ -6,7 +6,7 @@ export default (env: any) => {
   if (VITE_BUILD_COMPRESS) {
     const compressList = VITE_BUILD_COMPRESS.split(',');
     if (compressList.includes('gzip')) {
-      // http://doc.ruoyi.vip/ruoyi-vue/other/faq.html#使用gzip解压缩静态文件
+      // http://doc.yujin.vip/yujin-vue/other/faq.html#使用gzip解压缩静态文件
       plugin.push(
         compression({
           ext: '.gz',