wzh 3 jaren geleden
bovenliggende
commit
459df00e84
46 gewijzigde bestanden met toevoegingen van 6906 en 1 verwijderingen
  1. 2 0
      .env.dev
  2. 2 0
      .env.release
  3. 1 1
      .gitignore
  4. 34 0
      index.html
  5. 26 0
      package.json
  6. BIN
      public/汇很多-货主PC.ico
  7. 247 0
      src/App.vue
  8. 19 0
      src/apis/cloudLogin.js
  9. 24 0
      src/apis/config.js
  10. 184 0
      src/apis/fetch.js
  11. BIN
      src/assets/blue-circle.png
  12. BIN
      src/assets/icon-player.png
  13. BIN
      src/assets/login-back.png
  14. BIN
      src/assets/login-modal.png
  15. BIN
      src/assets/logo.png
  16. BIN
      src/assets/ship-red-icon.png
  17. BIN
      src/assets/three.png
  18. BIN
      src/assets/user.png
  19. BIN
      src/assets/white-logo.png
  20. 7 0
      src/auth/hasPermission.js
  21. 74 0
      src/auth/menuData.js
  22. 26 0
      src/components/Aside.vue
  23. 224 0
      src/components/Certs.vue
  24. 37 0
      src/components/Footer.vue
  25. 171 0
      src/components/Header.vue
  26. 79 0
      src/components/RemoteSearch.vue
  27. 1 0
      src/components/Table.vue
  28. 135 0
      src/components/Uploader.vue
  29. 78 0
      src/main.js
  30. 153 0
      src/router/index.js
  31. 104 0
      src/store/index.js
  32. 191 0
      src/styles/index.css
  33. 39 0
      src/utils/downloadBlobFile.js
  34. 13 0
      src/utils/utils.js
  35. 353 0
      src/views/accountManage/clientList.vue
  36. 353 0
      src/views/accountManage/subAccountList.vue
  37. 238 0
      src/views/agencyManage/agencyCompanyList.vue
  38. 208 0
      src/views/authManage/addRole.vue
  39. 236 0
      src/views/authManage/departmentList.vue
  40. 147 0
      src/views/authManage/roleList.vue
  41. 164 0
      src/views/cargoManage/cargoList.vue
  42. 3 0
      src/views/index/Index.vue
  43. 279 0
      src/views/index/Login.vue
  44. 1784 0
      src/views/voyage/voyageDetail.vue
  45. 1247 0
      src/views/voyage/voyageList.vue
  46. 23 0
      vite.config.js

+ 2 - 0
.env.dev

@@ -0,0 +1,2 @@
+VITE_PROJECT_ENV = 'dev'
+VITE_BASEURL = 'https://interface.huihenduo.cc/hhd-proxy-dev/'

+ 2 - 0
.env.release

@@ -0,0 +1,2 @@
+VITE_PROJECT_ENV = 'release'
+VITE_BASEURL = 'https://interface.huihenduo.cc/hhd-proxy/'

+ 1 - 1
.gitignore

@@ -27,4 +27,4 @@ build/Release
 # Dependency directory
 # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
 node_modules
-
+dist

+ 34 - 0
index.html

@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" href="/src/assets/logo.png" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <!-- <script
+      charset="utf-8"
+      src="https://map.qq.com/api/gljs?v=1.exp&key=Y2BBZ-IHRKU-V42VO-BFQEE-K7252-ZBBSF"
+    ></script> -->
+    <script src="https://webapi.amap.com/maps?v=2.0&key=0b84075e96d01623f704867a601139bb&&plugin=AMap.Scale,AMap.HawkEye,AMap.ToolBar,AMap.ControlBar"></script>
+    <title>Huihenduo App</title>
+    <style>
+      * {
+        margin: 0;
+        padding: 0;
+      }
+      html,
+      body {
+        height: 100%;
+        width: 100%;
+      }
+
+      #app {
+        height: 100%;
+        width: 100%;
+      }
+    </style>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>

+ 26 - 0
package.json

@@ -0,0 +1,26 @@
+{
+  "name": "agency",
+  "version": "0.0.0",
+  "scripts": {
+    "dev": "vite --mode dev",
+    "build":"vite build --mode dev",
+    "bm": "vite build --mode release",
+    "serve": "vite build --mode release && vite preview"
+  },
+  "dependencies": {
+    "@cloudbase/js-sdk": "^1.7.1",
+    "@element-plus/icons": "^0.0.11",
+    "axios": "^0.21.1",
+    "element-plus": "^1.1.0-beta.24",
+    "lodash": "^4.17.21",
+    "md5": "^2.3.0",
+    "vite-plugin-compression": "^0.3.5",
+    "vue": "^3.2.16",
+    "vue-router": "^4.0.4",
+    "vuex": "^4.0.2"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^1.9.3",
+    "vite": "^2.6.4"
+  }
+}

BIN
public/汇很多-货主PC.ico


+ 247 - 0
src/App.vue

@@ -0,0 +1,247 @@
+<template>
+  <div v-if="this.$store.state.isLogin" class="main-container">
+    <HeaderVue class="header"></HeaderVue>
+    <div class="main-app">
+      <div class="aside"><AsideVue></AsideVue></div>
+      <div class="section">
+        <div class="first-title">
+          {{ this.$store.state.firstTitle }}
+        </div>
+        <div class="main-section"><router-view></router-view></div>
+      </div>
+    </div>
+    <!-- <FooterVue></FooterVue> -->
+  </div>
+  <router-view v-else></router-view>
+</template>
+
+<script>
+import HeaderVue from "./components/Header.vue";
+import AsideVue from "./components/Aside.vue";
+import FooterVue from "./components/Footer.vue";
+export default {
+  components: {
+    HeaderVue,
+    AsideVue,
+    FooterVue,
+  },
+  data() {
+    return {};
+  },
+};
+</script>
+
+<style>
+.main-container {
+  height: 100%;
+  width: 100%;
+  min-height: 800px;
+  min-width: 1200px;
+}
+
+.aside {
+  width: 220px;
+}
+
+.footer {
+  text-align: center;
+}
+.main-app {
+  width: 100%;
+  height: calc(100% - 60px);
+  display: flex;
+}
+
+.section {
+  width: 100%;
+  background: #f4f5f6;
+  overflow: auto;
+}
+
+.first-title {
+  height: 52px;
+  line-height: 52px;
+  width: 100%;
+  box-sizing: border-box;
+  background: #fff;
+  font-size: 18px;
+  font-family: PingFangSC-Medium, PingFang SC;
+  font-weight: 500;
+  color: #333d43;
+  padding-left: 20px;
+}
+
+.main-section {
+  margin: 24px 0 0 24px;
+  height: calc(100% - 76px);
+  overflow-y: auto;
+}
+
+.line-container-p18 {
+  padding: 18px;
+  background: #fff;
+}
+.line-container-p24 {
+  padding: 24px;
+  background: #fff;
+}
+
+.full-container-p24 {
+  padding: 24px;
+  min-height: calc(100% - 48px);
+  background: #fff;
+}
+
+.df {
+  display: flex;
+}
+
+.ffw {
+  flex-flow: wrap;
+}
+
+.jcsa {
+  justify-content: space-around;
+}
+.jcsb {
+  justify-content: space-between;
+}
+
+.aic {
+  align-items: center;
+}
+
+.dib {
+  display: inline-block;
+}
+
+.ml8 {
+  margin-left: 8px;
+}
+
+.jcfe {
+  justify-content: flex-end;
+}
+
+.mt20 {
+  margin-top: 20px;
+}
+.mb20 {
+  margin-bottom: 20px;
+}
+
+.mt30 {
+  margin-top: 30px;
+}
+
+.mt50 {
+  margin-top: 50px;
+}
+
+.container-title {
+  font-size: 18px;
+  font-family: PingFangSC-Medium, PingFang SC;
+  font-weight: 500;
+  color: #0094fe;
+  margin: 15px 0 15px 0;
+}
+
+.container-second-title {
+  font-size: 16px;
+  font-family: PingFangSC-Medium, PingFang SC;
+  font-weight: 500;
+  color: #0094fe;
+  margin: 15px 0 15px 0;
+}
+
+.line {
+  display: flex;
+  align-items: center;
+  align-content: flex-start;
+  margin: 20px;
+}
+
+.info-line {
+  display: flex;
+  align-items: center;
+  align-content: flex-start;
+  margin-right: 20px;
+}
+
+.info-line-title {
+  width: 120px;
+  height: 100%;
+  font-size: 14px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: #353a42;
+  line-height: 100%;
+  text-align: right;
+  padding-right: 20px;
+}
+
+.info-line-text {
+  width: 240px !important;
+  height: 100%;
+  line-height: 100%;
+}
+
+.info-line-textarea {
+  width: 640px;
+  height: 100%;
+  line-height: 100%;
+}
+
+.pointer {
+  cursor: pointer;
+}
+
+.el-upload-list__item-thumbnail {
+  object-fit: contain !important;
+}
+.el-input__inner {
+  text-align: center;
+  color: #777 !important;
+}
+.el-upload--picture-card {
+  border: none;
+}
+
+.amap-logo {
+  display: none !important;
+  opacity: 0 !important;
+}
+
+.amap-copyright {
+  opacity: 0 !important;
+}
+
+.ml20 {
+  margin-left: 20px;
+}
+
+.hr {
+  border-bottom: 2px solid #3b91fa;
+  opacity: 0.2;
+}
+
+.m10-0 {
+  margin: 10px 0;
+}
+
+.m30-0 {
+  margin: 30px 0;
+}
+
+.fww {
+  flex-wrap: wrap;
+}
+
+.tar {
+  text-align: right;
+}
+
+.mr20 {
+  margin-right: 20px;
+}
+</style>

+ 19 - 0
src/apis/cloudLogin.js

@@ -0,0 +1,19 @@
+import cloudbase from "@cloudbase/js-sdk";
+
+const tcb = cloudbase.init({
+  env: "huihenduo-2gx127w7f837b584",
+});
+
+const auth = tcb.auth({
+  persistence: "local",
+});
+
+async function AnonymousLogin() {
+  return new Promise(async (resolve, reject) => {
+    let signIn = await auth.anonymousAuthProvider().signIn();
+    const loginState = await auth.getLoginState();
+    resolve();
+  });
+}
+
+export { AnonymousLogin, tcb };

+ 24 - 0
src/apis/config.js

@@ -0,0 +1,24 @@
+import store from "../store/index";
+import axios from "axios";
+
+let baseurl = import.meta.env.VITE_BASEURL;
+const uploadUrl = `${baseurl}cos/upload`;
+
+axios.interceptors.response.use(
+  function (response) {
+    return response;
+  },
+  function (error) {
+    return Promise.reject(error);
+  }
+);
+export const $http = function (url, data) {
+  return axios({
+    method: data ? "post" : "get",
+    url: baseurl + url,
+    data,
+    withCredentials: true,
+  });
+};
+
+export default { baseurl, uploadUrl, $http };

+ 184 - 0
src/apis/fetch.js

@@ -0,0 +1,184 @@
+import { $http } from "./config";
+export default {
+  // 货主登录
+  staffLogin(data) {
+    return $http("user/cargo/login", data);
+  },
+  // 获取航次列表
+  getVoyageList(data) {
+    return $http("voyage/list", data);
+  },
+
+  // 获取航次详情
+  getVoyageDetail(data) {
+    return $http("/voyage/wx/detail", data);
+  },
+
+  // 更新航次
+  updateVoyage(data) {
+    return $http("/voyage/backstage/update", data);
+  },
+
+  // 完成航次
+  finishVoyage(data) {
+    return $http("/voyage/backstage/finish", data);
+  },
+
+  // 添加航次
+  addVoyage(data) {
+    return $http("voyage/backstage/add", data);
+  },
+
+  // 获取媒体列表
+  getMediaList(data) {
+    return $http("/media/backstage/list", data);
+  },
+  // 导出excel
+  exportExcel(data) {
+    return $http("/voyage/exportExcel", data);
+  },
+
+  // 获取卸货列表
+  getDischargeList(data) {
+    return $http("/voyage/getDischargeList", data);
+  },
+
+  // 获取汽车装货记录
+  getTruckLoadRecord(data) {
+    return $http("/voyage/getCarLoadRecordList", data);
+  },
+
+  // 添加职位
+  addRole(data) {
+    return $http("/role/permission/addRole", data);
+  },
+
+  // 获取权限数据
+  getPermisiionData(data) {
+    return $http("/role/permission/getPermisiionData", data);
+  },
+
+  // 获取角色列表
+  getRoleList(data) {
+    return $http("/role/permission/list", data);
+  },
+
+  // 获取角色下拉列表
+  getRoleSelect(data) {
+    return $http("/role/permission/getRoleSelect", data);
+  },
+
+  // 修改角色信息
+  updateRole(data) {
+    return $http("/role/permission/updateRole", data);
+  },
+
+  // 添加货种
+  addCargo(data) {
+    return $http("/cargo/add", data);
+  },
+
+  // 货种列表
+  getCargoList(data) {
+    return $http("/cargo/list", data);
+  },
+
+  // 获取代理列表
+  getAgencyList(data) {
+    return $http("/user/cargo/proxy/list", data);
+  },
+
+  // 添加代理
+  addAgency(data) {
+    return $http("/user/cargo/add/proxy", data);
+  },
+
+  // 获取货主子账户列表
+  getSubAccountList(data) {
+    return $http("/user/cargo/account/list", data);
+  },
+
+  // 添加货主子账户
+  addSubAccount(data) {
+    return $http("/user/cargo/add/loginAccount", data);
+  },
+
+  // 修改货主子账户
+  updateSubAccount(data) {
+    return $http("/user/cargo/update/loginAccount", data);
+  },
+
+  // 获取角色详情
+  getRoleDetail(data) {
+    return $http("/role/permission/getRoleDetail", data);
+  },
+
+  // 获取登录人权限
+  getPermissionByUserId(data) {
+    return $http("/role/permission/getPermissionByUserId", data);
+  },
+
+  // 添加部门
+  addDepartment(data) {
+    return $http("/department/add", data);
+  },
+
+  // 获取部门列表
+  getDepartmentList(data) {
+    return $http("/department/list", data);
+  },
+
+  // 获取部门下拉
+  getDepartmentSelect(data) {
+    return $http("/department/select", data);
+  },
+
+  // 修改部门
+  updateDepartment(data) {
+    return $http("/department/update", data);
+  },
+
+  // 获取FYDI指数下载链接
+  getFYFIDownloadUrl(data) {
+    return $http("/fydi/getLastest", data);
+  },
+  // 获取提货单列表
+  getLabList(data) {
+    return $http("/voyage/getLabList", data);
+  },
+
+  // 获取港口天气列表
+  getPortWeatherList(data) {
+    return $http("/voyage/getPortWeatherList", data);
+  },
+
+  // 获取超期航次提醒
+  getLongDaysInPort(data) {
+    return $http("/voyage/cargo/longDaysInPort", data);
+  },
+
+  // 获取客户列表
+  getClientList(data) {
+    return $http("/client/account/list", data);
+  },
+
+  // 添加客户
+  addClient(data) {
+    return $http("/client/account/add", data);
+  },
+
+  // 修改客户
+  updateClient(data) {
+    return $http("/client/account/update", data);
+  },
+
+  // 获取港口列表
+  getCol(data) {
+    return $http("/port/backstage/getCol", data);
+  },
+
+  // 获取货种下拉
+  getCargoSelect(data) {
+    return $http("/cargo/select", data);
+  },
+};

BIN
src/assets/blue-circle.png


BIN
src/assets/icon-player.png


BIN
src/assets/login-back.png


BIN
src/assets/login-modal.png


BIN
src/assets/logo.png


BIN
src/assets/ship-red-icon.png


BIN
src/assets/three.png


BIN
src/assets/user.png


BIN
src/assets/white-logo.png


+ 7 - 0
src/auth/hasPermission.js

@@ -0,0 +1,7 @@
+export const hasPermission = (roles, route) => {
+  if (route.meta && route.meta.roles) {
+    return roles.some((role) => route.meta.roles.indexOf(role) >= 0);
+  } else {
+    return true;
+  }
+};

+ 74 - 0
src/auth/menuData.js

@@ -0,0 +1,74 @@
+let menuData = [
+  {
+    icon: "el-icon-s-data",
+    title: "航次管理",
+    code: "VOYAGEMANAGE",
+    items: [
+      {
+        path: "/voyage/voyageList",
+        name: "航次列表",
+        code: "VOYAGELIST",
+      },
+    ],
+  },
+  {
+    icon: "el-icon-s-data",
+    title: "代理管理",
+    code: "PROXYMANAGE",
+    items: [
+      {
+        path: "/agencyManage/agencyCompanyList",
+        name: "代理公司列表",
+        code: "PROXYLIST",
+      },
+    ],
+  },
+  {
+    icon: "el-icon-s-data",
+    title: "货种管理",
+    code: "CARGOMANAGE",
+    items: [
+      {
+        path: "/cargoManage/cargoList",
+        name: "货种列表",
+        code: "CARGOLIST",
+      },
+    ],
+  },
+  {
+    icon: "el-icon-s-data",
+    title: "账户管理",
+    code: "ACCOUNTMANAGE",
+    items: [
+      {
+        path: "/accountManage/subAccountList",
+        name: "员工列表",
+        code: "ACCOUNTLIST",
+      },
+      {
+        path: "/accountManage/clientList",
+        name: "客户列表",
+        code: "CLIENTLIST",
+      },
+    ],
+  },
+  {
+    icon: "el-icon-s-data",
+    title: "权限管理",
+    code: "PERMISSIONMANAGE",
+    items: [
+      {
+        path: "/authManage/departmentList",
+        name: "部门列表",
+        code: "DEPTMANAGE",
+      },
+      {
+        path: "/authManage/roleList",
+        name: "职位列表",
+        code: "ROLELIST",
+      },
+    ],
+  },
+];
+
+export default menuData;

+ 26 - 0
src/components/Aside.vue

@@ -0,0 +1,26 @@
+<template>
+  <el-menu
+    :default-active="this.$store.state.currentMenuItem"
+    style="width: 220px; height: 100%"
+    background-color="#141B29"
+    text-color="#fff"
+    active-text-color="#ffd04b"
+    :router="true"
+  >
+    <el-sub-menu
+      v-for="(item, index) in this.$store.state.menuData"
+      :key="item"
+      :index="`${index}`"
+    >
+      <template v-slot:title>
+        <i :class="item.icon"></i>
+        <span>{{ item.title }}</span>
+      </template>
+      <el-menu-item v-for="son in item.items" :index="son.path" :key="son">
+        {{ son.name }}
+      </el-menu-item>
+    </el-sub-menu>
+  </el-menu>
+</template>
+<script setup>
+</script>

+ 224 - 0
src/components/Certs.vue

@@ -0,0 +1,224 @@
+<template>
+  <div class="line" v-show="!disabled || shipFileList.length">
+    <div class="info-line">
+      <div class="info-line-title">船舶证书 :</div>
+      <Uploader
+        :uploaderId="certsId + 'shipFileList'"
+        :params="{ type: '2', userId: 0, location: '' }"
+        :disabled="disabled"
+        @onSendFileList="getShipFileList"
+        :fileList="shipFileList"
+      ></Uploader>
+    </div>
+  </div>
+  <div class="line" v-show="!disabled || annualFileList.length">
+    <div class="info-line">
+      <div class="info-line-title">船舶年审合格证 :</div>
+      <Uploader
+        :uploaderId="certsId + 'annualFileList'"
+        :params="{ type: '5', userId: 0, location: '' }"
+        :disabled="disabled"
+        @onSendFileList="getAnnualFileList"
+        :fileList="annualFileList"
+      ></Uploader>
+    </div>
+  </div>
+  <div class="line" v-show="!disabled || shipNationFileList.length">
+    <div class="info-line">
+      <div class="info-line-title">船舶国籍证书 :</div>
+      <Uploader
+        :uploaderId="certsId + 'shipNationFileList'"
+        :params="{ type: '6', userId: 0, location: '' }"
+        :disabled="disabled"
+        @onSendFileList="getShipNationFileList"
+        :fileList="shipNationFileList"
+      ></Uploader>
+    </div>
+  </div>
+  <div class="line" v-show="!disabled || operatingFileList.length">
+    <div class="info-line">
+      <div class="info-line-title">营运证 :</div>
+      <Uploader
+        :uploaderId="certsId + 'operatingFileList'"
+        :params="{ type: '7', userId: 0, location: '' }"
+        :disabled="disabled"
+        @onSendFileList="getOperatingFileList"
+        :fileList="operatingFileList"
+      ></Uploader>
+    </div>
+  </div>
+</template>
+
+<script>
+import { defineComponent, computed, ref, onMounted, watch } from "vue";
+import _ from "lodash";
+
+export default defineComponent({
+  props: {
+    certsId: {
+      type: String,
+      default: "cert",
+    },
+    disabled: {
+      type: Boolean,
+      default: true,
+    },
+  },
+  emits: ["onPreview", "onSendFileList"],
+  setup(props, { emit }) {
+    let disabled = ref(true);
+    let shipFileList = ref([]);
+    let annualFileList = ref([]);
+    let shipNationFileList = ref([]);
+    let operatingFileList = ref([]);
+    function getShipFileList(list) {
+      shipFileList.value = list;
+    }
+
+    function getAnnualFileList(list) {
+      annualFileList.value = list;
+    }
+
+    function getShipNationFileList(list) {
+      shipNationFileList.value = list;
+    }
+    function getOperatingFileList(list) {
+      operatingFileList.value = list;
+    }
+
+    function initCerts(arr) {
+      shipFileList.value = [];
+      annualFileList.value = [];
+      shipNationFileList.value = [];
+      operatingFileList.value = [];
+      let t = setTimeout(() => {
+        for (let i of arr) {
+          i.url = i.viewUrl;
+          switch (i.type) {
+            case 5: {
+              annualFileList.value.push(i);
+              break;
+            }
+
+            case 6: {
+              shipNationFileList.value.push(i);
+              break;
+            }
+
+            case 7: {
+              operatingFileList.value.push(i);
+              break;
+            }
+            default: {
+              shipFileList.value.push(i);
+              break;
+            }
+          }
+        }
+        clearTimeout(t);
+      }, 500);
+    }
+
+    let shipFileListCache = ref([]);
+    let annualFileListCache = ref([]);
+    let shipNationFileListCache = ref([]);
+    let operatingFileListCache = ref([]);
+
+    function editCerts() {
+      shipFileListCache.value = _.cloneDeep(shipFileList.value);
+      annualFileListCache.value = _.cloneDeep(annualFileList.value);
+      shipNationFileListCache.value = _.cloneDeep(shipNationFileList.value);
+      operatingFileListCache.value = _.cloneDeep(operatingFileList.value);
+      disabled.value = false;
+    }
+
+    function sendCerts() {
+      let certs = [];
+      for (let i of shipFileList.value) {
+        if (i.id) {
+          certs.push(i);
+        } else {
+          certs.push({
+            downloadUrl: i.response.result.downloadUrl,
+            fileKey: i.response.result.key,
+            viewUrl: i.response.result.viewUrl,
+            type: 2,
+          });
+        }
+      }
+      for (let i of annualFileList.value) {
+        if (i.id) {
+          certs.push(i);
+        } else {
+          certs.push({
+            downloadUrl: i.response.result.downloadUrl,
+            fileKey: i.response.result.key,
+            viewUrl: i.response.result.viewUrl,
+            type: 5,
+          });
+        }
+      }
+      for (let i of shipNationFileList.value) {
+        if (i.id) {
+          certs.push(i);
+        } else {
+          certs.push({
+            downloadUrl: i.response.result.downloadUrl,
+            fileKey: i.response.result.key,
+            viewUrl: i.response.result.viewUrl,
+            type: 6,
+          });
+        }
+      }
+      for (let i of operatingFileList.value) {
+        if (i.id) {
+          certs.push(i);
+        } else {
+          certs.push({
+            downloadUrl: i.response.result.downloadUrl,
+            fileKey: i.response.result.key,
+            viewUrl: i.response.result.viewUrl,
+            type: 7,
+          });
+        }
+      }
+      return certs;
+    }
+    function cancelEditCerts() {
+      if (!_.isEqual(shipFileList.value, shipFileListCache.value)) {
+        shipFileList.value = _.cloneDeep(shipFileListCache.value);
+      }
+      if (!_.isEqual(annualFileList.value, annualFileListCache.value)) {
+        annualFileList.value = _.cloneDeep(annualFileListCache.value);
+      }
+      if (!_.isEqual(shipNationFileList.value, shipNationFileListCache.value)) {
+        shipNationFileList.value = _.cloneDeep(shipNationFileListCache.value);
+      }
+      if (!_.isEqual(operatingFileList.value, operatingFileListCache.value)) {
+        operatingFileList.value = _.cloneDeep(operatingFileListCache.value);
+      }
+      disabled.value = true;
+    }
+    onMounted(() => {});
+
+    return {
+      disabled,
+      shipFileList,
+      annualFileList,
+      shipNationFileList,
+      operatingFileList,
+      getShipFileList,
+      getAnnualFileList,
+      getShipNationFileList,
+      getOperatingFileList,
+      initCerts,
+      sendCerts,
+      cancelEditCerts,
+      editCerts,
+    };
+  },
+});
+</script>
+
+<style>
+</style>

+ 37 - 0
src/components/Footer.vue

@@ -0,0 +1,37 @@
+<template>
+  <div class="copyright">
+    <div class="in" @click="goBeian">
+      Copyright © 2021 河南省汇很多科技有限公司 豫ICP备 2021029101 号
+    </div>
+  </div>
+</template>
+<script>
+export default {
+  setup() {
+    function goBeian() {
+      window.open("https://beian.miit.gov.cn/");
+    }
+    return {
+      goBeian,
+    };
+  },
+};
+</script>
+<style scoped>
+.copyright {
+  width: 100%;
+  height: 60px;
+  background: #b3c0d1;
+}
+
+.in {
+  width: 520px;
+  margin: 0 auto;
+  line-height: 60px;
+  font-family: PingFang SC;
+  font-weight: 400;
+  color: #333333;
+  opacity: 0.8;
+  cursor: pointer;
+}
+</style>

+ 171 - 0
src/components/Header.vue

@@ -0,0 +1,171 @@
+<template>
+  <div class="header">
+    <div class="left">
+      <!-- <img class="first" src="../assets/three.png" alt="" />
+      <div class="shu"></div> -->
+      <img class="logo" src="../assets/white-logo.png" alt="" />
+      <div
+        class="ml20"
+        style="color: #fff; font-size: 18px; height: 60px; padding-top: 50px"
+      >
+        version:{{ timelineData[0]?.version }}
+      </div>
+    </div>
+    <div class="right">
+      <img class="user-icon" src="../assets/user.png" alt="" />
+      <div class="user">{{ contactName }}</div>
+      <el-popover placement="bottom" trigger="hover" :width="280">
+        <div
+          style="
+            width: 100%;
+            height: 60vh;
+            overflow-y: auto;
+            padding-right: 10px;
+            margin-right: 10px;
+          "
+        >
+          <el-timeline>
+            <el-timeline-item
+              v-for="item in timelineData"
+              center
+              :timestamp="item.timer"
+              placement="top"
+            >
+              <div class="log-card">
+                <p style="margin-bottom: 10px">Version: {{ item.version }}</p>
+                <div
+                  style="margin-bottom: 5px; font-size: 12px"
+                  v-for="(item1, index) in item.remarks"
+                >
+                  {{ index + 1 }}. {{ item1.text }}
+                </div>
+              </div>
+            </el-timeline-item>
+          </el-timeline>
+        </div>
+        <template #reference>
+          <el-badge value="new">
+            <div class="log">新功能日志</div>
+          </el-badge>
+        </template>
+      </el-popover>
+      <div class="quit" @click="quit">[退出]</div>
+    </div>
+  </div>
+</template>
+<script>
+import store from "../store";
+import router from "../router";
+import { onMounted, ref } from "vue";
+import { AnonymousLogin, tcb } from "../apis/cloudLogin";
+const db = tcb.database();
+const v = db.collection("huihenduo_versions");
+const __ = db.command;
+
+export default {
+  setup() {
+    let contactName = localStorage.contactName;
+    function quit() {
+      localStorage.clear();
+      store.commit("changeLogin", false);
+      router.push({ path: "/login" });
+    }
+    let timelineData = ref([]);
+    onMounted(() => {
+      cloudLogin();
+    });
+    async function getAbledVersions() {
+      let res = await v
+        .aggregate()
+        .match({ deleted: __.neq(true) })
+        .sort({
+          createTime: -1,
+        })
+        .limit(10)
+        .end();
+      timelineData.value = res.data;
+    }
+
+    async function cloudLogin() {
+      await AnonymousLogin();
+      getAbledVersions();
+    }
+    return {
+      quit,
+      contactName,
+      timelineData,
+    };
+  },
+};
+</script>
+<style scoped>
+.header {
+  width: 100%;
+  height: 60px;
+  background: #212029;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.left,
+.right {
+  display: flex;
+  align-items: center;
+}
+
+.first {
+  width: 22px;
+  height: 20px;
+  margin: 0 22px;
+}
+
+.shu {
+  width: 1px;
+  height: 60px;
+  background: #101015;
+  margin-right: 22px;
+}
+
+.logo {
+  width: 120px;
+  height: 40px;
+  margin-left: 20px;
+}
+
+.title {
+  font-size: 21px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: #ffffff;
+  margin-left: 26px;
+}
+
+.user,
+.quit,
+.log {
+  font-size: 14px;
+  font-family: PingFangSC-Medium, PingFang SC;
+  font-weight: 500;
+  color: #ffffff;
+  cursor: pointer;
+  margin-right: 16px;
+}
+
+.log-card p {
+  font-size: 10px;
+}
+.user-icon {
+  width: 18px;
+  height: 18px;
+  margin-right: 16px;
+}
+
+.log {
+  margin-right: 4px;
+}
+
+.quit {
+  margin-left: 26px;
+}
+</style>

+ 79 - 0
src/components/RemoteSearch.vue

@@ -0,0 +1,79 @@
+<template>
+  <el-autocomplete
+    v-model="value"
+    :fetch-suggestions="getSelectList"
+    :placeholder="placeholder"
+    @select="selectItem"
+    :size="size"
+    clearable
+    @clear="clear"
+    @input="handleInput"
+    style="width: 240px"
+  />
+</template>
+
+<script>
+import { onMounted, ref } from "vue";
+import api from "../apis/fetch";
+import _ from "lodash";
+export default {
+  props: {
+    placeholder: {
+      type: String,
+      default: "请填写",
+    },
+    size: {
+      type: String,
+      default: "small",
+    },
+    clearable: {
+      type: Boolean,
+      default: true,
+    },
+    api: String,
+    params: Object,
+    value: String,
+  },
+  emits: ["input", "selectItem"],
+  setup(props, { emit }) {
+    let selectStr = ref("");
+    const getSelectList = _.debounce(
+      async (queryString, cb) => {
+        if (!queryString) return;
+        let res = await api[props.api]({
+          ...props.params,
+          term: queryString,
+        });
+        if (res.data.status == 0) {
+          cb(res.data.result);
+        }
+      },
+      1000,
+      { leading: true }
+    );
+
+    const selectItem = (item) => {
+      emit("selectItem", item);
+    };
+
+    function clear() {
+      selectStr.value = "";
+    }
+
+    function handleInput(e) {
+      emit("input", e);
+    }
+
+    onMounted(() => {});
+    return {
+      selectStr,
+      getSelectList,
+      clear,
+      selectItem,
+      handleInput,
+    };
+  },
+};
+</script>
+
+<style></style>

+ 1 - 0
src/components/Table.vue

@@ -0,0 +1 @@
+<template></template>

+ 135 - 0
src/components/Uploader.vue

@@ -0,0 +1,135 @@
+<template>
+  <el-upload
+    :id="uploaderId"
+    drag
+    multiple
+    :action="actionUrl"
+    list-type="picture-card"
+    :on-preview="preview"
+    :on-remove="remove"
+    :data="params"
+    :on-success="uploadSuccess"
+    :file-list="fileList"
+    :disabled="disabled"
+    :on-exceed="onExceed"
+    :limit="limit"
+  >
+    <div :class="['upload-plus-icon']">+</div>
+    <div :class="['upload-text']">{{ uploadText }}</div>
+  </el-upload>
+  <el-dialog v-model="dialogVisible" title="图片预览" width="30%">
+    <el-image
+      :src="dialogImageUrl"
+      style="height: 100%; width: 100%"
+    ></el-image>
+  </el-dialog>
+</template>
+<script>
+import { defineComponent, computed, ref, onMounted, watch } from "vue";
+import {
+  ElMessage,
+  ElNotification,
+} from "_element-plus@1.1.0-beta.24@element-plus";
+import store from "../store/index";
+
+export default defineComponent({
+  props: {
+    uploaderId: {
+      type: String,
+      default: "uploader",
+    },
+    limit: {
+      type: Number,
+      default: 100,
+    },
+    params: Object,
+    actionUrl: {
+      type: String,
+      default: store.state.uploadUrl,
+    },
+    disabled: {
+      type: Boolean,
+      default: false,
+    },
+    fileList: Array,
+    uploadText: {
+      type: String,
+      default: "拖拽或点击上传",
+    },
+  },
+  emits: ["onPreview", "onSendFileList"],
+  setup(props, { emit }) {
+    let dialogVisible = ref(false);
+    let dialogImageUrl = ref("");
+    function preview(file) {
+      dialogVisible.value = true;
+      dialogImageUrl.value = file.url;
+    }
+    function remove(file, list) {
+      emit("onSendFileList", list);
+    }
+    function uploadSuccess(res, file, list) {
+      emit("onSendFileList", list);
+    }
+    function onExceed(files, fileList) {
+      ElMessage({
+        message: `超出文件数量限制 (最大数量:${props.limit})`,
+        type: "warning",
+      });
+    }
+    watch(
+      () => props.disabled,
+      (a, b) => {
+        changeDragVisable(!a);
+      }
+    );
+    function changeDragVisable(boo) {
+      let d = document.getElementById(props.uploaderId);
+      d.childNodes[1].style.display = boo ? "inline-block" : "none";
+      return;
+      let a = document.getElementsByClassName("el-upload-dragger");
+      let b = document.getElementsByClassName("el-upload--picture-card");
+      for (let i of a) {
+        i.style.display = boo ? "inline-block" : "none";
+      }
+      for (let i of b) {
+        i.style.display = boo ? "inline-block" : "none";
+      }
+    }
+    onMounted(() => {
+      changeDragVisable(!props.disabled);
+    });
+
+    return {
+      preview,
+      remove,
+      uploadSuccess,
+      dialogVisible,
+      dialogImageUrl,
+      preview,
+      onExceed,
+    };
+  },
+});
+</script>
+<style scoped>
+.upload-plus-icon {
+  height: 15%;
+  color: rgb(139, 147, 156);
+  line-height: 100px;
+  font-size: 40px;
+  font-weight: 200;
+}
+.upload-text {
+  height: 25%;
+  color: rgb(139, 147, 156);
+}
+
+.dn {
+  display: none;
+}
+:deep().el-upload-dragger {
+  width: 100%;
+  height: 148px !important;
+}
+</style>

+ 78 - 0
src/main.js

@@ -0,0 +1,78 @@
+import { createApp } from "vue";
+import ElementPlus from "element-plus";
+import "element-plus/dist/index.css";
+import zhCn from "element-plus/es/locale/lang/zh-cn";
+import App from "./App.vue";
+import router from "./router";
+import store from "./store";
+import md5 from "md5";
+import "./styles/index.css";
+import Uploader from "./components/Uploader.vue";
+import Certs from "./components/Certs.vue";
+import RemoteSearch from "./components/RemoteSearch.vue";
+
+const app = createApp(App);
+app.component("RemoteSearch", RemoteSearch);
+app.component("Certs", Certs);
+app.component("Uploader", Uploader);
+
+let userId = localStorage.userId;
+if (userId) {
+  store.dispatch("GetBasePermissionData", localStorage.loginAccountId);
+  store.dispatch("GetUserPermission", localStorage.loginAccountId);
+}
+
+router.beforeEach(async (to, from, next) => {
+  if (localStorage.userId) {
+    store.commit("changeLogin", true);
+    let rolePermission = localStorage.rolePermission?.split(",") || [];
+    if (store.state.menuData.length) {
+      let path = store.state?.menuData[0]?.items[0].path;
+      if (0 === to.matched.length) {
+        next(path);
+      } else if (to.path == "/login" || to.path == "/") {
+        next(path);
+      } else if (rolePermission?.indexOf(to.meta.code) == -1) {
+        next(path);
+      } else {
+        next();
+      }
+    } else {
+      if (to.path == "/") {
+        next("/voyage/voyageList");
+      } else {
+        next();
+      }
+    }
+  } else {
+    store.commit("changeLogin", false);
+    if (to.path == "/login") {
+      next();
+    } else {
+      next("/login");
+    }
+  }
+});
+router.afterEach((to, from) => {
+  let { title } = to.meta;
+  document.title = "代理 - " + title;
+  store.commit("setCurrentMenuItem", to.path);
+  store.commit("changefirstTitle", title);
+});
+
+app.directive("auth", {
+  mounted(el, bind) {
+    let permissions = localStorage.rolePermission?.split(",") || [];
+    if (permissions.indexOf(bind.value) == -1) {
+      el.parentNode.removeChild(el);
+    }
+  },
+});
+
+app
+  .use(router)
+  .use(ElementPlus, {
+    locale: zhCn,
+  })
+  .use(store)
+  .mount("#app");

+ 153 - 0
src/router/index.js

@@ -0,0 +1,153 @@
+import {
+  createWebHistory,
+  createWebHashHistory,
+  createMemoryHistory,
+  createRouter,
+} from "vue-router";
+import Login from "../views/index/Login.vue";
+import VoyageList from "../views/voyage/voyageList.vue";
+
+const router = createRouter({
+  history: createWebHashHistory(),
+  routes: [
+    {
+      path: "/login",
+      name: "login",
+      meta: {
+        title: "登录",
+      },
+      component: Login,
+    },
+    {
+      path: "/voyage/voyageDetail",
+      name: "voyageDetail",
+      meta: {
+        title: "航次详情",
+        code: "VOYAGEDETAIL",
+      },
+      component: () => import("../views/voyage/voyageDetail.vue"),
+    },
+    {
+      path: "/voyage/voyageList",
+      name: "voyageList",
+      meta: {
+        title: "航次列表",
+        code: "VOYAGELIST",
+      },
+      component: VoyageList,
+    },
+    {
+      path: "/accountManage/subAccountList",
+      name: "subAccountList",
+      meta: {
+        title: "员工列表",
+        code: "ACCOUNTLIST",
+      },
+      component: () => import("../views/accountManage/subAccountList.vue"),
+    },
+    {
+      path: "/agencyManage/agencyCompanyList",
+      name: "agencyCompanyList",
+      meta: {
+        title: "代理公司列表",
+        code: "PROXYLIST",
+      },
+      component: () => import("../views/agencyManage/agencyCompanyList.vue"),
+    },
+    {
+      path: "/authManage/roleList",
+      name: "roleList",
+      meta: {
+        title: "职位列表",
+        code: "ROLELIST",
+      },
+      component: () => import("../views/authManage/roleList.vue"),
+    },
+    {
+      path: "/authManage/departmentList",
+      name: "departmentList",
+      meta: {
+        title: "部门列表",
+        code: "DEPTLIST",
+      },
+      component: () => import("../views/authManage/departmentList.vue"),
+    },
+    {
+      path: "/authManage/addRole",
+      name: "addRole",
+      meta: {
+        title: "新增职位",
+        code: "ADDUPDATEROLE",
+      },
+      component: () => import("../views/authManage/addRole.vue"),
+    },
+    {
+      path: "/cargoManage/cargoList",
+      name: "cargoList",
+      meta: {
+        title: "货种列表",
+        code: "CARGOLIST",
+      },
+      component: () => import("../views/cargoManage/cargoList.vue"),
+    },
+    {
+      path: "/accountManage/clientList",
+      name: "clientList",
+      meta: {
+        title: "客户列表",
+        code: "CLIENTLIST",
+      },
+      component: () => import("../views/accountManage/clientList.vue"),
+    },
+  ],
+});
+
+export const asyncRouterList = [
+  {
+    path: "/accountManage/subAccountList",
+    name: "subAccountList",
+    meta: {
+      title: "员工列表",
+      code: "ACCOUNTLIST",
+    },
+    component: () => import("../views/accountManage/subAccountList.vue"),
+  },
+  {
+    path: "/agencyManage/agencyCompanyList",
+    name: "agencyCompanyList",
+    meta: {
+      title: "代理公司列表",
+      code: "PROXYLIST",
+    },
+    component: () => import("../views/agencyManage/agencyCompanyList.vue"),
+  },
+  {
+    path: "/authManage/roleList",
+    name: "roleList",
+    meta: {
+      title: "职位列表",
+      code: "ROLELIST",
+    },
+    component: () => import("../views/authManage/roleList.vue"),
+  },
+  {
+    path: "/authManage/addRole",
+    name: "addRole",
+    meta: {
+      title: "新增职位",
+      code: "ADDUPDATEROLE",
+    },
+    component: () => import("../views/authManage/addRole.vue"),
+  },
+  {
+    path: "/cargoManage/cargoList",
+    name: "cargoList",
+    meta: {
+      title: "货种列表",
+      code: "CARGOLIST",
+    },
+    component: () => import("../views/cargoManage/cargoList.vue"),
+  },
+];
+
+export default router;

+ 104 - 0
src/store/index.js

@@ -0,0 +1,104 @@
+import { createStore } from "vuex";
+import api from "../apis/fetch";
+import router from "../router";
+import { asyncRouterList } from "../router";
+import menuData from "../auth/menuData";
+console.log(import.meta.env.VITE_PROJECT_ENV);
+let baseurl = import.meta.env.VITE_BASEURL;
+
+const store = createStore({
+  state: {
+    isLogin: false,
+    firstTitle: "",
+    secondTitle: "",
+    currentMenuItem: "/cargoOwnerManage/cargoOwnerList",
+    baseurl,
+    basePermissionData: [],
+    userPermission: [],
+    menuData: [],
+    baseParentNodes: [],
+  },
+  getters: {},
+  mutations: {
+    changefirstTitle(state, text) {
+      state.firstTitle = text;
+    },
+    changeTitleSecond(state, text) {
+      state.secondTitle = text;
+    },
+    changeLogin(state, b) {
+      state.isLogin = b;
+    },
+    setCurrentMenuItem(state, index) {
+      state.currentMenuItem = index;
+    },
+    setBasePermissionData(state, data) {
+      state.basePermissionData = data;
+    },
+    setUserPermissionData(state, data) {
+      state.userPermission = data;
+    },
+    setMenuData(state, data) {
+      state.menuData = data;
+    },
+    setBaseParentNodes(state, data) {
+      state.baseParentNodes = data;
+    },
+  },
+  actions: {
+    GetBasePermissionData({ commit }, loginAccountId) {
+      return new Promise((resolve, reject) => {
+        api.getPermisiionData({ loginAccountId }).then((e) => {
+          let data = e.data.result;
+          let arr = [];
+          function getParentNodes(data) {
+            for (let i of data) {
+              if (i.children.length) {
+                arr.push(i.code);
+                getParentNodes(i.children);
+              }
+            }
+          }
+          getParentNodes(data);
+          commit("setBaseParentNodes", arr);
+          commit("setBasePermissionData", data);
+          resolve(1);
+        });
+      });
+    },
+    GetUserPermission({ commit }, loginAccountId) {
+      return new Promise((resolve, reject) => {
+        api.getPermissionByUserId({ loginAccountId }).then((e) => {
+          let res = e.data.result || [];
+          let arr = [...new Set([...res])];
+          commit("setUserPermissionData", arr);
+          localStorage.setItem("rolePermission", arr);
+          let data = [];
+          for (let i in menuData) {
+            // if (menuData[i].title == "航次管理") {
+            //   data[i] = menuData[i];
+            // } else {
+            if (arr.indexOf(menuData[i].code) != -1) {
+              data[i] = menuData[i];
+              let arr0 = [];
+              for (let j in data[i].items) {
+                if (arr.indexOf(data[i].items[j].code) != -1) {
+                  arr0.push(data[i].items[j]);
+                }
+              }
+              data[i].items = arr0;
+            }
+            // }
+          }
+          data = data.filter((item) => {
+            return item;
+          });
+          commit("setMenuData", data);
+          resolve(1);
+        });
+      });
+    },
+  },
+});
+
+export default store;

+ 191 - 0
src/styles/index.css

@@ -0,0 +1,191 @@
+
+.df {
+  display: flex;
+}
+
+.ffw {
+  flex-flow: wrap;
+}
+
+.jcsa {
+  justify-content: space-around;
+}
+.jcsb {
+  justify-content: space-between;
+}
+
+.jcfe {
+  justify-content: flex-end;
+}
+
+.jcc {
+  justify-content: center;
+}
+
+.aic {
+  align-items: center;
+}
+
+
+.aifs {
+  align-items: flex-start;
+}
+
+.dib {
+  display: inline-block;
+}
+
+
+.pointer {
+  cursor: pointer;
+}
+
+.m0a{
+  margin: 0 auto;
+}
+
+.mt5{
+  margin-top: 5px;
+}
+
+.mt10{
+  margin-top: 10px;
+}
+
+.mt20{
+  margin-top: 20px;
+}
+
+.mt30{
+  margin-top: 30px;
+}
+
+.mr5{
+  margin-right: 5px;
+}
+
+.mr10{
+  margin-right: 10px;
+}
+
+.mr20{
+  margin-right: 20px;
+}
+
+.mr30{
+  margin-right: 30px;
+}
+
+.mb5{
+  margin-bottom: 5px;
+}
+
+.mb10{
+  margin-bottom: 10px;
+}
+
+.mb20{
+  margin-bottom: 20px;
+}
+
+.mb30{
+  margin-bottom: 30px;
+}
+
+.ml5{
+  margin-left: 5px;
+}
+
+.ml10{
+  margin-left: 10px;
+}
+
+.ml20{
+  margin-left: 20px;
+}
+
+.ml30{
+  margin-left: 30px;
+}
+
+.p5{
+  padding: 5px;
+}
+
+.p10{
+  padding: 10px;
+}
+
+.p20{
+  padding: 20px;
+}
+
+.p30{
+  padding: 30px;
+}
+
+.pt5{
+  padding-top: 5px;
+}
+
+.pt10{
+  padding-top: 10px;
+}
+
+.pt20{
+  padding-top: 20px;
+}
+
+.pt30{
+  padding-top: 30px;
+}
+
+.pr5{
+  padding-right: 5px;
+}
+
+.pr10{
+  padding-right: 10px;
+}
+
+.pr20{
+  padding-right: 20px;
+}
+
+.pr30{
+  padding-right: 30px;
+}
+
+.pb5{
+  padding-bottom: 5px;
+}
+
+.pb10{
+  padding-bottom: 10px;
+}
+
+.pb20{
+  padding-bottom: 20px;
+}
+
+.pb30{
+  padding-bottom: 30px;
+}
+
+.pl5{
+  padding-left: 5px;
+}
+
+.pl10{
+  padding-left: 10px;
+}
+
+.pl20{
+  padding-left: 20px;
+}
+
+.pl30{
+  padding-left: 30px;
+}
+
+

+ 39 - 0
src/utils/downloadBlobFile.js

@@ -0,0 +1,39 @@
+import axios from "axios";
+function downloadBlobFile(url, data, name, type, fileType) {
+  return new Promise((resolve, reject) => {
+    axios({
+      method: type,
+      url,
+      responseType: "blob",
+      data,
+    })
+      .then((res) => {
+        if (res.data.size) {
+          let blob = new Blob([res.data], {
+            type: fileType,
+          });
+          let downloadElement = document.createElement("a");
+          let href = window.URL.createObjectURL(blob); // 创建下载的链接
+          downloadElement.href = href;
+          downloadElement.download = name; // 下载后文件名
+          document.body.appendChild(downloadElement);
+          downloadElement.click(); // 点击下载
+          document.body.removeChild(downloadElement); // 下载完成移除元素
+          window.URL.revokeObjectURL(href); // 释放掉blob对象
+          resolve({
+            status: 0,
+          });
+        } else {
+          resolve({
+            status: -1,
+          });
+        }
+      })
+      .catch((e) => {
+        reject({
+          status: 1,
+        });
+      });
+  });
+}
+export default downloadBlobFile;

+ 13 - 0
src/utils/utils.js

@@ -0,0 +1,13 @@
+function subTimeStr(str, i) {
+  if (!str || typeof str != "string") return;
+  let index;
+  if (i) {
+    index = i;
+  } else {
+    index = str.indexOf(" ");
+  }
+
+  return str.substring(0, index);
+}
+
+export { subTimeStr };

+ 353 - 0
src/views/accountManage/clientList.vue

@@ -0,0 +1,353 @@
+<template>
+  <div class="full-container-p24">
+    <div style="display: flex; justify-content: space-between">
+      <div style="display: flex">
+        <el-input
+          placeholder="请输入姓名/手机号"
+          prefix-icon="el-icon-search"
+          v-model="term"
+          clearable
+          style="height: 32px; width: 330px; line-height: 32px"
+          @keyup.enter.native="getClientList"
+        ></el-input>
+        <div class="seach-btn" @click="getClientList">查询</div>
+      </div>
+      <el-button v-auth="'CLIENTACCOUNT'" type="primary" @click="showAddModal"
+        >添加客户</el-button
+      >
+    </div>
+
+    <el-dialog
+      v-model="visable"
+      :title="accountId ? '修改客户' : '添加客户'"
+      width="550px"
+      @close="resetForm()"
+    >
+      <template v-slot:default>
+        <div class="df jcc">
+          <el-form
+            :model="ruleForm"
+            :rules="rules"
+            ref="form"
+            label-width="110px"
+            label-position="left"
+          >
+            <el-form-item prop="name" label="姓名">
+              <el-input style="width: 280px" v-model="ruleForm.name"></el-input>
+            </el-form-item>
+            <el-form-item prop="phone" label="手机号">
+              <el-input
+                style="width: 280px"
+                v-model="ruleForm.phone"
+              ></el-input>
+            </el-form-item>
+
+            <el-form-item prop="sharePermission" label="查看分享">
+              <el-radio
+                v-model="ruleForm.sharePermission"
+                :label="1"
+                size="large"
+                >有权限</el-radio
+              >
+              <el-radio
+                v-model="ruleForm.sharePermission"
+                :label="0"
+                size="large"
+                >无权限</el-radio
+              >
+            </el-form-item>
+          </el-form>
+        </div>
+      </template>
+      <template v-slot:footer>
+        <div class="dialog-footer">
+          <el-button @click="resetForm">取 消</el-button>
+          <el-button type="primary" @click="addClient(ruleForm)">
+            确 定
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <div style="margin-top: 24px">
+      <el-table :data="tableData" stripe style="width: 100%">
+        <el-table-column
+          type="index"
+          label="序号"
+          min-width="40"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="userName"
+          label="姓名"
+          min-width="80"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="phone"
+          label="手机号"
+          min-width="100"
+          align="center"
+        ></el-table-column>
+        <!-- <el-table-column
+          prop="accountStatus"
+          label="状态"
+          min-width="80"
+          align="center"
+        >
+          <template v-slot="scope">
+            <el-switch
+              v-model="scope.row.accountStatus"
+              active-color="#13ce66"
+              inactive-color="#ff4949"
+              active-text="启用"
+              inactive-text="禁用"
+              :active-value="1"
+              :inactive-value="0"
+            /> </template
+        ></el-table-column> -->
+        <el-table-column
+          prop="sharePermission"
+          label="分享权限"
+          min-width="80"
+          align="center"
+        >
+          <template v-slot="scope">
+            <el-switch
+              v-model="scope.row.sharePermission"
+              active-color="#13ce66"
+              inactive-color="#ff4949"
+              active-text="启用"
+              inactive-text="禁用"
+              :active-value="1"
+              :inactive-value="0"
+              @change="changeSharePermission($event, scope.row)"
+            /> </template
+        ></el-table-column>
+
+        <el-table-column
+          v-auth="'ADDACCOUNT'"
+          label="操作"
+          min-width="120"
+          align="center"
+        >
+          <template v-slot="scope">
+            <div class="df aic jcsa">
+              <!-- <el-switch
+                v-model="scope.row.accountStatus"
+                active-color="#13ce66"
+                inactive-color="#ff4949"
+                active-text="启用"
+                inactive-text="禁用"
+                :active-value="1"
+                :inactive-value="0"
+              />
+
+              <div
+                style="
+                  color: red;
+                  margin-left: 10px;
+                  font-size: 14px;
+                  text-decoration: underline;
+                  cursor: pointer;
+                "
+              >
+                删除
+              </div> -->
+              <el-button
+                @click="showUpdateModal(scope.row, 1)"
+                size="small"
+                type="primary"
+                >修改客户权限</el-button
+              >
+            </div>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div style="width: 100%; text-align: right; margin-top: 43px">
+        <el-pagination
+          background
+          layout="prev, pager, next"
+          :total="total"
+          @current-change="pageChange"
+        ></el-pagination>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import api from "../../apis/fetch";
+import { ref, onMounted, reactive } from "vue";
+import { ElNotification, ElMessageBox } from "element-plus";
+import store from "../../store/index";
+
+let tableData = ref([]);
+let currentPage = ref(1);
+let total = ref(0);
+let term = ref("");
+let loginAccountId = ref(0);
+let accountId = ref("");
+let ruleForm = ref({
+  name: "",
+  phone: "",
+  roleId: "",
+  deptId: "",
+});
+const rules = reactive({
+  name: [
+    {
+      required: true,
+      message: "请填写姓名",
+      trigger: "blur",
+    },
+  ],
+  phone: [
+    {
+      required: true,
+      message: "请填写手机号",
+      trigger: "blur",
+    },
+  ],
+  sharePermission: [
+    {
+      required: true,
+      message: "请选择权限",
+      trigger: "blur",
+    },
+  ],
+});
+
+function showAddModal() {
+  visable.value = true;
+  ruleForm.value = {
+    name: "",
+    phone: "",
+    sharePermission: "",
+  };
+}
+
+async function getClientList() {
+  let res = await api.getClientList({
+    term: term.value,
+    currentPage: currentPage.value,
+    size: 10,
+    loginAccountId: loginAccountId.value,
+  });
+  if (res.data.status == 0) {
+    tableData.value = res.data.result;
+    total.value = res.data.total;
+  } else {
+    tableData.value = [];
+    total.value = 0;
+  }
+}
+
+function pageChange(e) {
+  currentPage.value = e;
+  getClientList();
+}
+let visable = ref(false);
+let form = ref(null);
+async function addClient() {
+  form.value.validate(async (valid) => {
+    if (valid) {
+      let postData = {
+        ...ruleForm.value,
+        loginAccountId: loginAccountId.value,
+      };
+      if (accountId.value) {
+        postData.accountId = accountId.value;
+      }
+      let res = await api[accountId.value ? "updateClient" : "addClient"](
+        postData
+      );
+      if (res.data.status == 0) {
+        ElNotification({
+          title: "成功",
+          duration: 1500,
+          message: res.data.msg,
+          type: "success",
+        });
+        resetForm();
+        getClientList();
+      } else {
+        ElNotification({
+          title: "失败",
+          duration: 1500,
+          message: res.data.msg,
+          type: "error",
+        });
+      }
+    }
+  });
+}
+function resetForm() {
+  visable.value = false;
+  accountId.value = "";
+  form.value.resetFields();
+}
+
+function showUpdateModal(item, type) {
+  visable.value = true;
+  accountId.value = item.id;
+  console.log(item);
+  ruleForm.value = {
+    name: item.userName,
+    phone: item.phone,
+    sharePermission: item.sharePermission,
+  };
+}
+
+async function changeSharePermission(sharePermission, item) {
+  let postData = {
+    accountId: item.id,
+    name: item.userName,
+    phone: item.phone,
+    sharePermission,
+    loginAccountId: loginAccountId.value,
+  };
+
+  let res = await api["updateClient"](postData);
+  if (res.data.status == 0) {
+    ElNotification({
+      title: "成功",
+      duration: 1500,
+      message: res.data.msg,
+      type: "success",
+    });
+    resetForm();
+    getClientList();
+  } else {
+    ElNotification({
+      title: "失败",
+      duration: 1500,
+      message: res.data.msg,
+      type: "error",
+    });
+  }
+}
+
+onMounted(() => {
+  loginAccountId.value = localStorage.loginAccountId;
+  getClientList();
+});
+</script>
+
+<style scoped>
+.seach-btn {
+  display: inline-block;
+  width: 60px;
+  height: 38px;
+  background: #0094fe;
+  border-radius: 2px;
+  font-size: 14px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: #ffffff;
+  text-align: center;
+  line-height: 38px;
+  margin-left: 10px;
+  cursor: pointer;
+}
+</style>

+ 353 - 0
src/views/accountManage/subAccountList.vue

@@ -0,0 +1,353 @@
+<template>
+  <div class="full-container-p24">
+    <div style="display: flex; justify-content: space-between">
+      <div style="display: flex">
+        <el-input
+          placeholder="请输入姓名/手机号"
+          prefix-icon="el-icon-search"
+          v-model="term"
+          clearable
+          style="height: 32px; width: 330px; line-height: 32px"
+          @keyup.enter.native="getSubAccountList"
+        ></el-input>
+        <div class="seach-btn" @click="getSubAccountList">查询</div>
+      </div>
+      <el-button v-auth="'ADDACCOUNT'" type="primary" @click="showAddModal"
+        >添加员工</el-button
+      >
+    </div>
+
+    <el-dialog
+      v-model="visable"
+      :title="accountId ? '修改员工' : '添加员工'"
+      width="550px"
+      @close="resetForm()"
+    >
+      <template v-slot:default>
+        <div class="df jcc">
+          <el-form
+            :model="ruleForm"
+            :rules="rules"
+            ref="form"
+            label-width="110px"
+            label-position="left"
+          >
+            <el-form-item prop="name" label="姓名">
+              <el-input style="width: 280px" v-model="ruleForm.name"></el-input>
+            </el-form-item>
+            <el-form-item prop="phone" label="手机号">
+              <el-input
+                style="width: 280px"
+                v-model="ruleForm.phone"
+              ></el-input>
+            </el-form-item>
+            <el-form-item prop="deptId" label="部门">
+              <el-select
+                style="width: 280px"
+                v-model="ruleForm.deptId"
+                placeholder="请选择部门"
+                @change="getRoleSelect"
+              >
+                <el-option
+                  v-for="item in departmentSelect"
+                  :key="item"
+                  :label="item.value"
+                  :value="item.key"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item v-if="ruleForm.deptId" prop="roleId" label="职位">
+              <el-select
+                style="width: 280px"
+                v-model="ruleForm.roleId"
+                placeholder="请选择职位"
+              >
+                <el-option
+                  v-for="item in roleSelect"
+                  :key="item"
+                  :label="item.value"
+                  :value="item.key"
+                />
+              </el-select>
+            </el-form-item>
+          </el-form>
+        </div>
+      </template>
+      <template v-slot:footer>
+        <div class="dialog-footer">
+          <el-button @click="resetForm">取 消</el-button>
+          <el-button type="primary" @click="addSubAccount(ruleForm)">
+            确 定
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <div style="margin-top: 24px">
+      <el-table :data="tableData" stripe style="width: 100%">
+        <el-table-column
+          type="index"
+          label="序号"
+          min-width="40"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="userName"
+          label="姓名"
+          min-width="80"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="phone"
+          label="手机号"
+          min-width="100"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="password"
+          label="密码"
+          min-width="80"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="deptName"
+          label="部门"
+          min-width="80"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="roleName"
+          label="职位"
+          min-width="80"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          v-auth="'ADDACCOUNT'"
+          label="操作"
+          min-width="120"
+          align="center"
+        >
+          <template v-slot="scope">
+            <div class="df aic jcsa">
+              <!-- <el-switch
+                v-model="scope.row.accountStatus"
+                active-color="#13ce66"
+                inactive-color="#ff4949"
+                active-text="启用"
+                inactive-text="禁用"
+                :active-value="1"
+                :inactive-value="0"
+              />
+
+              <div
+                style="
+                  color: red;
+                  margin-left: 10px;
+                  font-size: 14px;
+                  text-decoration: underline;
+                  cursor: pointer;
+                "
+              >
+                删除
+              </div> -->
+              <el-button
+                @click="showUpdateModal(scope.row, 1)"
+                size="small"
+                type="primary"
+                >修改员工权限</el-button
+              >
+            </div>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div style="width: 100%; text-align: right; margin-top: 43px">
+        <el-pagination
+          background
+          layout="prev, pager, next"
+          :total="total"
+          @current-change="pageChange"
+        ></el-pagination>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import api from "../../apis/fetch";
+import { ref, onMounted, reactive } from "vue";
+import { ElNotification, ElMessageBox } from "element-plus";
+import store from "../../store/index";
+
+let tableData = ref([]);
+let currentPage = ref(1);
+let total = ref(0);
+let term = ref("");
+let loginAccountId = ref(0);
+let accountId = ref("");
+let ruleForm = ref({
+  name: "",
+  phone: "",
+  roleId: "",
+  deptId: "",
+});
+const rules = reactive({
+  name: [
+    {
+      required: true,
+      message: "请填写名称",
+      trigger: "blur",
+    },
+  ],
+  phone: [
+    {
+      required: true,
+      message: "请填写手机号",
+      trigger: "blur",
+    },
+  ],
+  roleId: [
+    {
+      required: true,
+      message: "请选择职位",
+      trigger: "blur",
+    },
+  ],
+  deptId: [
+    {
+      required: true,
+      message: "请选择部门",
+      trigger: "blur",
+    },
+  ],
+});
+
+function showAddModal() {
+  visable.value = true;
+  ruleForm.value = {
+    name: "",
+    phone: "",
+    roleId: "",
+    deptId: "",
+  };
+}
+
+async function getSubAccountList() {
+  let res = await api.getSubAccountList({
+    term: term.value,
+    currentPage: currentPage.value,
+    size: 10,
+    loginAccountId: loginAccountId.value,
+  });
+  if (res.data.status == 0) {
+    tableData.value = res.data.result;
+    for (let i of tableData.value) {
+      if (i.roleId == 0) {
+        i.roleId = "";
+      }
+    }
+    total.value = res.data.total;
+  } else {
+    tableData.value = [];
+    total.value = 0;
+  }
+}
+
+function pageChange(e) {
+  currentPage.value = e;
+  getSubAccountList();
+}
+let visable = ref(false);
+let form = ref(null);
+async function addSubAccount() {
+  let postData = {
+    ...ruleForm.value,
+    loginAccountId: loginAccountId.value,
+  };
+  if (accountId.value) {
+    postData.accountId = accountId.value;
+  }
+  let res = await api[accountId.value ? "updateSubAccount" : "addSubAccount"](
+    postData
+  );
+  if (res.data.status == 0) {
+    ElNotification({
+      title: "成功",
+      duration: 1500,
+      message: res.data.msg,
+      type: "success",
+    });
+    store.dispatch("GetBasePermissionData", localStorage.loginAccountId);
+    store.dispatch("GetUserPermission", localStorage.loginAccountId);
+    resetForm();
+    getSubAccountList();
+  } else {
+    ElNotification({
+      title: "失败",
+      duration: 1500,
+      message: res.data.msg,
+      type: "error",
+    });
+  }
+}
+function resetForm() {
+  visable.value = false;
+  accountId.value = "";
+  form.value.resetFields();
+}
+
+function showUpdateModal(item, type) {
+  visable.value = true;
+  accountId.value = item.id;
+  ruleForm.value = {
+    name: item.userName,
+    phone: item.phone,
+    deptId: item.deptId,
+    roleId: item.roleId,
+  };
+  getRoleSelect(type);
+}
+
+let roleSelect = ref([]);
+
+async function getRoleSelect(type) {
+  roleSelect.value = [];
+  if (!ruleForm.value.deptId) return;
+  if (type != 1) ruleForm.value.roleId = "";
+  let res = await api.getRoleSelect({
+    loginAccountId: loginAccountId.value,
+    deptId: ruleForm.value.deptId,
+  });
+  roleSelect.value = res.data.result;
+}
+let departmentSelect = ref([]);
+async function getDepartmentSelect() {
+  let res = await api.getDepartmentSelect({
+    loginAccountId: loginAccountId.value,
+  });
+  departmentSelect.value = res.data.result;
+}
+
+onMounted(() => {
+  loginAccountId.value = localStorage.loginAccountId;
+  getSubAccountList();
+  getDepartmentSelect();
+});
+</script>
+
+<style scoped>
+.seach-btn {
+  display: inline-block;
+  width: 60px;
+  height: 38px;
+  background: #0094fe;
+  border-radius: 2px;
+  font-size: 14px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: #ffffff;
+  text-align: center;
+  line-height: 38px;
+  margin-left: 10px;
+  cursor: pointer;
+}
+</style>

+ 238 - 0
src/views/agencyManage/agencyCompanyList.vue

@@ -0,0 +1,238 @@
+<template>
+  <div class="full-container-p24">
+    <div style="display: flex; justify-content: space-between">
+      <div style="display: flex">
+        <el-input
+          placeholder="请输入名称/联系人/手机号"
+          prefix-icon="el-icon-search"
+          v-model="term"
+          clearable
+          style="height: 32px; width: 330px; line-height: 32px"
+          @keyup.enter.native="getAgencyList"
+        ></el-input>
+        <div class="seach-btn" @click="getAgencyList">查询</div>
+      </div>
+      <el-button v-auth="'ADDPROXY'" type="primary" @click="visable = true"
+        >添加代理</el-button
+      >
+    </div>
+
+    <el-dialog v-model="visable" title="添加代理" width="550px">
+      <template v-slot:default>
+        <div class="df jcc">
+          <el-form
+            :model="ruleForm"
+            :rules="rules"
+            ref="form"
+            label-width="110px"
+            label-position="left"
+          >
+            <el-form-item prop="proxyName" label="代理名称">
+              <el-input
+                style="width: 280px"
+                v-model="ruleForm.proxyName"
+              ></el-input>
+            </el-form-item>
+            <el-form-item prop="contactName" label="联系人">
+              <el-input
+                style="width: 280px"
+                v-model="ruleForm.contactName"
+              ></el-input>
+            </el-form-item>
+            <el-form-item prop="contactPhone" label="联系人手机号">
+              <el-input
+                style="width: 280px"
+                v-model="ruleForm.contactPhone"
+              ></el-input>
+            </el-form-item>
+          </el-form>
+        </div>
+      </template>
+      <template v-slot:footer>
+        <div class="dialog-footer">
+          <el-button @click="resetForm">取 消</el-button>
+          <el-button type="primary" @click="addAgency(ruleForm)">
+            确 定
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <div style="margin-top: 24px">
+      <el-table :data="tableData" stripe style="width: 100%">
+        <el-table-column
+          type="index"
+          label="序号"
+          min-width="80"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="companyName"
+          label="代理公司"
+          min-width="100"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="contactName"
+          label="联系人"
+          min-width="100"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="contactPhone"
+          label="手机号"
+          min-width="80"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="createTime"
+          label="入驻时间"
+          min-width="160"
+          align="center"
+        >
+          <template v-slot="scope">
+            {{ subTimeStr(scope.row.createTime) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" min-width="120" align="center">
+          <template v-slot="scope">
+            <div class="df aic jcsa">
+              <!-- <el-switch
+                v-model="scope.row.accountStatus"
+                active-color="#13ce66"
+                inactive-color="#ff4949"
+                active-text="启用"
+                inactive-text="禁用"
+                :active-value="1"
+                :inactive-value="0"
+              /> -->
+              <el-button disabled size="small" type="danger">删除</el-button>
+            </div>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div style="width: 100%; text-align: right; margin-top: 43px">
+        <el-pagination
+          background
+          layout="prev, pager, next"
+          :total="total"
+          @current-change="pageChange"
+        ></el-pagination>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import api from "../../apis/fetch";
+import { ref, onMounted, reactive } from "vue";
+import { ElNotification, ElMessageBox } from "element-plus";
+import { subTimeStr } from "../../utils/utils";
+
+let tableData = ref([]);
+let currentPage = ref(1);
+let total = ref(0);
+let term = ref("");
+let loginAccountId = ref(0);
+let ruleForm = ref({
+  proxyName: "",
+  contactName: "",
+  contactPhone: "",
+});
+const rules = reactive({
+  proxyName: [
+    {
+      required: true,
+      message: "请填写代理名称",
+      trigger: "blur",
+    },
+  ],
+  contactPhone: [
+    {
+      required: true,
+      message: "请填联系人手机号",
+      trigger: "blur",
+    },
+  ],
+  contactName: [
+    {
+      required: true,
+      message: "请填写联系人名称",
+      trigger: "blur",
+    },
+  ],
+});
+
+async function getAgencyList() {
+  let res = await api.getAgencyList({
+    term: term.value,
+    currentPage: currentPage.value,
+    size: 10,
+    loginAccountId: loginAccountId.value,
+  });
+  if (res.data.status == 0) {
+    tableData.value = res.data.result;
+    total.value = res.data.total;
+  } else {
+    tableData.value = [];
+    total.value = 0;
+  }
+}
+
+function pageChange(e) {
+  currentPage.value = e;
+  getAgencyList();
+}
+let visable = ref(false);
+let form = ref(null);
+async function addAgency() {
+  console.log(ruleForm.value);
+  let res = await api.addAgency({
+    ...ruleForm.value,
+    loginAccountId: loginAccountId.value,
+  });
+  console.log(res);
+  let status = res.data.status == 0;
+  ElNotification({
+    title: status ? "成功" : "失败",
+    duration: 1500,
+    message: res.data.msg,
+    type: status ? "success" : "error",
+  });
+  resetForm();
+  getAgencyList();
+}
+function resetForm() {
+  visable.value = false;
+  form.value.resetFields();
+}
+
+function closeModal() {
+  proxyName.value = "";
+  contactPhone.value = "";
+  contactName.value = "";
+}
+
+onMounted(() => {
+  loginAccountId.value = localStorage.loginAccountId;
+  getAgencyList();
+});
+</script>
+
+<style scoped>
+.seach-btn {
+  display: inline-block;
+  width: 60px;
+  height: 38px;
+  background: #0094fe;
+  border-radius: 2px;
+  font-size: 14px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: #ffffff;
+  text-align: center;
+  line-height: 38px;
+  margin-left: 10px;
+  cursor: pointer;
+}
+</style>

+ 208 - 0
src/views/authManage/addRole.vue

@@ -0,0 +1,208 @@
+<template>
+  <el-card>
+    <el-form
+      :model="ruleForm"
+      :rules="rules"
+      ref="form"
+      label-width="110px"
+      label-position="left"
+    >
+      <el-form-item prop="roleName" label="职位名称">
+        <el-input style="width: 280px" v-model="ruleForm.roleName"></el-input>
+      </el-form-item>
+      <el-form-item prop="deptId" label="部门">
+        <el-select
+          style="width: 280px"
+          v-model="ruleForm.deptId"
+          placeholder="请选择部门"
+        >
+          <el-option
+            v-for="item in departmentSelect"
+            :key="item"
+            :label="item.value"
+            :value="item.key"
+          />
+        </el-select>
+      </el-form-item>
+    </el-form>
+  </el-card>
+  <el-card class="mt30 p30">
+    <h4 class="mb30">权限设置</h4>
+    <el-tree
+      ref="treeRef"
+      :data="basePermissionData"
+      show-checkbox
+      default-expand-all
+      node-key="code"
+      highlight-current
+      :props="defaultProps"
+      :default-checked-keys="checkedNodes"
+      style="display: flex; justify-content: space-between"
+    />
+    <div class="df aic jcfe mt50">
+      <el-button type="primary" @click="addRole">{{
+        roleId ? "修改员工" : "添加职位"
+      }}</el-button>
+    </div>
+  </el-card>
+</template>
+
+<script setup>
+import api from "../../apis/fetch";
+import store from "../../store";
+import router from "../../router";
+import { ref, onMounted, reactive, computed } from "vue";
+import { ElNotification, ElMessageBox } from "element-plus";
+import { mapGetters } from "vuex";
+import { useRoute } from "vue-router";
+
+const route = useRoute();
+
+let loginAccountId = ref(0);
+let form = ref(null);
+let ruleForm = ref({
+  roleName: "",
+  deptId: "",
+});
+const rules = reactive({
+  roleName: [
+    {
+      required: true,
+      message: "请填角色名称",
+      trigger: "blur",
+    },
+  ],
+  deptId: [
+    {
+      required: true,
+      message: "请选择部门",
+      trigger: "blur",
+    },
+  ],
+});
+let roleId = ref("");
+let treeRef = ref(null);
+const defaultProps = {
+  children: "children",
+  label: "label",
+};
+
+let basePermissionData = computed(() => store.state.basePermissionData);
+let baseParentNodes = computed(() => store.state.baseParentNodes);
+let checkedNodes = ref([]);
+
+async function getRoleDetail(roleId) {
+  let res = await api.getRoleDetail({
+    loginAccountId: loginAccountId.value,
+    roleId,
+  });
+  if (res.data.status == 0) {
+    let { code, departmentId, roleName, permission, cargoLimit, proxyLimit } =
+      res.data.result;
+    ruleForm.value = {
+      roleName,
+      deptId: departmentId,
+    };
+    let c = cargoLimit.split(",");
+    let cargoLimitArr = [];
+    for (let i of c) {
+      cargoLimitArr.push(`CARGO_${i}`);
+    }
+    let p = proxyLimit.split(",");
+    let proxyLimitArr = [];
+    for (let i of p) {
+      proxyLimitArr.push(`PROXY_${i}`);
+    }
+    let arr = permission.split(",");
+    baseParentNodes.value.forEach((item) => {
+      for (let i of arr) {
+        if (i == item) {
+          let index = arr.indexOf(i);
+          arr.splice(index, 1);
+        }
+      }
+    });
+    checkedNodes.value = [...arr, ...cargoLimitArr, ...proxyLimitArr];
+  }
+}
+
+async function addRole() {
+  let fullChecked = treeRef.value.getCheckedNodes();
+  let halfCHecked = treeRef.value.getHalfCheckedNodes();
+  let permissionCodes = [];
+  for (let i of fullChecked) {
+    permissionCodes.push(i.code);
+  }
+  for (let i of halfCHecked) {
+    permissionCodes.push(i.code);
+  }
+
+  let postData = {
+    loginAccountId: loginAccountId.value,
+    ...ruleForm.value,
+    permissionCodes: permissionCodes.join(","),
+  };
+  if (roleId.value) {
+    postData.roleId = roleId.value;
+  }
+  let res = await api[roleId.value ? "updateRole" : "addRole"](postData);
+  if (res.data.status == 0) {
+    ElNotification({
+      title: "成功",
+      duration: 1500,
+      message: res.data.msg,
+      type: "success",
+    });
+    store.dispatch("GetBasePermissionData", localStorage.loginAccountId);
+    store.dispatch("GetUserPermission", localStorage.loginAccountId);
+    router.replace("/authManage/roleList");
+  } else {
+    console.log(res);
+    ElNotification({
+      title: "失败",
+      duration: 1500,
+      message: res.data.msg,
+      type: "error",
+    });
+  }
+}
+
+let departmentSelect = ref([]);
+async function getDepartmentSelect() {
+  let res = await api.getDepartmentSelect({
+    loginAccountId: loginAccountId.value,
+  });
+  departmentSelect.value = res.data.result;
+}
+
+onMounted(() => {
+  loginAccountId.value = localStorage.loginAccountId;
+  getDepartmentSelect();
+  store.dispatch("GetBasePermissionData", localStorage.loginAccountId);
+
+  let id = route.query.roleId;
+  if (id) {
+    getRoleDetail(id);
+    roleId.value = id;
+    store.commit("changefirstTitle", "修改员工");
+  }
+});
+</script>
+
+<style scoped>
+.seach-btn {
+  display: inline-block;
+  width: 60px;
+  height: 38px;
+  background: #0094fe;
+  border-radius: 2px;
+  font-size: 14px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: #ffffff;
+  text-align: center;
+  line-height: 38px;
+  margin-left: 10px;
+  cursor: pointer;
+}
+</style>

+ 236 - 0
src/views/authManage/departmentList.vue

@@ -0,0 +1,236 @@
+<template>
+  <div class="full-container-p24">
+    <div style="display: flex; justify-content: space-between">
+      <div style="display: flex">
+        <el-input
+          placeholder="请输入部门"
+          prefix-icon="el-icon-search"
+          v-model="term"
+          clearable
+          style="height: 32px; width: 330px; line-height: 32px"
+          @keyup.enter.native="getDepartmentList"
+        ></el-input>
+        <div class="seach-btn" @click="getDepartmentList">查询</div>
+      </div>
+      <el-button v-auth="'ADDUPDATEDEPT'" type="primary" @click="visable = true"
+        >添加部门</el-button
+      >
+    </div>
+
+    <el-dialog
+      v-model="visable"
+      :title="deptId ? '修改部门' : '添加部门'"
+      width="550px"
+      @close="resetForm()"
+    >
+      <template v-slot:default>
+        <div class="df jcc">
+          <el-form
+            :model="ruleForm"
+            :rules="rules"
+            ref="form"
+            label-width="110px"
+            label-position="left"
+          >
+            <el-form-item prop="name" label="部门名称">
+              <el-input
+                style="width: 280px"
+                v-model="ruleForm.deptName"
+              ></el-input>
+            </el-form-item>
+          </el-form>
+        </div>
+      </template>
+      <template v-slot:footer>
+        <div class="dialog-footer">
+          <el-button @click="resetForm">取 消</el-button>
+          <el-button type="primary" @click="addDepartment(ruleForm)">
+            确 定
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <div style="margin-top: 24px">
+      <el-table :data="tableData" stripe style="width: 100%">
+        <el-table-column
+          type="index"
+          label="序号"
+          min-width="40"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="departmentName"
+          label="部门名称"
+          min-width="80"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="createTime"
+          label="创建时间"
+          min-width="80"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          v-auth="'ADDUPDATEDEPT'"
+          label="操作"
+          min-width="120"
+          align="center"
+        >
+          <template v-slot="scope">
+            <div class="df aic jcsa">
+              <!-- <el-switch
+                v-model="scope.row.accountStatus"
+                active-color="#13ce66"
+                inactive-color="#ff4949"
+                active-text="启用"
+                inactive-text="禁用"
+                :active-value="1"
+                :inactive-value="0"
+              />
+
+              <div
+                style="
+                  color: red;
+                  margin-left: 10px;
+                  font-size: 14px;
+                  text-decoration: underline;
+                  cursor: pointer;
+                "
+              >
+                删除
+              </div> -->
+              <el-button
+                @click="showUpdateModal(scope.row)"
+                size="small"
+                type="primary"
+                >修改部门</el-button
+              >
+            </div>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div style="width: 100%; text-align: right; margin-top: 43px">
+        <el-pagination
+          background
+          layout="prev, pager, next"
+          :total="total"
+          @current-change="pageChange"
+        ></el-pagination>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import api from "../../apis/fetch";
+import { ref, onMounted, reactive } from "vue";
+import { ElNotification, ElMessageBox } from "element-plus";
+
+let tableData = ref([]);
+let currentPage = ref(1);
+let total = ref(0);
+let term = ref("");
+let loginAccountId = ref(0);
+let deptId = ref("");
+let ruleForm = ref({
+  deptName: "",
+});
+const rules = reactive({
+  deptName: [
+    {
+      required: true,
+      message: "请填写部门名称",
+      trigger: "blur",
+    },
+  ],
+});
+
+async function getDepartmentList() {
+  let res = await api.getDepartmentList({
+    term: term.value,
+    currentPage: currentPage.value,
+    size: 10,
+    loginAccountId: loginAccountId.value,
+  });
+  if (res.data.status == 0) {
+    tableData.value = res.data.result;
+    for (let i of tableData.value) {
+      if (i.roleId == 0) {
+        i.roleId = "";
+      }
+    }
+    total.value = res.data.total;
+  } else {
+    tableData.value = [];
+    total.value = 0;
+  }
+}
+
+function pageChange(e) {
+  currentPage.value = e;
+  getDepartmentList();
+}
+let visable = ref(false);
+let form = ref(null);
+async function addDepartment() {
+  let postData = {
+    ...ruleForm.value,
+    loginAccountId: loginAccountId.value,
+  };
+  if (deptId.value) {
+    postData.deptId = deptId.value;
+  }
+  let res = await api[deptId.value ? "updateDepartment" : "addDepartment"](
+    postData
+  );
+  let status = res.data.status == 0;
+  ElNotification({
+    title: status ? "成功" : "失败",
+    duration: 1500,
+    message: res.data.msg,
+    type: status ? "success" : "error",
+  });
+  resetForm();
+  getDepartmentList();
+}
+function resetForm() {
+  visable.value = false;
+  deptId.value = "";
+  ruleForm.value = {
+    deptName: "",
+  };
+  form.value.resetFields();
+}
+
+function showUpdateModal(item) {
+  visable.value = true;
+  deptId.value = item.id;
+  ruleForm.value = {
+    deptName: item.departmentName,
+  };
+}
+
+onMounted(() => {
+  loginAccountId.value = localStorage.loginAccountId;
+  getDepartmentList();
+});
+</script>
+
+<style scoped>
+.seach-btn {
+  display: inline-block;
+  width: 60px;
+  height: 38px;
+  background: #0094fe;
+  border-radius: 2px;
+  font-size: 14px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: #ffffff;
+  text-align: center;
+  line-height: 38px;
+  margin-left: 10px;
+  cursor: pointer;
+}
+</style>

+ 147 - 0
src/views/authManage/roleList.vue

@@ -0,0 +1,147 @@
+<template>
+  <div class="full-container-p24">
+    <div style="display: flex; justify-content: space-between">
+      <div style="display: flex">
+        <el-input
+          placeholder="请输入部门/职位名称"
+          prefix-icon="el-icon-search"
+          v-model="term"
+          clearable
+          style="height: 32px; width: 330px; line-height: 32px"
+          @keyup.enter.native="getRoleList"
+        ></el-input>
+        <div class="seach-btn" @click="getRoleList">查询</div>
+      </div>
+      <el-button v-auth="'ADDUPDATEROLE'" type="primary" @click="addRole"
+        >添加职位</el-button
+      >
+    </div>
+    <div style="margin-top: 24px">
+      <el-table :data="tableData" stripe style="width: 100%">
+        <el-table-column
+          type="index"
+          label="序号"
+          min-width="80"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="roleName"
+          label="职位名称"
+          min-width="100"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="departmentName"
+          label="部门"
+          min-width="100"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="createTime"
+          label="创建时间"
+          min-width="160"
+          align="center"
+        >
+          <template v-slot="scope">
+            {{ subTimeStr(scope.row.createTime) }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          v-auth="'ADDUPDATEROLE'"
+          label="操作"
+          min-width="120"
+          align="center"
+        >
+          <template v-slot="scope">
+            <el-button
+              @click="roleDetail(scope.row.id)"
+              size="small"
+              type="primary"
+              >详情</el-button
+            >
+          </template>
+        </el-table-column>
+      </el-table>
+      <div style="width: 100%; text-align: right; margin-top: 43px">
+        <el-pagination
+          background
+          layout="prev, pager, next"
+          :total="total"
+          @current-change="pageChange"
+        ></el-pagination>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import api from "../../apis/fetch";
+import { ref, onMounted, reactive } from "vue";
+import { ElNotification, ElMessageBox } from "element-plus";
+import router from "../../router";
+import store from "../../store";
+import { subTimeStr } from "../../utils/utils";
+
+let tableData = ref([]);
+let currentPage = ref(1);
+let total = ref(0);
+let term = ref("");
+let loginAccountId = ref(0);
+
+async function getRoleList() {
+  let res = await api.getRoleList({
+    term: term.value,
+    currentPage: currentPage.value,
+    size: 10,
+    loginAccountId: loginAccountId.value,
+  });
+  if (res.data.status == 0) {
+    tableData.value = res.data.result;
+    total.value = res.data.total;
+  } else {
+    tableData.value = [];
+    total.value = 0;
+  }
+}
+
+function pageChange(e) {
+  currentPage.value = e;
+  getRoleList();
+}
+
+function addRole() {
+  router.push("/authManage/addRole");
+}
+
+function roleDetail(roleId) {
+  router.push({
+    path: "/authManage/addRole",
+    query: {
+      roleId,
+    },
+  });
+}
+
+onMounted(() => {
+  loginAccountId.value = localStorage.loginAccountId;
+  getRoleList();
+});
+</script>
+
+<style scoped>
+.seach-btn {
+  display: inline-block;
+  width: 60px;
+  height: 38px;
+  background: #0094fe;
+  border-radius: 2px;
+  font-size: 14px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: #ffffff;
+  text-align: center;
+  line-height: 38px;
+  margin-left: 10px;
+  cursor: pointer;
+}
+</style>

+ 164 - 0
src/views/cargoManage/cargoList.vue

@@ -0,0 +1,164 @@
+<template>
+  <div class="full-container-p24">
+    <div style="display: flex; justify-content: space-between">
+      <div style="display: flex">
+        <el-input
+          placeholder="请输入货种名称"
+          prefix-icon="el-icon-search"
+          v-model="term"
+          clearable
+          style="height: 32px; width: 330px; line-height: 32px"
+          @keyup.enter.native="getCargoList"
+        ></el-input>
+        <div class="seach-btn" @click="getCargoList">查询</div>
+      </div>
+      <el-button v-auth="'ADDCARGO'" type="primary" @click="addCargo"
+        >添加货种</el-button
+      >
+    </div>
+
+    <div style="margin-top: 24px">
+      <el-table :data="tableData" stripe style="width: 100%">
+        <el-table-column
+          type="index"
+          label="序号"
+          min-width="80"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="cargo"
+          label="货种名称"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="createTime"
+          label="添加时间"
+          min-width="160"
+          align="center"
+        >
+          <template v-slot="scope">
+            {{ subTimeStr(scope.row.createTime) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" min-width="80" align="center">
+          <template v-slot="scope">
+            <div class="df aic jcsa">
+              <!-- <el-switch
+                v-model="scope.row.status"
+                active-color="#13ce66"
+                inactive-color="#ff4949"
+                active-text="启用"
+                inactive-text="禁用"
+                :active-value="1"
+                :inactive-value="0"
+              /> -->
+              <el-button disabled size="small" type="danger">删除</el-button>
+            </div>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div style="width: 100%; text-align: right; margin-top: 43px">
+        <el-pagination
+          background
+          layout="prev, pager, next"
+          :total="total"
+          @current-change="pageChange"
+        ></el-pagination>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import api from "../../apis/fetch";
+import { ref, onMounted } from "vue";
+import { ElNotification, ElMessageBox } from "element-plus";
+import { subTimeStr } from "../../utils/utils";
+
+let tableData = ref([]);
+let currentPage = ref(1);
+let total = ref(0);
+let term = ref("");
+let loginAccountId = ref(0);
+let cargo = ref("");
+
+async function getCargoList() {
+  let res = await api.getCargoList({
+    term: term.value,
+    currentPage: currentPage.value,
+    size: 10,
+    loginAccountId: loginAccountId.value,
+  });
+  if (res.data.status == 0) {
+    tableData.value = res.data.result;
+    total.value = res.data.total;
+  } else {
+    tableData.value = [];
+    total.value = 0;
+  }
+}
+
+function pageChange(e) {
+  currentPage.value = e;
+  getCargoList();
+}
+function addCargo() {
+  ElMessageBox.prompt("请输入货种名称", "添加货种", {
+    confirmButtonText: "添加",
+    cancelButtonText: "取消",
+    inputPattern: /^[\s\S]*.*[^\s][\s\S]*$/,
+    inputErrorMessage: "请输入货种名称",
+  })
+    .then(async ({ value }) => {
+      let res = await api.addCargo({
+        loginAccountId: loginAccountId.value,
+        cargo: value,
+      });
+      if (res.data.status == 0) {
+        ElNotification({
+          title: "成功",
+          duration: 1500,
+          message: res.data.msg,
+          type: "success",
+        });
+        getCargoList();
+      } else {
+        ElNotification({
+          title: "失败",
+          duration: 1500,
+          message: res.data.msg,
+          type: "error",
+        });
+      }
+    })
+    .catch(() => {});
+}
+
+function closeModal() {
+  cargo.value = "";
+}
+
+onMounted(() => {
+  loginAccountId.value = localStorage.loginAccountId;
+  getCargoList();
+});
+</script>
+
+<style scoped>
+.seach-btn {
+  display: inline-block;
+  width: 60px;
+  height: 38px;
+  background: #0094fe;
+  border-radius: 2px;
+  font-size: 14px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: #ffffff;
+  text-align: center;
+  line-height: 38px;
+  margin-left: 10px;
+  cursor: pointer;
+}
+</style>

+ 3 - 0
src/views/index/Index.vue

@@ -0,0 +1,3 @@
+<template>主页</template>
+<script setup>
+</script>

+ 279 - 0
src/views/index/Login.vue

@@ -0,0 +1,279 @@
+<template>
+  <div class="container">
+    <div class="login-box">
+      <div class="left">
+        <div class="left-up-icon"></div>
+      </div>
+      <div class="right">
+        <div class="title">
+          <div class="title-left">浙江物产</div>
+          <div class="title-mid">丨</div>
+          <div class="title-right">智慧航运管理平台</div>
+        </div>
+        <div class="form-container">
+          <el-form :model="ruleForm" :rules="rules" ref="form">
+            <el-form-item prop="phone">
+              <el-input placeholder="请输入手机号" v-model="ruleForm.phone">
+                <template v-slot:prepend>
+                  <el-button icon="el-icon-mobile-phone"></el-button>
+                </template>
+              </el-input>
+            </el-form-item>
+            <el-form-item prop="password">
+              <el-input
+                type="password"
+                placeholder="请输入密码"
+                v-model="ruleForm.password"
+              >
+                <template v-slot:prepend>
+                  <el-button icon="el-icon-lock"></el-button>
+                </template>
+              </el-input>
+            </el-form-item>
+            <el-form-item>
+              <el-button
+                style="
+                  width: 384px;
+                  height: 48px;
+                  border-radius: 2px;
+                  margin-top: 40px;
+                "
+                type="primary"
+                @click="login('ruleForm')"
+              >
+                登录
+              </el-button>
+            </el-form-item>
+          </el-form>
+        </div>
+        <div class="df aic jcc" style="margin-top: 55px">
+          <img
+            style="height: 20px"
+            src="https://6875-huihenduo-2gx127w7f837b584-1255802371.tcb.qcloud.la/miniapp-static/%E6%B1%87%E5%BE%88%E5%A4%9Alogo-%E5%B7%A6%E5%8F%B3.png"
+            alt=""
+          />
+          <div
+            style="
+              height: 20px;
+              line-height: 18px;
+              font-size: 10px;
+              color: #999;
+              margin-left: 15px;
+            "
+          >
+            汇很多智慧航运研发中心
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div @click="goBeian" class="copyright">
+      Copyright © 2021 河南省汇很多科技有限公司 豫ICP备 2021029101 号
+    </div>
+  </div>
+</template>
+<script>
+import { ref, reactive, toRefs, computed } from "vue";
+import { ElNotification } from "element-plus";
+import store from "../../store";
+import router from "../../router";
+
+import md5 from "md5";
+import api from "../../apis/fetch";
+
+export default {
+  setup() {
+    const form = ref(null);
+    const ruleForm = reactive({
+      ruleForm: {
+        phone: "",
+        password: "",
+      },
+    });
+
+    const rules = reactive({
+      rules: {
+        phone: [
+          { required: true, message: "请输入手机号", trigger: "blur" },
+          { min: 11, max: 11, message: "请正确输入手机号", trigger: "blur" },
+        ],
+        password: [
+          { required: true, message: "请输入密码", trigger: "blur" },
+          { min: 6, max: 20, message: "请正确输入手机号", trigger: "blur" },
+        ],
+      },
+    });
+    function check() {
+      // form.value.validate((valid) => {});
+    }
+    function login() {
+      form.value.validate(async (valid) => {
+        if (valid) {
+          let { phone, password } = ruleForm.ruleForm;
+          let res = await api.staffLogin({
+            phone,
+            // password: md5(password).toUpperCase(),
+            password,
+            cargoOwnerId: 31,
+          });
+          if (res.data.status == 0) {
+            let {
+              userId,
+              userName,
+              phone,
+              contactName,
+              loginAccountId,
+              rolePermission,
+            } = res.data.result;
+            if (!rolePermission.length) {
+              ElNotification.error({
+                title: "失败",
+                duration: 2000,
+                message: "暂无权限,请联系管理员",
+                type: "error",
+              });
+              return;
+            }
+            ElNotification.success({
+              title: "成功",
+              duration: 2000,
+              message: res.data.msg,
+              type: "success",
+            });
+            localStorage.setItem("userId", userId);
+            localStorage.setItem("userName", userName);
+            localStorage.setItem("phone", phone);
+            localStorage.setItem("contactName", contactName);
+            localStorage.setItem("userType", 1);
+            localStorage.setItem("loginAccountId", loginAccountId);
+            rolePermission = rolePermission || [];
+            let arr = [...new Set([...rolePermission])];
+            localStorage.setItem("rolePermission", arr);
+            store.dispatch(
+              "GetBasePermissionData",
+              localStorage.loginAccountId
+            );
+            let res1 = await store.dispatch(
+              "GetUserPermission",
+              localStorage.loginAccountId
+            );
+
+            router.replace({ path: store.state.menuData[0]?.items[0].path });
+            store.commit("changeLogin", true);
+          } else {
+            ElNotification.error({
+              title: "错误",
+              duration: 3000,
+              message: res.data.msg,
+            });
+          }
+        } else {
+          console.log("error submit!!");
+          return false;
+        }
+      });
+    }
+
+    function goBeian() {
+      window.open("https://beian.miit.gov.cn/");
+    }
+
+    return {
+      form,
+      ...toRefs(ruleForm),
+      ...toRefs(rules),
+      login,
+      goBeian,
+    };
+  },
+};
+</script>
+<style scoped>
+.container {
+  width: 100%;
+  height: 100%;
+  background-image: url(../../assets/login-back.png);
+  background-size: cover;
+}
+
+.login-box {
+  position: relative;
+  display: flex;
+  top: calc(50% - 255px);
+  left: calc(50% - 478px);
+  width: 966px;
+  height: 508px;
+  border-radius: 10px;
+  overflow: hidden;
+}
+
+.left {
+  height: 100%;
+  width: 450px;
+  background-image: url(../../assets/login-modal.png);
+}
+
+.left-up-icon {
+  width: 40px;
+  height: 40px;
+  border-radius: 50%;
+  margin: 24px;
+  background-image: url(../../assets/logo.png);
+  background-size: contain;
+}
+
+.right {
+  height: 100%;
+  width: 516px;
+  background: #fff;
+}
+
+.title {
+  width: 384px;
+  height: 38px;
+  display: flex;
+  margin: 0 auto;
+  margin-top: 100px;
+}
+
+.title-left {
+  height: 38px;
+  background: url(https://6875-huihenduo-2gx127w7f837b584-1255802371.tcb.qcloud.la/web-static/wuchan3.jpg);
+  background-size: contain;
+  background-repeat: no-repeat;
+  line-height: 38px;
+  padding-left: 46px;
+  font-size: 22px;
+}
+
+.title-mid {
+  font-size: 25px;
+  color: #e4e4e4;
+}
+
+.title-right {
+  font-size: 28px;
+  font-family: Adobe Heiti Std;
+  font-weight: normal;
+  color: #434343;
+  line-height: 38px;
+}
+
+.form-container {
+  margin: 0 auto;
+  margin-top: 60px;
+  width: 384px;
+}
+
+.copyright {
+  position: absolute;
+  width: 520px;
+  bottom: 70px;
+  left: calc(50% - 250px);
+  font-family: PingFang SC;
+  font-weight: 400;
+  color: #aaa;
+  opacity: 0.8;
+  cursor: pointer;
+}
+</style>

+ 1784 - 0
src/views/voyage/voyageDetail.vue

@@ -0,0 +1,1784 @@
+<template>
+  <div class="line-container-p24">
+    <i class="el-icon-arrow-left"></i>
+    <div
+      class="dib go-back ml8 pointer"
+      @click="router.replace('/voyage/voyageList')"
+    >
+      返回航次列表
+    </div>
+  </div>
+
+  <div class="container-title df aic jcsb">
+    <div class="df aic">
+      <div class="mr30">航次信息</div>
+      <el-tooltip
+        v-if="blockchainInfo"
+        class="box-item"
+        effect="light"
+        :content="blockchainInfo.hash"
+        placement="top"
+      >
+        <div class="pointer" style="font-size: 14px; font-weight: normal">
+          汇很多科技区块链认证
+        </div>
+      </el-tooltip>
+    </div>
+    <el-button
+      v-auth="'DOWNLOADSHIPTRACK'"
+      style="width: 220px; margin-right: 20px"
+      type="primary"
+      @click="downloadExcel"
+      :loading="isLoadingExcel"
+    >
+      下载船舶跟踪表
+    </el-button>
+  </div>
+  <div class="line-container-p24">
+    <div v-auth="'VOYAGEINFO'">
+      <div class="line">
+        <div class="info-line">
+          <div class="info-line-title">航次名称</div>
+          <el-input
+            class="info-line-text"
+            v-model="voyage.voyageName"
+            disabled
+          ></el-input>
+        </div>
+        <div class="info-gap" v-if="shipAudits.length"></div>
+        <div class="info-line">
+          <div class="info-line-title">货主</div>
+          <el-input
+            class="info-line-text"
+            v-model="voyage.cargoOwnerName"
+            disabled
+          ></el-input>
+        </div>
+      </div>
+      <!-- <div class="line">
+      <div class="info-line">
+        <div class="info-line-title">船东</div>
+        <el-input
+          class="info-line-text"
+          v-model="voyage.shipOwnerName"
+          disabled
+        ></el-input>
+      </div>
+      <div class="info-line">
+        <div class="info-line-title">船东手机号</div>
+        <el-input
+          class="info-line-text"
+          v-model="voyage.shipOwnerPhone"
+          disabled
+        ></el-input>
+      </div>
+    </div> -->
+      <div class="line">
+        <div class="info-line">
+          <div class="info-line-title">船舶名称</div>
+          <el-input
+            class="info-line-text"
+            v-model="voyage.shipName"
+            disabled
+          ></el-input>
+        </div>
+        <div
+          class="info-gap"
+          v-if="shipAudits.length"
+          @click="isCertsVisable = true"
+        >
+          汇很多认证
+        </div>
+        <el-dialog
+          @open="showCerts"
+          v-model="isCertsVisable"
+          destroy-on-close
+          width="30%"
+        >
+          <Certs ref="certs"></Certs>
+        </el-dialog>
+        <div class="info-line">
+          <div class="info-line-title">MMSI</div>
+          <el-input
+            class="info-line-text"
+            v-model="voyage.shipMmsi"
+            disabled
+          ></el-input>
+        </div>
+      </div>
+      <div id="map-container" class="map-container"></div>
+      <div class="line" style="margin-top: 30px">
+        <div class="info-line">
+          <div class="info-line-title">开始时间</div>
+          <el-input
+            class="info-line-text"
+            v-model="voyage.startTime"
+            disabled
+          ></el-input>
+        </div>
+        <div class="info-line">
+          <div class="info-line-title">结束时间</div>
+          <el-input
+            class="info-line-text"
+            v-model="voyage.endTime"
+            disabled
+          ></el-input>
+        </div>
+      </div>
+      <div class="line">
+        <div class="info-line">
+          <div class="info-line-title">装货港</div>
+          <el-input
+            class="info-line-text"
+            v-model="voyage.loadPort"
+            disabled
+          ></el-input>
+        </div>
+      </div>
+      <div class="line">
+        <div class="info-line" v-for="(item, index) in voyage.voyageDetails">
+          <div class="info-line-title">卸货港{{ " # " + (index + 1) }}</div>
+          <el-input
+            class="info-line-text"
+            v-model="item.portName"
+            disabled
+          ></el-input>
+        </div>
+      </div>
+      <div class="line">
+        <div class="info-line">
+          <div class="info-line-title">货种</div>
+          <el-input
+            class="info-line-text"
+            v-model="voyage.cargo"
+            disabled
+          ></el-input>
+        </div>
+        <div class="info-line">
+          <div style="width: 120px !important" class="info-line-title">
+            货量
+          </div>
+          <el-input
+            style="width: 100px !important"
+            class="info-line-text"
+            v-model="voyage.tons"
+            disabled
+          ></el-input>
+          <span class="unit">吨</span>
+          <el-input
+            style="width: 80px !important"
+            class="info-line-text"
+            v-model="voyage.pieces"
+            disabled
+          ></el-input>
+          <span class="unit">件</span>
+        </div>
+      </div>
+    </div>
+    <div v-auth="'SHIPTRANS'">
+      <div class="container-second-title df aic jcsb mt40">
+        <div>船舶运输记录详情</div>
+        <div style="margin-right: 20px">
+          信息更新时间 :
+          <span style="font-size: 18px">
+            {{ subStr(voyage.infoUpdateTime) }}</span
+          >
+        </div>
+      </div>
+      <div class="line-container-p24">
+        <div class="line">
+          <div class="info-line">
+            <div class="info-line-title">到达装货港时间</div>
+            <el-date-picker
+              class="info-line-text"
+              v-model="voyage.arrivalLoadPortTime"
+              type="datetime"
+              format="YYYY/MM/DD HH:mm"
+              value-format="YYYY/MM/DD HH:mm:ss"
+              placeholder="到达装货港时间"
+              :disabled="disabledStatus"
+            ></el-date-picker>
+          </div>
+          <div class="info-line">
+            <div class="info-line-title">实装货量</div>
+            <el-input
+              style="width: 100px !important"
+              class="info-line-text"
+              v-model="voyage.actualLoadTons"
+              :disabled="disabledStatus"
+              placeholder="实装吨位"
+            ></el-input>
+            <span class="unit">吨</span>
+            <el-input
+              style="width: 80px !important"
+              class="info-line-text"
+              v-model="voyage.actualLoadPieces"
+              :disabled="disabledStatus"
+              placeholder="实装件数"
+            ></el-input>
+            <span class="unit">件</span>
+          </div>
+        </div>
+        <div class="line">
+          <div class="info-line">
+            <div class="info-line-title">装货开始时间</div>
+            <el-date-picker
+              class="info-line-text"
+              v-model="voyage.loadStartTime"
+              type="datetime"
+              format="YYYY/MM/DD HH:mm"
+              value-format="YYYY/MM/DD HH:mm:ss"
+              placeholder="装货开始时间"
+              :disabled="disabledStatus"
+            ></el-date-picker>
+          </div>
+          <div class="info-line">
+            <div class="info-line-title">装货结束时间</div>
+            <el-date-picker
+              class="info-line-text"
+              v-model="voyage.loadEndTime"
+              type="datetime"
+              format="YYYY/MM/DD HH:mm"
+              value-format="YYYY/MM/DD HH:mm:ss"
+              placeholder="装货开始时间"
+              :disabled="disabledStatus"
+            ></el-date-picker>
+          </div>
+        </div>
+        <el-tabs
+          v-model="currentPortId"
+          type="border-card"
+          class="demo-tabs mb20"
+        >
+          <el-tab-pane
+            v-for="(item, index) in voyage.voyageDetails"
+            :label="item.portName + ' # ' + (index + 1)"
+            :name="item.portId + ''"
+          >
+            <div class="line">
+              <div class="info-line">
+                <div class="info-line-title">开航时间</div>
+                <el-date-picker
+                  class="info-line-text"
+                  v-model="item.setSailTime"
+                  type="datetime"
+                  @change="calExpectedArrivalTime"
+                  format="YYYY/MM/DD HH:mm"
+                  value-format="YYYY/MM/DD HH:mm:ss"
+                  placeholder="开航时间"
+                  :disabled="disabledStatus"
+                ></el-date-picker>
+              </div>
+              <div class="info-line">
+                <div class="info-line-title">预计到港时间</div>
+                <el-date-picker
+                  class="info-line-text"
+                  v-model="item.expectedArrivalTime"
+                  type="datetime"
+                  format="YYYY/MM/DD"
+                  value-format="YYYY/MM/DD HH:mm:ss"
+                  placeholder="预计到港时间"
+                  :disabled="disabledStatus"
+                ></el-date-picker>
+              </div>
+            </div>
+            <div class="line">
+              <div class="info-line">
+                <div class="info-line-title">实际到港时间</div>
+                <el-date-picker
+                  class="info-line-text"
+                  v-model="item.actualArrivalTime"
+                  type="datetime"
+                  format="YYYY/MM/DD HH:mm"
+                  value-format="YYYY/MM/DD HH:mm:ss"
+                  placeholder="实际到港时间"
+                  :disabled="disabledStatus"
+                ></el-date-picker>
+              </div>
+              <!-- <div class="info-line">
+        <div class="info-line-title">抵达目的地港时间</div>
+        <el-date-picker
+          class="info-line-text"
+          v-model="voyage.arrivalPortTime"
+          type="datetime"
+          format="YYYY/MM/DD HH:mm"
+          value-format="YYYY/MM/DD HH:mm:ss"
+          placeholder="抵达目的地港时间"
+          :disabled="disabledStatus"
+        ></el-date-picker>
+      </div> -->
+            </div>
+            <div class="line">
+              <div class="info-line">
+                <div class="info-line-title">卸货开始时间</div>
+                <el-date-picker
+                  class="info-line-text"
+                  v-model="item.dischargeStartTime"
+                  type="datetime"
+                  format="YYYY/MM/DD HH:mm"
+                  value-format="YYYY/MM/DD HH:mm:ss"
+                  placeholder="卸货开始时间"
+                  :disabled="disabledStatus"
+                ></el-date-picker>
+              </div>
+              <div class="info-line">
+                <div class="info-line-title">卸货结束时间</div>
+                <el-date-picker
+                  class="info-line-text"
+                  v-model="item.dischargeEndTime"
+                  type="datetime"
+                  format="YYYY/MM/DD HH:mm"
+                  value-format="YYYY/MM/DD HH:mm:ss"
+                  placeholder="卸货结束时间"
+                  :disabled="disabledStatus"
+                ></el-date-picker>
+              </div>
+            </div>
+            <div class="line">
+              <div class="info-line">
+                <div class="info-line-title">实际卸货量</div>
+                <el-input
+                  style="width: 100px !important"
+                  class="info-line-text"
+                  placeholder="实际卸货吨位"
+                  v-model="item.actualDischargeTons"
+                  :disabled="disabledStatus"
+                ></el-input>
+                <span class="unit">吨</span>
+                <el-input
+                  style="width: 80px !important"
+                  class="info-line-text"
+                  placeholder="实际卸货件数"
+                  v-model="item.actualDischargePieces"
+                  :disabled="disabledStatus"
+                ></el-input>
+                <span class="unit">件</span>
+              </div>
+              <!-- <div class="info-line">
+        <div class="info-line-title">是否购买保险</div>
+        <el-checkbox
+          v-model="voyage.hasInsurance"
+          :checked="voyage.hasInsurance == 1"
+          :disabled="disabledStatus"
+          label="购买保险"
+        ></el-checkbox>
+      </div> -->
+            </div>
+          </el-tab-pane>
+        </el-tabs>
+        <div class="line">
+          <div class="info-line">
+            <div class="info-line-title">备注</div>
+            <el-input
+              class="info-line-textarea"
+              v-model="voyage.remark"
+              autosize
+              type="textarea"
+              :disabled="disabledStatus"
+            ></el-input>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+  <div class="container-title">卸货信息</div>
+  <div class="line-container-p24">
+    <el-tabs
+      v-model="currentDiscPortId"
+      type="card"
+      class="demo-tabs"
+      @tab-click="changeDiscPortTab"
+    >
+      <el-tab-pane
+        v-for="(item, index) in voyage.voyageDetails"
+        :label="item.portName + ' # ' + (index + 1)"
+        :name="item.portId + ''"
+      ></el-tab-pane>
+    </el-tabs>
+    <div>
+      <div class="container-second-title df aic jcsb">
+        <div>天气信息</div>
+      </div>
+      <el-table style="width: 1200px" :data="weatherTableData" stripe>
+        <el-table-column
+          type="index"
+          label="序号"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="weather"
+          label="天气"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="temperature"
+          label="温度"
+          min-width="100"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="winddirection"
+          label="风向"
+          min-width="100"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="windpower"
+          label="风力"
+          min-width="100"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="reporttime"
+          label="记录时间"
+          min-width="100"
+          align="center"
+        >
+          <template v-slot="scope">
+            {{ subTimeStr(scope.row.reporttime, 16) }}
+          </template>
+        </el-table-column>
+      </el-table>
+      <div style="width: 1200px; text-align: right; margin-top: 43px">
+        <el-pagination
+          background
+          layout="prev, pager, next"
+          :total="weatherTotal"
+          @current-change="weatherPageChange"
+        ></el-pagination>
+      </div>
+      <div class="hr m30-0"></div>
+    </div>
+    <div v-auth="'LABDETAIL'">
+      <div class="container-second-title df aic jcsb">
+        <div>提单信息</div>
+      </div>
+      <el-table :data="labTableData" stripe style="width: 1200px">
+        <el-table-column
+          type="index"
+          label="序号"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="billingDate"
+          label="开单日期"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="billingNum"
+          label="开单数量"
+          min-width="100"
+          align="center"
+        ></el-table-column>
+
+        <el-table-column label="单据" min-width="150" align="center">
+          <template v-slot="scope">
+            <el-button
+              @click="showLab(scope.row, scope.$index, '查看提单')"
+              type="primary"
+              size="small"
+            >
+              查看
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div style="text-align: right; margin-top: 43px; width: 1200px">
+        <el-pagination
+          background
+          layout="prev, pager, next"
+          :total="labTotal"
+          @current-change="labPageChange"
+        ></el-pagination>
+      </div>
+      <el-dialog
+        v-model="isAddLabVisable"
+        :title="labModalType"
+        width="780px"
+        center
+        @close="cancelUploadLab"
+      >
+        <el-form
+          :model="labForm"
+          inline
+          style="margin-bottom: 20px"
+          label-width="100px"
+        >
+          <el-form-item label="开单日期">
+            <el-date-picker
+              class="info-line-text"
+              v-model="labForm.billingDate"
+              type="date"
+              format="YYYY/MM/DD"
+              value-format="YYYY/MM/DD"
+              placeholder="开单日期"
+              disabled
+            ></el-date-picker>
+          </el-form-item>
+          <el-form-item label="开单数量">
+            <el-input
+              style="width: 240px"
+              v-model="labForm.billingNum"
+              placeholder="开单数量"
+              disabled
+            ></el-input>
+          </el-form-item>
+          <el-form-item label="提单">
+            <Uploader
+              disabled
+              :actionUrl="store.state.wayBillUrl"
+              :uploaderId="'labLoad'"
+              :params="labParams"
+              @onSendFileList="getLabBillList"
+              :fileList="labBillList"
+              uploadText="上传提单"
+              :limit="1"
+            ></Uploader>
+          </el-form-item>
+        </el-form>
+      </el-dialog>
+      <div class="hr m30-0"></div>
+    </div>
+    <div v-auth="'SHIPDISCHARGE'">
+      <div class="container-second-title df aic jcsb">
+        <div>卸货记录</div>
+        <el-button
+          v-auth="'DOWNLOADDISCHARGE'"
+          @click="exportDischargeExcel"
+          style="width: 220px; margin-right: 20px"
+          type="primary"
+          :loading="isDischargeLoadingExcel"
+          >下载卸货信息</el-button
+        >
+      </div>
+      <el-table :data="dischargeList" stripe style="width: 1200px">
+        <el-table-column
+          type="index"
+          label="序号"
+          min-width="80"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="dischargeTime"
+          label="卸货时间"
+          min-width="120"
+          align="center"
+        >
+          <template v-slot="scope">
+            {{ subTimeStr(scope.row.dischargeTime, 16) }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="dischargeTons"
+          label="卸货吨位"
+          min-width="100"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="dischargePieces"
+          label="卸货件数"
+          min-width="100"
+          align="center"
+        ></el-table-column>
+        <el-table-column label="磅单" min-width="150" align="center">
+          <template v-slot="scope">
+            <el-button
+              @click="showUpdateDischarge(scope.row, scope.$index)"
+              type="primary"
+              size="small"
+              v-if="scope.row.files"
+            >
+              查看
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div style="width: 1200px; text-align: right; margin-top: 43px">
+        <el-pagination
+          background
+          layout="prev, pager, next"
+          :total="total"
+          @current-change="pageChange"
+        ></el-pagination>
+      </div>
+      <div class="hr m30-0"></div>
+    </div>
+    <el-dialog v-model="dialogVisible" title="图片预览" width="30%">
+      <el-image
+        :src="dialogImageUrl"
+        style="height: 100%; width: 100%"
+      ></el-image>
+    </el-dialog>
+    <div v-auth="'CARLOAD'">
+      <div class="container-second-title df aic jcsb mt40">
+        <div>汽车装货记录详情</div>
+      </div>
+      <el-table :data="truckTableData" stripe>
+        <el-table-column
+          type="index"
+          label="序号"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="portName"
+          label="港口名称"
+          min-width="100"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="weighTime"
+          label="称重时间"
+          min-width="120"
+          align="center"
+        >
+          <template v-slot="scope">
+            {{ subTimeStr(scope.row.weighTime, 16) }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="carNum"
+          label="车号"
+          min-width="100"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="cargoName"
+          label="货物名称"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="shippingUnit"
+          label="发货单位"
+          min-width="100"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="receivingUnit"
+          label="收货单位"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="grossWeight"
+          label="毛重"
+          min-width="100"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="tare"
+          label="皮重"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="netWeight"
+          label="净重"
+          min-width="100"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="shipName"
+          label="货船名称"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="weigher"
+          label="司磅员"
+          min-width="100"
+          align="center"
+        ></el-table-column>
+        <el-table-column label="单据" min-width="150" align="center">
+          <template v-slot="scope">
+            <el-button
+              @click="showTruckRecord(scope.row, scope.$index, '查看单据')"
+              type="primary"
+              size="small"
+            >
+              查看
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div style="text-align: right; margin-top: 43px">
+        <el-pagination
+          background
+          layout="prev, pager, next"
+          :total="truckTotal"
+          @current-change="truckPageChange"
+        ></el-pagination>
+      </div>
+    </div>
+    <el-dialog
+      v-model="isAddTruckRecordVisable"
+      title="查看记录"
+      width="780px"
+      center
+    >
+      <el-form
+        :model="truckRecordForm"
+        inline
+        style="margin-bottom: 20px"
+        label-width="100px"
+      >
+        <el-form-item label="港口名称">
+          <el-input
+            style="width: 240px"
+            v-model="truckRecordForm.portName"
+            placeholder="港口名称"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="称重时间">
+          <el-date-picker
+            class="info-line-text"
+            v-model="truckRecordForm.weighTime"
+            type="datetime"
+            format="YYYY/MM/DD HH:mm"
+            value-format="YYYY/MM/DD HH:mm:ss"
+            placeholder="称重时间"
+          ></el-date-picker>
+        </el-form-item>
+        <el-form-item label="车号">
+          <el-input
+            style="width: 240px"
+            v-model="truckRecordForm.carNum"
+            placeholder="车号"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="货物名称">
+          <el-input
+            style="width: 240px"
+            v-model="truckRecordForm.cargoName"
+            placeholder="货物名称"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="发货单位">
+          <el-input
+            style="width: 240px"
+            v-model="truckRecordForm.shippingUnit"
+            placeholder="发货单位"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="毛重">
+          <el-input
+            style="width: 240px"
+            v-model="truckRecordForm.grossWeight"
+            placeholder="毛重"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="收货单位">
+          <el-input
+            style="width: 240px"
+            v-model="truckRecordForm.receivingUnit"
+            placeholder="收货单位"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="皮重">
+          <el-input
+            style="width: 240px"
+            v-model="truckRecordForm.tare"
+            placeholder="皮重"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="运输单位">
+          <el-input
+            style="width: 240px"
+            v-model="truckRecordForm.transUnit"
+            placeholder="运输单位"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="净重">
+          <el-input
+            style="width: 240px"
+            v-model="truckRecordForm.netWeight"
+            placeholder="净重"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="货船名称">
+          <el-input
+            style="width: 240px"
+            v-model="truckRecordForm.shipName"
+            placeholder="货船名称"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="司磅员">
+          <el-input
+            style="width: 240px"
+            v-model="truckRecordForm.weigher"
+            placeholder="司磅员"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="汽车装货单">
+          <Uploader
+            actionUrl="#"
+            :uploaderId="'truckLoad'"
+            :params="truckLoadParams"
+            @onSendFileList="getTruckLoadBillList"
+            :fileList="truckRecordBillList"
+            uploadText="上传装货单"
+            :limit="1"
+            disabled
+          ></Uploader>
+        </el-form-item>
+      </el-form>
+    </el-dialog>
+  </div>
+  <div v-auth="'BILLINFO'">
+    <div class="container-title df aic jcsb">
+      单据信息
+      <el-button
+        style="margin-right: 30px"
+        size="medium"
+        type="primary"
+        v-auth="'BILLDOWNLOAD'"
+        @click="downloadBillZip"
+        :loading="billZipLoading"
+        >下载单据</el-button
+      >
+    </div>
+    <div class="line-container-p24" style="padding-left: 60px">
+      <div class="df aic">
+        <div class="info-line-title">保险单:</div>
+        <el-image
+          style="width: 200px; height: 200px; margin-right: 20px"
+          :src="item.viewUrl"
+          v-for="item in policyList"
+          :key="item"
+          @click="openMediaModal(item.viewUrl, 1, '保险单查看')"
+        ></el-image>
+      </div>
+      <hr class="hr m30-0" />
+      <div class="df aic">
+        <div class="info-line-title">运单:</div>
+        <el-image
+          style="width: 200px; height: 200px; margin-right: 20px"
+          :src="item.viewUrl"
+          v-for="item in voyageBill"
+          :key="item"
+          @click="openMediaModal(item.viewUrl, 1, '运单查看')"
+        ></el-image>
+      </div>
+    </div>
+  </div>
+
+  <div v-auth="'VOYAGEIMAGE'">
+    <div class="container-title">航次图片</div>
+    <div class="line-container-p24">
+      <div v-if="medias.length" class="medias-content df ffw">
+        <div class="pic-container">
+          <div v-for="(item, index) in medias" :key="item" class="pic-main">
+            <div
+              :class="[
+                'box',
+                index % 2 == 0 ? '' : 'bottom-box',
+                item.status == 1 ? 'now-box' : '',
+              ]"
+            >
+              <div class="card-note">
+                {{ item.shipName }} 拍摄于
+                <br />
+                {{ item.createTime }}
+                <br />
+                天气 : {{ item.weather?.weather }} - 气温 :
+                {{ item.weather?.temperature }}℃
+              </div>
+              <div class="medias-box mb10" style="position: relative">
+                <el-image
+                  v-if="item.mediaType == 1"
+                  style="width: 100%; height: 100%"
+                  fit="contain"
+                  :src="item.downloadUrl"
+                  @click="openMediaModal(item.downloadUrl, 1, '图片审核')"
+                ></el-image>
+                <video
+                  style="width: 100%; height: 100%"
+                  v-else
+                  :src="item.downloadUrl"
+                ></video>
+                <img
+                  @click="openMediaModal(item.downloadUrl, 2, '视频审核')"
+                  v-if="item.mediaType == 2"
+                  src="../../assets/icon-player.png"
+                  style="
+                    object-fit: contain;
+                    width: 40px;
+                    height: 40px;
+                    position: absolute;
+                    top: calc(50% - 20px);
+                    left: calc(50% - 20px);
+                    background: #fff;
+                    border-radius: 50%;
+                  "
+                  alt=""
+                />
+              </div>
+            </div>
+            <div
+              :class="[
+                's-line',
+                index % 2 == 0 ? '' : 'top210px',
+                item.status == 1 ? 'now-s-line' : '',
+              ]"
+            ></div>
+            <div :class="['point', item.status == 1 ? '' : 'now-point']"></div>
+            <div
+              :class="['l-line', item.status == 1 ? 'now-l-line' : '']"
+              v-if="index + 1 != medias.length"
+            ></div>
+          </div>
+        </div>
+      </div>
+      <el-dialog v-model="mediaModal" :title="modalTitle">
+        <el-image
+          v-if="modalType == 1"
+          style="height: 60vh; display: flex"
+          fit="contain"
+          :src="currentUrl"
+          :preview-src-list="modalPreview"
+        ></el-image>
+        <video
+          v-else
+          autoplay
+          controls
+          style="width: 100%; height: 100%"
+          :src="currentUrl"
+        ></video>
+      </el-dialog>
+      <el-dialog
+        v-model="updateDischargeDialog"
+        title="查看记录"
+        width="700px"
+        center
+      >
+        <el-form :model="updateForm" style="margin-bottom: 20px">
+          <!-- <el-form-item label="记录ID">
+            <span style="padding-left: 20px">{{ updateForm.id }}</span>
+          </el-form-item> -->
+          <el-form-item label="卸货时间">
+            <el-date-picker
+              class="info-line-text"
+              v-model="updateForm.dischargeTime"
+              type="datetime"
+              format="YYYY/MM/DD HH:mm"
+              value-format="YYYY/MM/DD HH:mm:ss"
+              placeholder="卸货时间"
+              disabled
+            ></el-date-picker>
+          </el-form-item>
+          <el-form-item label="卸货吨位">
+            <el-input
+              style="width: 240px"
+              v-model="updateForm.dischargeTons"
+              placeholder="卸货吨位"
+              disabled
+            ></el-input>
+          </el-form-item>
+          <el-form-item label="磅单查看">
+            <el-image
+              style="width: 200px; height: 200px; margin-right: 20px"
+              v-for="item in updatePoundBillList"
+              :src="item.viewUrl"
+              :key="item"
+              @click="openMediaModal(item.viewUrl, 1, '磅单查看')"
+            ></el-image>
+          </el-form-item>
+        </el-form>
+      </el-dialog>
+    </div>
+  </div>
+</template>
+<script setup>
+import { onMounted, reactive, ref, toRefs } from "vue";
+import api from "../../apis/fetch";
+import { useRoute } from "vue-router";
+import _ from "lodash";
+import router from "../../router";
+import store from "../../store";
+import { ElNotification, ElMessageBox, ElMessage } from "element-plus";
+import downloadBlobFile from "../../utils/downloadBlobFile";
+import url from "../../apis/config";
+import { subTimeStr } from "utils/utils";
+
+const route = useRoute();
+let map = ref();
+let voyage = ref({});
+let medias = ref([]);
+let coordinates = ref();
+let previewSrcList = ref([]);
+let policyList = ref([]);
+let previewPoundList = ref([]);
+let shipAudits = ref([]);
+
+let certs = ref(null);
+let isCertsVisable = ref(false);
+
+function showCerts() {
+  setTimeout(() => {
+    certs.value.initCerts(shipAudits.value);
+  });
+}
+
+async function getVoyageDetail(isInit) {
+  policyList.value = [];
+  voyageBill.value = [];
+  previewSrcList.value = [];
+  let res = await api.getVoyageDetail({
+    type: localStorage.userType,
+    voyageId: route.query.id,
+    loginAccountId: localStorage.loginAccountId,
+  });
+  if (res.data.status == 0) {
+    ElNotification({
+      type: "success",
+      title: res.data.msg,
+    });
+    blockchainInfo.value = res.data.result.blockChain;
+    coordinates.value = res.data.result.coordinates;
+    voyage.value = res.data.result.voyage;
+    voyage.value.startTime = voyage.value.startTime.substring(0, 16);
+    currentPortId.value = voyage.value.voyageDetails[0].portId + "";
+    currentDiscPortId.value = voyage.value.voyageDetails[0].portId + "";
+    medias.value = res.data.result.medias;
+    shipAudits.value = res.data.result.shipAudits;
+
+    for (let i of res.data.result.policys || []) {
+      policyList.value.push({
+        ...i,
+        url: i.viewUrl,
+      });
+    }
+    for (let i of res.data.result.waybills) {
+      voyageBill.value.push({
+        ...i,
+        url: i.viewUrl,
+      });
+    }
+    for (let i of medias.value) {
+      previewSrcList.value.push(i.downloadUrl);
+    }
+    if (isInit) {
+      getDischargeList();
+      getTruckLoadRecord();
+      getLabList();
+      getPortWeatherList();
+      initMap();
+    }
+  } else {
+    console.log(res);
+    ElNotification({
+      type: "error",
+      title: res.data.msg,
+    });
+  }
+}
+let total = ref(0);
+function pageChange(e) {
+  dischargeCurrentPage.value = e;
+  getDischargeList();
+}
+async function addDischarge() {
+  if (!formInline.value.dischargeTime || !formInline.value.dischargeTons)
+    return;
+  let res = await api.addDischarge({
+    ...formInline.value,
+    voyageId: route.query.id,
+  });
+  if (res.data.status == 0) {
+    getDischargeList(1);
+    formInline.value = {};
+  } else {
+    console.log(res);
+  }
+}
+async function deleteDischarge(id, index) {
+  let res = await api.deleteDischarge({
+    id,
+  });
+  if (res.data.status == 0) {
+    dischargeList.value.splice(index, 1);
+    ElNotification({
+      type: "success",
+      title: res.data.msg,
+    });
+  } else {
+    console.log(res);
+    ElNotification({
+      type: "error",
+      title: res.data.msg,
+    });
+  }
+}
+async function exportExcel() {}
+let dischargeCurrentPage = ref(1);
+let dischargeList = ref();
+let formInline = ref({});
+async function getDischargeList(type) {
+  let res = await api.getDischargeList({
+    voyageId: route.query.id,
+    portId: currentDiscPortId.value,
+    currentPage: dischargeCurrentPage.value,
+    size: 10,
+    loginAccountId: localStorage.loginAccountId,
+  });
+  if (res.data.status == 0) {
+    dischargeList.value = res.data.result;
+    total.value = res.data.total;
+  } else {
+    console.log(res);
+  }
+}
+let updateForm = ref({});
+let updateDischargeDialog = ref(false);
+let currentUpdateIndex = ref(-1);
+async function updateDischarge() {
+  let res = await api.updateDischarge({
+    ...updateForm.value,
+  });
+
+  if (res.data.status == 0) {
+    dischargeList.value[currentUpdateIndex.value] = res.data.result;
+    cancelUpdateDischarge();
+    ElNotification({
+      type: "success",
+      title: res.data.msg,
+    });
+  } else {
+    console.log(res);
+    ElNotification({
+      type: "error",
+      title: res.data.msg,
+    });
+  }
+}
+let updatePoundParams = ref([]);
+let updatePoundBillList = ref([]);
+
+function showUpdateDischarge(item, index) {
+  updateDischargeDialog.value = true;
+  updatePoundBillList.value = [];
+
+  let { id, dischargeTons, dischargeTime, voyageId, files } = item;
+  updatePoundBillList.value = files;
+
+  updateForm.value = {
+    id,
+    dischargeTons,
+    dischargeTime,
+  };
+  currentUpdateIndex.value = index;
+}
+function cancelUpdateDischarge() {
+  updateDischargeDialog.value = false;
+  updateForm.value = {};
+  currentUpdateIndex.value = -1;
+}
+function initMap() {
+  map.value = new AMap.Map("map-container", {
+    zoom: 11, //级别
+    center: [120.563757, 31.891174], //中心点坐标
+    mapStyle: "amap://styles/f48d96805f5fa7f5aada657c5ee37017",
+    zoomEnable: false,
+    dragEnable: false,
+  });
+  let toolBar = new AMap.ToolBar({
+    position: {
+      top: "40px",
+      right: "40px",
+    },
+  });
+  let hawkEye = new AMap.HawkEye({
+    opened: false,
+  });
+  // map.value.addControl(toolBar);
+  map.value.addControl(hawkEye);
+  let markers = [];
+  for (let i of medias.value) {
+    let content = `<div style='width:160px'>
+        ${
+          i.audit == 1
+            ? `<img id='img${i.id}' style='width:160px;height:160px;object-fit:contain;' src='${i.viewUrl}'/>`
+            : ``
+        }
+        <img src='https://hhd-pat-1255802371.cos.ap-shanghai.myqcloud.com/frontend/ship-red-icon.png' style='display:block;width:20px;height:20px;margin:6px auto'/
+      </div>`;
+
+    let marker = new AMap.Marker({
+      content,
+      position: new AMap.LngLat(i.longitude, i.latitude),
+      offset: new AMap.Pixel(-75, i.audit == 1 ? -195 : -30),
+    });
+    if (i.audit == 1) {
+      marker.on("click", () => {
+        openMediaModal(i.viewUrl, 1, "航次图片", i);
+      });
+    }
+
+    markers.push(marker);
+  }
+
+  let overlayGroups = new AMap.OverlayGroup(markers);
+  map.value.on("complete", function () {
+    let t = setTimeout(() => {
+      map.value.add(overlayGroups);
+      map.value.setFitView(markers, true, [200, 50, 0, 0], 18);
+      clearTimeout(t);
+    }, 2000);
+  });
+}
+
+function setShipMarker(longitude = 121.524761, latitude = 31.228721) {
+  map.value.setCenter([longitude, latitude]);
+  var marker = new AMap.Marker({
+    position: new AMap.LngLat(longitude, latitude),
+    offset: new AMap.Pixel(-20, -20),
+    size: new AMap.Size(80, 80),
+    icon: "https://hhd-pat-1255802371.cos.ap-shanghai.myqcloud.com/frontend/ship-red-icon.png", // 添加 Icon 图标 URL
+    title: "",
+  });
+  map.value.add(marker);
+}
+
+let disabledStatus = ref(true);
+let updateCache = {};
+function changeVoyageInfo() {
+  updateCache = _.cloneDeep(voyage.value);
+  disabledStatus.value = false;
+}
+function cancelVoyageChange() {
+  voyage.value = updateCache;
+  disabledStatus.value = true;
+}
+async function submitVoyageChange() {
+  let {
+    id,
+    transStatus,
+    loadStartTime,
+    loadEndTime,
+    dischargeStartTime,
+    dischargeEndTime,
+    actualDischargeTons,
+    remark,
+  } = voyage.value;
+  let res = await api.updateVoyage({
+    id,
+    transStatus,
+    loadStartTime,
+    loadEndTime,
+    dischargeStartTime,
+    dischargeEndTime,
+    actualDischargeTons,
+    remark,
+  });
+  if (res.data.status == 0) {
+    ElNotification({
+      type: "success",
+      title: res.data.msg,
+    });
+    disabledStatus.value = true;
+  } else {
+    ElNotification({
+      type: "error",
+      title: res.data.msg,
+    });
+    console.log(res);
+  }
+}
+let options = ref([
+  { value: 0, label: "请选择" },
+  {
+    value: 1,
+    label: "航行",
+  },
+  {
+    value: 2,
+    label: "停泊",
+  },
+  {
+    value: 3,
+    label: "装货",
+  },
+  {
+    value: 4,
+    label: "运输中",
+  },
+  {
+    value: 5,
+    label: "卸货",
+  },
+]);
+
+async function finishVoyage() {
+  if (!voyage.value.dischargeEndTime) return;
+  let res = await api.finishVoyage({
+    voyageId: route.query.id,
+  });
+
+  if (res.data.status == 0) {
+    voyage.value.voyageStatus = 2;
+    ElNotification({
+      type: "success",
+      title: res.data.msg,
+    });
+  } else {
+    ElNotification({
+      type: "error",
+      title: res.data.msg,
+    });
+  }
+}
+let currentUrl = ref("");
+let mediaModal = ref(false);
+let modalType = ref(1);
+let modalTitle = ref();
+let modalPreview = ref([]);
+function openMediaModal(url, type, title) {
+  console.log(url, type, title);
+  modalPreview.value = [url];
+  modalTitle.value = title;
+  modalType.value = type;
+  currentUrl.value = url;
+  mediaModal.value = true;
+}
+
+async function auditMedia(mediaId, a, index, mediaType) {
+  console.log(mediaId, a, index, mediaType);
+  let res = await api.auditMedia({
+    mediaId,
+    audit: a,
+  });
+  if (res.data.status == 0) {
+    medias.value[index].audit = a;
+    ElNotification({
+      type: "success",
+      title: res.data.msg,
+    });
+  } else {
+    console.log(res);
+  }
+}
+let dialogImageUrl = ref();
+let dialogVisible = ref(false);
+function handlePictureCardPreview(file) {
+  dialogVisible.value = true;
+  dialogImageUrl.value = file.url;
+}
+
+async function handleRemoveBill(file, list) {
+  let cache = _.cloneDeep(voyageBill.value);
+  console.log(cache);
+  ElMessageBox.confirm("确认删除运单?", "Warning", {
+    confirmButtonText: "删除",
+    cancelButtonText: "取消",
+    type: "warning",
+  })
+    .then(async () => {
+      let { id } = file;
+      let res = await api.deleteWaybill({
+        id,
+      });
+      if (res.data.status == 0) {
+        ElMessage({
+          message: "删除成功!",
+          type: "success",
+        });
+        voyageBill.value = list;
+      }
+    })
+    .catch(() => {
+      voyageBill.value = cache;
+      ElMessage({
+        type: "info",
+        message: "取消删除",
+      });
+    });
+}
+let voyageBill = ref([]);
+
+function billUploadSuccess(response, file, list) {
+  list[list.length - 1] = {
+    ...response.result,
+    url: response.result.viewUrl,
+  };
+  console.log(list);
+  voyageBill.value = list;
+}
+
+let billParams = ref({
+  voyageId: route.query.id,
+});
+
+async function calExpectedArrivalTime() {
+  let res = await api.calExpectedArrivalTime({
+    voyageId: route.query.id,
+    setSailTime: voyage.value.setSailTime,
+  });
+  if (res.data.status == 0) {
+    voyage.value.expectedArrivalTime = res.data.result;
+  }
+}
+let isLoadingExcel = ref(false);
+async function downloadExcel() {
+  isLoadingExcel.value = true;
+  let res = await downloadBlobFile(
+    `${url.baseurl}/voyage/exportExcel`,
+    { voyageId: route.query.id },
+    "船舶跟踪表",
+    "post",
+    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"
+  );
+  if (res) {
+    isLoadingExcel.value = false;
+  }
+}
+let isDischargeLoadingExcel = ref(false);
+async function exportDischargeExcel() {
+  isDischargeLoadingExcel.value = true;
+  let res = await downloadBlobFile(
+    `${url.baseurl}/voyage/exportDischargeExcel`,
+    { voyageId: route.query.id },
+    "卸货记录表",
+    "post",
+    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"
+  );
+  if (res) {
+    isDischargeLoadingExcel.value = false;
+  }
+}
+
+function subStr(str = "") {
+  return str.substring(0, 10);
+}
+let truckTableData = ref([]);
+let truckCurrentPage = ref(1);
+let truckTotal = ref(0);
+async function getTruckLoadRecord() {
+  let res = await api.getTruckLoadRecord({
+    voyageId: route.query.id,
+    portId: currentDiscPortId.value,
+    currentPage: truckCurrentPage.value,
+    size: 10,
+    loginAccountId: localStorage.loginAccountId,
+  });
+  truckTableData.value = res.data.result;
+  truckTotal.value = res.data.total;
+}
+function truckPageChange(e) {
+  truckCurrentPage.value = e;
+  getTruckLoadRecord();
+}
+let truckRecordForm = ref({});
+let isAddTruckRecordVisable = ref(false);
+let truckRecordBillList = ref([]);
+function showTruckRecord(item, index, text) {
+  isAddTruckRecordVisable.value = true;
+  if (item.file) {
+    truckRecordBillList.value[0] = {
+      ...item.file,
+      url: item.file.viewUrl,
+    };
+  }
+
+  truckRecordForm.value = { ...item };
+}
+
+let truckLoadParams = ref({});
+function getTruckLoadBillList() {}
+let blockchainInfo = ref("");
+
+let billZipLoading = ref(false);
+async function downloadBillZip() {
+  billZipLoading.value = true;
+  let res = await downloadBlobFile(
+    `${url.baseurl}/voyage/exportBillZip`,
+    { voyageId: route.query.id },
+    "单据文件",
+    "post",
+    "application/zip"
+  );
+
+  billZipLoading.value = false;
+}
+
+let weatherTableData = ref([]);
+let weatherCurrentPage = ref(1);
+let weatherTotal = ref(0);
+async function getPortWeatherList() {
+  let res = await api.getPortWeatherList({
+    voyageId: route.query.id,
+    portId: currentDiscPortId.value,
+    size: 10,
+    currentPage: weatherCurrentPage.value,
+  });
+  weatherTableData.value = res.data.result;
+  weatherTotal.value = res.data.total;
+}
+
+function weatherPageChange(e) {
+  weatherCurrentPage.value = e;
+  getPortWeatherList();
+}
+
+let labTableData = ref([]);
+let labTotal = ref(0);
+let labCurrentPage = ref(1);
+
+async function getLabList() {
+  let res = await api.getLabList({
+    voyageId: route.query.id,
+    portId: currentDiscPortId.value,
+    currentPage: labCurrentPage.value,
+    size: 10,
+    loginAccountId: localStorage.loginAccountId,
+  });
+  labTableData.value = res.data.result;
+  labTotal.value = res.data.total;
+}
+function labPageChange(e) {
+  labCurrentPage.value = e;
+  getLabList();
+}
+let isAddLabVisable = ref(false);
+let labBillList = ref([]);
+let labForm = ref({});
+function showLab(item) {
+  isAddLabVisable.value = true;
+  if (item.file) {
+    labBillList.value[0] = {
+      ...item.file,
+      url: item.file.viewUrl,
+    };
+  }
+
+  labForm.value = { ...item };
+}
+let labModalType = ref("");
+function cancelUploadLab() {}
+
+let currentPortId = ref("");
+let currentDiscPortId = ref("");
+let currentDiscPortIndex = ref(0);
+function changeDiscPortTab(e) {
+  currentDiscPortIndex.value = e.index;
+  currentDiscPortId.value =
+    voyage.value.voyageDetails[currentDiscPortIndex.value].portId + "";
+  weatherTableData.value = [];
+  weatherTotal.value = 0;
+  labTableData.value = [];
+  labTotal.value = 0;
+  dischargeList.value = [];
+  total.value = 0;
+  truckTableData.value = [];
+  truckTotal.value = 0;
+  labCurrentPage.value = 1;
+  dischargeCurrentPage.value = 1;
+  weatherCurrentPage.value = 1;
+  truckCurrentPage.value = 1;
+  getDischargeList();
+  getTruckLoadRecord();
+  getLabList();
+  getPortWeatherList();
+}
+
+onMounted(() => {
+  getVoyageDetail(true);
+});
+</script>
+<style scoped>
+.map-container {
+  width: 100%;
+  height: 500px;
+}
+
+.card-note {
+  height: 30px;
+  font-size: 12px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: #777777;
+}
+
+.medias-box {
+  width: 200px;
+  height: 200px;
+  margin-top: 20px;
+}
+
+.checkbox-group {
+  width: 200px;
+  height: 50px;
+  margin-top: 20px;
+}
+
+.medias-content {
+  width: 100%;
+  height: 600px;
+  background: #f7f7f7;
+  border-radius: 2px;
+}
+
+.pic-container {
+  width: 100%;
+  height: 100%;
+  box-sizing: border-box;
+  display: flex;
+  padding: 30px;
+  overflow-x: scroll;
+  overflow-y: hidden;
+  white-space: nowrap;
+}
+
+.pic-main {
+  position: relative;
+  width: 120px;
+}
+.box {
+  position: absolute;
+  height: 240px;
+  width: var(--box-width);
+  border: 5px solid #dddddd;
+  transition: all 0.5s;
+  background: #fff;
+  z-index: 10;
+}
+
+.point {
+  position: relative;
+  left: 93px;
+  top: 258px;
+  width: 16px;
+  height: 16px;
+  background-image: url(../../assets/blue-circle.png);
+}
+
+.s-line {
+  position: absolute;
+  left: 100px;
+  top: 242px;
+  height: 20px;
+  border-left: 2px dashed;
+  box-sizing: border-box;
+  border-color: #ddd;
+}
+
+.l-line {
+  position: relative;
+  bottom: 30px;
+  left: 111px;
+  top: 249px;
+  height: 3px;
+  width: 100px;
+  background-color: #dddddd;
+}
+
+.bottom-box {
+  top: 290px;
+}
+.top210px {
+  top: 270px;
+}
+
+.box:hover {
+  transform: scale(1.2);
+}
+
+.medias-box {
+  width: 80px;
+  height: 80px;
+  margin-top: 10px;
+}
+
+.card-note {
+  height: 30px;
+  font-size: 12px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: #777777;
+  padding: 10px 20px;
+}
+
+.medias-box {
+  width: 100%;
+  height: 100px;
+  margin-top: 20px;
+}
+
+.checkbox-group {
+  width: 180px;
+  height: 50px;
+  margin-top: 20px;
+}
+
+.el-checkbox {
+  margin: 0;
+}
+
+.now-box {
+  border: 5px solid #0094fe;
+}
+
+.now-l-line {
+  background-color: #0094fe;
+}
+
+.now-s-line {
+  border-color: #97caf6;
+}
+
+.now-point {
+  filter: grayscale(1);
+}
+
+.info-line-text-table {
+  width: 180px !important;
+}
+
+.upload-plus-icon {
+  height: 15%;
+  color: rgb(139, 147, 156);
+  line-height: 100px;
+  font-size: 40px;
+  font-weight: 200;
+}
+
+.upload-text {
+  height: 25%;
+  color: rgb(139, 147, 156);
+}
+
+.info-gap {
+  width: 40px;
+  font-size: 14px;
+  overflow: visible;
+  white-space: nowrap;
+  color: #006ebc;
+  cursor: pointer;
+}
+
+.unit {
+  font-size: 14px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: #353a42;
+  line-height: 100%;
+  margin: 0 10px;
+}
+
+.line {
+  flex-wrap: wrap;
+  margin: 0 20px;
+}
+
+.info-line {
+  margin-bottom: 20px;
+}
+
+.ml50 {
+  margin-left: 140px;
+}
+</style>

+ 1247 - 0
src/views/voyage/voyageList.vue

@@ -0,0 +1,1247 @@
+<template>
+  <div class="line-container-p24">
+    <div class="df jcsb aic">
+      <div class="df aic">
+        <div
+          @click="changeVoyageType(0)"
+          :class="
+            status == 0
+              ? 'currentbtn radio-btns left-radius'
+              : 'radio-btns left-radius'
+          "
+        >
+          全部航次
+        </div>
+        <div
+          style="border-left: none"
+          @click="changeVoyageType(1)"
+          :class="status == 1 ? 'currentbtn radio-btns' : 'radio-btns'"
+        >
+          装货中
+        </div>
+
+        <div
+          style="border-left: none"
+          @click="changeVoyageType(2)"
+          :class="status == 2 ? 'currentbtn radio-btns' : 'radio-btns'"
+        >
+          运输中
+        </div>
+        <div
+          style="border-left: none"
+          @click="changeVoyageType(3)"
+          :class="status == 3 ? 'currentbtn radio-btns' : 'radio-btns'"
+        >
+          卸货中
+        </div>
+        <div
+          @click="changeVoyageType(4)"
+          :class="
+            status == 4
+              ? 'currentbtn radio-btns right-radius'
+              : 'radio-btns right-radius '
+          "
+          style="margin-right: 40px; border-left: none"
+        >
+          历史航次
+        </div>
+        <!-- <div style="color: #333; margin-right: 10px; font-size: 14px">
+          预计到港时间:
+        </div>
+        <el-radio-group v-model="sortradio">
+          <el-radio :label="-1">默认排序</el-radio>
+          <el-radio :label="0">降序</el-radio>
+          <el-radio :label="1">升序</el-radio>
+        </el-radio-group> -->
+        <el-input
+          placeholder="请输入船名/MMSI"
+          prefix-icon="el-icon-search"
+          v-model="term"
+          clearable
+          style="width: 240px"
+          @keyup.enter.native="getVoyageList()"
+        ></el-input>
+        <div class="search-btn" @click="getVoyageList()">查询</div>
+      </div>
+      <!-- <div class="cargo-owner-add" @click="voyageAddDialogVisible = true">
+        添加航次
+      </div> -->
+      <div>
+        <el-button
+          type="primary"
+          size="small"
+          v-auth="'DOWNLOADVOYAGELIST'"
+          @click="showExportModal('航次列表')"
+          style="margin-left: 10px; margin-bottom: 10px"
+          >导出航次列表</el-button
+        >
+        <el-button
+          type="primary"
+          size="small"
+          v-auth="'MULTDOWNLOADSHIPTRACK'"
+          @click="showExportModal('航次跟踪')"
+          >导出航次跟踪</el-button
+        >
+        <el-button
+          type="primary"
+          size="small"
+          @click="showExportModal('卸货记录')"
+          v-auth="'MULTDOWNLOADDISCHARGE'"
+          >导出卸货记录</el-button
+        >
+        <el-button
+          v-auth="'DOWNLOADFYDI'"
+          type="primary"
+          size="small"
+          @click="downloadFYDI"
+          >下载FYDI指数</el-button
+        >
+      </div>
+    </div>
+    <el-dialog
+      v-model="exportModalVisable"
+      :title="exportModalTitle"
+      :close-on-click-modal="false"
+      width="200px"
+    >
+      <div class="df aic jcsb">
+        <div v-if="exportModalTitle != '航次列表'" class="df aic">
+          <div class="mr20">请选择月份:</div>
+          <el-date-picker
+            v-model="currentMonth"
+            type="month"
+            placeholder="请选择年月"
+            value-format="YYYYMM"
+            :disabled="isLoadingZip"
+          />
+        </div>
+        <div></div>
+        <el-button type="primary" @click="exportZip" :loading="isLoadingZip"
+          >导出{{ exportModalTitle }}</el-button
+        >
+      </div>
+    </el-dialog>
+    <el-dialog v-model="voyageAddDialogVisible" title="添加航次">
+      <el-form
+        :rules="rules"
+        label-position="right"
+        label-width="80px"
+        ref="addVoyageForm"
+        :model="voyageForm"
+        :before-close="resetAddVoyageForm"
+      >
+        <div class="df ffw">
+          <!-- <el-form-item prop="voyageName" label="航次名称">
+            <el-input v-model="voyageForm.voyageName"></el-input>
+          </el-form-item>
+          <el-form-item label=""></el-form-item> -->
+          <el-form-item prop="shipName" label="船舶">
+            <!-- <el-input v-model="voyageForm.shipOwnerId"></el-input> -->
+            <el-autocomplete
+              v-model="voyageForm.shipName"
+              :fetch-suggestions="searchShip"
+              placeholder="选择船舶"
+              @blur="clear('shipId')"
+              @select="selectShip"
+            />
+          </el-form-item>
+          <el-form-item prop="cargoOwnerName" label="货主">
+            <el-autocomplete
+              v-model="voyageForm.cargoOwnerName"
+              :fetch-suggestions="searchCargoOwner"
+              @blur="clear('cargoOwnerId')"
+              placeholder="选择货主"
+              @select="selectCargoOwner"
+            />
+          </el-form-item>
+          <el-form-item prop="startTime" label="开始时间">
+            <el-date-picker
+              v-model="voyageForm.startTime"
+              type="date"
+              value-format="YYYY/MM/DD"
+              placeholder="航次开始时间"
+            ></el-date-picker>
+          </el-form-item>
+          <el-form-item prop="endTime" label="结束时间">
+            <el-date-picker
+              v-model="voyageForm.endTime"
+              type="date"
+              value-format="YYYY/MM/DD"
+              placeholder="航次结束时间"
+              disabled
+            ></el-date-picker>
+          </el-form-item>
+          <el-form-item prop="loadPort" label="装货港">
+            <el-autocomplete
+              v-model="voyageForm.loadPort"
+              :fetch-suggestions="getCol"
+              @blur="clear('loadPort')"
+              placeholder="选择装货港"
+              @select="selectLoadPort"
+            />
+          </el-form-item>
+          <el-form-item prop="dischargeProt" label="卸货港">
+            <el-autocomplete
+              v-model="voyageForm.dischargeProt"
+              :fetch-suggestions="getCol"
+              @blur="clear('dischargeProt')"
+              placeholder="选择卸货港"
+              @select="selectDischargeProt"
+            />
+          </el-form-item>
+          <el-form-item prop="cargo" label="货种">
+            <el-input v-model="voyageForm.cargo"></el-input>
+          </el-form-item>
+          <el-form-item prop="tons" label="吨位">
+            <el-input v-model="voyageForm.tons"></el-input>
+          </el-form-item>
+        </div>
+      </el-form>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="resetAddVoyageForm">取消</el-button>
+          <el-button type="primary" @click="addVoyage">确定</el-button>
+        </span>
+      </template>
+    </el-dialog>
+    <!-- <div class="mt20">
+      <p class="mr20 df aic" style="font-size: 14px; color: #333">
+        列表筛选:
+        <el-checkbox
+          class="ml20"
+          v-model="selectAllVisable"
+          label="全选"
+          size="default"
+          @change="selectAll"
+        />
+      </p>
+      <el-checkbox
+        v-model="voyageNameVisable"
+        label="航次名称"
+        size="default"
+        @change="selectSingle"
+        disabled
+      />
+      <el-checkbox
+        v-model="loadPortVisable"
+        label="装货港"
+        size="default"
+        @change="selectSingle"
+      />
+      <el-checkbox
+        v-model="dischargePortVisable"
+        label="卸货港"
+        size="default"
+        @change="selectSingle"
+      />
+      <el-checkbox
+        v-model="expectedArrivalTimeVisable"
+        label="预计到港时间"
+        size="default"
+        @change="selectSingle"
+      />
+      <el-checkbox
+        v-model="abnormalStatusVisable"
+        label="航次状态"
+        size="default"
+        @change="selectSingle"
+      />
+      <el-checkbox
+        v-model="daysInPortVisable"
+        label="在港天数"
+        size="default"
+        @change="selectSingle"
+      />
+      <el-checkbox
+        v-model="todayPhotoCountVisable"
+        label="今日照片"
+        size="default"
+        @change="selectSingle"
+      />
+      <el-checkbox
+        v-model="cargoVisable"
+        label="货种"
+        size="default"
+        @change="selectSingle"
+      />
+      <el-checkbox
+        v-model="actualLoadTonsVisable"
+        label="装载吨位"
+        size="default"
+        @change="selectSingle"
+      />
+      <el-checkbox
+        v-model="unloadedtonsVisable"
+        label="已卸货吨位"
+        size="default"
+        @change="selectSingle"
+      />
+      <el-checkbox
+        v-model="remainTonsVisable"
+        label="剩余吨位"
+        size="default"
+        @change="selectSingle"
+      />
+      <el-checkbox
+        v-model="hasInsuranceVisable"
+        label="保险状态"
+        size="default"
+        @change="selectSingle"
+      />
+    </div> -->
+    <div
+      class="df aic jcfs mt20"
+      style="
+        font-size: 14px;
+        color: #333;
+        width: calc(100vw - 300px);
+        flex-wrap: wrap;
+      "
+    >
+      <div class="df jcsb aic mb10 mr20">
+        <div class="mr10">装货港:</div>
+        <el-select
+          style="width: 120px"
+          v-model="voyageListPostData.loadPortId"
+          placeholder="装货港"
+          size="small"
+          @change="getVoyageList"
+          @focus="getPortSelect"
+          filterable
+        >
+          <el-option
+            v-for="item in portOptions"
+            :key="item"
+            :label="item.value"
+            :value="item.key"
+          />
+        </el-select>
+      </div>
+      <div class="df jcsb aic mb10 mr20">
+        <div class="mr10">卸货港:</div>
+        <el-select
+          style="width: 120px"
+          v-model="voyageListPostData.discPortId"
+          placeholder="卸货港"
+          size="small"
+          @change="getVoyageList"
+          @focus="getPortSelect"
+          filterable
+        >
+          <el-option
+            v-for="item in portOptions"
+            :key="item"
+            :label="item.value"
+            :value="item.key"
+          />
+        </el-select>
+      </div>
+      <div class="df jcsb aic mb10 mr20">
+        <div class="mr10">预计到港时间:</div>
+        <el-select
+          style="width: 120px"
+          v-model="voyageListPostData.isArrived"
+          placeholder="到港状态"
+          size="small"
+          @change="getVoyageList"
+        >
+          <el-option label="已到港" :value="0" />
+          <el-option label="未到港" :value="1" />
+        </el-select>
+      </div>
+      <div class="df jcsb aic mb10 mr20">
+        <div class="mr10">航次状态:</div>
+        <el-select
+          style="width: 120px"
+          v-model="voyageListPostData.abnormalStatus"
+          placeholder="航次状态"
+          size="small"
+          @change="getVoyageList"
+        >
+          <el-option label="正常" :value="0" />
+          <el-option label="异常" :value="1" />
+        </el-select>
+      </div>
+      <div class="df jcsb aic mb10 mr20">
+        <div class="mr10">货种:</div>
+        <el-select
+          style="width: 100px"
+          v-model="voyageListPostData.cargo"
+          placeholder="货种"
+          size="small"
+          @change="getVoyageList"
+          @focus="getCargoSelect"
+          filterable
+        >
+          <el-option
+            v-for="item in cargoOptions"
+            :key="item"
+            :label="item.key"
+            :value="item.value"
+          />
+        </el-select>
+      </div>
+      <div class="df jcsb aic mb10 mr20">
+        <div class="mr10">保险状态:</div>
+        <el-select
+          style="width: 120px"
+          v-model="voyageListPostData.hasInsurance"
+          placeholder="保险状态"
+          size="small"
+          @change="getVoyageList"
+        >
+          <el-option label="未购买" :value="0" />
+          <el-option label="已购买" :value="1" />
+        </el-select>
+      </div>
+      <el-button @click="resetFilter" class="mb10" size="small" type="primary"
+        >重置</el-button
+      >
+    </div>
+    <el-table
+      :data="tableData"
+      stripe
+      style="width: 100%; margin-top: 12px"
+      :row-style="rowStyle"
+    >
+      <!-- <el-table-column
+        type="index"
+        label="序号"
+        min-width="80"
+        align="center"
+      ></el-table-column> -->
+      <el-table-column
+        prop="voyageName"
+        label="航次名称"
+        v-if="voyageNameVisable"
+        min-width="140"
+        align="center"
+      ></el-table-column>
+      <el-table-column
+        prop="loadPort"
+        label="装货港"
+        v-if="loadPortVisable"
+        min-width="90"
+        align="center"
+      ></el-table-column>
+      <el-table-column
+        prop="dischargePort"
+        label="卸货港"
+        v-if="dischargePortVisable"
+        min-width="80"
+        align="center"
+      ></el-table-column>
+      <!-- <el-table-column
+        prop="setSailTime"
+        label="开航时间"
+        min-width="100"
+        align="center"
+      ></el-table-column> -->
+      <el-table-column
+        prop="expectedArrivalTime"
+        label="预计到港时间"
+        v-if="expectedArrivalTimeVisable"
+        sortable
+        min-width="140"
+        align="center"
+      >
+        <template v-slot="scope">
+          {{ scope.row.arrived ? "已到港" : scope.row.expectedArrivalTime }}
+        </template>
+      </el-table-column>
+      <el-table-column
+        prop="abnormalStatus"
+        label="航次状态"
+        v-if="abnormalStatusVisable"
+        min-width="80"
+        align="center"
+      >
+        <template v-slot="scope">
+          {{ scope.row.abnormalStatus == 0 ? "正常" : "异常" }}
+        </template>
+      </el-table-column>
+      <el-table-column
+        prop="daysInPortStr"
+        label="在港天数"
+        v-if="daysInPortVisable"
+        sortable
+        min-width="100"
+        align="center"
+      ></el-table-column>
+      <el-table-column
+        prop="todayPhotoCount"
+        label="今日照片"
+        v-if="todayPhotoCountVisable"
+        min-width="100"
+        align="center"
+      ></el-table-column>
+      <el-table-column
+        prop="cargo"
+        v-if="cargoVisable"
+        label="货种"
+        min-width="70"
+        align="center"
+      ></el-table-column>
+      <el-table-column
+        prop="actualLoadTons"
+        label="装载吨位"
+        v-if="actualLoadTonsVisable"
+        min-width="80"
+        align="center"
+      ></el-table-column>
+      <el-table-column
+        prop="unloadedtons"
+        label="已卸货吨位"
+        v-if="unloadedtonsVisable"
+        min-width="100"
+        align="center"
+      ></el-table-column>
+      <el-table-column
+        prop="remainTons"
+        label="剩余吨位"
+        v-if="remainTonsVisable"
+        min-width="80"
+        align="center"
+      ></el-table-column>
+      <!-- <el-table-column
+        prop="waybillStatus"
+        sortable
+        label="签单状态"
+        min-width="100"
+        align="center"
+      >
+        <template v-slot="scope">
+          {{
+            scope.row.waybillStatus == 0
+              ? ""
+              : scope.row.waybillStatus == 1
+              ? "未签单"
+              : "已签单"
+          }}
+        </template>
+      </el-table-column> -->
+      <!-- <el-table-column
+        prop="transStatus"
+        label="船舶状态"
+        min-width="100"
+        align="center"
+      ></el-table-column> -->
+      <el-table-column
+        prop="hasInsurance"
+        label="保险状态"
+        v-if="hasInsuranceVisable"
+        min-width="100"
+        align="center"
+      >
+        <template v-slot="scope">
+          {{ scope.row.hasInsurance == 0 ? "未购买" : "已购买" }}
+        </template>
+      </el-table-column>
+      <!-- <el-table-column
+        sortable
+        prop="createTime"
+        label="创建时间"
+        min-width="100"
+        align="center"
+      >
+        <template v-slot="scope">
+          {{ subTimeStr(scope.row.createTime) }}
+        </template>
+      </el-table-column>
+      <el-table-column
+        prop="remark"
+        label="备注"
+        min-width="100"
+        align="center"
+      ></el-table-column> -->
+      <el-table-column
+        v-auth="'VOYAGEDETAIL'"
+        label="操作"
+        min-width="80"
+        align="center"
+        fixed="right"
+      >
+        <template v-slot="scope">
+          <el-button
+            @click="voyageDetail(scope.row.id, tableData)"
+            type="text"
+            size="small"
+          >
+            查看详情
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <div style="width: 100%; text-align: right; margin-top: 43px">
+      <el-pagination
+        background
+        layout="prev, pager, next"
+        :total="total"
+        @current-change="pageChange"
+      ></el-pagination>
+    </div>
+  </div>
+</template>
+<script setup>
+import { ref, h, reactive, toRefs, onMounted } from "vue";
+import { ElNotification, ElMessageBox, ElMessage } from "element-plus";
+import store from "../../store";
+import router from "../../router";
+import md5 from "md5";
+import api from "../../apis/fetch";
+import _ from "lodash";
+import { subTimeStr } from "../../utils/utils";
+import downloadBlobFile from "../../utils/downloadBlobFile";
+import url from "../../apis/config";
+
+let currentPage = ref(1);
+let term = ref("");
+let tableData = ref([]);
+let total = ref(0);
+let status = ref(0);
+let loginAccountId = ref("");
+let voyageListPostData = ref({});
+async function getVoyageList() {
+  tableData.value = [];
+
+  let res = await api.getVoyageList({
+    ...voyageListPostData.value,
+    loginAccountId: localStorage.loginAccountId,
+    cargoOwnerId: localStorage.userId,
+    shipId: 0,
+    status: status.value,
+    term: term.value,
+    currentPage: currentPage.value,
+    size: 10,
+  });
+  if (res.data.status == 0) {
+    tableData.value = res.data.result;
+    total.value = res.data.total;
+  } else {
+    ElNotification({
+      type: "error",
+      title: res.data.msg,
+    });
+  }
+}
+function changeVoyageType(s) {
+  currentPage.value = 1;
+  status.value = s;
+  getVoyageList();
+}
+async function voyageDetail(id) {
+  router.push({
+    path: "/voyage/voyageDetail",
+    query: {
+      id,
+    },
+  });
+}
+function pageChange(e) {
+  currentPage.value = e;
+  getVoyageList();
+}
+
+function goToVoyageAdd() {
+  router.push({
+    path: "/voyage/voyageAdd",
+  });
+}
+let voyageAddDialogVisible = ref(false);
+const rules = reactive({
+  rules: {
+    voyageName: [
+      { required: false, message: "请填写航次名称", trigger: "blur" },
+    ],
+    shipName: [{ required: true, message: "请选择船舶", trigger: "blur" }],
+    cargoOwnerName: [
+      { required: true, message: "请选择货主", trigger: "blur" },
+    ],
+    startTime: [{ required: true, message: "请填写开始时间", trigger: "blur" }],
+    loadPort: [{ required: true, message: "请填写装货港", trigger: "blur" }],
+    dischargeProt: [
+      { required: true, message: "请填写卸货港", trigger: "blur" },
+    ],
+    cargo: [{ required: true, message: "请填写货种", trigger: "blur" }],
+    tons: [{ required: true, message: "请填写吨位", trigger: "blur" }],
+  },
+});
+let voyageForm = reactive({
+  voyageForm: {
+    voyageName: "",
+    cargoOwnerId: "",
+    cargoOwnerName: "",
+    startTime: "",
+    endTime: "",
+    loadPort: "",
+    dischargeProt: "",
+    cargo: "",
+    tons: "",
+    loadPortId: "",
+    dischargeProtId: "",
+    shipId: "",
+    shipName: "",
+  },
+});
+
+function clear(type) {
+  setTimeout(() => {
+    switch (type) {
+      case "shipId": {
+        let index = ref(-1);
+        for (let i in shipsCache.value) {
+          if (voyageForm.voyageForm.shipName == shipsCache.value[i].shipName) {
+            index.value = i;
+            break;
+          }
+        }
+        if (index.value != -1) {
+          voyageForm.voyageForm.shipId = shipsCache.value[index.value].shipId;
+        } else {
+          let b = shipsCache.value.some((item) => {
+            return (
+              item.shipId == voyageForm.voyageForm.shipId &&
+              item.shipName == voyageForm.voyageForm.shipName
+            );
+          });
+          voyageForm.voyageForm["shipId"] = "";
+          voyageForm.voyageForm["shipName"] = "";
+        }
+        break;
+      }
+      case "cargoOwnerId": {
+        let index = ref(-1);
+        for (let i in cargoOwnersCache.value) {
+          if (
+            voyageForm.voyageForm.cargoOwnerName ==
+            cargoOwnersCache.value[i].userName
+          ) {
+            index.value = i;
+            break;
+          }
+        }
+        if (index.value != -1) {
+          voyageForm.voyageForm.cargoOwnerId =
+            cargoOwnersCache.value[index.value].userId;
+        } else {
+          let b = cargoOwnersCache.value.some((item) => {
+            return (
+              item.userId == voyageForm.voyageForm.cargoOwnerId &&
+              item.userName == voyageForm.voyageForm.cargoOwnerName
+            );
+          });
+          if (!b) {
+            voyageForm.voyageForm["cargoOwnerId"] = "";
+            voyageForm.voyageForm["cargoOwnerName"] = "";
+          }
+        }
+
+        break;
+      }
+      case "loadPort": {
+        let index = ref(-1);
+        for (let i in colCache.value) {
+          if (voyageForm.voyageForm.loadPort == colCache.value[i].value) {
+            index.value = i;
+            break;
+          }
+        }
+        if (index.value != -1) {
+          voyageForm.voyageForm.loadPortId = colCache.value[index.value].key;
+        } else {
+          let b = colCache.value.some((item) => {
+            return (
+              item.value == voyageForm.voyageForm.loadPort &&
+              item.key == voyageForm.voyageForm.loadPortId
+            );
+          });
+          if (!b) {
+            voyageForm.voyageForm["loadPort"] = "";
+            voyageForm.voyageForm["loadPortId"] = "";
+          }
+        }
+
+        break;
+      }
+
+      case "dischargeProt": {
+        let index = ref(-1);
+        for (let i in colCache.value) {
+          if (voyageForm.voyageForm.dischargeProt == colCache.value[i].value) {
+            index.value = i;
+            break;
+          }
+        }
+        if (index.value != -1) {
+          voyageForm.voyageForm.dischargeProtId =
+            colCache.value[index.value].key;
+        } else {
+          let b = colCache.value.some((item) => {
+            return (
+              item.value == voyageForm.voyageForm.dischargeProt &&
+              item.key == voyageForm.voyageForm.dischargeProtId
+            );
+          });
+          if (!b) {
+            voyageForm.voyageForm["dischargeProt"] = "";
+            voyageForm.voyageForm["dischargeProtId"] = "";
+          }
+        }
+        break;
+      }
+    }
+  }, 200);
+}
+let addVoyageForm = ref(null);
+
+async function addVoyage() {
+  addVoyageForm.value.validate(async (valid) => {
+    if (valid) {
+      console.log("提交", voyageForm.voyageForm);
+      let res = await api.addVoyage({
+        ...voyageForm.voyageForm,
+      });
+      if (res.data.status == 0) {
+        ElNotification({
+          title: res.data.msg,
+          type: "success",
+        });
+        resetAddVoyageForm();
+        getVoyageList();
+      } else {
+        console.log(res);
+        ElNotification({
+          title: res.data.msg,
+          type: "error",
+        });
+      }
+    } else {
+      console.log("未提交", voyageForm.voyageForm);
+    }
+  });
+}
+
+let shipsCache = ref([]);
+let colCache = ref([]);
+let cargoOwnersCache = ref([]);
+
+const searchShip = _.debounce(
+  async (queryString, cb) => {
+    if (!queryString) return;
+    let res = await api.searchShip({
+      term: queryString,
+    });
+    let ships = [];
+    if (res.data.status == 0) {
+      ships = res.data.result;
+      for (let i of ships) {
+        i.value = `${i.shipName}`;
+      }
+      shipsCache.value = ships;
+      cb(ships);
+    }
+  },
+  1000,
+  { leading: true }
+);
+
+const selectShip = (item) => {
+  voyageForm.voyageForm.shipId = item.shipId;
+};
+
+const searchCargoOwner = _.debounce(
+  async (queryString, cb) => {
+    if (!queryString) return;
+    let res = await api.searchUser({
+      term: queryString,
+      identity: 2,
+    });
+    let cargoOwners = [];
+    if (res.data.status == 0) {
+      cargoOwners = res.data.result;
+      for (let i of cargoOwners) {
+        i.value = `${i.userName}`;
+      }
+      cargoOwnersCache.value = cargoOwners;
+      cb(cargoOwners);
+    }
+  },
+  1000,
+  { leading: true }
+);
+
+const selectCargoOwner = (item) => {
+  voyageForm.voyageForm.cargoOwnerId = item.userId;
+};
+
+const getCol = _.debounce(
+  async (queryString, cb) => {
+    if (!queryString) return;
+    let res = await api.getCol({
+      term: queryString,
+    });
+    if (res.data.status == 0) {
+      colCache.value = [...colCache.value, ...res.data.result];
+      colCache.value = _.uniqBy(colCache.value, "key");
+
+      cb(res.data.result);
+    }
+  },
+  1000,
+  { leading: true }
+);
+
+const selectLoadPort = (item) => {
+  voyageForm.voyageForm.loadPortId = item.key;
+  voyageForm.voyageForm.loadPort = item.value;
+};
+
+const selectDischargeProt = (item) => {
+  voyageForm.voyageForm.dischargeProtId = item.key;
+  voyageForm.voyageForm.dischargeProt = item.value;
+};
+
+function resetAddVoyageForm() {
+  voyageAddDialogVisible.value = false;
+  addVoyageForm.value.resetFields();
+}
+
+let sortradio = ref(0);
+async function downloadFYDI() {
+  let res0 = await api.getFYFIDownloadUrl({
+    loginAccountId: localStorage.loginAccountId,
+  });
+
+  if (res0.data.result == 1) {
+    ElNotification({
+      type: "info",
+      title: "更新中",
+    });
+  } else {
+    let url = res0.data.result;
+    let a = document.createElement("a");
+    a.setAttribute("href", url);
+    a.click();
+  }
+}
+
+let exportModalVisable = ref(false);
+let exportModalTitle = ref("");
+let currentMonth = ref("");
+
+function showExportModal(type) {
+  exportModalVisable.value = true;
+  exportModalTitle.value = type;
+}
+
+let isLoadingZip = ref(false);
+
+async function exportZip() {
+  if (!currentMonth.value && exportModalTitle.value != "航次列表") return;
+
+  isLoadingZip.value = true;
+  let path = "";
+  let type = "";
+  let postData = {
+    loginAccountId: localStorage.loginAccountId,
+  };
+  let title = "";
+  switch (exportModalTitle.value) {
+    case "航次列表": {
+      path = "/voyage/exportListExcel";
+      type =
+        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
+      title = `${exportModalTitle.value}`;
+      let arr = [];
+      for (let i of tableData.value) {
+        arr.push(i.id);
+      }
+      postData.voyageIds = arr.join(",");
+      break;
+    }
+    case "航次跟踪": {
+      path = "/voyage/exportMultExcel";
+      type = "application/zip";
+      postData.date = currentMonth.value;
+      title = `${exportModalTitle.value}${currentMonth.value}`;
+
+      break;
+    }
+    case "卸货记录": {
+      path = "/voyage/exportMultDischargeExcel";
+      type = "application/zip";
+      postData.date = currentMonth.value;
+      title = `${exportModalTitle.value}${currentMonth.value}`;
+      break;
+    }
+  }
+  try {
+    let res = await downloadBlobFile(
+      `${url.baseurl}${path}`,
+      postData,
+      title,
+      "post",
+      type
+    );
+    if (res.status == 0) {
+      ElNotification({
+        title: "导出成功!",
+        type: "success",
+      });
+    } else {
+      ElNotification({
+        title: "暂无数据",
+      });
+    }
+  } catch (error) {
+    ElNotification({
+      title: "暂无数据",
+    });
+  } finally {
+    isLoadingZip.value = false;
+    currentMonth.value = "";
+    exportModalVisable.value = false;
+  }
+}
+
+function rowStyle({ row }) {
+  let rowStyle = {};
+
+  if (row.daysInPort >= 20 && row.daysInPort < 25) {
+    rowStyle.color = "#f9ca24";
+  } else if (row.daysInPort >= 25 && row.daysInPort < 30) {
+    rowStyle.color = "#f0932b";
+  } else if (row.daysInPort >= 30) {
+    rowStyle.color = "#eb4d4b";
+  }
+
+  return rowStyle;
+}
+
+let voyageNameVisable = ref(true);
+let loadPortVisable = ref(true);
+let dischargePortVisable = ref(true);
+let expectedArrivalTimeVisable = ref(true);
+let abnormalStatusVisable = ref(true);
+let daysInPortVisable = ref(true);
+let todayPhotoCountVisable = ref(true);
+let cargoVisable = ref(true);
+let actualLoadTonsVisable = ref(true);
+let unloadedtonsVisable = ref(true);
+let remainTonsVisable = ref(true);
+let hasInsuranceVisable = ref(true);
+
+let selectAllVisable = ref(true);
+function selectAll(e) {
+  loadPortVisable.value = e;
+  dischargePortVisable.value = e;
+  expectedArrivalTimeVisable.value = e;
+  abnormalStatusVisable.value = e;
+  daysInPortVisable.value = e;
+  todayPhotoCountVisable.value = e;
+  cargoVisable.value = e;
+  actualLoadTonsVisable.value = e;
+  unloadedtonsVisable.value = e;
+  remainTonsVisable.value = e;
+  hasInsuranceVisable.value = e;
+}
+
+function selectSingle(e) {
+  if (!e) {
+    selectAllVisable.value = e;
+  }
+}
+
+let loadPortFilterStr = ref("");
+function selectLoadPortFilter(item) {
+  voyageListPostData.value.loadPortId = item.key;
+  getVoyageList();
+}
+let discPortFilterStr = ref("");
+function selectDiscPortFilter(item) {
+  voyageListPostData.value.discPortId = item.key;
+  getVoyageList();
+}
+let cargoFilterStr = ref("");
+function selectCargoFilter(item) {
+  voyageListPostData.value.cargo = item.key;
+  getVoyageList();
+}
+
+function resetFilter() {
+  loadPortFilterStr.value = "";
+  discPortFilterStr.value = "";
+  cargoFilterStr.value = "";
+  voyageListPostData.value = {};
+  getVoyageList();
+}
+let cargoOptions = ref([]);
+async function getCargoSelect() {
+  if (cargoOptions.value.length) return;
+  let res = await api.getCargoSelect({
+    loginAccountId: loginAccountId.value,
+    status: 2,
+    term: "",
+  });
+  cargoOptions.value = res.data.result;
+}
+
+let portOptions = ref([]);
+async function getPortSelect() {
+  if (portOptions.value.length) return;
+  let res = await api.getCol({
+    term: "",
+  });
+  portOptions.value = res.data.result;
+}
+
+onMounted(() => {
+  getVoyageList();
+  loginAccountId.value = localStorage.loginAccountId;
+});
+</script>
+<style scoped>
+.search-btn {
+  display: inline-block;
+  width: 60px;
+  height: 32px;
+  background: #0094fe;
+  border-radius: 2px;
+  font-size: 14px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: #ffffff;
+  text-align: center;
+  line-height: 32px;
+  margin-left: 10px;
+  cursor: pointer;
+}
+
+.cargo-owner-add {
+  width: 80px;
+  height: 32px;
+  border-radius: 2px;
+  border: 1px solid #0094fe;
+  font-size: 14px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: #0094fe;
+  line-height: 32px;
+  text-align: center;
+  cursor: pointer;
+}
+:deep().el-dialog {
+  width: 560px;
+  padding: 20px 50px;
+  border-radius: 6px;
+}
+
+:deep() .el-dialog__title {
+  font-size: 18px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: #0094fe;
+}
+
+.normal-label {
+  font-size: 14px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: #353a42;
+  margin-right: 10px;
+}
+
+.show-input {
+  width: 280px;
+  height: 32px;
+  background: #ffffff;
+  border-radius: 2px;
+  border: 1px solid #dee0e3;
+  font-size: 14px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: #333333;
+  line-height: 32px;
+  padding-left: 12px;
+  margin-right: 40px;
+}
+
+.radio-btns {
+  height: 38px;
+  width: 70px;
+  border: 1px solid #1486f9;
+  line-height: 38px;
+  text-align: center;
+  font-size: 14px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: #0094fe;
+  cursor: pointer;
+}
+
+.left-radius {
+  border-top-left-radius: 19px;
+  border-bottom-left-radius: 19px;
+  width: 80px;
+}
+
+.right-radius {
+  border-top-right-radius: 19px;
+  border-bottom-right-radius: 19px;
+  width: 80px;
+}
+.currentbtn {
+  background: #1486f9;
+  color: #fff;
+}
+
+.search-btn {
+  display: inline-block;
+  width: 60px;
+  height: 38px;
+  background: #0094fe;
+  border-radius: 2px;
+  font-size: 14px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: #ffffff;
+  text-align: center;
+  line-height: 38px;
+  margin-left: 10px;
+  cursor: pointer;
+}
+
+.voyage-add {
+  width: 80px;
+  height: 36px;
+  border-radius: 2px;
+  border: 1px solid #0094fe;
+  font-size: 14px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: #0094fe;
+  line-height: 36px;
+  text-align: center;
+  cursor: pointer;
+}
+
+:deep() .el-dialog {
+  width: 800px;
+}
+
+:deep() .el-form-item {
+  margin-right: 22px;
+  width: 300px;
+}
+
+:deep() .el-autocomplete {
+  width: 220px;
+}
+
+.el-radio {
+  margin-right: 10px;
+}
+.el-radio:last-child {
+  margin-right: 20px;
+}
+</style>

+ 23 - 0
vite.config.js

@@ -0,0 +1,23 @@
+import { defineConfig } from "vite";
+import vue from "@vitejs/plugin-vue";
+import viteCompression from "vite-plugin-compression";
+import path from "path";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [vue(), viteCompression()],
+  resolve: {
+    alias: {
+      "@": path.resolve(__dirname, "src"),
+      comps: path.resolve(__dirname, "src/components"),
+      apis: path.resolve(__dirname, "src/apis"),
+      router: path.resolve(__dirname, "src/router"),
+      store: path.resolve(__dirname, "src/store"),
+      views: path.resolve(__dirname, "src/views"),
+      utils: path.resolve(__dirname, "src/utils"),
+    },
+  },
+  server: {
+    port: 8899,
+  },
+});